package service

import (
	"context"
	"fmt"
	"log/slog"
	"math"
	"strings"
	"time"

	"github.com/redis/go-redis/v9"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
)

const (
	roomGiftLeaderboardPeriodToday = "today"
	roomGiftLeaderboardPeriodWeek  = "week"
	roomGiftLeaderboardPeriodMonth = "month"

	defaultRoomGiftLeaderboardPageSize = 20
	maxRoomGiftLeaderboardPageSize     = 100
)

// RoomGiftLeaderboardStore 是跨房间贡献榜的轻量读模型边界；生产实现使用 Redis zset。
type RoomGiftLeaderboardStore interface {
	IncrementRoomGift(ctx context.Context, input RoomGiftLeaderboardIncrement) error
	ListRoomGiftLeaderboard(ctx context.Context, query RoomGiftLeaderboardQuery) (RoomGiftLeaderboardPage, error)
}

type RoomGiftLeaderboardIncrement struct {
	AppCode string
	RoomID  string
	// RegionID 是送礼发生时房间的 visible_region_id；跨区换房不会改写历史周期归属。
	RegionID int64
	// CoinSpent 沿用 proto 字段名，运行值实际是 wallet 折算后的房间贡献值。
	CoinSpent    int64
	OccurredAtMS int64
}

type RoomGiftLeaderboardQuery struct {
	AppCode  string
	RegionID int64
	Period   string
	Page     int
	PageSize int
	NowMS    int64
}

type RoomGiftLeaderboardPage struct {
	Items        []RoomGiftLeaderboardEntry
	Total        int64
	Period       string
	StartAtMS    int64
	EndAtMS      int64
	ServerTimeMS int64
}

type RoomGiftLeaderboardEntry struct {
	Rank      int64
	RoomID    string
	CoinSpent int64
}

// RedisRoomGiftLeaderboardStore 只在 zset 中保存 room_id -> 周期房间贡献分。
type RedisRoomGiftLeaderboardStore struct {
	client *redis.Client
}

func NewRedisRoomGiftLeaderboardStore(client *redis.Client) *RedisRoomGiftLeaderboardStore {
	return &RedisRoomGiftLeaderboardStore{client: client}
}

func (s *RedisRoomGiftLeaderboardStore) IncrementRoomGift(ctx context.Context, input RoomGiftLeaderboardIncrement) error {
	if s == nil || s.client == nil {
		return xerr.New(xerr.Unavailable, "room gift leaderboard store is not configured")
	}
	app := appcode.Normalize(input.AppCode)
	roomID := strings.TrimSpace(input.RoomID)
	if roomID == "" || input.CoinSpent <= 0 {
		return nil
	}
	occurredAt := time.UnixMilli(input.OccurredAtMS).UTC()
	if input.OccurredAtMS <= 0 {
		occurredAt = time.Now().UTC()
	}

	pipe := s.client.Pipeline()
	for _, period := range []string{roomGiftLeaderboardPeriodToday, roomGiftLeaderboardPeriodWeek, roomGiftLeaderboardPeriodMonth} {
		start, end := roomGiftLeaderboardWindow(period, occurredAt)
		legacyKey := roomGiftLeaderboardKey(app, period, start)
		pipe.ZIncrBy(ctx, legacyKey, float64(input.CoinSpent), roomID)
		// Redis 榜单是可重建读模型，TTL 只需要覆盖当前周期和排查窗口。
		pipe.Expire(ctx, legacyKey, roomGiftLeaderboardTTL(period, start, end))
		if input.RegionID > 0 {
			// 所有 App 都同时维护全局与区域读模型；Gateway 根据后台配置选择读取范围，room-service 不识别产品。
			regionalKey := roomGiftLeaderboardScopedKey(app, input.RegionID, period, start)
			pipe.ZIncrBy(ctx, regionalKey, float64(input.CoinSpent), roomID)
			pipe.Expire(ctx, regionalKey, roomGiftLeaderboardTTL(period, start, end))
		}
	}
	_, err := pipe.Exec(ctx)
	return err
}

func (s *RedisRoomGiftLeaderboardStore) ListRoomGiftLeaderboard(ctx context.Context, query RoomGiftLeaderboardQuery) (RoomGiftLeaderboardPage, error) {
	if s == nil || s.client == nil {
		return RoomGiftLeaderboardPage{}, xerr.New(xerr.Unavailable, "room gift leaderboard store is not configured")
	}
	query = normalizeRoomGiftLeaderboardQuery(query)
	now := time.UnixMilli(query.NowMS).UTC()
	start, end := roomGiftLeaderboardWindow(query.Period, now)
	key := roomGiftLeaderboardScopedKey(query.AppCode, query.RegionID, query.Period, start)
	offset := int64((query.Page - 1) * query.PageSize)
	stop := offset + int64(query.PageSize) - 1

	total, err := s.client.ZCard(ctx, key).Result()
	if err != nil {
		return RoomGiftLeaderboardPage{}, err
	}
	rows, err := s.client.ZRevRangeWithScores(ctx, key, offset, stop).Result()
	if err != nil {
		return RoomGiftLeaderboardPage{}, err
	}

	items := make([]RoomGiftLeaderboardEntry, 0, len(rows))
	for i, row := range rows {
		roomID, ok := row.Member.(string)
		if !ok || strings.TrimSpace(roomID) == "" {
			continue
		}
		items = append(items, RoomGiftLeaderboardEntry{
			Rank:      offset + int64(i) + 1,
			RoomID:    roomID,
			CoinSpent: int64(math.Round(row.Score)),
		})
	}
	return RoomGiftLeaderboardPage{
		Items:        items,
		Total:        total,
		Period:       query.Period,
		StartAtMS:    start.UnixMilli(),
		EndAtMS:      end.UnixMilli(),
		ServerTimeMS: now.UnixMilli(),
	}, nil
}

func (s *Service) ListRoomGiftLeaderboard(ctx context.Context, req *roomv1.ListRoomGiftLeaderboardRequest) (*roomv1.ListRoomGiftLeaderboardResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	if s.roomGiftLeaderboard == nil {
		return nil, xerr.New(xerr.Unavailable, "room gift leaderboard store is not configured")
	}
	period := normalizeRoomGiftLeaderboardPeriod(req.GetPeriod())
	if period == "" {
		return nil, xerr.New(xerr.InvalidArgument, "period is invalid")
	}
	page := int(req.GetPage())
	if page <= 0 {
		page = 1
	}
	pageSize := int(req.GetPageSize())
	if pageSize <= 0 {
		pageSize = defaultRoomGiftLeaderboardPageSize
	}
	if pageSize > maxRoomGiftLeaderboardPageSize {
		pageSize = maxRoomGiftLeaderboardPageSize
	}

	result, err := s.roomGiftLeaderboard.ListRoomGiftLeaderboard(ctx, RoomGiftLeaderboardQuery{
		AppCode:  appcode.FromContext(ctx),
		RegionID: req.GetRegionId(),
		Period:   period,
		Page:     page,
		PageSize: pageSize,
		NowMS:    s.clock.Now().UnixMilli(),
	})
	if err != nil {
		return nil, err
	}

	roomIDs := make([]string, 0, len(result.Items))
	for _, item := range result.Items {
		roomIDs = append(roomIDs, item.RoomID)
	}
	cards, err := s.repository.ListRoomListEntriesByIDs(ctx, RoomListEntriesByIDQuery{
		AppCode: appcode.FromContext(ctx),
		RoomIDs: roomIDs,
	})
	if err != nil {
		return nil, err
	}

	items := make([]*roomv1.RoomGiftLeaderboardItem, 0, len(result.Items))
	for _, item := range result.Items {
		protoItem := &roomv1.RoomGiftLeaderboardItem{
			Rank:      item.Rank,
			RoomId:    item.RoomID,
			CoinSpent: item.CoinSpent,
		}
		if card, ok := cards[item.RoomID]; ok {
			protoItem.Room = roomListItemToProto(card)
		}
		items = append(items, protoItem)
	}
	return &roomv1.ListRoomGiftLeaderboardResponse{
		Items:        items,
		Total:        result.Total,
		Period:       result.Period,
		StartAtMs:    result.StartAtMS,
		EndAtMs:      result.EndAtMS,
		ServerTimeMs: result.ServerTimeMS,
	}, nil
}

func (s *Service) recordRoomGiftLeaderboardBestEffort(ctx context.Context, input *RoomGiftLeaderboardIncrement) {
	if s == nil || s.roomGiftLeaderboard == nil || input == nil {
		return
	}
	if err := s.roomGiftLeaderboard.IncrementRoomGift(ctx, *input); err != nil {
		// 榜单 zset 是可重建读模型，失败不能回滚已提交房间命令和账务事实。
		logx.Warn(ctx, "room_gift_leaderboard_update_failed", slog.String("room_id", input.RoomID), slog.Int64("coin_spent", input.CoinSpent), slog.String("error", err.Error()))
		return
	}
}

func normalizeRoomGiftLeaderboardQuery(query RoomGiftLeaderboardQuery) RoomGiftLeaderboardQuery {
	query.AppCode = appcode.Normalize(query.AppCode)
	query.Period = normalizeRoomGiftLeaderboardPeriod(query.Period)
	if query.Period == "" {
		query.Period = roomGiftLeaderboardPeriodToday
	}
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = defaultRoomGiftLeaderboardPageSize
	}
	if query.PageSize > maxRoomGiftLeaderboardPageSize {
		query.PageSize = maxRoomGiftLeaderboardPageSize
	}
	if query.NowMS <= 0 {
		query.NowMS = time.Now().UTC().UnixMilli()
	}
	return query
}

func normalizeRoomGiftLeaderboardPeriod(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", roomGiftLeaderboardPeriodToday, "day", "daily":
		return roomGiftLeaderboardPeriodToday
	case roomGiftLeaderboardPeriodWeek, "weekly":
		return roomGiftLeaderboardPeriodWeek
	case roomGiftLeaderboardPeriodMonth, "monthly":
		return roomGiftLeaderboardPeriodMonth
	default:
		return ""
	}
}

func roomGiftLeaderboardWindow(period string, now time.Time) (time.Time, time.Time) {
	dayStart := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC)
	switch period {
	case roomGiftLeaderboardPeriodWeek:
		// 周房间贡献榜按 UTC 周一 01:00 切周，避免 00:00 结算任务消费上一周期时 Redis 周榜已经换 key。
		if now.UTC().Before(dayStart.Add(time.Hour)) {
			dayStart = dayStart.AddDate(0, 0, -1)
		}
		weekday := int(dayStart.Weekday())
		if weekday == 0 {
			weekday = 7
		}
		start := dayStart.AddDate(0, 0, 1-weekday).Add(time.Hour)
		return start, start.AddDate(0, 0, 7)
	case roomGiftLeaderboardPeriodMonth:
		start := time.Date(dayStart.Year(), dayStart.Month(), 1, 0, 0, 0, 0, time.UTC)
		return start, start.AddDate(0, 1, 0)
	default:
		return dayStart, dayStart.AddDate(0, 0, 1)
	}
}

func roomGiftLeaderboardKey(app string, period string, start time.Time) string {
	switch period {
	case roomGiftLeaderboardPeriodWeek:
		year, week := start.ISOWeek()
		return fmt.Sprintf("room:gift_leaderboard:%s:week:%04d%02d", appcode.Normalize(app), year, week)
	case roomGiftLeaderboardPeriodMonth:
		return fmt.Sprintf("room:gift_leaderboard:%s:month:%04d%02d", appcode.Normalize(app), start.Year(), int(start.Month()))
	default:
		return fmt.Sprintf("room:gift_leaderboard:%s:day:%s", appcode.Normalize(app), start.Format("20060102"))
	}
}

func roomGiftLeaderboardScopedKey(app string, regionID int64, period string, start time.Time) string {
	if regionID <= 0 {
		return roomGiftLeaderboardKey(app, period, start)
	}
	switch period {
	case roomGiftLeaderboardPeriodWeek:
		year, week := start.ISOWeek()
		return fmt.Sprintf("room:gift_leaderboard:%s:region:%d:week:%04d%02d", appcode.Normalize(app), regionID, year, week)
	case roomGiftLeaderboardPeriodMonth:
		return fmt.Sprintf("room:gift_leaderboard:%s:region:%d:month:%04d%02d", appcode.Normalize(app), regionID, start.Year(), int(start.Month()))
	default:
		return fmt.Sprintf("room:gift_leaderboard:%s:region:%d:day:%s", appcode.Normalize(app), regionID, start.Format("20060102"))
	}
}

func roomGiftLeaderboardTTL(period string, start time.Time, end time.Time) time.Duration {
	switch period {
	case roomGiftLeaderboardPeriodMonth:
		return end.Sub(start) + 35*24*time.Hour
	case roomGiftLeaderboardPeriodWeek:
		return end.Sub(start) + 21*24*time.Hour
	default:
		return end.Sub(start) + 7*24*time.Hour
	}
}
