package service

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

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/logx"
	"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"
)

// JoinRoom 把用户业务态接入房间。
func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// JoinRoom 只维护 room-service presence，不创建任何本地长连接订阅。
	cmd := command.JoinRoom{
		Base: baseFromMeta(req.GetMeta()),
		Role: req.GetRole(),
	}

	if cmd.Role == "" {
		// 客户端不传角色时按普通观众进入房间。
		cmd.Role = "audience"
	}
	if err := s.requireRoomOpenForEntry(ctx, cmd.RoomID()); err != nil {
		return nil, err
	}

	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if current.BanUsers[cmd.ActorUserID()] {
			// 被踢或封禁用户不能重新进入业务 presence。
			return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
		}

		if existing, exists := current.OnlineUsers[cmd.ActorUserID()]; exists {
			// 重复 JoinRoom 表示重连或刷新业务 presence，只更新 last_seen，不重复发进房系统消息。
			existing.LastSeenAtMS = now.UnixMilli()
			current.Version++

			snapshot := current.ToProto()
			return mutationResult{
				snapshot: snapshot,
				user:     findProtoUser(snapshot, cmd.ActorUserID()),
			}, nil, nil
		}

		current.OnlineUsers[cmd.ActorUserID()] = &state.RoomUserState{
			UserID:       cmd.ActorUserID(),
			Role:         cmd.Role,
			JoinedAtMS:   now.UnixMilli(),
			LastSeenAtMS: now.UnixMilli(),
		}
		current.Version++

		// JoinRoom 成功需要 outbox 事件，腾讯云 IM 房间系统消息由 worker 异步投递。
		joinedEvent, err := outbox.Build(current.RoomID, "RoomUserJoined", current.Version, now, &roomeventsv1.RoomUserJoined{
			UserId: cmd.ActorUserID(),
			Role:   cmd.Role,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}

		snapshot := current.ToProto()

		return mutationResult{
			snapshot: snapshot,
			user:     findProtoUser(snapshot, cmd.ActorUserID()),
			syncEvent: &tencentim.RoomEvent{
				EventID:      joinedEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_user_joined",
				ActorUserID:  cmd.ActorUserID(),
				TargetUserID: cmd.ActorUserID(),
				RoomVersion:  current.Version,
			},
		}, []outbox.Record{joinedEvent}, nil
	})
	if err != nil {
		return nil, err
	}
	s.projectRoomVisitBestEffort(ctx, cmd.ActorUserID(), cmd.RoomID())

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

// projectRoomVisitBestEffort 记录 Mine/Visited tab 的房间访问索引。
// 这是列表读模型，不参与 Room Cell 提交；失败只记录日志，不能影响已经成功的 JoinRoom。
func (s *Service) projectRoomVisitBestEffort(ctx context.Context, userID int64, roomID string) {
	if userID <= 0 || roomID == "" {
		return
	}
	nowMS := s.clock.Now().UnixMilli()
	if err := s.repository.UpsertRoomUserFeedEntry(ctx, RoomUserFeedEntry{
		AppCode:     appcode.FromContext(ctx),
		UserID:      userID,
		FeedType:    roomListTabVisited,
		RoomID:      roomID,
		CreatedAtMS: nowMS,
		UpdatedAtMS: nowMS,
	}); err != nil {
		logx.Error(ctx, "room_visit_feed_upsert_failed", err,
			slog.String("component", "room_user_feed_projector"),
			slog.String("room_id", roomID),
			slog.Int64("user_id", userID),
		)
	}
}

// RoomHeartbeat 显式刷新已在房间内用户的业务 presence。
func (s *Service) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.RoomHeartbeat{
		Base: baseFromMeta(req.GetMeta()),
	}

	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if err := requireActiveRoom(current); err != nil {
			return mutationResult{}, nil, err
		}
		if current.BanUsers[cmd.ActorUserID()] {
			// ban 用户不能通过心跳续住业务 presence；KickUser 已经清理 presence，这里是防御脏状态。
			return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
		}

		existing, exists := current.OnlineUsers[cmd.ActorUserID()]
		if !exists {
			// 心跳只刷新已存在 presence，不能把已离房用户重新加入房间。
			return mutationResult{}, nil, xerr.New(xerr.NotFound, "user not in room")
		}

		existing.LastSeenAtMS = now.UnixMilli()
		current.Version++

		snapshot := current.ToProto()
		return mutationResult{
			snapshot: snapshot,
			user:     findProtoUser(snapshot, cmd.ActorUserID()),
		}, nil, nil
	})
	if err != nil {
		return nil, err
	}

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

// LeaveRoom 把用户从房间业务态移出。
func (s *Service) LeaveRoom(ctx context.Context, req *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// LeaveRoom 只移除 room-service 业务 presence，真实 IM 连接态由腾讯云 IM SDK 处理。
	cmd := command.LeaveRoom{
		Base: baseFromMeta(req.GetMeta()),
	}

	result, err := s.leaveRoom(ctx, cmd, "")
	if err != nil {
		return nil, err
	}

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

func (s *Service) leaveRoom(ctx context.Context, cmd command.LeaveRoom, reason string) (mutationResult, error) {
	return s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		_, inRoom := current.OnlineUsers[cmd.ActorUserID()]
		seat, onSeat := current.SeatByUser(cmd.ActorUserID())
		if !inRoom && !onSeat {
			// 离房幂等：没有业务 presence 且不占麦时不写命令日志和 outbox。
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}

		delete(current.OnlineUsers, cmd.ActorUserID())
		var micSessionID string
		if onSeat {
			// 离房必须同步释放麦位；同时写 RoomMicChanged/down，给时长聚合一个稳定闭合事件。
			index := current.SeatIndex(seat.SeatNo)
			if index >= 0 {
				micSessionID = current.MicSeats[index].MicSessionID
				current.ClearMicSession(index)
			}
		}
		current.Version++

		records := make([]outbox.Record, 0, 2)
		if onSeat && micSessionID != "" {
			micReason := reason
			if micReason == "" {
				micReason = "leave"
			}
			micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), cmd.ActorUserID(), seat.SeatNo, micSessionID, micReason)
			if err != nil {
				return mutationResult{}, nil, err
			}
			records = append(records, micEvent)
		}

		// 离房事件进入 outbox；stale 清理会通过 attributes 标记原因，腾讯云 IM 由 worker 异步投递。
		leftEvent, err := outbox.Build(current.RoomID, "RoomUserLeft", current.Version, now, &roomeventsv1.RoomUserLeft{
			UserId: cmd.ActorUserID(),
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		records = append(records, leftEvent)

		attributes := map[string]string{}
		if reason != "" {
			attributes["reason"] = reason
		}

		return mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:      leftEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_user_left",
				ActorUserID:  cmd.ActorUserID(),
				TargetUserID: cmd.ActorUserID(),
				RoomVersion:  current.Version,
				Attributes:   attributes,
			},
		}, records, nil
	})
}

// RunPresenceStaleWorker 周期性清理本节点已装载房间中的过期业务 presence。
// worker 只调用 room-service 命令链路，不触碰腾讯云 IM 连接态，客户端重连后仍需重新 JoinRoom。
func (s *Service) RunPresenceStaleWorker(ctx context.Context, interval time.Duration) {
	if interval <= 0 || s.presenceStaleAfter <= 0 {
		// 非正数表示显式关闭 worker，测试或特殊环境可用。
		return
	}

	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			_ = s.SweepStalePresence(ctx)
		}
	}
}

// SweepStalePresence 扫描并清理已经超过 last_seen 阈值的业务 presence。
// 该方法暴露给测试和后台 worker 复用，所有清理都复用 LeaveRoom 的持久化、快照和 outbox 语义。
func (s *Service) SweepStalePresence(ctx context.Context) error {
	if s.presenceStaleAfter <= 0 {
		return nil
	}

	now := s.clock.Now()
	cutoffMs := now.Add(-s.presenceStaleAfter).UnixMilli()
	for _, roomRef := range s.loadedRoomRefs() {
		roomCtx := appcode.WithContext(ctx, roomRef.AppCode)
		lease, owned, err := s.loadedRoomLease(roomCtx, roomRef, now)
		if err != nil {
			return err
		}
		if !owned {
			continue
		}

		roomCell := s.loadCell(roomCtx, roomRef.RoomID)
		if roomCell == nil {
			continue
		}

		currentState, _, err := roomCell.Snapshot(roomCtx)
		if err != nil {
			return err
		}
		owned, err = s.verifyLoadedRoomLease(roomCtx, roomRef, lease, s.clock.Now())
		if err != nil {
			return err
		}
		if !owned {
			continue
		}

		for _, userID := range stalePresenceUserIDs(currentState, cutoffMs) {
			owned, err = s.verifyLoadedRoomLease(roomCtx, roomRef, lease, s.clock.Now())
			if err != nil {
				return err
			}
			if !owned {
				break
			}

			cmd := command.LeaveRoom{
				Base: command.Base{
					RequestID:     idgen.New("req_presence_stale"),
					CommandID:     idgen.New("cmd_presence_stale"),
					ActorID:       userID,
					Room:          roomRef.RoomID,
					GatewayNodeID: s.nodeID,
					SessionID:     "presence-stale",
					SentAtMS:      now.UnixMilli(),
				},
			}
			if _, err := s.leaveRoom(roomCtx, cmd, "stale_timeout"); err != nil {
				return err
			}
		}
	}

	return nil
}

func stalePresenceUserIDs(current *state.RoomState, cutoffMs int64) []int64 {
	if current == nil {
		return nil
	}

	userIDs := make([]int64, 0)
	for userID, userState := range current.OnlineUsers {
		if userState != nil && userState.LastSeenAtMS <= cutoffMs {
			userIDs = append(userIDs, userID)
		}
	}

	return userIDs
}
