// Package service 定义 room-service 领域服务及其持久化边界。
package service

import (
	"context"
	"errors"

	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/room/state"
)

// RoomMeta 对应 rooms 表中房间的持久元数据。
type RoomMeta struct {
	// AppCode 是房间所属 App，和 RoomID 共同构成房间持久化隔离边界。
	AppCode string
	// RoomID 是房间主键，也是恢复和 lease 路由的分区键。
	RoomID string
	// RoomShortID 是客户端可搜索/展示的短房号，首版等于 owner 展示短号。
	RoomShortID string
	// OwnerUserID 是房间所有者，恢复无快照房间时用于重建管理员集合。
	OwnerUserID int64
	// SeatCount 是初始麦位数量，只有无快照恢复时需要它重建麦位。
	SeatCount int32
	// Mode 是房间模式，当前作为元数据持久化。
	Mode string
	// Status 是房间基础状态，快照缺失时用于恢复 RoomState.Status。
	Status string
	// RoomPasswordHash 是当前入房密码哈希；为空表示未锁房，明文从不落库。
	RoomPasswordHash string
	// VisibleRegionID 是房间列表可见区域，0 表示 GLOBAL 兜底桶。
	VisibleRegionID int64
}

// CommandRecord 对应 room_command_log 中一条成功命令。
type CommandRecord struct {
	// AppCode 是命令所属 App，保证 command_id 幂等只在单个 App 内生效。
	AppCode string
	// RoomID 是命令所属房间。
	RoomID string
	// RoomVersion 是命令成功应用后的房间版本，恢复时按它排序回放。
	RoomVersion int64
	// CommandID 是幂等键，repository 必须保证同房间唯一。
	CommandID string
	// CommandType 是反序列化命令 payload 的分发键。
	CommandType string
	// Payload 是命令 JSON 序列化结果。
	Payload []byte
	// ResultPayload 是首次成功时固化的 typed 响应；恢复仍使用 Payload，幂等重试只能读取该结果，不能重新结算。
	ResultPayload []byte
	// Replayable 标记该命令是否参与恢复回放。
	Replayable bool
	// OwnerNodeID 是提交命令时持有房间 lease 的 room-service 节点。
	OwnerNodeID string
	// LeaseToken 是提交命令前通过 Redis fencing 校验的 lease token。
	LeaseToken string
	// CreatedAtMS 是命令成功落盘的 UTC 毫秒时间戳，恢复和审计不依赖数据库时区。
	CreatedAtMS int64
}

// MutationCommit 表达一次 Room Cell 命令需要一起提交的持久化副作用。
type MutationCommit struct {
	// CreateRoomMeta 非 nil 时在命令事务内插入房间元数据；创建封面凭证消费不能与 rooms 行分步提交。
	CreateRoomMeta *RoomMeta
	// Command 是本次命令日志，所有成功命令都必须写入。
	Command CommandRecord
	// OutboxRecords 是本次命令产生的房间外事件。
	OutboxRecords []outbox.Record
	// DeleteRoom 表示后台确认删除房间后清理该房间的持久恢复来源和列表投影。
	// 该标记只用于 AdminDeleteRoom；关闭房间必须继续走 RoomStatus=closed，保留可恢复的房间事实。
	DeleteRoom bool
	// RoomStatus 非空时同步更新 rooms 和 room_list_entries 的生命周期状态。
	RoomStatus string
	// RoomSeatCount 非 nil 时同步更新 rooms 元数据中的麦位总数。
	RoomSeatCount *int32
	// RoomPasswordHash 非 nil 时同步更新 rooms 中的当前密码哈希，供最新快照恢复锁房态。
	RoomPasswordHash *string
	// RoomMode 非 nil 时同步更新 rooms 元数据中的房间模式。
	RoomMode *string
	// VisibleRegionID 非 nil 时同步更新 rooms 元数据中的列表可见区域。
	VisibleRegionID *int64
	// CloseInfo 非 nil 时同步更新后台生命周期审计字段。
	CloseInfo *RoomCloseInfo
	// RoomUserGiftStats 是本次命令产生的房间用户送礼价值增量，必须和命令日志同事务提交。
	RoomUserGiftStats []RoomUserGiftStatIncrement
	// RoomUserContributionPeriodStats 是本次命令产生的房间用户周期贡献增量，必须和命令日志同事务提交。
	RoomUserContributionPeriodStats []RoomUserContributionPeriodStatIncrement
	// RoomWeeklyContribution 是后台房间贡献列的 UTC 周期读模型增量；它和命令日志同事务提交，避免送礼成功后榜单缺分。
	RoomWeeklyContribution *RoomWeeklyContributionIncrement
	// GiftOperation 非 nil 时必须在同一事务内把 saga 推进到 committed，并保存与 command log 完全一致的结果。
	GiftOperation *GiftOperationCommit
	// RoomDecorationAssignment 与装扮命令同事务更新，供 wallet 状态事件按索引即时定位并纠正 Room Cell。
	RoomDecorationAssignment *RoomDecorationAssignmentChange
	// RoomCoverUploadConsumption 把上传凭证绑定到当前房间并推进 consumed；必须与资料命令一起成功或回滚。
	RoomCoverUploadConsumption *RoomCoverUploadConsumption
}

var (
	// ErrCommandAlreadyCommitted 表示同一 app/room/command_id 已经提交；调用方必须重放旧结果，不能继续提交副作用。
	ErrCommandAlreadyCommitted = errors.New("room command already committed")
)

const (
	GiftOperationStatusPending     = "pending"
	GiftOperationStatusDebited     = "debited"
	GiftOperationStatusDrawn       = "drawn"
	GiftOperationStatusCommitted   = "committed"
	GiftOperationStatusCompensated = "compensated"
)

// GiftOperation 是 SendGift 跨钱包、抽奖和 Room Cell 提交的持久化 saga。
// request_payload 用于服务端恢复，command_payload 用于同 command_id 载荷冲突判断；两者不能互换。
type GiftOperation struct {
	AppCode        string
	RoomID         string
	CommandID      string
	ActorUserID    int64
	CommandPayload []byte
	RequestPayload []byte
	Status         string
	BillingPayload []byte
	LuckyPayload   []byte
	ResultPayload  []byte
	LastError      string
	RetryCount     int
	NextRetryAtMS  int64
	WorkerID       string
	LockUntilMS    int64
	CreatedAtMS    int64
	UpdatedAtMS    int64
}

// GiftOperationCommit 是 Room Cell mutation 事务内的最终 saga 变更。
type GiftOperationCommit struct {
	CommandID     string
	ResultPayload []byte
	UpdatedAtMS   int64
}

// GiftOperationProgress 保存外部副作用后的稳定回执；同状态重写必须幂等。
type GiftOperationProgress struct {
	RoomID         string
	CommandID      string
	Status         string
	BillingPayload []byte
	LuckyPayload   []byte
	LastError      string
	NextRetryAtMS  int64
	UpdatedAtMS    int64
}

// RoomUserGiftStatIncrement 表达当前房间内某个送礼用户的礼物价值增量。
type RoomUserGiftStatIncrement struct {
	AppCode      string
	RoomID       string
	UserID       int64
	GiftValue    int64
	LastGiftAtMS int64
}

// RoomUserContributionPeriodStatIncrement 表达当前房间内某个送礼用户在固定 UTC 周期内的贡献增量。
type RoomUserContributionPeriodStatIncrement struct {
	AppCode       string
	RoomID        string
	Period        string
	PeriodStartMS int64
	PeriodEndMS   int64
	UserID        int64
	GiftValue     int64
	LastGiftAtMS  int64
}

// RoomWeeklyContributionIncrement 表达一个房间在当前 UTC 周内新增的展示贡献。
type RoomWeeklyContributionIncrement struct {
	AppCode    string
	RoomID     string
	GiftValue  int64
	OccurredMS int64
}

type RoomCloseInfo struct {
	Reason      string
	AdminID     uint64
	AdminName   string
	ClosedAtMS  int64
	ClearOnOpen bool
}

// RoomBackgroundImage 是房间 owner 保存过的背景图素材。
// 当前生效背景仍属于 Room Cell 快照；该表只保存可选素材列表。
type RoomBackgroundImage struct {
	AppCode      string
	BackgroundID int64
	RoomID       string
	ImageURL     string
	// CommandID 让“上传后保存素材”具备业务幂等语义；同 ID 不同媒体必须显式冲突。
	CommandID string
	// Media 是 gateway 专用上传入口验证后的完整媒体事实，禁止后续按 URL 后缀猜格式。
	Media           state.RoomMedia
	CreatedByUserID int64
	CreatedAtMS     int64
	UpdatedAtMS     int64
}

// ErrRoomBackgroundCommandConflict 表示 command_id 已用于另一份背景媒体，service 映射为幂等冲突。
var ErrRoomBackgroundCommandConflict = errors.New("room background command payload conflict")

// RoomMediaUploadRegistration 是 COS PutObject 前由 room owner 持久化的上传授权事实。
// 同一 app/room/command 只能绑定一个 actor、purpose 和规范化媒体 payload，避免换文件重试产生第二个对象。
type RoomMediaUploadRegistration struct {
	AppCode            string
	RoomID             string
	CommandID          string
	ActorUserID        int64
	Purpose            string
	MediaPayloadSHA256 string
	ExpectedObjectKey  string
	ObjectURL          string
	Status             string
	Media              state.RoomMedia
	AuthorizedVersion  int64
	CreatedAtMS        int64
	UpdatedAtMS        int64
}

// ErrRoomMediaUploadCommandConflict 表示上传 command_id 已绑定另一份文件或用途。
var ErrRoomMediaUploadCommandConflict = errors.New("room media upload command payload conflict")

// ErrRoomCoverUploadNotConsumable 表示凭证不存在、已被消费或与当前 actor 不匹配。
// service 必须映射成稳定业务冲突，不能把事务条件未命中作为数据库内部错误暴露。
var ErrRoomCoverUploadNotConsumable = errors.New("room cover upload is not consumable")

// RoomCoverUploadConsumption 是创建/更新命令对预上传封面的单次消费条件。
// SQL 还会用 app_code、空 room_id、purpose 和 active 状态做条件更新，防止跨租户、跨用户和重复使用。
type RoomCoverUploadConsumption struct {
	CoverUploadID string
	ActorUserID   int64
}

// RoomDecorationAssignment 是 Room Cell 当前装扮对应的低频反向索引。
// wallet 事件只能通过该表按 resource_id/owner_user_id 定位房间，禁止扫描 room_list_entries JSON。
type RoomDecorationAssignment struct {
	AppCode        string
	RoomID         string
	DecorationType string
	OwnerUserID    int64
	BenefitCode    string
	ResourceID     int64
	EntitlementID  string
	ExpiresAtMS    int64
	UpdatedAtMS    int64
}

// RoomDecorationAssignmentChange 与应用/清除 Room Cell 装扮在同一 SaveMutation 事务提交。
// Assignment=nil 表示删除该 decoration_type 的当前反向索引。
type RoomDecorationAssignmentChange struct {
	DecorationType     string
	ExpectedResourceID int64
	Assignment         *RoomDecorationAssignment
}

type RoomDecorationAssignmentQuery struct {
	OwnerUserID         int64
	ResourceID          int64
	AfterRoomID         string
	AfterDecorationType string
	Limit               int
}

// SnapshotRecord 对应 room_snapshots 中的一条最新快照。
type SnapshotRecord struct {
	// AppCode 是快照所属 App，恢复时必须和房间 meta 保持一致。
	AppCode string
	// RoomID 是快照所属房间。
	RoomID string
	// RoomVersion 是快照覆盖到的房间版本。
	RoomVersion int64
	// Payload 是 roomv1.RoomSnapshot 的 protobuf 序列化结果。
	Payload []byte
	// CreatedAtMS 是快照生成的 UTC 毫秒时间戳。
	CreatedAtMS int64
}

// RoomListEntry 是 room_list_entries 的领域投影，只保存列表卡片所需字段。
type RoomListEntry struct {
	// AppCode 是列表投影所属 App，发现页不能跨 App 读到房间。
	AppCode string
	// RoomID 是列表卡片和 JoinRoom 的跳转目标。
	RoomID string
	// RoomShortID 是列表卡片展示/搜索用短房号。
	RoomShortID string
	// VisibleRegionID 是房间自身的列表区域；公共发现列表会用它把当前区域房间排在其他区域之前。
	VisibleRegionID int64
	// OwnerCountryCode 是房主国家码；公共发现列表国家 tab 使用它做服务端筛选。
	OwnerCountryCode string
	// OwnerUserID 是房间所有者，用于卡片展示和运营排查。
	OwnerUserID int64
	// OwnerVIPLevel/OwnerVIPExpiresAtMS 是 wallet effective VIP 的异步读模型。
	// 过期边界只在服务端判断，客户端只收到仍有效的 owner_vip_level。
	OwnerVIPLevel       int32
	OwnerVIPExpiresAtMS int64
	// OwnerVIPFallback* 是 effective source 为 trial 时由 wallet 同快照给出的 paid 回落计划。
	// 它只在主投影到期后生效，避免体验卡自然过期窗口错误显示 0。
	OwnerVIPFallbackLevel       int32
	OwnerVIPFallbackExpiresAtMS int64
	// OwnerVIPProjectionKnown 区分“投影明确为 0”和“Room Cell 快照没有携带该外部事实”；它不写入 Redis payload。
	OwnerVIPProjectionKnown bool `json:"-"`
	// RocketNearCompletion 是 Room Cell 当前火箭进度的列表投影；普通房间命令会随最新快照覆盖它。
	RocketNearCompletion bool
	// HasActiveRedPacket/ActiveRedPacketExpiresAtMS 是 wallet 红包事实经 MQ 派生的列表投影。
	// 到期时间让 Redis/MySQL 卡片无需同步回查 wallet，也能在过期边界立即停止展示。
	HasActiveRedPacket         bool
	ActiveRedPacketExpiresAtMS int64
	// ActiveRedPacketProjectionKnown 区分 wallet 事件投影与普通 Room Cell 快照，避免后者覆盖 Redis 外部事实。
	ActiveRedPacketProjectionKnown bool `json:"-"`
	// Title 是卡片标题；空值由客户端或后续资料聚合兜底。
	Title string
	// CoverURL 是卡片封面；空值表示没有单独封面。
	CoverURL string
	// Mode 是房间模式，来自 RoomState/RoomMeta。
	Mode string
	// Status 是房间生命周期状态，列表默认只查 active。
	Status string
	// Locked 表示进入房间是否需要密码；列表和搜索只暴露布尔标识，不暴露密码哈希。
	Locked bool
	// Heat 是房间热度，当前主要由送礼累计。
	Heat int64
	// OnlineCount 是 room-service 业务 presence 数量。
	OnlineCount int32
	// SeatCount 是麦位总数。
	SeatCount int32
	// OccupiedSeatCount 是当前被占用麦位数。
	OccupiedSeatCount int32
	// SortScore 是热门列表排序分，算法不进入 RoomState。
	SortScore int64
	// RoomBorderJSON/RoomNameStyleJSON 是低频装扮快照；读取时必须再次按 expires_at_ms 过滤。
	RoomBorderJSON    string
	RoomNameStyleJSON string
	// CreatedAtMS 是创建房间时的毫秒时间，new tab 使用它排序。
	CreatedAtMS int64
	// UpdatedAtMS 是投影最后更新时间。
	UpdatedAtMS int64
	// FeedSubjectUserID 是关系房间流命中的好友/关注用户；只用于生成稳定 cursor，不出现在 HTTP 卡片里。
	FeedSubjectUserID int64
	// IsPinned 表达该卡片在当前公共列表查询中是否命中有效置顶。
	IsPinned bool
	// PinListRank 是公共发现列表排序桶；hot 会把有人房和空房再拆层，new 仍只区分本区和外区。
	PinListRank int64
	// PinWeight 是命中置顶的排序权重；只用于公共房间列表排序和 cursor。
	PinWeight int64
	// PinnedUntilMS 是命中置顶的过期时间；只用于公共房间列表排序和 cursor。
	PinnedUntilMS int64
}

type OwnerCountryCodeRenameResult struct {
	// RoomListEntries 是 room_list_entries 中从旧国家码切到新国家码的当前列表投影行数。
	RoomListEntries int64
	// RoomSnapshots 是 room_snapshots 当前 protobuf 快照中 owner_country_code 被改写的房间数。
	RoomSnapshots int64
}

// RoomListPinEntry 是公共发现页置顶读模型的最小事实。
type RoomListPinEntry struct {
	// ID 是 room_region_pins 的自增主键，缓存增量更新和删除都用它定位旧 zset member。
	ID int64
	// AppCode 限定置顶所属 App，避免多 App 共用 Redis key 时互相污染。
	AppCode string
	// VisibleRegionID 是置顶作用域；global 统一写 0，region/country 使用房间区域。
	VisibleRegionID int64
	// PinType 区分 global、region、country 三种公共列表置顶语义。
	PinType string
	// RoomID 是置顶命中的房间卡片。
	RoomID string
	// Weight 是置顶同桶排序的第一关键字，值越大越靠前。
	Weight int64
	// Status 保留 owner 表状态；非 active 必须从缓存索引移除。
	Status string
	// PinnedAtMS 是置顶生效开始时间，未来置顶可以先写缓存但读时不能提前展示。
	PinnedAtMS int64
	// ExpiresAtMS 是置顶失效时间，读缓存时必须按当前 UTC 毫秒过滤。
	ExpiresAtMS int64
}

type AdminRoomListEntry struct {
	RoomID            string
	RoomShortID       string
	VisibleRegionID   int64
	OwnerUserID       int64
	Title             string
	CoverURL          string
	Mode              string
	Status            string
	Locked            bool
	CloseReason       string
	ClosedByAdminID   uint64
	ClosedByAdminName string
	ClosedAtMS        int64
	Heat              int64
	OnlineCount       int32
	SeatCount         int32
	OccupiedSeatCount int32
	SortScore         int64
	CreatedAtMS       int64
	UpdatedAtMS       int64
}

type AdminRoomListQuery struct {
	AppCode     string
	Page        int
	PageSize    int
	Keyword     string
	Status      string
	RegionID    int64
	OwnerUserID int64
	// OwnerUserIDs 是服务端认证后的团队 owner 集合；repository 必须在 count 和数据页上使用同一条件。
	OwnerUserIDs  []int64
	SortBy        string
	SortDirection string
	// NowMS 用于把后台房间贡献收敛到当前 UTC 周；调用方传服务时钟，测试可以固定边界。
	NowMS int64
}

type AdminRoomPinEntry struct {
	AppCode             string
	ID                  int64
	VisibleRegionID     int64
	PinType             string
	RoomID              string
	Weight              int64
	Status              string
	PinnedAtMS          int64
	ExpiresAtMS         int64
	CancelledAtMS       int64
	CreatedByAdminID    uint64
	CancelledByAdminID  uint64
	CreatedAtMS         int64
	UpdatedAtMS         int64
	RoomShortID         string
	RoomVisibleRegionID int64
	Title               string
	CoverURL            string
	OwnerUserID         int64
	RoomStatus          string
}

type AdminRoomPinQuery struct {
	AppCode  string
	Page     int
	PageSize int
	Keyword  string
	Status   string
	RegionID int64
	PinType  string
	NowMS    int64
}

type AdminCreateRoomPinInput struct {
	RoomID       string
	PinType      string
	Weight       int64
	DurationDays int64
	PinnedAtMS   int64
	ExpiresAtMS  int64
	AdminID      uint64
	NowMS        int64
}

type HumanRoomRobotCountryPool struct {
	CountryCode  string
	RobotUserIDs []int64
}

type HumanRoomRobotCountryRule struct {
	CountryCode     string
	MaxRoomCount    int32
	AllowedOwnerIDs []string
}

type HumanRoomRobotConfig struct {
	AppCode                 string
	Enabled                 bool
	CandidateRoomMaxOnline  int32
	RoomFullStopOnline      int32
	RoomTargetMinOnline     int32
	RoomTargetMaxOnline     int32
	RobotStayMinMS          int64
	RobotStayMaxMS          int64
	RobotReplaceMinMS       int64
	RobotReplaceMaxMS       int64
	NormalGiftIntervalMS    int64
	NormalGiftIntervalMinMS int64
	NormalGiftIntervalMaxMS int64
	GiftIDs                 []string
	LuckyGiftIDs            []string
	SuperLuckyGiftIDs       []string
	LuckyComboMin           int64
	LuckyComboMax           int64
	LuckyPauseMinMS         int64
	LuckyPauseMaxMS         int64
	MaxGiftSenders          int64
	CountryPools            []HumanRoomRobotCountryPool
	AllowedOwnerIDs         []string
	CountryLimitEnabled     bool
	CountryRules            []HumanRoomRobotCountryRule
	UpdatedByAdminID        uint64
	CreatedAtMS             int64
	UpdatedAtMS             int64
}

// RoomSeatConfig 是后台房间配置里控制创建/修改房间座位数的低频配置。
type RoomSeatConfig struct {
	// AppCode 是配置所属 App。
	AppCode string
	// AllowedSeatCounts 是当前 App 允许客户端使用的麦位总数。
	AllowedSeatCounts []int32
	// DefaultSeatCount 是 CreateRoom 未传 seat_count 时使用的默认麦位总数。
	DefaultSeatCount int32
	// UpdatedAtMS 是配置最后更新时间，主要用于后台展示和排障。
	UpdatedAtMS int64
}

// RoomRocketConfig 是后台语音房火箭配置的运行时快照。
type RoomRocketConfig struct {
	AppCode           string
	Enabled           bool
	ConfigVersion     int64
	FuelSource        string
	LaunchDelayMS     int64
	BroadcastEnabled  bool
	BroadcastScope    string
	BroadcastDelayMS  int64
	RewardStackPolicy string
	Levels            []RoomRocketLevelConfig
	GiftFuelRules     []GiftFuelRuleConfig
	UpdatedByAdminID  int64
	CreatedAtMS       int64
	UpdatedAtMS       int64
}

// RoomRocketLevelConfig 描述单个等级的物料、阈值和三类奖励池。
type RoomRocketLevelConfig struct {
	Level              int32                  `json:"level"`
	FuelThreshold      int64                  `json:"fuelThreshold"`
	CoverURL           string                 `json:"coverUrl"`
	AnimationURL       string                 `json:"animationUrl"`
	LaunchAnimationURL string                 `json:"launchAnimationUrl"`
	LaunchedImageURL   string                 `json:"launchedImageUrl"`
	InRoomRewards      []RoomRocketRewardItem `json:"inRoomRewards"`
	Top1Rewards        []RoomRocketRewardItem `json:"top1Rewards"`
	IgniterRewards     []RoomRocketRewardItem `json:"igniterRewards"`
}

// RoomRocketRewardItem 是后台配置中的奖励候选项。
type RoomRocketRewardItem struct {
	RewardItemID    string `json:"rewardItemId"`
	ResourceGroupID int64  `json:"resourceGroupId"`
	Weight          int64  `json:"weight"`
	Quantity        int64  `json:"quantity"`
	DisplayName     string `json:"displayName"`
	IconURL         string `json:"iconUrl"`
}

// GiftFuelRuleConfig 控制特定礼物或礼物类型如何覆盖默认燃料计算。
type GiftFuelRuleConfig struct {
	RuleID        string `json:"ruleId"`
	GiftID        string `json:"giftId"`
	GiftTypeCode  string `json:"giftTypeCode"`
	MultiplierPPM int64  `json:"multiplierPpm"`
	FixedFuel     int64  `json:"fixedFuel"`
	Excluded      bool   `json:"excluded"`
}

// RobotRoomGiftRule 是后台配置的机器人房间送礼节奏；它只驱动机器人展示链路，不进入真实活动奖池。
type RobotRoomGiftRule struct {
	GiftIDs              []string
	LuckyGiftIDs         []string
	NormalGiftIntervalMS int64
	LuckyComboMin        int64
	LuckyComboMax        int64
	LuckyPauseMinMS      int64
	LuckyPauseMaxMS      int64
	RobotStayMinMS       int64
	RobotStayMaxMS       int64
	RobotReplaceMinMS    int64
	RobotReplaceMaxMS    int64
	MaxGiftSenders       int64
}

// RobotRoomConfig 是 room-service 持久化的机器人房间配置和账号占用事实。
type RobotRoomConfig struct {
	AppCode          string
	RoomID           string
	RoomShortID      string
	Title            string
	CoverURL         string
	VisibleRegionID  int64
	OwnerCountryCode string
	Status           string
	OwnerRobotUserID int64
	RobotUserIDs     []int64
	ActiveRobotCount int32
	SeatCount        int32
	GiftRule         RobotRoomGiftRule
	CreatedByAdminID uint64
	CreatedAtMS      int64
	UpdatedAtMS      int64
}

// RobotRoomListQuery 是后台机器人房间列表查询条件。
type RobotRoomListQuery struct {
	AppCode  string
	Status   string
	Page     int
	PageSize int
}

// CreateRobotRoomConfigInput 在同一事务中锁定机器人账号并写入房间配置。
type CreateRobotRoomConfigInput struct {
	Config RobotRoomConfig
	NowMS  int64
}

// RoomPresence 是当前用户可恢复房间的轻量读模型。
// 它从 RoomSnapshot 投影而来，只用于 App 启动/回前台探测，不替代 Room Cell 权威状态。
type RoomPresence struct {
	// AppCode 是 presence 所属 App，和 user_id 一起限定用户当前房间。
	AppCode string
	// UserID 是业务 presence 所属用户。
	UserID int64
	// RoomID 是用户当前仍在的房间；status=left 时只保留排障参考。
	RoomID string
	// Role 是房间内轻量角色，来自 RoomUser.role。
	Role string
	// PublishState 是用户当前麦位发流状态，非上麦用户为空。
	PublishState string
	// MicSessionID 是当前麦位发流会话，非上麦用户为空。
	MicSessionID string
	// RoomVersion 是投影来自的房间版本。
	RoomVersion int64
	// Status 表示该用户 presence 是否仍 active。
	Status string
	// JoinedAtMS 是用户进入当前房间的时间。
	JoinedAtMS int64
	// LastSeenAtMS 是用户在 Room Cell 内的最后业务心跳时间。
	LastSeenAtMS int64
	// UpdatedAtMS 是投影写入时间。
	UpdatedAtMS int64
	// GiftValue 是该用户在当前房间生命周期内作为送礼方累计送出的礼物价值。
	GiftValue int64
}

// RoomOnlineUserQuery 是房间在线用户分页读模型查询条件。
type RoomOnlineUserQuery struct {
	AppCode  string
	RoomID   string
	Page     int
	PageSize int
	Sort     string
}

// RoomOnlineUserPage 是 room_user_presence 中某房间 active 用户的一页结果。
type RoomOnlineUserPage struct {
	Users    []RoomPresence
	Total    int64
	Page     int
	PageSize int
}

// RoomContributionRankStore 管理房间内用户周期贡献榜读模型。
type RoomContributionRankStore interface {
	// ListRoomContributionRank 查询房间内某周期 Top 用户贡献，不扫描命令日志或 outbox。
	ListRoomContributionRank(ctx context.Context, query RoomContributionRankQuery) (RoomContributionRankPage, error)
}

// RoomListQuery 是 room-service 房间列表读模型查询条件。
type RoomListQuery struct {
	// AppCode 是列表查询所属 App，通常来自 gateway RequestMeta。
	AppCode string
	// VisibleRegionID 是服务端解析出的用户区域，用于未显式筛选时的默认范围。
	VisibleRegionID int64
	// FilterRegionID 是 gateway 校验过的客户端区域筛选；非零时覆盖默认 VisibleRegionID。
	FilterRegionID int64
	// AllVisibleRegions 由 gateway 根据 App 策略或白名单注入；为 true 时列表读模型不再按 VisibleRegionID 收敛。
	AllVisibleRegions bool
	// Tab 只允许 hot/new，决定公共发现列表排序字段。
	Tab string
	// OwnerUserID 保留给内部定向读模型；公开 Mine 顶部卡片走 GetMyRoom。
	OwnerUserID int64
	// OwnerUserIDs 是业务方预先解析出的房主集合，用于 Agency 等组织页批量取房间卡片。
	OwnerUserIDs []int64
	// Query 非空时按 room_id 或 title 做包含匹配。
	Query string
	// CountryCode 非空时只返回房主国家命中的房间；gateway 必须先校验它属于 active 区域。
	CountryCode string
	// ViewerCountryCode 是当前用户国家；公共列表不按它过滤，只用它在 All/区域列表中把本国家房间排到同在线状态的其他房间前。
	ViewerCountryCode string
	// Limit 是实际查询数量，service 层会限制最大值。
	Limit int
	// CursorSortScore 是 hot tab 的游标 score。
	CursorSortScore int64
	// CursorCreatedAtMS 是 new tab 的游标创建时间。
	CursorCreatedAtMS int64
	// CursorUpdatedAtMS 是用户关系 feed tab 的游标更新时间。
	CursorUpdatedAtMS int64
	// CursorRoomID 是同分或同时间下的稳定翻页边界。
	CursorRoomID string
	// CursorPinListRank 是上一页最后一条所在排序桶，公共列表用它跨全区、区域和普通房分区翻页。
	CursorPinListRank int64
	// CursorPinned 是旧游标兼容字段；新排序以 CursorPinListRank 为准。
	CursorPinned bool
	// CursorPinWeight 是上一页最后一条置顶权重。
	CursorPinWeight int64
	// CursorPinnedUntilMS 是上一页最后一条置顶过期时间。
	CursorPinnedUntilMS int64
	// NowMS 是有效置顶判断时间，service 层使用 UTC 业务时钟注入。
	NowMS int64
}

// RoomListCacheEntryScanQuery 是缓存全量刷新时按主键批量扫描 MySQL 列表投影的游标。
type RoomListCacheEntryScanQuery struct {
	// AfterAppCode 和 AfterRoomID 组成严格递增游标，避免 OFFSET 在大表上反复扫描。
	AfterAppCode string
	AfterRoomID  string
	// Limit 是单批扫描数量，由 worker 固定小批量推进。
	Limit int
}

// RoomListPinScanQuery 是缓存全量刷新时扫描有效置顶事实的游标。
type RoomListPinScanQuery struct {
	// AfterID 是 room_region_pins 自增主键游标。
	AfterID int64
	// Limit 是单批扫描数量。
	Limit int
	// NowMS 用于排除已过期置顶；未来置顶保留，读缓存时按 pinned_at_ms 再判断。
	NowMS int64
}

// RoomListEntriesByIDQuery 是榜单等定向补全房间卡片时的批量读取条件。
type RoomListEntriesByIDQuery struct {
	// AppCode 限定 App 边界，避免一个榜单读到其他 App 的同名 room_id。
	AppCode string
	// RoomIDs 是榜单已经命中的房间集合；repository 必须按这些 key 精确读取。
	RoomIDs []string
}

// RoomUserFeedEntry 是“访问过/好友/关注”房间流的读模型索引。
// 它只保存用户到房间的列表关系，不复制房间卡片字段，卡片内容仍来自 room_list_entries。
type RoomUserFeedEntry struct {
	// AppCode 是 feed 所属 App，防止跨 App 看到关系流。
	AppCode string
	// UserID 是拥有该 feed 的用户。
	UserID int64
	// FeedType 是 visited/friend/following 之一；关系事实 owner 后续可通过事件填充 friend/following。
	FeedType string
	// RoomID 是 feed 指向的房间。
	RoomID string
	// CreatedAtMS 是首次建立该 feed 关系的 UTC 毫秒。
	CreatedAtMS int64
	// UpdatedAtMS 是 feed 排序时间，visited 会在 JoinRoom 成功后刷新。
	UpdatedAtMS int64
}

// RoomUserFeedQuery 是用户房间关系流的分页查询条件。
type RoomUserFeedQuery struct {
	// AppCode 是 feed 查询所属 App。
	AppCode string
	// UserID 是当前登录用户。
	UserID int64
	// FeedType 是 visited/friend/following 之一。
	FeedType string
	// VisibleRegionID 沿用房间发现页区域桶，避免关系流跨区域露出。
	VisibleRegionID int64
	// Query 非空时按 room_id、room_short_id 或 title 做包含匹配。
	Query string
	// Limit 是实际查询数量，service 层会限制最大值。
	Limit int
	// CursorUpdatedAtMS 是上一页最后一条 feed 更新时间。
	CursorUpdatedAtMS int64
	// CursorRoomID 是同时间下的稳定翻页边界。
	CursorRoomID string
}

// RoomFollowRecord 是用户关注房间的低频关系事实。
// 它不进入 Room Cell 快照，避免把用户个性化关系扩散到房间核心状态。
type RoomFollowRecord struct {
	// AppCode 是关系所属 App，防止多 App 之间互相看到关注关系。
	AppCode string
	// UserID 是发起关注的登录用户。
	UserID int64
	// RoomID 是被关注房间。
	RoomID string
	// Status 表达当前关系是否仍 active；取消关注只改状态，保留审计时间。
	Status string
	// FollowedAtMS 是最近一次从非 active 转为 active 的 UTC 毫秒。
	FollowedAtMS int64
	// UpdatedAtMS 是关系最后一次写入时间。
	UpdatedAtMS int64
}

// RoomFollowQuery 是 Mine 页“关注的房间”分页查询条件。
type RoomFollowQuery struct {
	// AppCode 是查询所属 App。
	AppCode string
	// UserID 是当前登录用户。
	UserID int64
	// VisibleRegionID 沿用 Mine 房间流区域桶，避免跨区域露出房间卡片。
	VisibleRegionID int64
	// Query 非空时按 room_id、room_short_id 或 title 做包含匹配。
	Query string
	// Limit 是实际查询数量，service 层会限制最大值。
	Limit int
	// CursorFollowedAtMS 是上一页最后一条关注关系排序时间。
	CursorFollowedAtMS int64
	// CursorRoomID 是同时间下的稳定翻页边界。
	CursorRoomID string
}

// RoomFeedRelatedUser 是 gateway 从 user-service 读取到的关系事实快照。
// room-service 只使用它做 active 房间卡片查询和排序，不保存好友/关注关系。
type RoomFeedRelatedUser struct {
	// UserID 是好友或关注对象的用户 ID。
	UserID int64
	// RelationUpdatedAtMS 是该关系的排序时间，followed_at_ms/friended_at_ms 都映射到这里。
	RelationUpdatedAtMS int64
}

// RoomRelatedFeedQuery 根据一批关系用户查询他们当前 active 房间卡片。
type RoomRelatedFeedQuery struct {
	// AppCode 是 feed 查询所属 App。
	AppCode string
	// VisibleRegionID 沿用房间发现页区域桶，避免关系流跨区域露出。
	VisibleRegionID int64
	// RelatedUsers 是 user-service 已授权返回的好友/关注对象。
	RelatedUsers []RoomFeedRelatedUser
	// Query 非空时按 room_id、room_short_id 或 title 做包含匹配。
	Query string
	// Limit 是实际查询数量，service 层会限制最大值。
	Limit int
	// CursorUpdatedAtMS 是上一页最后一条关系排序时间。
	CursorUpdatedAtMS int64
	// CursorSubjectUserID 是关系排序时间相同时的好友/关注用户边界。
	CursorSubjectUserID int64
	// CursorRoomID 是同时间下的稳定翻页边界。
	CursorRoomID string
}

// Repository 聚合 room-service 当前运行需要的全部持久化能力。
//
// 总接口继续保留给 Service 装配使用，子接口按变化原因拆开，后续单测可以只依赖
// CommandStore、RoomListStore 或 OutboxStore 这类窄接口，避免为一个用例 mock 整个仓储面。
type Repository interface {
	RoomMetaStore
	CommandStore
	SnapshotStore
	RoomOwnerProjectionStore
	RoomConfigStore
	OutboxStore
	RoomListStore
	PresenceStore
	RoomContributionRankStore
	AdminRoomStore
	RobotRoomStore
	GiftOperationStore
}

// GiftOperationStore 管理送礼 saga。它独立于 command log：command log 只代表房间提交完成，operation 还覆盖扣款后的恢复窗口。
type GiftOperationStore interface {
	EnsureGiftOperation(ctx context.Context, operation GiftOperation) (GiftOperation, bool, error)
	// ReactivateCompensatedGiftOperation is a CAS executed only after the service has compared the
	// stored and current idempotency payloads. EnsureGiftOperation itself must never make an unverified
	// historical request claimable by the recovery worker.
	ReactivateCompensatedGiftOperation(ctx context.Context, roomID string, commandID string, updatedAtMS int64) (GiftOperation, bool, error)
	GetGiftOperation(ctx context.Context, roomID string, commandID string) (GiftOperation, bool, error)
	SaveGiftOperationProgress(ctx context.Context, progress GiftOperationProgress) error
	ClaimRecoverableGiftOperations(ctx context.Context, workerID string, limit int, nowMS int64, lockUntilMS int64) ([]GiftOperation, error)
	// preserveLease is true only when a recovery attempt's total deadline fired after its Cell task was
	// submitted. In that case Room Cell may still be using the detached post-debit commit budget, so the
	// claim must remain until lock_until_ms instead of being exposed to a second worker immediately.
	MarkGiftOperationRetry(ctx context.Context, roomID string, commandID string, lastError string, nextRetryAtMS int64, updatedAtMS int64, preserveLease bool) error
	MarkGiftOperationCompensated(ctx context.Context, roomID string, commandID string, lastError string, updatedAtMS int64) error
	HasRecoverableGiftOperations(ctx context.Context, roomID string) (bool, error)
}

// RoomOwnerProjectionStore 管理房主低频资料投影；只改当前读模型，不写 command log。
type RoomOwnerProjectionStore interface {
	RenameOwnerCountryCode(ctx context.Context, oldCountryCode string, newCountryCode string, nowMS int64) (OwnerCountryCodeRenameResult, error)
}

// RoomOwnerVIPProjectionStore 把 wallet 的低频 VIP 事实写入列表读模型。
// 该能力保持为可选窄接口，避免列表查询为了展示字段同步调用 wallet 或扩散到纯内存测试仓储。
type RoomOwnerVIPProjectionStore interface {
	UpdateRoomOwnerVIP(ctx context.Context, ownerUserID int64, level int32, expiresAtMS int64) (RoomListEntry, bool, error)
}

// RoomOwnerVIPScheduledProjectionStore 在主 effective VIP 是体验卡时额外保存 paid 回落计划。
// 旧测试仓储仍可只实现 RoomOwnerVIPProjectionStore，生产 MySQL 使用该扩展能力。
type RoomOwnerVIPScheduledProjectionStore interface {
	UpdateRoomOwnerVIPSchedule(ctx context.Context, ownerUserID int64, level int32, expiresAtMS int64, fallbackLevel int32, fallbackExpiresAtMS int64) (RoomListEntry, bool, error)
}

// RoomRedPacketProjectionEvent 是 wallet owner 红包事实进入房间列表读模型的最小契约。
// 它不进入 Room Cell 核心状态；packet 级投影只用于正确聚合同房间多个并存红包。
type RoomRedPacketProjectionEvent struct {
	EventID         string
	EventType       string
	PacketID        string
	RoomID          string
	Status          string
	RemainingAmount int64
	RemainingCount  int32
	ExpiresAtMS     int64
	OccurredAtMS    int64
	ProjectedAtMS   int64
	EventRank       int32
}

// RoomRedPacketProjectionStore 以独立 wallet consumer 位点更新红包卡片投影。
// 生产 MySQL 实现必须在同一事务内完成事件幂等、packet 投影和 room_list_entries 聚合更新。
type RoomRedPacketProjectionStore interface {
	ApplyRoomRedPacketProjectionEvent(ctx context.Context, event RoomRedPacketProjectionEvent) (RoomListEntry, bool, error)
}

// RoomListCacheStore 是公共发现页 Redis 展示读模型边界。
//
// MySQL 仍然是房间列表和置顶事实 owner；缓存只服务高频 hot/new 发现页，
// 因此所有写入都必须 best-effort，任何错误都回落到 MySQL 查询。
type RoomListCacheStore interface {
	// ListRoomListEntries 尝试从缓存返回一页公共列表；ok=false 表示缓存未就绪或扫描保护触发，调用方应走 MySQL。
	ListRoomListEntries(ctx context.Context, query RoomListQuery) ([]RoomListEntry, bool, error)
	// UpsertRoomListEntry 根据最新列表投影刷新缓存；非 active 房间必须从所有展示 zset 移除。
	UpsertRoomListEntry(ctx context.Context, entry RoomListEntry) error
	// DeleteRoomListEntry 从缓存删除指定房间的卡片和所有展示索引。
	DeleteRoomListEntry(ctx context.Context, appCode string, roomID string) error
	// UpsertRoomPin 根据后台置顶事实刷新 pin 索引；非 active 或已过期置顶由实现移除。
	UpsertRoomPin(ctx context.Context, pin RoomListPinEntry) error
	// RebuildRoomList 用 MySQL 扫描结果重建某个 App 的缓存，并在成功后标记 ready。
	RebuildRoomList(ctx context.Context, appCode string, entries []RoomListEntry, pins []RoomListPinEntry, nowMS int64) error
}

// RoomMetaStore 保存房间基础元数据和房主背景素材；它是 Room Cell 恢复前的轻量入口。
type RoomMetaStore interface {
	// SaveRoomMeta 保存房间基础元数据，创建房间时必须先建立它。
	SaveRoomMeta(ctx context.Context, meta RoomMeta) error
	// GetRoomMeta 读取房间基础元数据，恢复无内存 Cell 的房间依赖它。
	GetRoomMeta(ctx context.Context, roomID string) (RoomMeta, bool, error)
	// GetRoomMetaByGlobalID 只供已验签的 RTC 等外部回调解析房间租户。
	// room_id 由全局 ID 生成器保证跨 App 唯一；普通业务查询仍必须使用 app_code + room_id。
	GetRoomMetaByGlobalID(ctx context.Context, roomID string) (RoomMeta, bool, error)
	// GetRoomMetaByShortID 用客户端展示短房号解析内部 room_id；命令链路必须先 canonicalize 后再取 lease 和写 command log。
	GetRoomMetaByShortID(ctx context.Context, roomShortID string) (RoomMeta, bool, error)
	// GetRoomMetaByOwner 查询 owner 已创建的房间；产品规则要求一个用户只能创建一个房间。
	GetRoomMetaByOwner(ctx context.Context, ownerUserID int64) (RoomMeta, bool, error)
	// SaveRoomBackground 保存房间 owner 上传过的背景图 URL；相同房间相同 URL 幂等返回同一条素材。
	SaveRoomBackground(ctx context.Context, background RoomBackgroundImage) (RoomBackgroundImage, error)
	// GetRoomBackground 精确读取一个背景图素材，用于 SetRoomBackground 校验归属。
	GetRoomBackground(ctx context.Context, roomID string, backgroundID int64) (RoomBackgroundImage, bool, error)
	// ListRoomBackgrounds 按房间读取背景图素材列表，不读取 Room Cell 状态。
	ListRoomBackgrounds(ctx context.Context, roomID string) ([]RoomBackgroundImage, error)
}

// RoomBackgroundCommandReader 是新版背景保存的可选强幂等读能力。
// 它不并入 RoomMetaStore，避免旧测试/内存仓储为纯优化路径扩散接口；生产 MySQL 实现该接口后，
// 已成功 command_id 的重试可以在房间关闭、owner 元数据变化或 VIP 到期后仍返回第一次保存结果。
type RoomBackgroundCommandReader interface {
	GetRoomBackgroundByCommand(ctx context.Context, roomID string, commandID string) (RoomBackgroundImage, bool, error)
}

// RoomMediaUploadGuardStore 是专用上传入口的强幂等持久边界。
// Get 必须先于可变房态/VIP 校验，Register 则只在首次授权通过后原子占用 command_id。
type RoomMediaUploadGuardStore interface {
	GetRoomMediaUploadRegistration(ctx context.Context, roomID string, commandID string) (RoomMediaUploadRegistration, bool, error)
	GetRoomMediaUploadByObjectKey(ctx context.Context, objectKey string) (RoomMediaUploadRegistration, bool, error)
	GetActiveRoomMediaUploadByObjectKey(ctx context.Context, objectKey string) (RoomMediaUploadRegistration, bool, error)
	RegisterRoomMediaUpload(ctx context.Context, registration RoomMediaUploadRegistration) (RoomMediaUploadRegistration, error)
	CompleteRoomMediaUpload(ctx context.Context, registration RoomMediaUploadRegistration) (RoomMediaUploadRegistration, error)
}

// RoomDecorationReconciliationStore 保存装扮反向索引和 wallet consumer 本地幂等事实。
type RoomDecorationReconciliationStore interface {
	ListRoomDecorationAssignments(ctx context.Context, query RoomDecorationAssignmentQuery) ([]RoomDecorationAssignment, error)
	HasConsumedWalletDecorationEvent(ctx context.Context, eventID string) (bool, error)
	MarkWalletDecorationEventConsumed(ctx context.Context, eventID string, eventType string, consumedAtMS int64) error
}

// CommandStore 保存命令幂等记录和一次 Room Cell mutation 的事务提交边界。
type CommandStore interface {
	// GetCommand 读取已提交命令，用于幂等重试和 command_id 冲突判定。
	GetCommand(ctx context.Context, roomID string, commandID string) (CommandRecord, bool, error)
	// SaveCommand 追加成功命令日志，关键命令恢复必须依赖它。
	SaveCommand(ctx context.Context, record CommandRecord) error
	// SaveMutation 在同一事务中提交命令日志、outbox 事件和可选房间生命周期状态。
	SaveMutation(ctx context.Context, commit MutationCommit) error
	// ListCommandsAfter 读取快照版本之后的命令，恢复时按版本顺序回放。
	ListCommandsAfter(ctx context.Context, roomID string, roomVersion int64) ([]CommandRecord, error)
}

// SnapshotStore 保存和读取最新快照；它只负责降低恢复成本，不替代 command log。
type SnapshotStore interface {
	// SaveSnapshot 保存最新房间快照，允许覆盖较低版本号快照但不能倒退。
	SaveSnapshot(ctx context.Context, snapshot SnapshotRecord) error
	// GetLatestSnapshot 读取当前最新快照，恢复时优先使用它减少回放成本。
	GetLatestSnapshot(ctx context.Context, roomID string) (SnapshotRecord, bool, error)
}

// RoomConfigStore 管理后台低频配置；读取失败由调用方决定是否使用内置默认值。
type RoomConfigStore interface {
	// GetRoomSeatConfig 读取后台配置的房间座位数规则；不存在时 service 层使用内置默认值。
	GetRoomSeatConfig(ctx context.Context) (RoomSeatConfig, bool, error)
	// UpsertRoomSeatConfig 写入房间座位数配置，供本地验证和后台配置共享同一张表。
	UpsertRoomSeatConfig(ctx context.Context, config RoomSeatConfig) error
	// GetRoomRocketConfig 读取语音房火箭低频配置；不存在时 service 层使用关闭态默认配置。
	GetRoomRocketConfig(ctx context.Context) (RoomRocketConfig, bool, error)
	// UpsertRoomRocketConfig 写入语音房火箭配置，主要供集成测试和本地验证复用生产表结构。
	UpsertRoomRocketConfig(ctx context.Context, config RoomRocketConfig) error
}

// OutboxStore 管理 room_outbox 的补偿投递生命周期。
type OutboxStore interface {
	// SaveOutbox 写入房间外事件。Room Cell 事件必须和成功命令同事务提交；外部 owner 派生展示则以 source event_id 幂等单独提交。
	SaveOutbox(ctx context.Context, records []outbox.Record) error
	// ListPendingOutbox 扫描待补偿投递事件。
	ListPendingOutbox(ctx context.Context, limit int) ([]outbox.Record, error)
	// ClaimPendingOutbox 抢占一批待投递事件，避免多 worker 同时投递同一批。
	ClaimPendingOutbox(ctx context.Context, workerID string, limit int, lockUntilMS int64) ([]outbox.Record, error)
	// MarkOutboxDelivered 标记事件已经投递成功。
	MarkOutboxDelivered(ctx context.Context, eventID string) error
	// MarkOutboxRetryable 记录一次投递失败，并写入下一次允许抢占的退避时间。
	MarkOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64) error
	// MarkOutboxDead 把超过最大重试次数的事件转入 failed，避免固定污染日志和外部依赖。
	MarkOutboxDead(ctx context.Context, eventID string, lastErr string) error
	// DeleteDeliveredOutboxBefore 删除一小批 delivered 且 updated_at_ms 早于 cutoffMS 的历史事件，返回本批删除行数。
	DeleteDeliveredOutboxBefore(ctx context.Context, cutoffMS int64, limit int) (int64, error)
}

// RoomListStore 管理发现页、Mine 页和关系房间流读模型；它不读取 Room Cell 内存。
type RoomListStore interface {
	// UpsertRoomListEntry 写入或更新房间列表读模型；失败不应破坏 Room Cell 已提交状态。
	UpsertRoomListEntry(ctx context.Context, entry RoomListEntry) error
	// ListRoomListEntries 按区域和 tab 查询房间列表，不访问 Room Cell 内存。
	ListRoomListEntries(ctx context.Context, query RoomListQuery) ([]RoomListEntry, error)
	// ListRoomListEntriesByIDs 按房间 ID 精确读取列表卡片，用于榜单展示补全。
	ListRoomListEntriesByIDs(ctx context.Context, query RoomListEntriesByIDQuery) (map[string]RoomListEntry, error)
	// UpsertRoomUserFeedEntry 写入用户房间关系流读模型；失败不应影响房间主链路。
	UpsertRoomUserFeedEntry(ctx context.Context, entry RoomUserFeedEntry) error
	// ListRoomUserFeedEntries 按用户关系流读取房间卡片，不访问 Room Cell 内存集合。
	ListRoomUserFeedEntries(ctx context.Context, query RoomUserFeedQuery) ([]RoomListEntry, error)
	// FollowRoom 建立或恢复用户对房间的关注关系；重复关注必须幂等。
	FollowRoom(ctx context.Context, userID int64, roomID string, nowMS int64) (RoomFollowRecord, error)
	// UnfollowRoom 取消用户对房间的关注关系；未关注时必须按幂等成功处理。
	UnfollowRoom(ctx context.Context, userID int64, roomID string, nowMS int64) error
	// IsRoomFollowed 查询 viewer 是否已经关注房间，用于房间详情个性化字段。
	IsRoomFollowed(ctx context.Context, userID int64, roomID string) (bool, error)
	// ListRoomFollowEntries 按当前用户关注关系读取 active 房间卡片。
	ListRoomFollowEntries(ctx context.Context, query RoomFollowQuery) ([]RoomListEntry, error)
	// ListRoomRelatedFeedEntries 根据 user-service 关系事实查询 active 房间卡片，不持久化关系状态。
	ListRoomRelatedFeedEntries(ctx context.Context, query RoomRelatedFeedQuery) ([]RoomListEntry, error)
	// ListRoomListCacheEntries 按主键小批量扫描 active 列表投影，只用于 Redis 展示读模型重建。
	ListRoomListCacheEntries(ctx context.Context, query RoomListCacheEntryScanQuery) ([]RoomListEntry, error)
	// ListRoomListCachePins 按主键小批量扫描 active 置顶事实，只用于 Redis 展示读模型重建。
	ListRoomListCachePins(ctx context.Context, query RoomListPinScanQuery) ([]RoomListPinEntry, error)
}

// PresenceStore 管理当前房间 presence 投影；权威状态仍然来自 Room Cell 快照。
type PresenceStore interface {
	// ProjectRoomPresence 用最新快照刷新用户当前房间读模型；失败不应破坏 Room Cell 已提交状态。
	ProjectRoomPresence(ctx context.Context, snapshot RoomPresenceSnapshot) error
	// UpsertRoomUserPresence 只刷新单个用户 active presence，服务 JoinRoom/Heartbeat 等单用户热路径。
	UpsertRoomUserPresence(ctx context.Context, presence RoomPresence) error
	// MarkRoomUserPresenceLeft 只把指定用户在指定房间标记为 left，避免 LeaveRoom 重新写整房间在线用户。
	MarkRoomUserPresenceLeft(ctx context.Context, presence RoomPresence) error
	// GetCurrentRoomPresence 查询用户当前 active 房间 presence，不扫描房间列表或快照。
	GetCurrentRoomPresence(ctx context.Context, userID int64) (RoomPresence, bool, error)
	// BatchGetCurrentRoomPresences 用用户主键批量读取候选房间；Room Cell 快照仍是最终上麦事实。
	BatchGetCurrentRoomPresences(ctx context.Context, userIDs []int64) (map[int64]RoomPresence, error)
	// ListRoomOnlineUsers 查询房间 active 在线用户分页，不读取完整 RoomSnapshot。
	ListRoomOnlineUsers(ctx context.Context, query RoomOnlineUserQuery) (RoomOnlineUserPage, error)
}

// AdminRoomStore 服务后台房间列表、详情和置顶管理；后台仍通过 room-service 访问 owner 数据。
type AdminRoomStore interface {
	AdminListRooms(ctx context.Context, query AdminRoomListQuery) ([]AdminRoomListEntry, int64, error)
	AdminGetRoom(ctx context.Context, roomID string, nowMS int64) (AdminRoomListEntry, bool, error)
	AdminListRoomPins(ctx context.Context, query AdminRoomPinQuery) ([]AdminRoomPinEntry, int64, error)
	AdminCreateRoomPin(ctx context.Context, input AdminCreateRoomPinInput) (AdminRoomPinEntry, bool, error)
	AdminCancelRoomPin(ctx context.Context, pinID int64, adminID uint64, nowMS int64) (AdminRoomPinEntry, bool, error)
}

// RobotRoomStore 管理机器人房间配置、账号占用和真人房补机器人候选查询。
type RobotRoomStore interface {
	ListRobotRooms(ctx context.Context, query RobotRoomListQuery) ([]RobotRoomConfig, int64, error)
	GetRobotRoom(ctx context.Context, roomID string) (RobotRoomConfig, bool, error)
	CreateRobotRoomConfig(ctx context.Context, input CreateRobotRoomConfigInput) (RobotRoomConfig, error)
	UpdateRobotRoomStatus(ctx context.Context, roomID string, status string, nowMS int64) (RobotRoomConfig, bool, error)
	OccupiedRobotUserIDs(ctx context.Context, userIDs []int64) (map[int64]bool, error)
	ListActiveRobotRooms(ctx context.Context) ([]RobotRoomConfig, error)
	GetHumanRoomRobotConfig(ctx context.Context) (HumanRoomRobotConfig, bool, error)
	UpsertHumanRoomRobotConfig(ctx context.Context, config HumanRoomRobotConfig) (HumanRoomRobotConfig, error)
	ListHumanRobotCandidateRooms(ctx context.Context, appCode string, countryCode string, maxOnline int32, fullStopOnline int32, limit int, allowedOwnerIDs []string) ([]RoomListEntry, error)
}

// RoomPresenceSnapshot 是 repository 投影接口的稳定入参，避免存储层反向依赖 protobuf。
type RoomPresenceSnapshot struct {
	AppCode     string
	RoomID      string
	RoomVersion int64
	RoomStatus  string
	Users       []RoomPresence
	UpdatedAtMS int64
}
