// Package service 承载 room-service 的 Room Cell 管理、命令流水线、恢复、快照和 outbox 补偿。
package service

import (
	"context"
	"errors"
	"sync"
	"sync/atomic"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/clock"
	"hyapp/pkg/tencentim"
	"hyapp/services/room-service/internal/integration"
	"hyapp/services/room-service/internal/room/cell"
	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/router"
)

// Config 收敛 room-service 领域服务的运行参数。
type Config struct {
	// NodeID 是当前 room-service 实例标识，Redis lease 以它作为房间 owner。
	NodeID string
	// LeaseTTL 是房间 owner 租约有效期，过期后其他节点可以接管并恢复房间。
	LeaseTTL time.Duration
	// RankLimit 是房间本地礼物榜保留长度。
	RankLimit int
	// SnapshotEveryN 表示每 N 个房间版本保存一次快照。
	SnapshotEveryN int64
	// PresenceStaleAfter 控制业务 presence 多久未刷新后由后台 worker 清理。
	PresenceStaleAfter time.Duration
	// MicPublishTimeout 控制 MicUp 后等待 RTC 发流确认的最长时间。
	MicPublishTimeout time.Duration
	// RTCUserRemover 是平台级封禁后把用户从腾讯 RTC 房间移出的外部边界。
	RTCUserRemover integration.RTCUserRemover
	// RTCUserAudioBlocker 把 Room Cell 禁言事实投影到腾讯 RTC 服务端音频上行闸门。
	RTCUserAudioBlocker integration.RTCUserAudioBlocker
	// IMUserMuter 把 Room Cell 禁言事实异步投影到腾讯 IM 原生群成员禁言。
	IMUserMuter integration.IMUserMuter
	// RoomRocketLaunchScheduler 把倒计时发射唤醒交给外部延迟消息，避免只靠已加载 Cell 扫描。
	RoomRocketLaunchScheduler integration.RoomRocketLaunchScheduler
	// RobotDisplayPublisher 专门 best-effort 投递机器人房间展示事件；失败只打日志，不进入 MySQL outbox。
	RobotDisplayPublisher integration.OutboxPublisher
	// RoomDisplayPublisher 在 Room Cell 提交后直投低延迟展示，也由幸运礼物派生 outbox 补偿投递房间 IM。
	RoomDisplayPublisher integration.OutboxPublisher
	// HumanRobotPoolProvider 自动读取全站机器人账号和用户国家，用于真人房间机器人运行时按国家分组。
	HumanRobotPoolProvider integration.HumanRoomRobotPoolProvider
	// RoomGiftLeaderboard 是跨房间贡献榜读模型，SendGift 提交后 best-effort 写入。
	RoomGiftLeaderboard RoomGiftLeaderboardStore
	// RoomListCache 是公共发现页 Redis 展示读模型；未配置或未就绪时 ListRooms 直接读 MySQL。
	RoomListCache RoomListCacheStore
	// LuckyGiftSendLocker 串行化同一用户的幸运礼物扣费、抽奖和同步返奖链路。
	LuckyGiftSendLocker LuckyGiftSendLocker
	// LuckyGiftSendLockTTL 是幸运礼物短锁过期时间。
	LuckyGiftSendLockTTL time.Duration
	// Clock 允许测试注入稳定时间，生产默认使用系统时钟。
	Clock clock.Clock
}

// Service 承载 room-service 的核心房间领域逻辑。
type Service struct {
	// nodeID 是当前实例 ID，必须和 Redis lease 中的 owner 对齐。
	nodeID string
	// leaseTTL 控制当前节点持有房间执行权的时间窗口。
	leaseTTL time.Duration
	// rankLimit 控制每个 Room Cell 的 LocalRank top N。
	rankLimit int
	// snapshotEveryN 控制快照频率，值越小恢复越快但写放大越高。
	snapshotEveryN int64
	// presenceStaleAfter 是断线或无心跳用户从 room-service presence 中移除的阈值。
	presenceStaleAfter time.Duration
	// micPublishTimeout 是 pending_publish 自动下麦的确认窗口。
	micPublishTimeout time.Duration
	// clock 是房间命令、事件和快照的统一时间来源。
	clock clock.Clock
	// directory 负责 room_id -> node_id lease，生产路径是 Redis。
	directory router.Directory
	// repository 是 MySQL 持久化边界，保存 meta、snapshot、command log 和 outbox。
	repository Repository
	// wallet 是 SendGift 的同步扣费依赖，扣费失败不能进入 Room Cell 状态变更。
	wallet integration.WalletClient
	// luckyGift 是幸运礼物同步检查和抽奖依赖；未配置时幸运礼物按未启用处理。
	luckyGift integration.LuckyGiftClient
	// syncPublisher 是 room-service 到腾讯云 IM 的低时延房间系统事件桥。
	syncPublisher integration.RoomEventPublisher
	// outboxPublisher 是补偿 worker 对外部消费者的异步投递抽象。
	outboxPublisher integration.OutboxPublisher
	// robotDisplayPublisher 只服务机器人房间展示事件，和主 room_outbox 持久事实隔离。
	robotDisplayPublisher integration.OutboxPublisher
	// roomDisplayPublisher 只承载腾讯 IM 展示，不作为业务事实源；真人幸运飘屏由本地 room_outbox 兜住失败重试。
	roomDisplayPublisher integration.OutboxPublisher
	// outboxWake 提交成功后唤醒主 outbox worker；它只是低延迟提示，事实源仍然是 MySQL room_outbox。
	outboxWake chan struct{}
	// outboxClaimCursor 让跨 App claim 轮转起点，避免默认 lalu 长期有积压时饿死 fami/huwaa 等租户。
	outboxClaimCursor atomic.Uint64
	// humanRobotPoolProvider 运行时自动获取全站机器人池，避免后台手工维护国家机器人池。
	humanRobotPoolProvider integration.HumanRoomRobotPoolProvider
	// roomRocketLaunchScheduler 在 ignited outbox 投递成功后安排 launch_at_ms 唤醒。
	roomRocketLaunchScheduler integration.RoomRocketLaunchScheduler
	// roomRocketProgressCoalescer 把高频燃料变化压成每秒每轮一条 outbox，降低 room_outbox/MQ/IM 写放大。
	roomRocketProgressCoalescer *roomRocketProgressCoalescer
	// robotDisplaySampler 把同房间同展示类型在短窗口内收敛成一条直发 IM；机器人送礼本身不进入房间持久化命令链路。
	robotDisplaySampler *robotDisplaySampler
	// robotDisplayPublishOnce 让手工构造 Service 的内部测试和生产 New 都能安全初始化有界发布槽位。
	robotDisplayPublishOnce sync.Once
	// robotDisplayPublishSlots 限制腾讯 IM 慢请求占用的 goroutine；槽位满时只丢展示，绝不回写 DB/MQ。
	robotDisplayPublishSlots chan struct{}
	// robotGiftCatalogMu 保护机器人展示使用的只读礼物目录缓存；缓存只减少 wallet 查询，不保存送礼事实。
	robotGiftCatalogMu sync.Mutex
	// robotGiftCatalog 按 App 和房间区域缓存 active 礼物展示快照，绝不能承载余额、流水或幂等语义。
	robotGiftCatalog map[robotGiftCatalogKey]robotGiftCatalogEntry
	// robotGiftCatalogLoads 只保存进行中的 App/区域目录加载；等待者共享结果且可响应自身 ctx 取消。
	robotGiftCatalogLoads map[robotGiftCatalogKey]*robotGiftCatalogLoad
	// roomGiftLeaderboard 只保存房间维度贡献值 zset，不承载 Room Cell 核心状态。
	roomGiftLeaderboard RoomGiftLeaderboardStore
	// roomListCache 只承载发现页展示顺序；MySQL 仍然是房间列表和置顶事实 owner。
	roomListCache RoomListCacheStore
	// luckyGiftSendLocker 只保护同一用户幸运礼物扣费到同步返奖的小事务链路。
	luckyGiftSendLocker LuckyGiftSendLocker
	// luckyGiftSendLockTTL 控制 Redis 短锁自动释放窗口。
	luckyGiftSendLockTTL time.Duration
	// rtcUserRemover 在 Room Cell 提交驱逐后同步移除实时音频房连接。
	rtcUserRemover integration.RTCUserRemover
	// rtcUserAudioBlocker 在房间禁言提交后关闭目标用户的 RTC 上行音频；状态仍只保存在 Room Cell。
	rtcUserAudioBlocker integration.RTCUserAudioBlocker
	// imUserMuter 只由 durable outbox worker 调用，普通 IM 发消息链路永远不会经过 room-service。
	imUserMuter integration.IMUserMuter
	// imMuteGateMu 保护按 app/room/user 划分的 IM 投影门闩，避免同一用户的旧 mute 与新 unmute 并发写腾讯。
	imMuteGateMu sync.Mutex
	imMuteGates  map[imMuteProjectionKey]*imMuteProjectionGate
	// IM forbid_send_msg 官方上限为 200 QPS；单节点节流给双节点部署保留共享 SDKAppID 余量。
	imMuteRateMu   sync.Mutex
	imMuteNextCall time.Time
	// rtcMuteGateMu 保护按 app/room/user 划分的 RTC 投影门闩；门闩只串行化本进程请求，
	// 跨实例竞态由每次云 API 返回后的 latest-state recheck 继续收敛。
	rtcMuteGateMu sync.Mutex
	rtcMuteGates  map[rtcMuteProjectionKey]*rtcMuteProjectionGate
	// rtcMuteRateMu 和 nextCall 在进程内把 SetUserBlockedByStrRoomId 压到官方 20 QPS 以内，
	// 避免 HTTP 直调与 outbox 补偿叠加后在故障时放大限流。
	rtcMuteRateMu   sync.Mutex
	rtcMuteNextCall time.Time
	// draining 表示当前节点正在下线，只允许已经进入执行链路的命令收尾。
	draining atomic.Bool

	// mu 保护本进程内 room_id -> RoomCell 注册表。
	mu sync.Mutex
	// cells 保存当前节点已经装载的单房间执行单元；写路径必须同时校验 lease token，避免 lease 断档后复用旧内存分支。
	cells map[string]*loadedCell

	// robotRuntimeMu 保护后台机器人房间运行循环；配置变更时要取消旧循环后重建。
	robotRuntimeMu sync.Mutex
	// robotRuntimes 保存 room_id -> runtime，避免同一房间被重复启动多个送礼循环。
	robotRuntimes map[string]robotRoomRuntime
	// humanRobotRuntimeMu 保护真人房间机器人运行循环，和机器人房间 runtime 分开避免互相取消。
	humanRobotRuntimeMu sync.Mutex
	// humanRobotRuntimes 保存 room_id -> runtime，避免同一真人房间被重复补机器人。
	humanRobotRuntimes map[string]robotRoomRuntime
}

type robotRoomRuntime struct {
	// cancel 停止当前房间的所有机器人送礼 goroutine。
	cancel context.CancelFunc
	// token 区分同一房间的前后两次启动，旧 goroutine 退出时不能误删新 runtime。
	token string
	// countryCode 记录真人房间机器人 runtime 所属国家，用于后台国家限额变更后精确清理。
	countryCode string
	// configUpdatedAtMS 记录启动 runtime 时采用的配置版本；后台改配置后扫描器据此重启真人房间机器人 runtime。
	configUpdatedAtMS int64
}

type loadedCell struct {
	// roomCell 是当前进程内的 Room Cell 状态副本，只有 leaseToken 连续匹配时才能继续承载写命令。
	roomCell *cell.RoomCell
	// leaseToken 记录装载该 Cell 时持有的 Redis lease 实例；空值只允许读路径使用，不能参与写路径复用。
	leaseToken string
}

// mutationResult 是命令在 Room Cell 内执行后的统一结果包。
type mutationResult struct {
	// applied 表示本次命令确实改变并提交了房间状态。
	applied bool
	// snapshot 是命令执行后对外返回的房间快照。
	snapshot *roomv1.RoomSnapshot
	// forceSnapshot 表示本命令必须立即保存快照，适用于低频资料 backfill，避免等待 snapshot_every_n。
	forceSnapshot bool
	// user 是 JoinRoom 等命令需要返回的用户投影。
	user *roomv1.RoomUser
	// seatNo 是麦位命令返回的目标或原麦位编号。
	seatNo int32
	// micSessionID 是 MicUp 创建的发流会话 ID，客户端确认发流时必须带回。
	micSessionID string
	// publishDeadlineMS 是 pending_publish 自动释放的截止时间。
	publishDeadlineMS int64
	// micHeartbeatAtMS 是服务端接受当前麦上心跳的时间，只属于麦位活跃探测，不改变时长口径。
	micHeartbeatAtMS int64
	// billingReceiptID 是 SendGift 从 wallet-service 得到的账务回执。
	billingReceiptID string
	// coinBalanceAfter 是送礼人可展示的最终 COIN 余额；幸运礼物返奖成功时优先使用返奖后的余额。
	coinBalanceAfter int64
	// giftIncomeBalanceAfter 是收礼人返币后的 COIN 余额；单目标返币可同步返回给发起方确认。
	giftIncomeBalanceAfter int64
	// roomHeat 是 SendGift 后的房间热度。
	roomHeat int64
	// giftRank 是 SendGift 后的本地礼物榜投影。
	giftRank []*roomv1.RankItem
	// luckyGift 是扣费成功后同步执行的幸运礼物抽奖结果；普通礼物保持 nil。
	luckyGift *roomv1.LuckyGiftDrawResult
	// luckyGifts 是多目标幸运礼物的逐接收方抽奖结果；单目标时只包含 luckyGift。
	luckyGifts []*roomv1.LuckyGiftDrawResult
	// batchDisplay 是 batch-send 的发送方本地展示快照；它不替代逐目标 RoomGiftSent 事实。
	batchDisplay *roomv1.SendGiftBatchDisplay
	// giftV2 是首次提交后持久化并在相同 command_id 重试时原样重放的精简结算结果。
	giftV2 *roomv1.SendGiftResultV2
	// suppressDirectIMEventTypes 只影响提交后的低延迟展示 lane，不能改变 durable outbox 事实。
	suppressDirectIMEventTypes map[string]bool
	// commandPayload 允许少数命令把外部依赖的结算快照写入 command log，用于恢复。
	commandPayload []byte
	// syncEvent 标记该命令存在腾讯云 IM 房间消息形态；保留旧字段兼容命令结果，room UI 现在由提交后 direct IM 发送。
	syncEvent *tencentim.RoomEvent
	// outboxRecords 是本次命令已经持久化的房间事实，提交后可投给 activity 等外部消费者。
	outboxRecords []outbox.Record
	// robotDisplayRecords 是机器人房间展示事实，提交成功后 best-effort 直发 Tencent IM，不占主 room_outbox。
	robotDisplayRecords []outbox.Record
	// directIMRecords 是提交成功后直接发腾讯 IM 的展示事件；失败不回滚已提交房间命令。
	directIMRecords []outbox.Record
	// roomRocketProgress 是提交成功后才进入内存合并器的火箭进度展示事件，不和送礼命令事务逐笔写 outbox。
	roomRocketProgress *roomRocketProgressPending
	// roomStatus 非空时代表命令提交时需要同步更新 rooms 和列表投影生命周期状态。
	roomStatus string
	// deleteRoom 表示本命令成功后房间应从持久恢复来源、列表投影和当前节点 Cell 注册表中移除。
	deleteRoom bool
	// roomSeatCount 非 nil 时代表命令提交时需要同步更新 rooms 元数据座位数。
	roomSeatCount *int32
	// roomPasswordHash 非 nil 时代表命令提交时需要同步更新 rooms 当前密码哈希。
	roomPasswordHash *string
	roomMode         *string
	visibleRegionID  *int64
	closeInfo        *RoomCloseInfo
	// walletDebitMS 记录 SendGift 同步扣费耗时；非钱包命令保持 0。
	walletDebitMS int64
	// roomGiftLeaderboard 是 SendGift 成功后写入跨房间贡献榜的轻量增量。
	roomGiftLeaderboard *RoomGiftLeaderboardIncrement
	// roomUserContributionPeriodStats 是当前房间用户周期贡献统计，和命令日志同事务提交。
	roomUserContributionPeriodStats []RoomUserContributionPeriodStatIncrement
	// roomUserGiftStats 是当前房间用户送礼价值统计，和命令日志同事务提交。
	roomUserGiftStats []RoomUserGiftStatIncrement
	// roomWeeklyContribution 是后台房间贡献列的当前 UTC 周增量，和命令日志同事务提交。
	roomWeeklyContribution *RoomWeeklyContributionIncrement
	// roomDecorationAssignment 和 Room Cell 装扮状态同事务更新，供 wallet 事件按索引即时重算。
	roomDecorationAssignment *RoomDecorationAssignmentChange
	// roomCoverUploadConsumption 让房间资料 URL 与一次性上传授权在同一命令事务内绑定。
	roomCoverUploadConsumption *RoomCoverUploadConsumption
}

// New 初始化 room-service 领域服务。
func New(cfg Config, directory router.Directory, repository Repository, wallet integration.WalletClient, syncPublisher integration.RoomEventPublisher, outboxPublisher integration.OutboxPublisher, luckyGift ...integration.LuckyGiftClient) *Service {
	usedClock := cfg.Clock
	if usedClock == nil {
		// 生产默认系统时钟，测试可注入固定时钟保证断言稳定。
		usedClock = clock.System{}
	}

	snapshotEveryN := cfg.SnapshotEveryN
	if snapshotEveryN <= 0 {
		// 默认每次状态变更都保存快照，优先保证首版恢复简单。
		snapshotEveryN = 1
	}

	rankLimit := cfg.RankLimit
	if rankLimit <= 0 {
		// 默认只保留房间内前 20 名礼物榜，避免快照和广播载荷过大。
		rankLimit = 20
	}

	presenceStaleAfter := cfg.PresenceStaleAfter
	if presenceStaleAfter <= 0 {
		// 默认 2 分钟清理无刷新 presence，避免重连窗口过短导致误踢。
		presenceStaleAfter = 2 * time.Minute
	}

	micPublishTimeout := cfg.MicPublishTimeout
	if micPublishTimeout <= 0 {
		// MicUp 只是业务占麦，客户端必须在短窗口内确认 RTC 发流成功。
		micPublishTimeout = 15 * time.Second
	}
	luckyGiftSendLockTTL := cfg.LuckyGiftSendLockTTL
	if luckyGiftSendLockTTL <= 0 {
		luckyGiftSendLockTTL = 5 * time.Second
	}

	var luckyGiftClient integration.LuckyGiftClient
	if len(luckyGift) > 0 {
		luckyGiftClient = luckyGift[0]
	}

	return &Service{
		nodeID:                      cfg.NodeID,
		leaseTTL:                    cfg.LeaseTTL,
		rankLimit:                   rankLimit,
		snapshotEveryN:              snapshotEveryN,
		presenceStaleAfter:          presenceStaleAfter,
		micPublishTimeout:           micPublishTimeout,
		clock:                       usedClock,
		directory:                   directory,
		repository:                  repository,
		wallet:                      wallet,
		luckyGift:                   luckyGiftClient,
		syncPublisher:               syncPublisher,
		outboxPublisher:             outboxPublisher,
		robotDisplayPublisher:       cfg.RobotDisplayPublisher,
		roomDisplayPublisher:        cfg.RoomDisplayPublisher,
		outboxWake:                  make(chan struct{}, 1024),
		humanRobotPoolProvider:      cfg.HumanRobotPoolProvider,
		roomRocketLaunchScheduler:   cfg.RoomRocketLaunchScheduler,
		roomRocketProgressCoalescer: newRoomRocketProgressCoalescer(cfg.RoomDisplayPublisher),
		robotDisplaySampler:         newRobotDisplaySampler(),
		robotGiftCatalog:            make(map[robotGiftCatalogKey]robotGiftCatalogEntry),
		robotGiftCatalogLoads:       make(map[robotGiftCatalogKey]*robotGiftCatalogLoad),
		roomGiftLeaderboard:         cfg.RoomGiftLeaderboard,
		roomListCache:               cfg.RoomListCache,
		luckyGiftSendLocker:         cfg.LuckyGiftSendLocker,
		luckyGiftSendLockTTL:        luckyGiftSendLockTTL,
		rtcUserRemover:              cfg.RTCUserRemover,
		rtcUserAudioBlocker:         cfg.RTCUserAudioBlocker,
		imUserMuter:                 cfg.IMUserMuter,
		imMuteGates:                 make(map[imMuteProjectionKey]*imMuteProjectionGate),
		rtcMuteGates:                make(map[rtcMuteProjectionKey]*rtcMuteProjectionGate),
		cells:                       make(map[string]*loadedCell),
		robotRuntimes:               make(map[string]robotRoomRuntime),
		humanRobotRuntimes:          make(map[string]robotRoomRuntime),
	}
}

// HealthCheck 校验 room-service 核心状态 owner 和持久化依赖已经完成装配。
func (s *Service) HealthCheck() error {
	if s == nil {
		// nil service 说明 App 装配失败，live/ready 都不能放行。
		return errors.New("room service is not initialized")
	}
	if s.directory == nil {
		// 没有 directory 就无法保证单房间单活 owner。
		return errors.New("room directory is not initialized")
	}
	if s.repository == nil {
		// 没有 repository 就无法保证 command log、snapshot 和 outbox 持久化。
		return errors.New("room repository is not initialized")
	}
	if s.cells == nil {
		// cells 注册表缺失会导致已恢复房间无法复用同一个 Room Cell。
		return errors.New("room cell registry is not initialized")
	}

	return nil
}

// MarkDraining 让领域服务停止接收新的 Room Cell 命令和新 lease 接管。
func (s *Service) MarkDraining() {
	if s == nil {
		return
	}
	s.draining.Store(true)
}

func (s *Service) isDraining() bool {
	return s != nil && s.draining.Load()
}
