package service

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

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

const (
	roomPresenceStatusActive = "active"
	roomPresenceStatusLeft   = "left"
)

// GetCurrentRoom 返回当前用户可恢复房间的只读探测结果。
// 该方法先查用户级 presence 读模型，再用 Room Cell 快照二次校验，避免读模型滞后导致恢复已关闭或已踢出的房间。
func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	userID := req.GetUserId()
	if userID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
	}

	serverTimeMS := s.clock.Now().UnixMilli()
	presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, userID)
	if err != nil {
		return nil, err
	}
	if !exists {
		// 没有用户当前房间投影时直接返回空结果，不扫描房间列表或所有 Room Cell。
		return emptyCurrentRoomResponse(serverTimeMS), nil
	}

	// presence 只是恢复入口的索引，必须用 Room Cell 快照确认房间仍 active 且用户仍在房间内。
	snapshot, err := s.currentSnapshot(ctx, presence.RoomID)
	if err != nil {
		return nil, err
	}
	if !currentRoomSnapshotRestorable(snapshot, userID, serverTimeMS) {
		// 查询语义必须保持只读；读模型滞后由后续命令投影或专项补偿修复。
		return emptyCurrentRoomResponse(serverTimeMS), nil
	}

	roomUser := findProtoUser(snapshot, userID)
	seat := protoSeatByUser(snapshot, userID)
	role := presence.Role
	if roomUser != nil {
		role = roomUser.GetRole()
	}
	publishState := ""
	micSessionID := ""
	needRTCToken := false
	if seat != nil {
		// 上麦用户回前台需要 RTC token 恢复媒体链路；普通房间用户只需要重新加 IM 群。
		publishState = seat.GetPublishState()
		micSessionID = seat.GetMicSessionId()
		needRTCToken = publishState == state.MicPublishPending || publishState == state.MicPublishPublishing
	}

	return &roomv1.GetCurrentRoomResponse{
		HasCurrentRoom:  true,
		RoomId:          snapshot.GetRoomId(),
		RoomVersion:     snapshot.GetVersion(),
		Role:            role,
		MicSessionId:    micSessionID,
		PublishState:    publishState,
		NeedJoinImGroup: true,
		NeedRtcToken:    needRTCToken,
		ServerTimeMs:    serverTimeMS,
	}, nil
}

// emptyCurrentRoomResponse 保留 server_time_ms，方便客户端用同一套时钟校正逻辑处理空结果。
func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse {
	return &roomv1.GetCurrentRoomResponse{ServerTimeMs: serverTimeMS}
}

// currentRoomSnapshotRestorable 判定读模型指向的房间是否仍可被客户端恢复。
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
	if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
		// 非 active 房间不能恢复，即使用户级 presence 投影还没被清理。
		return false
	}
	if snapshotUserBanned(snapshot, userID, nowMS) {
		// ban 优先于旧 presence，避免被踢用户通过本地恢复入口重新收消息。
		return false
	}
	return findProtoUser(snapshot, userID) != nil
}

// projectRoomPresenceBestEffort 用最新快照刷新“用户当前房间”读模型。
// 读模型失败不能影响命令成功语义，但必须记录日志，避免恢复入口长期缺失无人感知。
func (s *Service) projectRoomPresenceBestEffort(ctx context.Context, snapshot *roomv1.RoomSnapshot) {
	projection := roomPresenceProjectionFromSnapshot(snapshot, s.clock.Now().UnixMilli())
	if projection.RoomID == "" {
		return
	}
	if err := s.repository.ProjectRoomPresence(ctx, projection); err != nil {
		logx.Error(ctx, "room_presence_project_failed", err,
			slog.String("component", "room_presence_projector"),
			slog.String("room_id", projection.RoomID),
			slog.Int64("room_version", projection.RoomVersion),
		)
	}
}

// projectRoomPresenceForCommandBestEffort 按命令类型选择最小投影；Join/Leave 热路径不能再重写整房间在线集合。
func (s *Service) projectRoomPresenceForCommandBestEffort(ctx context.Context, cmd command.Command, snapshot *roomv1.RoomSnapshot, joinedUser *roomv1.RoomUser) {
	switch cmd.Type() {
	case (command.JoinRoom{}).Type():
		s.upsertJoinedRoomPresenceBestEffort(ctx, snapshot, cmd.ActorUserID(), joinedUser)
	case (command.LeaveRoom{}).Type():
		s.markLeftRoomPresenceBestEffort(ctx, snapshot, cmd.ActorUserID())
	default:
		// Kick/Close/SystemEvict/Mic 变更仍先保留全量投影，确保复杂状态收敛不因本次 Join/Leave 优化改变语义。
		s.projectRoomPresenceBestEffort(ctx, snapshot)
	}
}

func (s *Service) upsertJoinedRoomPresenceBestEffort(ctx context.Context, snapshot *roomv1.RoomSnapshot, userID int64, joinedUser *roomv1.RoomUser) {
	projection := roomPresenceProjectionForUser(snapshot, userID, joinedUser, s.clock.Now().UnixMilli())
	if projection.RoomID == "" || projection.UserID <= 0 {
		return
	}
	if err := s.repository.UpsertRoomUserPresence(ctx, projection); err != nil {
		logx.Error(ctx, "room_presence_join_project_failed", err,
			slog.String("component", "room_presence_projector"),
			slog.String("room_id", projection.RoomID),
			slog.Int64("user_id", projection.UserID),
			slog.Int64("room_version", projection.RoomVersion),
		)
	}
}

func (s *Service) markLeftRoomPresenceBestEffort(ctx context.Context, snapshot *roomv1.RoomSnapshot, userID int64) {
	projection := roomPresenceLeaveProjection(snapshot, userID, s.clock.Now().UnixMilli())
	if projection.RoomID == "" || projection.UserID <= 0 {
		return
	}
	if err := s.repository.MarkRoomUserPresenceLeft(ctx, projection); err != nil {
		logx.Error(ctx, "room_presence_leave_project_failed", err,
			slog.String("component", "room_presence_projector"),
			slog.String("room_id", projection.RoomID),
			slog.Int64("user_id", projection.UserID),
			slog.Int64("room_version", projection.RoomVersion),
		)
	}
}

// roomPresenceProjectionFromSnapshot 把 Room Cell 快照压缩成用户当前房间读模型。
// 读模型只保存恢复入口需要的字段，不复制完整 RoomState，也不作为权限权威来源。
func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAtMS int64) RoomPresenceSnapshot {
	if snapshot == nil || snapshot.GetRoomId() == "" {
		return RoomPresenceSnapshot{}
	}

	// 先按 user_id 建立麦位索引，投影用户时能补齐发流状态而不做双层循环。
	seatsByUser := make(map[int64]*roomv1.SeatState, len(snapshot.GetMicSeats()))
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() > 0 {
			seatsByUser[seat.GetUserId()] = seat
		}
	}

	users := make([]RoomPresence, 0, len(snapshot.GetOnlineUsers()))
	if snapshot.GetStatus() == state.RoomStatusActive {
		// 只有 active 房间才投影 active 用户；关闭房间通过空 users 让 repository 收敛旧 presence。
		for _, user := range snapshot.GetOnlineUsers() {
			if user.GetUserId() <= 0 {
				continue
			}
			entry := RoomPresence{
				AppCode:      snapshot.GetAppCode(),
				UserID:       user.GetUserId(),
				RoomID:       snapshot.GetRoomId(),
				Role:         user.GetRole(),
				RoomVersion:  snapshot.GetVersion(),
				Status:       roomPresenceStatusActive,
				JoinedAtMS:   user.GetJoinedAtMs(),
				LastSeenAtMS: user.GetLastSeenAtMs(),
				UpdatedAtMS:  updatedAtMS,
			}
			if seat := seatsByUser[user.GetUserId()]; seat != nil {
				// 发流状态只对当前上麦用户有意义，非上麦用户保持空字段。
				entry.PublishState = seat.GetPublishState()
				entry.MicSessionID = seat.GetMicSessionId()
			}
			users = append(users, entry)
		}
	}

	return RoomPresenceSnapshot{
		AppCode:     snapshot.GetAppCode(),
		RoomID:      snapshot.GetRoomId(),
		RoomVersion: snapshot.GetVersion(),
		RoomStatus:  snapshot.GetStatus(),
		Users:       users,
		UpdatedAtMS: updatedAtMS,
	}
}

func roomPresenceProjectionForUser(snapshot *roomv1.RoomSnapshot, userID int64, user *roomv1.RoomUser, updatedAtMS int64) RoomPresence {
	if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive || userID <= 0 {
		return RoomPresence{}
	}
	if user == nil || user.GetUserId() != userID {
		user = findProtoUser(snapshot, userID)
	}
	if user == nil || user.GetUserId() <= 0 {
		return RoomPresence{}
	}
	entry := RoomPresence{
		AppCode:      snapshot.GetAppCode(),
		UserID:       user.GetUserId(),
		RoomID:       snapshot.GetRoomId(),
		Role:         user.GetRole(),
		RoomVersion:  snapshot.GetVersion(),
		Status:       roomPresenceStatusActive,
		JoinedAtMS:   user.GetJoinedAtMs(),
		LastSeenAtMS: user.GetLastSeenAtMs(),
		UpdatedAtMS:  updatedAtMS,
	}
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() != user.GetUserId() {
			continue
		}
		// JoinRoom 可能是重连到已在麦用户；单用户投影仍要保留麦上发流状态。
		entry.PublishState = seat.GetPublishState()
		entry.MicSessionID = seat.GetMicSessionId()
		break
	}
	return entry
}

func roomPresenceLeaveProjection(snapshot *roomv1.RoomSnapshot, userID int64, updatedAtMS int64) RoomPresence {
	if snapshot == nil || snapshot.GetRoomId() == "" || userID <= 0 {
		return RoomPresence{}
	}
	return RoomPresence{
		AppCode:     snapshot.GetAppCode(),
		UserID:      userID,
		RoomID:      snapshot.GetRoomId(),
		RoomVersion: snapshot.GetVersion(),
		Status:      roomPresenceStatusLeft,
		UpdatedAtMS: updatedAtMS,
	}
}

// containsUserID 是当前房间恢复入口的本地集合查询，独立保留便于后续替换为二分查找。
func containsUserID(values []int64, target int64) bool {
	return slices.Contains(values, target)
}
