package service

import (
	"context"
	"errors"
	"log/slog"
	"strings"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
)

var adminSeatCandidates = []int32{10, 15, 20, 25, 30}

const (
	roomPinTypeRegion  = "region"
	roomPinTypeGlobal  = "global"
	roomPinTypeCountry = "country"
	roomPinDayMillis   = int64(24 * 60 * 60 * 1000)
)

func (s *Service) AdminGetRoomSeatConfig(ctx context.Context, _ *roomv1.AdminGetRoomSeatConfigRequest) (*roomv1.AdminGetRoomSeatConfigResponse, error) {
	config, err := s.currentRoomSeatConfig(ctx)
	if err != nil {
		return nil, err
	}
	return &roomv1.AdminGetRoomSeatConfigResponse{Config: seatConfigToAdminProto(config), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
}

func (s *Service) AdminUpdateRoomSeatConfig(ctx context.Context, req *roomv1.AdminUpdateRoomSeatConfigRequest) (*roomv1.AdminUpdateRoomSeatConfigResponse, error) {
	if req == nil {
		return nil, xerr.New(xerr.InvalidArgument, "room seat config is required")
	}
	config, err := normalizeRoomSeatConfig(RoomSeatConfig{
		AppCode:           appcode.FromContext(ctx),
		AllowedSeatCounts: req.GetAllowedSeatCounts(),
		DefaultSeatCount:  req.GetDefaultSeatCount(),
		UpdatedAtMS:       s.clock.Now().UTC().UnixMilli(),
	})
	if err != nil {
		return nil, xerr.New(xerr.InvalidArgument, err.Error())
	}
	if err := s.repository.UpsertRoomSeatConfig(ctx, config); err != nil {
		return nil, err
	}
	return &roomv1.AdminUpdateRoomSeatConfigResponse{Config: seatConfigToAdminProto(config), ServerTimeMs: config.UpdatedAtMS}, nil
}

func (s *Service) AdminListRoomPins(ctx context.Context, req *roomv1.AdminListRoomPinsRequest) (*roomv1.AdminListRoomPinsResponse, error) {
	nowMS := s.clock.Now().UTC().UnixMilli()
	pins, total, err := s.repository.AdminListRoomPins(ctx, AdminRoomPinQuery{
		Page:     int(req.GetPage()),
		PageSize: int(req.GetPageSize()),
		Keyword:  strings.TrimSpace(req.GetKeyword()),
		Status:   strings.TrimSpace(req.GetStatus()),
		RegionID: req.GetVisibleRegionId(),
		PinType:  normalizeRoomPinListType(req.GetPinType()),
		NowMS:    nowMS,
	})
	if err != nil {
		// gRPC 传输层会把普通 repository error 统一隐藏成 INTERNAL_ERROR；这里保留原始错误和筛选边界，
		// 让线上排障能直接区分 SQL/schema 问题、上下文租户问题和输入条件问题。
		logx.Error(ctx, "admin_list_room_pins_failed", err,
			slog.Int("page", int(req.GetPage())),
			slog.Int("page_size", int(req.GetPageSize())),
			slog.String("status", strings.TrimSpace(req.GetStatus())),
			slog.Int64("visible_region_id", req.GetVisibleRegionId()),
			slog.String("pin_type", normalizeRoomPinListType(req.GetPinType())),
			slog.Bool("has_keyword", strings.TrimSpace(req.GetKeyword()) != ""),
		)
		return nil, err
	}
	out := make([]*roomv1.AdminRoomPin, 0, len(pins))
	for _, pin := range pins {
		if pin.Status == "active" && pin.ExpiresAtMS <= nowMS {
			// 过期不需要回写房间状态；对后台读模型呈现 expired，公共列表查询也会自动把它排除出置顶 join。
			pin.Status = "expired"
		}
		out = append(out, roomPinToAdminProto(pin))
	}
	return &roomv1.AdminListRoomPinsResponse{Pins: out, Total: total, ServerTimeMs: nowMS}, nil
}

func (s *Service) AdminCreateRoomPin(ctx context.Context, req *roomv1.AdminCreateRoomPinRequest) (*roomv1.AdminCreateRoomPinResponse, error) {
	if req == nil || strings.TrimSpace(req.GetRoomId()) == "" {
		return nil, xerr.New(xerr.InvalidArgument, "room id is required")
	}
	if req.GetWeight() < 0 {
		return nil, xerr.New(xerr.InvalidArgument, "room pin weight is invalid")
	}
	nowMS := s.clock.Now().UTC().UnixMilli()
	pinnedAtMS, expiresAtMS, err := normalizeAdminRoomPinInterval(req.GetPinnedAtMs(), req.GetExpiresAtMs(), req.GetDurationDays(), nowMS)
	if err != nil {
		return nil, xerr.New(xerr.InvalidArgument, err.Error())
	}
	pin, ok, err := s.repository.AdminCreateRoomPin(ctx, AdminCreateRoomPinInput{
		RoomID:       strings.TrimSpace(req.GetRoomId()),
		PinType:      normalizeRoomPinType(req.GetPinType()),
		Weight:       req.GetWeight(),
		DurationDays: req.GetDurationDays(),
		PinnedAtMS:   pinnedAtMS,
		ExpiresAtMS:  expiresAtMS,
		AdminID:      req.GetAdminId(),
		NowMS:        nowMS,
	})
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, xerr.New(xerr.Conflict, "room is not active or not found")
	}
	s.projectRoomPinCacheBestEffort(ctx, roomListPinEntryFromAdmin(pin))
	return &roomv1.AdminCreateRoomPinResponse{Pin: roomPinToAdminProto(pin), ServerTimeMs: nowMS}, nil
}

func (s *Service) AdminCancelRoomPin(ctx context.Context, req *roomv1.AdminCancelRoomPinRequest) (*roomv1.AdminCancelRoomPinResponse, error) {
	if req == nil || req.GetPinId() <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "room pin id is invalid")
	}
	nowMS := s.clock.Now().UTC().UnixMilli()
	pin, ok, err := s.repository.AdminCancelRoomPin(ctx, req.GetPinId(), req.GetAdminId(), nowMS)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, xerr.New(xerr.NotFound, "room pin not found")
	}
	s.projectRoomPinCacheBestEffort(ctx, roomListPinEntryFromAdmin(pin))
	return &roomv1.AdminCancelRoomPinResponse{Pin: roomPinToAdminProto(pin), ServerTimeMs: nowMS}, nil
}

func (s *Service) projectRoomPinCacheBestEffort(ctx context.Context, pin RoomListPinEntry) {
	if s == nil || s.roomListCache == nil {
		return
	}
	if err := s.roomListCache.UpsertRoomPin(ctx, pin); err != nil {
		// 后台置顶 owner 已经写入 MySQL；缓存失败只影响短暂展示顺序，后续 refresh 会从 owner 表重建。
		logx.Warn(ctx, "room_pin_cache_project_failed", slog.Int64("pin_id", pin.ID), slog.String("room_id", pin.RoomID), slog.String("pin_type", pin.PinType), slog.String("error", err.Error()))
	}
}

func roomListPinEntryFromAdmin(pin AdminRoomPinEntry) RoomListPinEntry {
	return RoomListPinEntry{
		ID:              pin.ID,
		AppCode:         pin.AppCode,
		VisibleRegionID: pin.VisibleRegionID,
		PinType:         pin.PinType,
		RoomID:          pin.RoomID,
		Weight:          pin.Weight,
		Status:          pin.Status,
		PinnedAtMS:      pin.PinnedAtMS,
		ExpiresAtMS:     pin.ExpiresAtMS,
	}
}

func seatConfigToAdminProto(config RoomSeatConfig) *roomv1.AdminRoomSeatConfig {
	return &roomv1.AdminRoomSeatConfig{
		CandidateSeatCounts: append([]int32(nil), adminSeatCandidates...),
		AllowedSeatCounts:   append([]int32(nil), config.AllowedSeatCounts...),
		DefaultSeatCount:    config.DefaultSeatCount,
		UpdatedAtMs:         config.UpdatedAtMS,
	}
}

func roomPinToAdminProto(pin AdminRoomPinEntry) *roomv1.AdminRoomPin {
	return &roomv1.AdminRoomPin{
		Id:                 pin.ID,
		VisibleRegionId:    pin.VisibleRegionID,
		RoomId:             pin.RoomID,
		Weight:             pin.Weight,
		Status:             pin.Status,
		PinType:            pin.PinType,
		PinnedAtMs:         pin.PinnedAtMS,
		ExpiresAtMs:        pin.ExpiresAtMS,
		CancelledAtMs:      pin.CancelledAtMS,
		CreatedByAdminId:   pin.CreatedByAdminID,
		CancelledByAdminId: pin.CancelledByAdminID,
		CreatedAtMs:        pin.CreatedAtMS,
		UpdatedAtMs:        pin.UpdatedAtMS,
		Room: &roomv1.AdminRoomPinRoom{
			RoomId:          pin.RoomID,
			RoomShortId:     pin.RoomShortID,
			VisibleRegionId: pin.RoomVisibleRegionID,
			OwnerUserId:     pin.OwnerUserID,
			Title:           pin.Title,
			CoverUrl:        pin.CoverURL,
			Status:          pin.RoomStatus,
		},
	}
}

func normalizeRoomPinType(pinType string) string {
	switch strings.ToLower(strings.TrimSpace(pinType)) {
	case roomPinTypeGlobal:
		return roomPinTypeGlobal
	case roomPinTypeCountry:
		return roomPinTypeCountry
	default:
		// 空值和未知值都回落到区域置顶，兼容旧后台只传 room_id/duration_days 的请求。
		return roomPinTypeRegion
	}
}

func normalizeRoomPinListType(pinType string) string {
	switch strings.ToLower(strings.TrimSpace(pinType)) {
	case roomPinTypeRegion:
		return roomPinTypeRegion
	case roomPinTypeGlobal:
		return roomPinTypeGlobal
	case roomPinTypeCountry:
		return roomPinTypeCountry
	default:
		// 列表空类型代表“不按置顶类型筛选”；不能复用创建默认 region，否则国家置顶会从后台总列表消失。
		return ""
	}
}

func normalizeAdminRoomPinInterval(pinnedAtMS int64, expiresAtMS int64, durationDays int64, nowMS int64) (int64, int64, error) {
	if pinnedAtMS <= 0 {
		pinnedAtMS = nowMS
	}
	if expiresAtMS <= 0 && durationDays > 0 {
		expiresAtMS = pinnedAtMS + durationDays*roomPinDayMillis
	}
	if expiresAtMS <= pinnedAtMS {
		return 0, 0, errors.New("room pin time range is invalid")
	}
	if expiresAtMS-pinnedAtMS > 3650*roomPinDayMillis {
		return 0, 0, errors.New("room pin time range is too long")
	}
	return pinnedAtMS, expiresAtMS, nil
}
