package service

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/services/room-service/internal/room/outbox"
)

const (
	// outboxRetryExponentialBackoff 是房间事件补偿的默认策略，避免外部故障时每秒刷同一条错误。
	outboxRetryExponentialBackoff = "exponential_backoff"
	// outboxWorkerName 固定写入日志，便于后续按 worker 聚合检索。
	outboxWorkerName = "room_outbox"
)

// OutboxWorkerOptions 控制 room_outbox pending 事件的持续补偿循环。
type OutboxWorkerOptions struct {
	// PollInterval 是两轮 pending 扫描之间的间隔；非正数会被归一到安全默认值。
	PollInterval time.Duration
	// BatchSize 是单轮最多处理条数；非正数会被归一，避免全表扫描。
	BatchSize int
	// PublishTimeout 限制单条外部投递耗时，shutdown 时当前事件最多阻塞该时长。
	PublishTimeout time.Duration
	// RetryStrategy 当前只支持 exponential_backoff。
	RetryStrategy string
	// MaxRetryCount 是转入 failed 前允许的最大失败次数。
	MaxRetryCount int
	// InitialBackoff 是首次失败后的等待时间。
	InitialBackoff time.Duration
	// MaxBackoff 是指数退避上限。
	MaxBackoff time.Duration
}

func defaultOutboxWorkerOptions() OutboxWorkerOptions {
	// 默认值优先避免 busy loop、无限重试和日志污染。
	return OutboxWorkerOptions{
		PollInterval:   time.Second,
		BatchSize:      100,
		PublishTimeout: 3 * time.Second,
		RetryStrategy:  outboxRetryExponentialBackoff,
		MaxRetryCount:  10,
		InitialBackoff: 5 * time.Second,
		MaxBackoff:     5 * time.Minute,
	}
}

func normalizeOutboxWorkerOptions(options OutboxWorkerOptions) OutboxWorkerOptions {
	// worker 层再次归一化，防止测试或手动构造绕过 config.Normalize。
	defaults := defaultOutboxWorkerOptions()
	if options.PollInterval <= 0 {
		// 非正间隔不能直接传给 ticker，否则会 panic 或形成忙循环。
		options.PollInterval = defaults.PollInterval
	}
	if options.BatchSize <= 0 {
		// 非正批量按安全默认值处理，避免 repository 扫描不受控。
		options.BatchSize = defaults.BatchSize
	}
	if options.PublishTimeout <= 0 {
		// 单条外部投递必须有 deadline，shutdown 才能等待有界。
		options.PublishTimeout = defaults.PublishTimeout
	}
	if options.MaxRetryCount <= 0 {
		options.MaxRetryCount = defaults.MaxRetryCount
	}
	if options.InitialBackoff <= 0 {
		options.InitialBackoff = defaults.InitialBackoff
	}
	if options.MaxBackoff <= 0 {
		options.MaxBackoff = defaults.MaxBackoff
	}
	if options.MaxBackoff < options.InitialBackoff {
		options.MaxBackoff = options.InitialBackoff
	}
	options.RetryStrategy = strings.TrimSpace(options.RetryStrategy)
	if options.RetryStrategy == "" {
		// 空策略按指数退避处理，保持和配置默认值一致。
		options.RetryStrategy = defaults.RetryStrategy
	}

	return options
}

// RunOutboxWorker 持续扫描 pending outbox；它不执行房间命令，只投递已持久化事件。
func (s *Service) RunOutboxWorker(ctx context.Context, options OutboxWorkerOptions) {
	options = normalizeOutboxWorkerOptions(options)
	if options.RetryStrategy != outboxRetryExponentialBackoff {
		// 配置层会拒绝未知策略；这里兜底避免测试或手写构造静默改变语义。
		logx.Error(ctx, "worker_stopped", fmt.Errorf("unsupported retry_strategy"), slog.String("worker", outboxWorkerName), slog.String("node_id", s.nodeID))
		return
	}

	if err := s.ProcessPendingOutbox(ctx, options); err != nil && ctx.Err() == nil {
		logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", outboxWorkerName), slog.String("node_id", s.nodeID), slog.Int("batch_size", options.BatchSize))
	}

	ticker := time.NewTicker(options.PollInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := s.ProcessPendingOutbox(ctx, options); err != nil && ctx.Err() == nil {
				logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", outboxWorkerName), slog.String("node_id", s.nodeID), slog.Int("batch_size", options.BatchSize))
			}
		}
	}
}

// ProcessPendingOutbox 把待投递事件补偿性地推送给异步消费者。
func (s *Service) ProcessPendingOutbox(ctx context.Context, options OutboxWorkerOptions) error {
	options = normalizeOutboxWorkerOptions(options)
	if options.RetryStrategy != outboxRetryExponentialBackoff {
		return fmt.Errorf("unsupported outbox retry_strategy %q", options.RetryStrategy)
	}

	// 补偿 worker 先抢占 pending/retryable 事件，再投递，避免多机同时处理同一批。
	records, err := s.repository.ClaimPendingOutbox(ctx, s.nodeID, options.BatchSize, time.Now().UTC().Add(options.PublishTimeout).UnixMilli())
	if err != nil {
		return err
	}

	for _, record := range records {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		if record.RetryCount >= options.MaxRetryCount {
			// 已达到最大失败次数的历史事件直接转死信，防止每轮 worker 继续打外部系统。
			markCtx, markCancel := context.WithTimeout(appcode.WithContext(context.Background(), record.AppCode), options.PublishTimeout)
			markErr := s.repository.MarkOutboxDead(markCtx, record.EventID, deadLetterReason(record))
			markCancel()
			if markErr != nil {
				return markErr
			}
			logx.Error(ctx, "outbox_dead_letter", nil,
				slog.String("worker", outboxWorkerName),
				slog.String("node_id", s.nodeID),
				slog.String("event_id", record.EventID),
				slog.String("event_type", record.EventType),
				slog.String("room_id", record.RoomID),
				slog.Int("retry_count", record.RetryCount),
				slog.String("last_error", trimOutboxError(record.LastError)),
			)
			continue
		}

		publishCtx, cancel := context.WithTimeout(context.Background(), options.PublishTimeout)
		err := s.outboxPublisher.PublishOutboxEvent(publishCtx, record.Envelope)
		cancel()
		if err != nil {
			// 投递失败转为 retryable，并写入指数退避时间，等待下一轮 worker 到期后再抢占。
			nextRetryAtMS := time.Now().UTC().Add(outboxBackoff(record.RetryCount+1, options)).UnixMilli()
			markCtx, markCancel := context.WithTimeout(appcode.WithContext(context.Background(), record.AppCode), options.PublishTimeout)
			markErr := s.repository.MarkOutboxRetryable(markCtx, record.EventID, trimOutboxError(err.Error()), nextRetryAtMS)
			markCancel()
			if markErr != nil {
				return markErr
			}

			logx.Warn(ctx, "outbox_publish_retryable",
				slog.String("worker", outboxWorkerName),
				slog.String("node_id", s.nodeID),
				slog.String("event_id", record.EventID),
				slog.String("event_type", record.EventType),
				slog.String("room_id", record.RoomID),
				slog.Int("retry_count", record.RetryCount+1),
				slog.Int64("next_retry_at_ms", nextRetryAtMS),
				slog.String("error", trimOutboxError(err.Error())),
			)
			continue
		}

		// 只有外部发布成功后才能标记 delivered。
		markCtx, markCancel := context.WithTimeout(appcode.WithContext(context.Background(), record.AppCode), options.PublishTimeout)
		markErr := s.repository.MarkOutboxDelivered(markCtx, record.EventID)
		markCancel()
		if markErr != nil {
			return markErr
		}

		logx.Info(ctx, "outbox_delivered",
			slog.String("worker", outboxWorkerName),
			slog.String("node_id", s.nodeID),
			slog.String("event_id", record.EventID),
			slog.String("event_type", record.EventType),
			slog.String("room_id", record.RoomID),
			slog.Int("retry_count", record.RetryCount),
		)
	}

	return nil
}

func outboxBackoff(retryCount int, options OutboxWorkerOptions) time.Duration {
	if retryCount <= 1 {
		return options.InitialBackoff
	}
	backoff := options.InitialBackoff
	for i := 1; i < retryCount; i++ {
		if backoff >= options.MaxBackoff/2 {
			return options.MaxBackoff
		}
		backoff *= 2
	}
	if backoff > options.MaxBackoff {
		return options.MaxBackoff
	}
	return backoff
}

func deadLetterReason(record outbox.Record) string {
	if strings.TrimSpace(record.LastError) == "" {
		return "outbox retry limit exceeded"
	}
	return trimOutboxError("outbox retry limit exceeded: " + record.LastError)
}

func trimOutboxError(value string) string {
	// last_error 会写入 MySQL 和日志，先去掉首尾空白避免不可见差异影响排障。
	value = strings.TrimSpace(value)
	if len(value) <= 512 {
		return value
	}

	// V1 只保留错误前缀，避免外部响应过大撑爆日志或 TEXT 字段查询成本。
	return value[:512]
}
