package service

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

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/logx"
	"hyapp/services/room-service/internal/room/cell"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/state"
	"hyapp/services/room-service/internal/router"
)

type loadedRoomRef struct {
	// AppCode 是运行态 Cell 所属租户，后台 worker 必须用它恢复 ctx。
	AppCode string
	// RoomID 是已装载 Room Cell 的业务房间 ID。
	RoomID string
}

func (s *Service) installCell(ctx context.Context, roomID string, roomCell *cell.RoomCell, leaseToken string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	// 注册表只保存当前进程内已装载 Cell；leaseToken 决定它能否继续承载写路径。
	s.cells[runtimeRoomKey(appcode.FromContext(ctx), roomID)] = &loadedCell{roomCell: roomCell, leaseToken: leaseToken}
}

func (s *Service) loadCell(ctx context.Context, roomID string) *cell.RoomCell {
	s.mu.Lock()
	defer s.mu.Unlock()

	// 返回指针后由 RoomCell 自己的 mailbox 保证状态串行化。
	loaded := s.cells[runtimeRoomKey(appcode.FromContext(ctx), roomID)]
	if loaded == nil {
		return nil
	}
	return loaded.roomCell
}

func (s *Service) loadCellForLease(ctx context.Context, roomID string, lease router.Lease) *cell.RoomCell {
	key := runtimeRoomKey(appcode.FromContext(ctx), roomID)
	s.mu.Lock()
	defer s.mu.Unlock()

	loaded := s.cells[key]
	if loaded == nil {
		return nil
	}
	if loaded.leaseToken == "" || loaded.leaseToken != lease.LeaseToken {
		// lease token 变化代表当前节点经历过执行权断档；旧 Cell 可能落后于新 owner 已提交的 command log，必须丢弃后走恢复。
		delete(s.cells, key)
		return nil
	}
	return loaded.roomCell
}

func (s *Service) loadCellForCurrentOwner(ctx context.Context, roomID string) (*cell.RoomCell, error) {
	if s.directory == nil {
		// 测试中的极简 Service 可能没有装配 directory；生产 HealthCheck 会拒绝这种配置。
		return s.loadCell(ctx, roomID), nil
	}
	lease, exists, err := s.directory.Lookup(ctx, runtimeRoomKeyFromContext(ctx, roomID))
	if err != nil {
		return nil, err
	}
	if !exists || lease.NodeID != s.nodeID || !lease.ValidAt(s.clock.Now()) {
		// 只读路径不能主动接管 lease；没有当前 owner 身份时必须走持久化恢复，避免旧 Cell 放行过期状态。
		return nil, nil
	}
	return s.loadCellForLease(ctx, roomID, lease), nil
}

func (s *Service) forgetLoadedRoom(ctx context.Context, roomID string) {
	if s == nil {
		return
	}
	app := appcode.FromContext(ctx)
	key := runtimeRoomKey(app, roomID)
	s.mu.Lock()
	delete(s.cells, key)
	s.mu.Unlock()

	if s.directory != nil {
		if _, err := s.directory.ReleaseOwner(ctx, key, s.nodeID); err != nil {
			// 删除事实已在 MySQL 提交，释放 lease 失败只影响其他节点接管等待 TTL，不能回滚删除结果。
			logx.Warn(ctx, "room_delete_lease_release_failed",
				slog.String("node_id", s.nodeID),
				slog.String("app_code", app),
				slog.String("room_id", roomID),
				slog.String("error", err.Error()),
			)
		}
	}
}

func (s *Service) loadedRoomRefs() []loadedRoomRef {
	s.mu.Lock()
	defer s.mu.Unlock()

	// 复制运行态房间引用后释放锁，避免后台清理期间阻塞其他房间装载。
	roomRefs := make([]loadedRoomRef, 0, len(s.cells))
	for key := range s.cells {
		roomRefs = append(roomRefs, runtimeRoomRef(key))
	}

	return roomRefs
}

func runtimeRoomKey(appCode string, roomID string) string {
	appCode = appcode.Normalize(appCode)
	// 使用 NUL 分隔 app_code 和 room_id，避免普通字符串拼接出现歧义。
	return appCode + "\x00" + roomID
}

// RuntimeRoomKey 返回 Redis owner lease 使用的运行时路由 key。
// transport 层 owner 转发必须和领域层 EnsureOwner 使用同一个 key，否则会出现同一房间多 owner。
func RuntimeRoomKey(appCode string, roomID string) string {
	return runtimeRoomKey(appCode, roomID)
}

func runtimeRoomKeyFromContext(ctx context.Context, roomID string) string {
	// 命令入口统一从 ctx 读取 app_code，避免 service 方法忘记把租户键传给 Redis lease。
	return runtimeRoomKey(appcode.FromContext(ctx), roomID)
}

// roomLeaseOwnedElsewhere 判断房间执行权当前是否由其他节点持有有效 lease；只读判定，不接管也不续租。
func (s *Service) roomLeaseOwnedElsewhere(ctx context.Context, appCode string, roomID string) (string, bool, error) {
	if s.directory == nil {
		// 测试中的极简 Service 可能没有装配 directory；生产 HealthCheck 会拒绝这种配置。
		return "", false, nil
	}
	lease, exists, err := s.directory.Lookup(ctx, runtimeRoomKey(appCode, roomID))
	if err != nil {
		return "", false, err
	}
	if exists && lease.ValidAt(s.clock.Now()) && lease.NodeID != s.nodeID {
		return lease.NodeID, true, nil
	}
	return "", false, nil
}

func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, now time.Time) (router.Lease, bool, error) {
	// 后台 worker 通过 EnsureOwner 续租当前 Cell，不能在没有 lease 的情况下读取旧内存态。
	lease, err := s.directory.EnsureOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, now, s.leaseTTL)
	if err != nil {
		return router.Lease{}, false, err
	}
	if lease.NodeID != s.nodeID || !lease.ValidAt(now) {
		// 后台 worker 只处理自己仍然拥有或可安全续租的 Cell。
		// 如果其他节点已经接管并持有有效 lease，当前节点必须跳过，避免旧内存态覆盖新 owner。
		return router.Lease{}, false, nil
	}
	if roomCell := s.loadCellForLease(ctx, roomRef.RoomID, lease); roomCell == nil {
		// 旧 Cell 的 token 不连续时，后台 worker 不能靠重新抢到的 lease 继续写旧内存分支。
		return router.Lease{}, false, nil
	}

	return lease, true, nil
}

func (s *Service) verifyLoadedRoomLease(ctx context.Context, roomRef loadedRoomRef, lease router.Lease, now time.Time) (bool, error) {
	// 使用同一个 lease token 做 fencing 校验，防止 worker 批处理期间 owner 已经切走。
	return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
}

func runtimeRoomRef(key string) loadedRoomRef {
	// runtime key 是内部格式；解析失败时保留 room_id，便于旧测试数据仍能被安全跳过或排查。
	for index := 0; index < len(key); index++ {
		if key[index] == 0 {
			return loadedRoomRef{AppCode: appcode.Normalize(key[:index]), RoomID: key[index+1:]}
		}
	}

	return loadedRoomRef{AppCode: "", RoomID: key}
}

func firstNonZeroInt64(values ...int64) int64 {
	for _, value := range values {
		if value != 0 {
			return value
		}
	}
	return 0
}

func commandResult(applied bool, roomVersion int64, now time.Time) *roomv1.CommandResult {
	// 所有命令响应统一带 applied、room_version 和服务端时间，便于客户端处理幂等结果。
	return &roomv1.CommandResult{
		Applied:      applied,
		RoomVersion:  roomVersion,
		ServerTimeMs: now.UnixMilli(),
	}
}

func baseFromMeta(meta *roomv1.RequestMeta) command.Base {
	if meta == nil {
		// 上层 mutateRoom 会校验关键字段，nil meta 在这里先收敛为空 Base。
		return command.Base{}
	}

	// RequestMeta 是跨服务 protobuf 契约，领域层统一转换成 command.Base。
	return command.Base{
		AppCode:       appcode.Normalize(meta.GetAppCode()),
		RequestID:     meta.GetRequestId(),
		CommandID:     meta.GetCommandId(),
		ActorID:       meta.GetActorUserId(),
		Room:          meta.GetRoomId(),
		GatewayNodeID: meta.GetGatewayNodeId(),
		SessionID:     meta.GetSessionId(),
		DeviceID:      meta.GetDeviceId(),
		SentAtMS:      meta.GetSentAtMs(),
	}
}

func (s *Service) canonicalRoomCommandMeta(ctx context.Context, meta *roomv1.RequestMeta) (*roomv1.RequestMeta, error) {
	if meta == nil {
		return nil, nil
	}
	requestedRoomID := strings.TrimSpace(meta.GetRoomId())
	if requestedRoomID == "" {
		return meta, nil
	}

	if roomMeta, exists, err := s.repository.GetRoomMeta(ctx, requestedRoomID); err != nil {
		return nil, err
	} else if exists {
		if roomMeta.RoomID == meta.GetRoomId() {
			return meta, nil
		}
		// 精确 room_id 命中时也用数据库里的规范值，避免首尾空白进入 lease key 或 command log。
		return requestMetaWithRoomID(meta, roomMeta.RoomID), nil
	}

	roomMeta, exists, err := s.repository.GetRoomMetaByShortID(ctx, requestedRoomID)
	if err != nil || !exists {
		return meta, err
	}
	// 客户端可能只知道展示短房号；进入 Room Cell 前必须收敛到内部 room_id，保证 lease、command log、snapshot 分区一致。
	return requestMetaWithRoomID(meta, roomMeta.RoomID), nil
}

func requestMetaWithRoomID(meta *roomv1.RequestMeta, roomID string) *roomv1.RequestMeta {
	if meta == nil {
		return nil
	}
	return &roomv1.RequestMeta{
		RequestId:     meta.GetRequestId(),
		CommandId:     meta.GetCommandId(),
		ActorUserId:   meta.GetActorUserId(),
		RoomId:        roomID,
		GatewayNodeId: meta.GetGatewayNodeId(),
		SessionId:     meta.GetSessionId(),
		DeviceId:      meta.GetDeviceId(),
		SentAtMs:      meta.GetSentAtMs(),
		AppCode:       meta.GetAppCode(),
	}
}

func contextFromMeta(ctx context.Context, meta *roomv1.RequestMeta) context.Context {
	if meta == nil {
		return appcode.WithContext(ctx, "")
	}

	return appcode.WithContext(ctx, meta.GetAppCode())
}

func findProtoUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
	if snapshot == nil {
		return nil
	}

	// protobuf 快照用户列表规模受房间在线人数限制，线性扫描足够简单。
	for _, user := range snapshot.GetOnlineUsers() {
		if user.GetUserId() == userID {
			return user
		}
	}

	return nil
}

func containsID(values []int64, target int64) bool {
	// 快照里的集合以有序数组表达，守卫查询用线性查找即可。
	return slices.Contains(values, target)
}

func snapshotUserBanned(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
	if snapshot == nil || userID <= 0 {
		return false
	}
	for _, ban := range snapshot.GetBanStates() {
		if ban.GetUserId() != userID {
			continue
		}
		// ban_states 是带时间边界的权威状态；过期后即使 ban_user_ids 还没刷新也不能继续拦截。
		return ban.GetExpiresAtMs() == 0 || ban.GetExpiresAtMs() > nowMS
	}
	// 旧快照没有 ban_states 时只能按永久 ban 解释 ban_user_ids，避免恢复路径放宽权限。
	return containsID(snapshot.GetBanUserIds(), userID)
}

func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem {
	// SendGift 响应需要 protobuf 榜单项，不能直接暴露内部 state.RankItem。
	result := make([]*roomv1.RankItem, 0, len(items))

	for _, item := range items {
		result = append(result, &roomv1.RankItem{
			UserId:      item.UserID,
			Score:       item.Score,
			GiftValue:   item.GiftValue,
			UpdatedAtMs: item.UpdatedAtMS,
		})
	}

	return result
}

// NewRequestMeta 是测试和 stub 组装命令元信息时的便捷函数。
func NewRequestMeta(roomID string, actorUserID int64) *roomv1.RequestMeta {
	// 该函数只用于测试和 stub；真实入口由 gateway 生成 request_id，并透传客户端动作级 command_id。
	return &roomv1.RequestMeta{
		AppCode:       appcode.Default,
		RequestId:     idgen.New("req"),
		CommandId:     idgen.New("cmd"),
		ActorUserId:   actorUserID,
		RoomId:        roomID,
		GatewayNodeId: "gateway-local",
		SessionId:     idgen.New("sess"),
		SentAtMs:      time.Now().UnixMilli(),
	}
}
