package service

import (
	"context"
	"log/slog"
	"sync"
	"time"

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

const (
	// roomRocketProgressCoalesceWindow 固定按 UTC 毫秒切 1 秒桶，把高频燃料变化压缩成客户端可接受的展示粒度。
	roomRocketProgressCoalesceWindow = time.Second
	// roomRocketProgressFlushInterval 控制到期 bucket 的扫描频率；100ms 让实际延迟稳定落在 1 秒窗口附近。
	roomRocketProgressFlushInterval = 100 * time.Millisecond
)

type roomRocketProgressPending struct {
	record      outbox.Record
	robotLane   bool
	rocketID    string
	level       int32
	currentFuel int64
	roomVersion int64
}

type roomRocketProgressKey struct {
	appCode       string
	roomID        string
	rocketID      string
	level         int32
	robotLane     bool
	windowStartMS int64
}

type roomRocketProgressEntry struct {
	key         roomRocketProgressKey
	record      outbox.Record
	roomVersion int64
	currentFuel int64
	flushAtMS   int64
}

type roomRocketProgressCoalescer struct {
	mu        sync.Mutex
	publisher integration.OutboxPublisher
	pending   map[roomRocketProgressKey]roomRocketProgressEntry
}

func newRoomRocketProgressCoalescer(publisher integration.OutboxPublisher) *roomRocketProgressCoalescer {
	return &roomRocketProgressCoalescer{
		publisher: publisher,
		pending:   make(map[roomRocketProgressKey]roomRocketProgressEntry),
	}
}

func (c *roomRocketProgressCoalescer) enqueue(progress roomRocketProgressPending) {
	if c == nil || progress.record.Envelope == nil || progress.rocketID == "" || progress.level <= 0 {
		// 入队只接受已经构造成 outbox envelope 的火箭进度；异常输入说明调用方没有走标准 SendGift 构造链路。
		return
	}

	record := progress.record
	record.AppCode = appcode.Normalize(record.AppCode)
	record.Envelope.AppCode = record.AppCode
	if record.CreatedAtMS <= 0 {
		record.CreatedAtMS = record.Envelope.GetOccurredAtMs()
	}
	if progress.roomVersion <= 0 {
		progress.roomVersion = record.Envelope.GetRoomVersion()
	}
	windowStartMS := roomRocketProgressWindowStartMS(record.CreatedAtMS)
	key := roomRocketProgressKey{
		appCode:       record.AppCode,
		roomID:        record.RoomID,
		rocketID:      progress.rocketID,
		level:         progress.level,
		robotLane:     progress.robotLane,
		windowStartMS: windowStartMS,
	}
	entry := roomRocketProgressEntry{
		key:         key,
		record:      record,
		roomVersion: progress.roomVersion,
		currentFuel: progress.currentFuel,
		flushAtMS:   windowStartMS + roomRocketProgressCoalesceWindow.Milliseconds(),
	}

	c.mu.Lock()
	defer c.mu.Unlock()
	c.mergeLocked(entry)
}

func (c *roomRocketProgressCoalescer) flushDue(ctx context.Context, nowMS int64) ([]outbox.Record, error) {
	if c == nil {
		return nil, nil
	}
	due := c.takeDue(nowMS)
	if len(due) == 0 {
		return nil, nil
	}
	normalEntries, robotRecords := splitRoomRocketProgressDue(due)
	if err := c.publishDue(ctx, normalEntries); err != nil {
		c.requeue(normalEntries, nowMS+roomRocketProgressFlushInterval.Milliseconds())
		return robotRecords, err
	}
	return robotRecords, nil
}

func (c *roomRocketProgressCoalescer) takeDue(nowMS int64) []roomRocketProgressEntry {
	c.mu.Lock()
	defer c.mu.Unlock()

	due := make([]roomRocketProgressEntry, 0)
	for key, entry := range c.pending {
		if entry.flushAtMS > nowMS {
			continue
		}
		due = append(due, entry)
		delete(c.pending, key)
	}
	return due
}

func (c *roomRocketProgressCoalescer) requeue(entries []roomRocketProgressEntry, nextFlushAtMS int64) {
	c.mu.Lock()
	defer c.mu.Unlock()

	for _, entry := range entries {
		entry.flushAtMS = nextFlushAtMS
		c.mergeLocked(entry)
	}
}

func (c *roomRocketProgressCoalescer) mergeLocked(entry roomRocketProgressEntry) {
	current, exists := c.pending[entry.key]
	if !exists || roomRocketProgressEntryNewer(entry, current) {
		c.pending[entry.key] = entry
	}
}

func splitRoomRocketProgressDue(entries []roomRocketProgressEntry) ([]roomRocketProgressEntry, []outbox.Record) {
	normal := make([]roomRocketProgressEntry, 0, len(entries))
	robotRecords := make([]outbox.Record, 0)
	for _, entry := range entries {
		record := entry.record
		record.AppCode = appcode.Normalize(record.AppCode)
		if record.Envelope != nil {
			record.Envelope.AppCode = record.AppCode
		}
		if entry.key.robotLane {
			robotRecords = append(robotRecords, record)
			continue
		}
		entry.record = record
		normal = append(normal, entry)
	}
	return normal, robotRecords
}

func (c *roomRocketProgressCoalescer) publishDue(ctx context.Context, entries []roomRocketProgressEntry) error {
	if c.publisher == nil {
		return nil
	}
	for _, entry := range entries {
		record := entry.record
		if record.Envelope == nil {
			continue
		}
		record.AppCode = appcode.Normalize(record.AppCode)
		record.Envelope.AppCode = record.AppCode
		if err := c.publisher.PublishOutboxEvent(appcode.WithContext(ctx, record.AppCode), record.Envelope); err != nil {
			return err
		}
	}
	return nil
}

func roomRocketProgressEntryNewer(candidate roomRocketProgressEntry, current roomRocketProgressEntry) bool {
	if candidate.roomVersion != current.roomVersion {
		return candidate.roomVersion > current.roomVersion
	}
	if candidate.currentFuel != current.currentFuel {
		return candidate.currentFuel > current.currentFuel
	}
	return candidate.record.CreatedAtMS >= current.record.CreatedAtMS
}

func roomRocketProgressWindowStartMS(createdAtMS int64) int64 {
	windowMS := roomRocketProgressCoalesceWindow.Milliseconds()
	if createdAtMS <= 0 {
		return 0
	}
	return createdAtMS / windowMS * windowMS
}

// FlushRoomRocketProgress 把已经超过 1 秒窗口的火箭进度直接发腾讯 IM；机器人 lane 保持原展示直发路径。
func (s *Service) FlushRoomRocketProgress(ctx context.Context) error {
	if s == nil || s.roomRocketProgressCoalescer == nil {
		return nil
	}
	robotRecords, err := s.roomRocketProgressCoalescer.flushDue(ctx, s.clock.Now().UTC().UnixMilli())
	s.publishRobotDisplayRecordsBestEffort(ctx, robotRecords)
	return err
}

// RunRoomRocketProgressFlushWorker 周期性把内存合并后的普通火箭进度直接投递到房间 IM。
func (s *Service) RunRoomRocketProgressFlushWorker(ctx context.Context, interval time.Duration) {
	if interval <= 0 {
		interval = roomRocketProgressFlushInterval
	}
	if err := s.FlushRoomRocketProgress(ctx); err != nil && ctx.Err() == nil {
		logx.Warn(ctx, "room_rocket_progress_flush_failed", slog.String("error", err.Error()))
	}

	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := s.FlushRoomRocketProgress(ctx); err != nil && ctx.Err() == nil {
				logx.Warn(ctx, "room_rocket_progress_flush_failed", slog.String("error", err.Error()))
			}
		}
	}
}
