package service

import (
	"context"
	"strings"
	"time"
	"unicode/utf8"

	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"
)

// UpdateRoomProfile 修改房间展示资料和麦位数量，所有状态变化仍由 Room Cell 串行提交。
func (s *Service) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error) {
	if req == nil || req.GetMeta() == nil {
		return nil, xerr.New(xerr.InvalidArgument, "request is required")
	}
	ctx = contextFromMeta(ctx, req.GetMeta())
	patch, err := s.normalizeUpdateRoomProfile(ctx, req)
	if err != nil {
		return nil, err
	}
	var coverConsumption *RoomCoverUploadConsumption
	if req.CoverUploadId != nil {
		if req.RoomAvatar != nil {
			return nil, xerr.New(xerr.InvalidArgument, "cover_upload_id and room_avatar are mutually exclusive")
		}
		coverURL, consumption, resolveErr := s.resolveSubmittedRoomCover(
			ctx, req.GetMeta().GetRequestId(), req.GetMeta().GetRoomId(), req.GetMeta().GetActorUserId(),
			req.GetCoverUploadId(), "", false,
		)
		if resolveErr != nil {
			return nil, resolveErr
		}
		patch.Avatar, coverConsumption = &coverURL, consumption
	} else if patch.Avatar != nil {
		coverURL, _, resolveErr := s.resolveSubmittedRoomCover(
			ctx, req.GetMeta().GetRequestId(), req.GetMeta().GetRoomId(), req.GetMeta().GetActorUserId(),
			"", *patch.Avatar, false,
		)
		if resolveErr != nil {
			return nil, resolveErr
		}
		patch.Avatar = &coverURL
	}
	cmd := command.UpdateRoomProfile{
		Base:            baseFromMeta(req.GetMeta()),
		RoomName:        patch.Name,
		RoomAvatar:      patch.Avatar,
		CoverUploadID:   strings.TrimSpace(req.GetCoverUploadId()),
		RoomDescription: patch.Description,
		SeatCount:       patch.SeatCount,
	}

	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 err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}

		profileChanged := applyProfilePatch(current, cmd)
		seatChanged, err := applySeatCountPatch(current, cmd.SeatCount)
		if err != nil {
			return mutationResult{}, nil, err
		}
		if !profileChanged && !seatChanged {
			// 未提交任何字段变化时仍通过 command log 保护 command_id 幂等，但不广播系统消息。
			return mutationResult{snapshot: current.ToProto(), roomCoverUploadConsumption: coverConsumption}, nil, nil
		}

		current.Version++
		seatCount := int32(len(current.MicSeats))
		profileEvent, err := outbox.Build(current.RoomID, "RoomProfileUpdated", current.Version, now, &roomeventsv1.RoomProfileUpdated{
			ActorUserId:       cmd.ActorUserID(),
			RoomName:          current.RoomExt[roomExtTitleKey],
			RoomAvatar:        current.RoomExt[roomExtCoverURLKey],
			RoomDescription:   current.RoomExt[roomExtDescriptionKey],
			SeatCount:         seatCount,
			RoomBackgroundUrl: current.RoomExt[roomExtBackgroundKey],
		})
		if err != nil {
			return mutationResult{}, nil, err
		}

		result := mutationResult{
			snapshot:                   current.ToProto(),
			roomCoverUploadConsumption: coverConsumption,
			syncEvent: &tencentim.RoomEvent{
				EventID:     profileEvent.EventID,
				RoomID:      current.RoomID,
				EventType:   "room_profile_updated",
				ActorUserID: tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				RoomVersion: current.Version,
				Attributes: map[string]string{
					"room_name":           current.RoomExt[roomExtTitleKey],
					"room_avatar":         current.RoomExt[roomExtCoverURLKey],
					"room_description":    current.RoomExt[roomExtDescriptionKey],
					"seat_count":          int32String(seatCount),
					"room_background_url": current.RoomExt[roomExtBackgroundKey],
				},
			},
		}
		if seatChanged {
			result.roomSeatCount = &seatCount
		}
		return result, []outbox.Record{profileEvent}, nil
	})
	if err != nil {
		return nil, err
	}

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

type updateRoomProfilePatch struct {
	// Name 为 nil 表示本次请求不修改房间名；非 nil 已完成裁剪和长度校验。
	Name *string
	// Avatar 为 nil 表示不修改头像；空头像在 normalize 阶段已被替换为系统默认头像。
	Avatar *string
	// Description 为 nil 表示不修改简介；非 nil 可以是空字符串，用于清空简介。
	Description *string
	// SeatCount 为 nil 表示不调整麦位数量；非 nil 已通过后台座位数配置校验。
	SeatCount *int32
}

// normalizeUpdateRoomProfile 把 protobuf patch 收敛为明确的领域补丁。
// 这里区分 nil 和空字符串：nil 是不改字段，空字符串是否允许由具体字段语义决定。
func (s *Service) normalizeUpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (updateRoomProfilePatch, error) {
	var patch updateRoomProfilePatch
	if req == nil {
		return patch, xerr.New(xerr.InvalidArgument, "request is required")
	}
	if req.RoomName != nil {
		// 房间名不允许清空，列表卡片和房间详情都依赖它提供最小展示信息。
		value := strings.TrimSpace(req.GetRoomName())
		if value == "" {
			return patch, xerr.New(xerr.InvalidArgument, "room_name is required")
		}
		if utf8.RuneCountInString(value) > maxRoomNameRunes {
			return patch, xerr.New(xerr.InvalidArgument, "room_name is too long")
		}
		patch.Name = &value
	}
	if req.RoomAvatar != nil {
		value := strings.TrimSpace(req.GetRoomAvatar())
		if value == "" {
			// 房主清空头像时恢复系统默认资源，避免列表卡片落空。
			value = defaultRoomAvatar
		}
		if utf8.RuneCountInString(value) > maxRoomAvatarRunes {
			return patch, xerr.New(xerr.InvalidArgument, "room_avatar is too long")
		}
		patch.Avatar = &value
	}
	if req.RoomDescription != nil {
		// 简介允许清空，但不能允许无限长内容进入 snapshot、列表投影和 command log。
		value := strings.TrimSpace(req.GetRoomDescription())
		if utf8.RuneCountInString(value) > maxRoomDescriptionRunes {
			return patch, xerr.New(xerr.InvalidArgument, "room_description is too long")
		}
		patch.Description = &value
	}
	if req.SeatCount != nil {
		// 座位数只能使用后台启用值，避免客户端直接制造超大麦位快照。
		value, err := s.validateRoomSeatCount(ctx, req.GetSeatCount())
		if err != nil {
			return patch, err
		}
		patch.SeatCount = &value
	}
	return patch, nil
}

// applyProfilePatch 只修改显式传入且确实变化的展示资料字段。
// 返回值用于决定是否递增房间版本和生成 RoomProfileUpdated 事件。
func applyProfilePatch(current *state.RoomState, cmd command.UpdateRoomProfile) bool {
	if current.RoomExt == nil {
		// 恢复旧快照或测试构造状态可能没有 RoomExt，写展示字段前必须补齐 map。
		current.RoomExt = make(map[string]string)
	}
	changed := false
	if cmd.RoomName != nil && current.RoomExt[roomExtTitleKey] != *cmd.RoomName {
		// RoomExt 是房间资料的低频扩展容器，避免把展示字段塞进高频核心结构。
		current.RoomExt[roomExtTitleKey] = *cmd.RoomName
		changed = true
	}
	if cmd.RoomAvatar != nil && current.RoomExt[roomExtCoverURLKey] != *cmd.RoomAvatar {
		current.RoomExt[roomExtCoverURLKey] = *cmd.RoomAvatar
		changed = true
	}
	if cmd.RoomDescription != nil && current.RoomExt[roomExtDescriptionKey] != *cmd.RoomDescription {
		current.RoomExt[roomExtDescriptionKey] = *cmd.RoomDescription
		changed = true
	}
	return changed
}

// applySeatCountPatch 调整麦位数组大小，不迁移用户、不隐式下麦、不自动解锁。
// 缩容只能删除完全空闲的高编号麦位，避免一个资料修改请求制造复合业务事件。
func applySeatCountPatch(current *state.RoomState, seatCount *int32) (bool, error) {
	if seatCount == nil {
		return false, nil
	}
	target := *seatCount
	currentCount := int32(len(current.MicSeats))
	if target == currentCount {
		// 相同座位数是 no-op，仍由外层 command log 负责幂等记录。
		return false, nil
	}
	if target > currentCount {
		// 扩容只追加高编号空麦位，现有麦位编号和用户占用状态保持稳定。
		for seatNo := currentCount + 1; seatNo <= target; seatNo++ {
			current.MicSeats = append(current.MicSeats, state.MicSeat{SeatNo: seatNo})
		}
		return true, nil
	}

	nextSeats := make([]state.MicSeat, 0, target)
	for _, seat := range current.MicSeats {
		if seat.SeatNo <= target {
			// 保留目标范围内的原麦位，锁定、占用和发流状态都原样保留。
			nextSeats = append(nextSeats, seat)
			continue
		}
		if seat.UserID != 0 || seat.Locked || seat.PublishState != "" || seat.MicSessionID != "" {
			// 缩容不能隐式踢人、清理发流会话或解锁麦位；房主需要先把高编号麦位处理干净。
			return false, xerr.New(xerr.Conflict, "removed seats must be empty and unlocked")
		}
	}
	current.MicSeats = nextSeats
	return true, nil
}
