package service

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

	"golang.org/x/crypto/bcrypt"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"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"
)

const maxRoomPasswordRunes = 64

// SetRoomPassword 设置或清空房间入房密码。
// 密码属于 Room Cell 核心状态：JoinRoom、快照恢复和列表投影都必须以同一份状态为准。
func (s *Service) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	password, passwordHash, err := normalizeRoomPassword(req.GetLocked(), req.GetPassword())
	if err != nil {
		return nil, err
	}

	cmd := command.SetRoomPassword{
		Base:         baseFromMeta(req.GetMeta()),
		Locked:       req.GetLocked(),
		Password:     password,
		PasswordHash: passwordHash,
	}

	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 {
			// 锁房只允许 owner 操作，避免普通房管把房主挡在自己的房间外。
			return mutationResult{}, nil, err
		}

		if !cmd.Locked {
			if current.RoomPasswordHash == "" {
				// 重复解锁是 no-op，只写 command log 保护幂等。
				return mutationResult{snapshot: current.ToProto()}, nil, nil
			}
			current.RoomPasswordHash = ""
		} else {
			if roomPasswordMatches(current.RoomPasswordHash, password) {
				// 同一个密码重复提交不产生版本和系统消息，避免客户端收到无意义状态变更。
				return mutationResult{snapshot: current.ToProto()}, nil, nil
			}
			current.RoomPasswordHash = cmd.PasswordHash
		}

		current.Version++
		currentHash := current.RoomPasswordHash
		passwordEvent, err := outbox.Build(current.RoomID, "RoomPasswordChanged", current.Version, now, &roomeventsv1.RoomPasswordChanged{
			ActorUserId:     cmd.ActorUserID(),
			Locked:          cmd.Locked,
			VisibleRegionId: current.VisibleRegionID,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}

		return mutationResult{
			snapshot:         current.ToProto(),
			roomPasswordHash: &currentHash,
		}, []outbox.Record{passwordEvent}, nil
	})
	if err != nil {
		return nil, err
	}

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

func normalizeRoomPassword(locked bool, raw string) (string, string, error) {
	password := strings.TrimSpace(raw)
	if !locked {
		return "", "", nil
	}
	if password == "" {
		return "", "", xerr.New(xerr.InvalidArgument, "room password is required")
	}
	if utf8.RuneCountInString(password) > maxRoomPasswordRunes {
		// 密码只服务入房校验，必须给 snapshot 和 command log 一个有界载荷。
		return "", "", xerr.New(xerr.InvalidArgument, "room password is too long")
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", "", err
	}
	return password, string(hash), nil
}

func roomPasswordMatches(hash string, password string) bool {
	if hash == "" || password == "" {
		return false
	}
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
