package service

import (
	"context"
	"sort"
	"strings"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/room/rank"
	"hyapp/services/room-service/internal/room/state"
)

func (s *Service) roomEntryPolicy(ctx context.Context, roomID string, actorUserID int64) (bool, error) {
	// 进房前先读 rooms 元数据，避免为了拒绝关闭/删除房间而恢复完整 Room Cell。
	roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
	if err != nil {
		return false, err
	}
	if !exists {
		// 硬删除后的房间必须表现为不存在，不能因为旧内存 Cell 或旧快照被再次加入。
		return false, xerr.New(xerr.NotFound, "room not found")
	}
	switch strings.TrimSpace(roomMeta.Status) {
	case "", state.RoomStatusActive:
		return false, nil
	case state.RoomStatusBanned:
		// banned 是人工封禁终态；房主与普通用户都不能通过 JoinRoom 自动恢复。
		return false, xerr.New(xerr.RoomClosed, "room banned")
	case state.RoomStatusClosed:
		if roomMeta.OwnerUserID == actorUserID && actorUserID > 0 {
			// 只有房主本人进入 closed 房间时才允许在 JoinRoom 命令内重新激活。
			return true, nil
		}
		return false, xerr.New(xerr.RoomClosed, "room closed")
	case state.RoomStatusDeleted:
		// deleted 只兼容历史软删除数据；新删除会物理清理，旧 deleted 也不能被房主重新激活。
		return false, xerr.New(xerr.NotFound, "room not found")
	default:
		return false, xerr.New(xerr.RoomClosed, "room closed")
	}
}

func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error) {
	// 关闭状态先读 rooms 元数据，避免为了拒绝进房恢复完整 Room Cell。
	roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
	if err != nil || !exists {
		return false, err
	}
	return isClosedRoomStatus(roomMeta.Status), nil
}

func isClosedRoomStatus(status string) bool {
	value := strings.TrimSpace(status)
	// 空状态兼容测试构造；非 active 一律按不可进入处理，closing 也不接收新用户。
	return value != "" && !strings.EqualFold(value, state.RoomStatusActive)
}

// CloseRoom 关闭房间，统一清理 presence、麦位、读模型状态和房间外事件。
func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (*roomv1.CloseRoomResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.CloseRoom{
		Base:   baseFromMeta(req.GetMeta()),
		Reason: strings.TrimSpace(req.GetReason()),
	}
	if cmd.Reason == "" {
		cmd.Reason = "closed_by_owner"
	}
	if isAdminReopenReason(cmd.Reason) {
		return s.reopenRoom(ctx, cmd)
	}

	rtcKickTargets := make([]int64, 0)
	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if current == nil {
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
		}
		if current.Status == state.RoomStatusClosed {
			// 关房命令幂等，重复请求只返回当前关闭快照，不重复写 RoomClosed 事件。
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}
		if current.Status != state.RoomStatusActive && current.Status != state.RoomStatusClosing {
			// creating 等非开放状态不允许用 CloseRoom 直接跳过创建收尾语义。
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active")
		}
		if !isAdminCloseReason(cmd.Reason) {
			if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
				return mutationResult{}, nil, err
			}
		}
		if err := s.ensureNoRecoverableGiftOperations(ctx, current.RoomID); err != nil {
			return mutationResult{}, nil, err
		}

		kickTargets := closeRoomKickTargets(current)
		if len(kickTargets) > 0 {
			rtcKickTargets = append(rtcKickTargets[:0], kickTargets...)
		}

		type occupiedMicSeat struct {
			userID       int64
			seatNo       int32
			micSessionID string
		}
		occupiedSeats := make([]occupiedMicSeat, 0, len(current.MicSeats))
		for _, seat := range current.MicSeats {
			if seat.UserID != 0 && seat.MicSessionID != "" {
				// 清场前先保存占麦会话，后续 ClearMicSession 会抹掉 mic_session_id。
				occupiedSeats = append(occupiedSeats, occupiedMicSeat{
					userID:       seat.UserID,
					seatNo:       seat.SeatNo,
					micSessionID: seat.MicSessionID,
				})
			}
		}

		// 关房是 Room Cell 的终态变更，presence 和麦位都在同一版本下清空。
		current.Status = state.RoomStatusClosed
		current.OnlineUsers = map[int64]*state.RoomUserState{}
		for index := range current.MicSeats {
			current.ClearMicSession(index)
		}
		current.Version++

		records := make([]outbox.Record, 0, len(occupiedSeats)+len(kickTargets)+1)
		closedEvent, err := outbox.Build(current.RoomID, "RoomClosed", current.Version, now, &roomeventsv1.RoomClosed{
			ActorUserId: cmd.ActorUserID(),
			Reason:      cmd.Reason,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		records = append(records, closedEvent)

		for _, targetUserID := range kickTargets {
			// 关房清场复用 RoomUserKicked 事实，notice-service 和腾讯 IM 群成员移除可沿用同一条消费链路。
			kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
				ActorUserId:  cmd.ActorUserID(),
				TargetUserId: targetUserID,
			})
			if err != nil {
				return mutationResult{}, nil, err
			}
			records = append(records, kickEvent)
		}

		for _, seat := range occupiedSeats {
			// 关房是 Room Cell 统一清场，给每个占麦 session 单独写 down 事件，保证用户时长能闭合。
			micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), seat.userID, seat.seatNo, seat.micSessionID, "room_closed")
			if err != nil {
				return mutationResult{}, nil, err
			}
			records = append(records, micEvent)
		}

		return mutationResult{
			snapshot:   current.ToProto(),
			roomStatus: state.RoomStatusClosed,
			syncEvent: &tencentim.RoomEvent{
				EventID:     closedEvent.EventID,
				RoomID:      current.RoomID,
				EventType:   "room_closed",
				ActorUserID: tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				RoomVersion: current.Version,
				Attributes: map[string]string{
					"reason": cmd.Reason,
				},
			},
		}, records, nil
	})
	if err != nil {
		return nil, err
	}

	response := &roomv1.CloseRoomResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:   result.snapshot,
	}
	if result.applied && s.rtcUserRemover != nil {
		for _, targetUserID := range rtcKickTargets {
			// RTC 是房间外连接态：关房事实已提交后再逐个移除，失败不回滚 Room Cell。
			_ = s.rtcUserRemover.RemoveUserByStrRoomID(ctx, cmd.RoomID(), targetUserID)
		}
	}

	return response, nil
}

func isAdminCloseReason(reason string) bool {
	return strings.EqualFold(strings.TrimSpace(reason), "admin_closed")
}

func isAdminReopenReason(reason string) bool {
	return strings.EqualFold(strings.TrimSpace(reason), "admin_reopen")
}

func (s *Service) reopenRoom(ctx context.Context, cmd command.CloseRoom) (*roomv1.CloseRoomResponse, error) {
	result, err := s.mutateRoom(ctx, cmd, true, func(_ time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if current == nil {
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
		}
		if current.Status == state.RoomStatusActive {
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}
		if current.Status != state.RoomStatusClosed {
			return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not closed")
		}

		current.Status = state.RoomStatusActive
		current.Version++
		return mutationResult{
			snapshot:   current.ToProto(),
			roomStatus: state.RoomStatusActive,
		}, nil, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.CloseRoomResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:   result.snapshot,
	}, nil
}

func closeRoomKickTargets(current *state.RoomState) []int64 {
	if current == nil {
		return nil
	}
	seen := make(map[int64]struct{}, len(current.OnlineUsers)+len(current.MicSeats))
	for userID := range current.OnlineUsers {
		if userID > 0 {
			seen[userID] = struct{}{}
		}
	}
	for _, seat := range current.MicSeats {
		if seat.UserID > 0 {
			seen[seat.UserID] = struct{}{}
		}
	}
	targets := make([]int64, 0, len(seen))
	for userID := range seen {
		targets = append(targets, userID)
	}
	sort.Slice(targets, func(left, right int) bool {
		return targets[left] < targets[right]
	})
	return targets
}
