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) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	patch, err := s.normalizeUpdateRoomProfile(ctx, req)
	if err != nil {
		return nil, err
	}
	cmd := command.UpdateRoomProfile{
		Base:            baseFromMeta(req.GetMeta()),
		RoomName:        patch.Name,
		RoomAvatar:      patch.Avatar,
		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()}, 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,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}

		result := mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:     profileEvent.EventID,
				RoomID:      current.RoomID,
				EventType:   "room_profile_updated",
				ActorUserID: 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),
				},
			},
		}
		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        *string
	Avatar      *string
	Description *string
	SeatCount   *int32
}

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 {
		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
}

func applyProfilePatch(current *state.RoomState, cmd command.UpdateRoomProfile) bool {
	if current.RoomExt == nil {
		current.RoomExt = make(map[string]string)
	}
	changed := false
	if cmd.RoomName != nil && current.RoomExt[roomExtTitleKey] != *cmd.RoomName {
		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
}

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 {
		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
}
