// Package state 定义 Room Cell 内部持有的房间强关联状态和 protobuf 快照转换。
package state

import (
	"maps"
	"slices"

	roomv1 "hyapp.local/api/proto/room/v1"
)

const (
	// RoomStatusCreating 表示房间正在创建外部依赖，尚未对客户端开放。
	RoomStatusCreating = "creating"
	// RoomStatusActive 表示房间可进入、可管理、可发麦位和礼物命令。
	RoomStatusActive = "active"
	// RoomStatusClosing 表示房间正在收尾，不再接受新业务命令。
	RoomStatusClosing = "closing"
	// RoomStatusClosed 表示房间已经关闭，guard 和进房都必须拒绝。
	RoomStatusClosed = "closed"

	// MicPublishIdle 表示麦位没有正在确认或已确认的 RTC 发流会话。
	MicPublishIdle = "idle"
	// MicPublishPending 表示业务上麦成功，但客户端/RTC 尚未确认音频发布成功。
	MicPublishPending = "pending_publish"
	// MicPublishPublishing 表示当前 mic_session 已确认在 RTC 侧发布音频。
	MicPublishPublishing = "publishing"
)

// MicSeat 表达房间内单个麦位的占用状态。
type MicSeat struct {
	// SeatNo 是房间内稳定麦位编号，不使用切片下标作为对外标识。
	SeatNo int32
	// UserID 为 0 表示麦位空闲，非 0 表示当前占用用户。
	UserID int64
	// Locked 表示麦位被锁定，锁定麦位不能上麦或换入。
	Locked bool
	// PublishState 区分业务占麦和 RTC 发流确认状态，避免把 MicUp 当作已发流。
	PublishState string
	// MicSessionID 标识单次上麦发流会话，确认和超时清理都必须匹配它。
	MicSessionID string
	// PublishDeadlineMS 是 pending_publish 自动释放麦位的截止时间。
	PublishDeadlineMS int64
	// MicSessionRoomVersion 是创建该会话的房间版本，用于丢弃旧版本 RTC 事件。
	MicSessionRoomVersion int64
	// LastPublishEventTimeMS 是已接受的最新 RTC/客户端发布事件时间。
	LastPublishEventTimeMS int64
	// MicMuted 是服务端可见的麦克风静音状态，用于同步其他用户 UI。
	MicMuted bool
}

// RoomUserState 只保存 room-service 真正需要的轻量用户态。
type RoomUserState struct {
	// UserID 是房间 presence 的用户标识。
	UserID int64
	// Role 是房间内轻量角色，不承载完整用户资料。
	Role string
	// JoinedAtMS 是进入房间业务态的时间。
	JoinedAtMS int64
	// LastSeenAtMS 是最后一次房间业务操作时间，不等同于腾讯云 IM 心跳。
	LastSeenAtMS int64
}

// RankItem 表达房间内本地礼物榜的单个用户累计值。
type RankItem struct {
	// UserID 是榜单归属用户。
	UserID int64
	// Score 是展示排序分值，当前版本等于 GiftValue。
	Score int64
	// GiftValue 是房间内礼物价值累计。
	GiftValue int64
	// UpdatedAtMS 是榜单项最后更新时间，分数相同时用于排序。
	UpdatedAtMS int64
}

// RoomState 是 Room Cell 的核心业务状态。
// 这里刻意不放钱包余额、完整用户资料和跨房活动状态。
type RoomState struct {
	// RoomID 是该状态所属房间。
	RoomID string
	// OwnerUserID 是房间所有者，进入 AdminUsers。
	OwnerUserID int64
	// Mode 是房间模式，当前只作为状态字段透出。
	Mode string
	// VisibleRegionID 是房间礼物、列表和区域可见性策略使用的区域桶。
	VisibleRegionID int64
	// Status 是房间生命周期状态。
	Status string
	// ChatEnabled 控制腾讯云 IM 公屏发言守卫。
	ChatEnabled bool
	// MicSeats 是麦位占用状态，所有麦位变更必须在 Room Cell 内完成。
	MicSeats []MicSeat
	// OnlineUsers 是 room-service 业务 presence，不保存真实 socket。
	OnlineUsers map[int64]*RoomUserState
	// AdminUsers 是房间管理权限集合。
	AdminUsers map[int64]bool
	// BanUsers 是被踢或封禁的房间用户集合。
	BanUsers map[int64]bool
	// MuteUsers 是被禁言用户集合。
	MuteUsers map[int64]bool
	// Heat 是房间热度，当前主要由送礼累计。
	Heat int64
	// GiftRank 是房间内本地礼物榜快照结果。
	GiftRank []RankItem
	// RoomExt 预留少量房间扩展字段，避免 v1 频繁改结构。
	RoomExt map[string]string
	// Version 是房间状态版本，成功变更后递增，快照和 command log 都依赖它。
	Version int64
}

// NewRoomState 初始化一个空房间状态。
func NewRoomState(roomID string, ownerUserID int64, seatCount int32, mode string) *RoomState {
	// 麦位编号从 1 开始，避免对外暴露 0 号麦位和切片下标。
	seats := make([]MicSeat, 0, seatCount)

	for seatNo := int32(1); seatNo <= seatCount; seatNo++ {
		seats = append(seats, MicSeat{SeatNo: seatNo})
	}

	return &RoomState{
		RoomID:      roomID,
		OwnerUserID: ownerUserID,
		Mode:        mode,
		Status:      RoomStatusActive,
		ChatEnabled: true,
		MicSeats:    seats,
		OnlineUsers: make(map[int64]*RoomUserState),
		AdminUsers: map[int64]bool{
			// owner 默认具备管理权限，后续可以扩展管理员命令。
			ownerUserID: true,
		},
		BanUsers:  make(map[int64]bool),
		MuteUsers: make(map[int64]bool),
		RoomExt:   make(map[string]string),
	}
}

// Clone 深拷贝当前房间状态，供命令流水线在副本上计算新状态。
func (s *RoomState) Clone() *RoomState {
	if s == nil {
		// nil 输入保持 nil，便于防御性快照调用。
		return nil
	}

	// 所有 map 和 slice 都重新分配，避免 mutate 失败时污染当前 Cell 状态。
	cloned := &RoomState{
		RoomID:          s.RoomID,
		OwnerUserID:     s.OwnerUserID,
		Mode:            s.Mode,
		VisibleRegionID: s.VisibleRegionID,
		Status:          s.Status,
		ChatEnabled:     s.ChatEnabled,
		MicSeats:        append([]MicSeat(nil), s.MicSeats...),
		OnlineUsers:     make(map[int64]*RoomUserState, len(s.OnlineUsers)),
		AdminUsers:      make(map[int64]bool, len(s.AdminUsers)),
		BanUsers:        make(map[int64]bool, len(s.BanUsers)),
		MuteUsers:       make(map[int64]bool, len(s.MuteUsers)),
		Heat:            s.Heat,
		GiftRank:        append([]RankItem(nil), s.GiftRank...),
		RoomExt:         make(map[string]string, len(s.RoomExt)),
		Version:         s.Version,
	}

	for userID, userState := range s.OnlineUsers {
		// 用户态用指针保存，必须逐项复制结构体。
		copied := *userState
		cloned.OnlineUsers[userID] = &copied
	}

	maps.Copy(cloned.AdminUsers, s.AdminUsers)

	maps.Copy(cloned.BanUsers, s.BanUsers)

	maps.Copy(cloned.MuteUsers, s.MuteUsers)

	maps.Copy(cloned.RoomExt, s.RoomExt)

	return cloned
}

// ReplaceWith 用新状态整体替换当前状态。
// 这里用整对象替换，是为了避免部分字段更新时残留旧引用。
func (s *RoomState) ReplaceWith(next *RoomState) {
	// Clone 后再整体赋值，避免调用方把 next 的 map/slice 引用留在当前状态里。
	*s = *next.Clone()
}

// SeatByUser 返回用户当前所在麦位。
func (s *RoomState) SeatByUser(userID int64) (MicSeat, bool) {
	// 麦位数量较小，线性扫描比维护额外索引更简单可靠。
	for _, seat := range s.MicSeats {
		if seat.UserID == userID {
			return seat, true
		}
	}

	return MicSeat{}, false
}

// ClearMicSession 清空麦位占用和 RTC 发流会话字段。
func (s *RoomState) ClearMicSession(index int) {
	if index < 0 || index >= len(s.MicSeats) {
		return
	}

	seatNo := s.MicSeats[index].SeatNo
	locked := s.MicSeats[index].Locked
	s.MicSeats[index] = MicSeat{
		SeatNo: seatNo,
		Locked: locked,
	}
}

// SeatIndex 返回指定麦位在切片中的下标。
func (s *RoomState) SeatIndex(seatNo int32) int {
	// 返回 -1 表示外部传入的麦位编号不存在。
	for index, seat := range s.MicSeats {
		if seat.SeatNo == seatNo {
			return index
		}
	}

	return -1
}

// ToProto 把当前内部房间态投影成对外使用的 protobuf 快照。
func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
	if s == nil {
		// nil 状态没有可投影快照，调用方按房间不存在处理。
		return nil
	}

	// OnlineUsers 是 map，先排序 user_id，保证快照输出稳定，便于测试和排障。
	users := make([]*roomv1.RoomUser, 0, len(s.OnlineUsers))
	userIDs := make([]int64, 0, len(s.OnlineUsers))

	for userID := range s.OnlineUsers {
		userIDs = append(userIDs, userID)
	}

	slices.Sort(userIDs)

	for _, userID := range userIDs {
		// protobuf 快照只暴露轻量 room user，不包含 user-service 主资料。
		userState := s.OnlineUsers[userID]
		users = append(users, &roomv1.RoomUser{
			UserId:       userState.UserID,
			Role:         userState.Role,
			JoinedAtMs:   userState.JoinedAtMS,
			LastSeenAtMs: userState.LastSeenAtMS,
		})
	}

	seats := make([]*roomv1.SeatState, 0, len(s.MicSeats))

	for _, seat := range s.MicSeats {
		// 麦位顺序沿用内部切片顺序，即初始化的 SeatNo 升序和后续原位修改。
		seats = append(seats, &roomv1.SeatState{
			SeatNo:                 seat.SeatNo,
			UserId:                 seat.UserID,
			Locked:                 seat.Locked,
			PublishState:           seat.PublishState,
			MicSessionId:           seat.MicSessionID,
			PublishDeadlineMs:      seat.PublishDeadlineMS,
			MicSessionRoomVersion:  seat.MicSessionRoomVersion,
			LastPublishEventTimeMs: seat.LastPublishEventTimeMS,
			MicMuted:               seat.MicMuted,
		})
	}

	rankItems := make([]*roomv1.RankItem, 0, len(s.GiftRank))

	for _, item := range s.GiftRank {
		// GiftRank 已由 LocalRank 排好序，这里只做结构转换。
		rankItems = append(rankItems, &roomv1.RankItem{
			UserId:      item.UserID,
			Score:       item.Score,
			GiftValue:   item.GiftValue,
			UpdatedAtMs: item.UpdatedAtMS,
		})
	}

	return &roomv1.RoomSnapshot{
		RoomId:          s.RoomID,
		OwnerUserId:     s.OwnerUserID,
		Mode:            s.Mode,
		VisibleRegionId: s.VisibleRegionID,
		Status:          s.Status,
		ChatEnabled:     s.ChatEnabled,
		MicSeats:        seats,
		OnlineUsers:     users,
		AdminUserIds:    sortedSetIDs(s.AdminUsers),
		BanUserIds:      sortedSetIDs(s.BanUsers),
		MuteUserIds:     sortedSetIDs(s.MuteUsers),
		GiftRank:        rankItems,
		RoomExt:         cloneStringMap(s.RoomExt),
		Heat:            s.Heat,
		Version:         s.Version,
		RoomShortId:     s.RoomExt["room_short_id"],
	}
}

// FromProto 把持久化快照恢复回内部房间态。
func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
	if snapshot == nil {
		// 没有快照时由调用方基于 RoomMeta 新建初始状态。
		return nil
	}

	// 所有集合重新分配，恢复后的状态可以直接交给 Room Cell 独占使用。
	restored := &RoomState{
		RoomID:          snapshot.GetRoomId(),
		OwnerUserID:     snapshot.GetOwnerUserId(),
		Mode:            snapshot.GetMode(),
		VisibleRegionID: snapshot.GetVisibleRegionId(),
		Status:          snapshot.GetStatus(),
		ChatEnabled:     snapshot.GetChatEnabled(),
		MicSeats:        make([]MicSeat, 0, len(snapshot.GetMicSeats())),
		OnlineUsers:     make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())),
		AdminUsers:      make(map[int64]bool, len(snapshot.GetAdminUserIds())),
		BanUsers:        make(map[int64]bool, len(snapshot.GetBanUserIds())),
		MuteUsers:       make(map[int64]bool, len(snapshot.GetMuteUserIds())),
		GiftRank:        make([]RankItem, 0, len(snapshot.GetGiftRank())),
		RoomExt:         cloneStringMap(snapshot.GetRoomExt()),
		Heat:            snapshot.GetHeat(),
		Version:         snapshot.GetVersion(),
	}

	for _, seat := range snapshot.GetMicSeats() {
		// protobuf 空字段会映射成零值，UserID=0 仍表示空麦位。
		restored.MicSeats = append(restored.MicSeats, MicSeat{
			SeatNo:                 seat.GetSeatNo(),
			UserID:                 seat.GetUserId(),
			Locked:                 seat.GetLocked(),
			PublishState:           seat.GetPublishState(),
			MicSessionID:           seat.GetMicSessionId(),
			PublishDeadlineMS:      seat.GetPublishDeadlineMs(),
			MicSessionRoomVersion:  seat.GetMicSessionRoomVersion(),
			LastPublishEventTimeMS: seat.GetLastPublishEventTimeMs(),
			MicMuted:               seat.GetMicMuted(),
		})
	}

	for _, user := range snapshot.GetOnlineUsers() {
		// 快照中的 online users 是业务 presence，重启后腾讯云 IM 连接由客户端重新登录进群。
		restored.OnlineUsers[user.GetUserId()] = &RoomUserState{
			UserID:       user.GetUserId(),
			Role:         user.GetRole(),
			JoinedAtMS:   user.GetJoinedAtMs(),
			LastSeenAtMS: user.GetLastSeenAtMs(),
		}
	}

	for _, userID := range snapshot.GetAdminUserIds() {
		// set 在 protobuf 中用数组表达，恢复时重新转为 map。
		restored.AdminUsers[userID] = true
	}
	// owner 是派生管理员身份，快照恢复后必须重新压实该不变量。
	restored.AdminUsers[restored.OwnerUserID] = true

	for _, userID := range snapshot.GetBanUserIds() {
		restored.BanUsers[userID] = true
	}

	for _, userID := range snapshot.GetMuteUserIds() {
		restored.MuteUsers[userID] = true
	}

	for _, item := range snapshot.GetGiftRank() {
		restored.GiftRank = append(restored.GiftRank, RankItem{
			UserID:      item.GetUserId(),
			Score:       item.GetScore(),
			GiftValue:   item.GetGiftValue(),
			UpdatedAtMS: item.GetUpdatedAtMs(),
		})
	}

	return restored
}

func sortedSetIDs(values map[int64]bool) []int64 {
	// map 转数组时只导出 value=true 的成员，false 等价于不存在。
	ids := make([]int64, 0, len(values))

	for userID, enabled := range values {
		if enabled {
			ids = append(ids, userID)
		}
	}

	slices.Sort(ids)

	return ids
}

func cloneStringMap(input map[string]string) map[string]string {
	if input == nil {
		// protobuf map 缺省按空 map 处理，调用方可以安全写入。
		return map[string]string{}
	}

	// RoomExt 是扩展字段集合，转换时必须复制，避免共享 map 引用。
	cloned := make(map[string]string, len(input))

	maps.Copy(cloned, input)

	return cloned
}
