package service

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/logx"
	"hyapp/pkg/roomid"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/cell"
	"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"
)

const (
	roomExtTitleKey        = "title"
	roomExtCoverURLKey     = "cover_url"
	roomExtDescriptionKey  = "description"
	roomExtShortIDKey      = "room_short_id"
	roomExtOwnerCountryKey = "owner_country_code"
	roomExtBackgroundKey   = "background_url"
	roomExtRobotRoomKey    = "robot_room"
	roomExtRobotUserIDsKey = "robot_user_ids"

	// defaultRoomAvatar 只服务房间资料更新/后台清空封面后的兜底；创建房间必须显式提交封面。
	defaultRoomAvatar = "hyapp://room/default-avatar"

	// RoomExt 的展示字段最终会进入 room_list_entries 和客户端快照，服务端在入口裁剪长度边界。
	maxRoomNameRunes        = 128
	maxRoomAvatarRunes      = 512
	maxRoomDescriptionRunes = 512
	maxRoomBackgroundRunes  = 256
)

// roomProfileInput 是创建房间时需要落到 RoomExt 的展示资料集合。
type roomProfileInput struct {
	Name             string
	Avatar           string
	Description      string
	ShortID          string
	OwnerCountryCode string
}

// CreateRoom 创建一个新的房间执行单元，并把首个快照和事件落盘。
func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error) {
	if req == nil || req.GetMeta() == nil {
		return nil, xerr.New(xerr.InvalidArgument, "request is required")
	}
	ctx = contextFromMeta(ctx, req.GetMeta())
	startedAt := time.Now()
	var idempotencyMS int64
	var leaseMS int64
	var saveMutationMS int64
	var snapshotMS int64
	var projectionMS int64
	var tencentIMMS int64
	if req.GetMeta() == nil || req.GetMeta().GetRoomId() == "" {
		// 创建房间必须指定 room_id，后续 lease、meta、snapshot 都依赖它。
		return nil, xerr.New(xerr.InvalidArgument, "room_id is required")
	}
	if strings.TrimSpace(req.GetMeta().GetCommandId()) == "" {
		// 创建房间同样写 command log；command_id 必须来自客户端用户动作，不能用 request_id 代替。
		return nil, xerr.New(xerr.InvalidArgument, "command_id is required")
	}
	if !roomid.ValidStringID(req.GetMeta().GetRoomId()) {
		// 新房间必须能同时映射到腾讯云 IM GroupID 和 RTC strRoomId，避免创建后无法进语音。
		return nil, xerr.New(xerr.InvalidArgument, "room_id format is invalid")
	}
	mode := strings.TrimSpace(req.GetMode())
	if mode == "" {
		// mode 是房间状态对客户端和后续策略分发的基础维度，不能创建空模式房。
		return nil, xerr.New(xerr.InvalidArgument, "mode is required")
	}
	actorUserID := req.GetMeta().GetActorUserId()
	if actorUserID == 0 {
		// CreateRoom 的 owner 来自已鉴权调用方，不允许匿名内部命令造房间。
		return nil, xerr.New(xerr.InvalidArgument, "actor_user_id is required")
	}
	coverURL, coverConsumption, err := s.resolveSubmittedRoomCover(
		ctx, req.GetMeta().GetRequestId(), req.GetMeta().GetRoomId(), actorUserID,
		req.GetCoverUploadId(), req.GetRoomAvatar(), req.GetRobotRoom(),
	)
	if err != nil {
		return nil, err
	}
	profile, err := normalizeCreateRoomProfile(req, coverURL)
	if err != nil {
		return nil, err
	}
	seatCount, err := s.resolveRoomSeatCount(ctx, req.GetSeatCount())
	if err != nil {
		return nil, err
	}
	if s.isDraining() {
		return nil, xerr.New(xerr.Unavailable, "room service is draining")
	}

	// protobuf 请求先收敛为 command，后续幂等、持久化和恢复都只认识 command 模型。
	cmd := command.CreateRoom{
		Base:             baseFromMeta(req.GetMeta()),
		OwnerUserID:      actorUserID,
		SeatCount:        seatCount,
		Mode:             mode,
		VisibleRegionID:  normalizeVisibleRegionID(req.GetVisibleRegionId()),
		OwnerCountryCode: normalizeRoomCountryCode(req.GetOwnerCountryCode()),
		RoomName:         profile.Name,
		RoomAvatar:       profile.Avatar,
		CoverUploadID:    strings.TrimSpace(req.GetCoverUploadId()),
		RoomDescription:  profile.Description,
		RoomShortID:      normalizeRoomShortID(req.GetRoomShortId(), actorUserID),
		RobotRoom:        req.GetRobotRoom(),
		RobotUserIDs:     normalizeRobotUserIDs(req.GetRobotUserIds()),
	}

	idempotencyStartedAt := time.Now()
	payload, seen, err := s.commandPayloadForIdempotency(ctx, cmd)
	idempotencyMS = elapsedMS(idempotencyStartedAt)
	if err != nil {
		return nil, err
	}
	if seen {
		// 重复 CreateRoom 不再写入状态，直接返回当前快照保证命令幂等。
		snapshot, err := s.currentSnapshot(ctx, cmd.RoomID())
		if err != nil {
			return nil, err
		}
		// CreateRoom 不经过 mutateRoom 管线，幂等重试也要补齐恢复读模型，避免旧请求成功但投影缺失。
		projectionStartedAt := time.Now()
		s.projectRoomPresenceBestEffort(ctx, snapshot)
		// 幂等重试也补齐低频 VIP 列表投影，兼容首次创建成功但 wallet 当时短暂不可用的情况。
		s.refreshRoomOwnerVIPProjectionBestEffort(ctx, cmd.RequestID, cmd.OwnerUserID)
		projectionMS = elapsedMS(projectionStartedAt)
		logRoomCommandTiming(ctx, cmd.RoomID(), cmd.ID(), cmd.Type(), false, elapsedMS(startedAt), idempotencyMS, 0, 0, 0, projectionMS, 0, 0, 0, false, false)

		return &roomv1.CreateRoomResponse{
			Result: commandResult(false, snapshot.GetVersion(), s.clock.Now()),
			Room:   snapshot,
		}, nil
	}

	// 创建房间前先获得 owner lease，避免两个节点同时创建同一个 room_id。
	leaseStartedAt := time.Now()
	lease, err := s.directory.EnsureOwner(ctx, runtimeRoomKeyFromContext(ctx, cmd.RoomID()), s.nodeID, s.clock.Now(), s.leaseTTL)
	leaseMS = elapsedMS(leaseStartedAt)
	if err != nil {
		return nil, err
	}

	if lease.NodeID != s.nodeID {
		// 有其他有效 owner 时当前节点不能创建或接管该房间。
		return nil, roomOwnedByOtherNodeError(lease.NodeID)
	}

	if _, exists, err := s.repository.GetRoomMeta(ctx, cmd.RoomID()); err != nil {
		return nil, err
	} else if exists {
		// meta 已存在但 command 未命中，说明 room_id 已被其他创建命令占用。
		return nil, xerr.New(xerr.Conflict, "room already exists")
	}
	if existing, exists, err := s.repository.GetRoomMetaByOwner(ctx, cmd.OwnerUserID); err != nil {
		return nil, err
	} else if exists {
		// 一个用户只能创建一个房间；加入或管理别人的房间不受这个 owner 限制影响。
		return nil, xerr.New(xerr.Conflict, fmt.Sprintf("owner already has room: %s", existing.RoomID))
	}

	if err := s.ensureLeaseStillOwned(ctx, cmd.RoomID(), lease); err != nil {
		return nil, err
	}
	if s.syncPublisher != nil {
		tencentIMStartedAt := time.Now()
		if err := s.syncPublisher.EnsureRoomGroup(ctx, cmd.RoomID(), cmd.OwnerUserID); err != nil {
			// 创建房间是低频动作；房间群必须在返回前存在，避免 App JoinRoom 成功后遇到腾讯 IM 群未 ready。
			return nil, xerr.New(xerr.Unavailable, fmt.Sprintf("ensure room im group failed: %v", err))
		}
		tencentIMMS = elapsedMS(tencentIMStartedAt)
	}
	if err := s.ensureLeaseStillOwned(ctx, cmd.RoomID(), lease); err != nil {
		return nil, err
	}

	// 创建态直接初始化 RoomState，owner 作为首个在线用户进入房间。
	now := s.clock.Now()
	roomState := state.NewRoomState(cmd.RoomID(), cmd.OwnerUserID, cmd.SeatCount, cmd.Mode)
	roomState.Version = 1
	roomState.VisibleRegionID = cmd.VisibleRegionID
	applyRoomProfileExt(roomState, roomProfileInput{
		Name:             cmd.RoomName,
		Avatar:           cmd.RoomAvatar,
		Description:      cmd.RoomDescription,
		ShortID:          cmd.RoomShortID,
		OwnerCountryCode: cmd.OwnerCountryCode,
	})
	applyRobotRoomExt(roomState, cmd.RobotRoom, cmd.RobotUserIDs)
	roomState.OnlineUsers[cmd.ActorUserID()] = &state.RoomUserState{
		UserID:       cmd.ActorUserID(),
		Role:         "owner",
		JoinedAtMS:   now.UnixMilli(),
		LastSeenAtMS: now.UnixMilli(),
	}

	// RoomCreated 进入 outbox，供 activity/audit 等房间外系统异步消费。
	createdEvent, err := outbox.Build(cmd.RoomID(), "RoomCreated", roomState.Version, now, &roomeventsv1.RoomCreated{
		OwnerUserId:     cmd.OwnerUserID,
		SeatCount:       cmd.SeatCount,
		Mode:            cmd.Mode,
		RoomName:        cmd.RoomName,
		RoomAvatar:      cmd.RoomAvatar,
		RoomDescription: cmd.RoomDescription,
	})
	if err != nil {
		return nil, err
	}

	saveStartedAt := time.Now()
	if err := s.repository.SaveMutation(ctx, MutationCommit{
		CreateRoomMeta: &RoomMeta{
			AppCode:          cmd.AppCode,
			RoomID:           cmd.RoomID(),
			OwnerUserID:      cmd.OwnerUserID,
			SeatCount:        cmd.SeatCount,
			Mode:             cmd.Mode,
			Status:           state.RoomStatusActive,
			RoomPasswordHash: "",
			VisibleRegionID:  cmd.VisibleRegionID,
			RoomShortID:      cmd.RoomShortID,
		},
		Command: CommandRecord{
			AppCode:     cmd.AppCode,
			RoomID:      cmd.RoomID(),
			RoomVersion: roomState.Version,
			CommandID:   cmd.ID(),
			CommandType: cmd.Type(),
			Payload:     payload,
			Replayable:  true,
			OwnerNodeID: s.nodeID,
			LeaseToken:  lease.LeaseToken,
			CreatedAtMS: now.UnixMilli(),
		},
		OutboxRecords:              []outbox.Record{createdEvent},
		RoomCoverUploadConsumption: coverConsumption,
	}); err != nil {
		if errors.Is(err, ErrRoomCoverUploadNotConsumable) {
			return nil, xerr.New(xerr.Conflict, "room cover upload is already consumed")
		}
		return nil, err
	}
	saveMutationMS = elapsedMS(saveStartedAt)
	// SaveMutation 成功代表 RoomCreated outbox 已经随命令事务提交，可以立即唤醒 worker 解除 1 秒轮询下限。
	s.notifyOutboxCommitted([]outbox.Record{createdEvent})

	snapshot := snapshotWithApp(ctx, roomState.ToProto())
	snapshotStartedAt := time.Now()
	if err := s.persistSnapshot(ctx, snapshot); err != nil {
		return nil, err
	}
	snapshotMS = elapsedMS(snapshotStartedAt)
	projectionStartedAt := time.Now()
	s.projectRoomListBestEffort(ctx, snapshot)
	// 创建房间是低频写路径；在这里固化房主 VIP，列表接口无需新增 wallet RPC 或 SQL join。
	s.refreshRoomOwnerVIPProjectionBestEffort(ctx, cmd.RequestID, cmd.OwnerUserID)
	// owner 创建房间后已经是在线成员，必须立即投影到用户当前房间读模型。
	s.projectRoomPresenceBestEffort(ctx, snapshot)
	projectionMS = elapsedMS(projectionStartedAt)

	// 持久化成功后再安装内存 Cell，避免内存有房间但恢复来源不完整。
	s.installCell(ctx, cmd.RoomID(), cell.New(roomState.Clone(), rank.FromState(roomState.GiftRank, s.rankLimit)), lease.LeaseToken)
	logRoomCommandTiming(ctx, cmd.RoomID(), cmd.ID(), cmd.Type(), true, elapsedMS(startedAt), idempotencyMS, leaseMS, saveMutationMS, snapshotMS, projectionMS, 0, 0, tencentIMMS, true, true)

	return &roomv1.CreateRoomResponse{
		Result: commandResult(true, snapshot.GetVersion(), now),
		Room:   snapshot,
	}, nil
}

func logRoomCommandTiming(ctx context.Context, roomID string, commandID string, commandType string, applied bool, totalMS int64, idempotencyMS int64, leaseMS int64, saveMutationMS int64, snapshotMS int64, projectionMS int64, walletDebitMS int64, activityMS int64, tencentIMMS int64, activityAsyncOutbox bool, tencentIMAsyncOutbox bool, extra ...slog.Attr) {
	attrs := []slog.Attr{
		slog.String("room_id", roomID),
		slog.String("command_id", commandID),
		slog.String("command_type", commandType),
		slog.Bool("applied", applied),
		slog.Int64("total_ms", totalMS),
		slog.Int64("idempotency_ms", idempotencyMS),
		slog.Int64("lease_ms", leaseMS),
		slog.Int64("save_mutation_ms", saveMutationMS),
		slog.Int64("snapshot_ms", snapshotMS),
		slog.Int64("projection_ms", projectionMS),
		slog.Int64("wallet_debit_ms", walletDebitMS),
		slog.Int64("activity_ms", activityMS),
		slog.Int64("tencent_im_ms", tencentIMMS),
		slog.Bool("activity_async_outbox", activityAsyncOutbox),
		slog.Bool("tencent_im_async_outbox", tencentIMAsyncOutbox),
	}
	attrs = append(attrs, extra...)
	logx.Info(ctx, "room_command_timing", attrs...)
}

func normalizeRoomShortID(raw string, actorUserID int64) string {
	if value := strings.TrimSpace(raw); value != "" {
		return value
	}

	return fmt.Sprintf("%d", actorUserID)
}

// normalizeCreateRoomProfile 固定创建房间展示资料的入口语义，避免 gateway 和恢复路径各自猜默认值。
func normalizeCreateRoomProfile(req *roomv1.CreateRoomRequest, resolvedAvatar string) (roomProfileInput, error) {
	name := strings.TrimSpace(req.GetRoomName())
	if name == "" {
		// 房间名是创建页必填字段，也是列表卡片的最小可展示信息。
		return roomProfileInput{}, xerr.New(xerr.InvalidArgument, "room_name is required")
	}
	if utf8.RuneCountInString(name) > maxRoomNameRunes {
		// 使用 rune 计数避免中文房名被字节长度误伤；DB 和列表读模型仍按保守上限收敛。
		return roomProfileInput{}, xerr.New(xerr.InvalidArgument, "room_name is too long")
	}

	avatar := strings.TrimSpace(resolvedAvatar)
	if avatar == "" {
		// 权益制 App 的封面 URL 必须由已完成的 cover_upload_id 解析；legacy URL 也在进入这里前完成策略判断。
		return roomProfileInput{}, xerr.New(xerr.InvalidArgument, "room cover is required")
	}
	if utf8.RuneCountInString(avatar) > maxRoomAvatarRunes {
		// cover_url 会投影到 room_list_entries.cover_url，超长值必须在写入前拒绝。
		return roomProfileInput{}, xerr.New(xerr.InvalidArgument, "room_avatar is too long")
	}

	description := strings.TrimSpace(req.GetRoomDescription())
	if utf8.RuneCountInString(description) > maxRoomDescriptionRunes {
		// 简介暂时只服务房间详情，不允许无限大字符串进入 snapshot/command log。
		return roomProfileInput{}, xerr.New(xerr.InvalidArgument, "room_description is too long")
	}

	return roomProfileInput{Name: name, Avatar: avatar, Description: description}, nil
}

// applyRoomProfileExt 把房间展示资料写入 RoomExt，保持 RoomState 主结构只承载高频核心字段。
func applyRoomProfileExt(roomState *state.RoomState, profile roomProfileInput) {
	if roomState.RoomExt == nil {
		// 恢复旧快照或测试态可能拿到 nil map，写入前必须补齐容器。
		roomState.RoomExt = make(map[string]string)
	}

	roomState.RoomExt[roomExtTitleKey] = profile.Name
	roomState.RoomExt[roomExtCoverURLKey] = profile.Avatar
	roomState.RoomExt[roomExtDescriptionKey] = profile.Description
	roomState.RoomExt[roomExtShortIDKey] = profile.ShortID
	roomState.RoomExt[roomExtOwnerCountryKey] = normalizeRoomCountryCode(profile.OwnerCountryCode)
}

func applyRobotRoomExt(roomState *state.RoomState, robotRoom bool, robotUserIDs []int64) {
	if roomState == nil || !robotRoom {
		return
	}
	if roomState.RoomExt == nil {
		roomState.RoomExt = make(map[string]string)
	}
	roomState.RoomExt[roomExtRobotRoomKey] = "true"
	roomState.RoomExt[roomExtRobotUserIDsKey] = joinInt64CSV(normalizeRobotUserIDs(robotUserIDs))
}

func normalizeRobotUserIDs(values []int64) []int64 {
	seen := make(map[int64]bool, len(values))
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}

func joinInt64CSV(values []int64) string {
	parts := make([]string, 0, len(values))
	for _, value := range values {
		parts = append(parts, strconv.FormatInt(value, 10))
	}
	return strings.Join(parts, ",")
}

func parseInt64CSV(raw string) map[int64]bool {
	result := make(map[int64]bool)
	for _, part := range strings.Split(raw, ",") {
		value, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64)
		if err == nil && value > 0 {
			result[value] = true
		}
	}
	return result
}
