package service

import (
	"context"
	"slices"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"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 string
	RoomID  string
}

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

	// 注册表只保存当前进程内已装载 Cell，不代表 Redis lease 的全局 owner。
	s.cells[runtimeRoomKey(appcode.FromContext(ctx), roomID)] = roomCell
}

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

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

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)
	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 {
	return runtimeRoomKey(appcode.FromContext(ctx), roomID)
}

func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, now time.Time) (router.Lease, bool, error) {
	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
	}

	return lease, true, nil
}

func (s *Service) verifyLoadedRoomLease(ctx context.Context, roomRef loadedRoomRef, lease router.Lease, now time.Time) (bool, error) {
	return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
}

func runtimeRoomRef(key string) loadedRoomRef {
	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 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(),
		SentAtMS:      meta.GetSentAtMs(),
	}
}

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 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(),
	}
}
