package userleaderboard

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"hyapp-admin-server/internal/integration/roomclient"
)

const (
	BoardTypeSent     = "sent"
	BoardTypeReceived = "received"
	BoardTypeRoom     = "room"

	PeriodToday = "today"
	PeriodWeek  = "week"
	PeriodMonth = "month"

	maxPageSize = 100
)

var errInvalidArgument = errors.New("invalid argument")

type Service struct {
	walletDB *sql.DB
	userDB   *sql.DB
	room     roomclient.Client
}

type Query struct {
	BoardType    string
	Period       string
	Page         int
	PageSize     int
	ViewerUserID int64
}

type Response struct {
	Items        []Item `json:"items"`
	Total        int64  `json:"total"`
	Page         int    `json:"page"`
	PageSize     int    `json:"page_size"`
	BoardType    string `json:"board_type"`
	Period       string `json:"period"`
	StartAtMS    int64  `json:"start_at_ms"`
	EndAtMS      int64  `json:"end_at_ms"`
	ServerTimeMS int64  `json:"server_time_ms"`
	MyRank       *Item  `json:"my_rank,omitempty"`
}

type Item struct {
	Rank             int64  `json:"rank"`
	UserID           string `json:"user_id,omitempty"`
	RoomID           string `json:"room_id,omitempty"`
	GiftValue        int64  `json:"gift_value"`
	GiftCount        int64  `json:"gift_count"`
	TransactionCount int64  `json:"transaction_count"`
	LastGiftAtMS     int64  `json:"last_gift_at_ms"`
	User             *User  `json:"user,omitempty"`
	Room             *Room  `json:"room,omitempty"`
}

type User struct {
	UserID        string `json:"user_id"`
	DisplayUserID string `json:"display_user_id,omitempty"`
	Username      string `json:"username,omitempty"`
	Avatar        string `json:"avatar,omitempty"`
}

type Room struct {
	RoomID      string `json:"room_id"`
	RoomShortID string `json:"room_short_id,omitempty"`
	Title       string `json:"title,omitempty"`
	CoverURL    string `json:"cover_url,omitempty"`
	OwnerUserID string `json:"owner_user_id,omitempty"`
	Status      string `json:"status,omitempty"`
}

type userProfile struct {
	UserID        int64
	DisplayUserID string
	Username      string
	Avatar        string
}

type roomProfile struct {
	RoomID      string
	RoomShortID string
	Title       string
	CoverURL    string
	OwnerUserID int64
	Status      string
}

func NewService(walletDB *sql.DB, userDB *sql.DB, room roomclient.Client) *Service {
	return &Service{walletDB: walletDB, userDB: userDB, room: room}
}

func (s *Service) List(ctx context.Context, appCode string, query Query) (Response, error) {
	query = NormalizeQuery(query)
	if query.BoardType == "" || query.Period == "" {
		return Response{}, errInvalidArgument
	}
	if s == nil || s.walletDB == nil {
		return Response{}, fmt.Errorf("wallet mysql is not configured")
	}

	now := time.Now().UTC()
	start, end := periodWindow(query.Period, now)
	startMS := start.UnixMilli()
	endMS := end.UnixMilli()

	total, err := s.count(ctx, appCode, query.BoardType, startMS, endMS)
	if err != nil {
		return Response{}, err
	}
	items, err := s.listItems(ctx, appCode, query.BoardType, startMS, endMS, query.PageSize, offset(query.Page, query.PageSize))
	if err != nil {
		return Response{}, err
	}

	var myRank *Item
	if query.ViewerUserID > 0 && query.BoardType != BoardTypeRoom {
		item, found, err := s.rankForUser(ctx, appCode, query.BoardType, startMS, endMS, query.ViewerUserID)
		if err != nil {
			return Response{}, err
		}
		if found {
			myRank = &item
		}
	}

	if err := s.enrich(ctx, appCode, query.BoardType, items, myRank); err != nil {
		return Response{}, err
	}

	return Response{
		Items:        items,
		Total:        total,
		Page:         query.Page,
		PageSize:     query.PageSize,
		BoardType:    query.BoardType,
		Period:       query.Period,
		StartAtMS:    startMS,
		EndAtMS:      endMS,
		ServerTimeMS: now.UnixMilli(),
		MyRank:       myRank,
	}, nil
}

func NormalizeBoardType(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "sent", "send", "sender", "gift_sent", "user_sent":
		return BoardTypeSent
	case "received", "receive", "receiver", "gift_received", "user_received":
		return BoardTypeReceived
	case "room", "rooms", "room_gift", "room_gifts":
		return BoardTypeRoom
	default:
		return ""
	}
}

func NormalizePeriod(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "today", "day", "daily":
		return PeriodToday
	case "week", "weekly":
		return PeriodWeek
	case "month", "monthly":
		return PeriodMonth
	default:
		return ""
	}
}

func NormalizeQuery(query Query) Query {
	query.BoardType = NormalizeBoardType(query.BoardType)
	query.Period = NormalizePeriod(query.Period)
	if query.Page <= 0 {
		query.Page = 1
	}
	if query.PageSize <= 0 {
		query.PageSize = 20
	}
	if query.PageSize > maxPageSize {
		query.PageSize = maxPageSize
	}
	return query
}

func periodWindow(period string, now time.Time) (time.Time, time.Time) {
	utcNow := now.UTC()
	dayStart := time.Date(utcNow.Year(), utcNow.Month(), utcNow.Day(), 0, 0, 0, 0, time.UTC)
	switch period {
	case PeriodWeek:
		weekday := int(dayStart.Weekday())
		if weekday == 0 {
			weekday = 7
		}
		return dayStart.AddDate(0, 0, 1-weekday), utcNow
	case PeriodMonth:
		return time.Date(utcNow.Year(), utcNow.Month(), 1, 0, 0, 0, 0, time.UTC), utcNow
	default:
		return dayStart, utcNow
	}
}

func (s *Service) count(ctx context.Context, appCode string, boardType string, startMS int64, endMS int64) (int64, error) {
	subject, predicate := leaderboardDimension(boardType)
	// 排行榜价值优先使用送礼时按真实扣费比例生成的 heat_value；旧交易缺字段时只退回 coin_spent/charge_amount，不再读取 GIFT_POINT。
	query := fmt.Sprintf(`
		WITH gift_facts AS (
			SELECT
				CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.sender_user_id')) AS SIGNED) AS sender_user_id,
				CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id,
				COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id,
				COALESCE(
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED),
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED),
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED),
					0
				) AS gift_value,
				COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count,
				created_at_ms
			FROM wallet_transactions
			WHERE app_code = ? AND biz_type = 'gift_debit' AND status = 'succeeded'
			  AND created_at_ms >= ? AND created_at_ms < ?
		),
		aggregated AS (
			SELECT %s AS subject_id
			FROM gift_facts
			WHERE %s
			GROUP BY %s
		)
		SELECT COUNT(*) FROM aggregated`, subject, predicate, subject)

	var total int64
	err := s.walletDB.QueryRowContext(ctx, query, appCode, startMS, endMS).Scan(&total)
	return total, err
}

func (s *Service) listItems(ctx context.Context, appCode string, boardType string, startMS int64, endMS int64, limit int, offset int) ([]Item, error) {
	subject, predicate := leaderboardDimension(boardType)
	// 排行榜列表和值排序统一使用 heat_value 口径，避免历史 gift_point 字段重新参与运营排行。
	query := fmt.Sprintf(`
		WITH gift_facts AS (
			SELECT
				CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.sender_user_id')) AS SIGNED) AS sender_user_id,
				CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id,
				COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id,
				COALESCE(
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED),
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED),
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED),
					0
				) AS gift_value,
				COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count,
				created_at_ms
			FROM wallet_transactions
			WHERE app_code = ? AND biz_type = 'gift_debit' AND status = 'succeeded'
			  AND created_at_ms >= ? AND created_at_ms < ?
		),
		aggregated AS (
			SELECT %s AS subject_id,
			       SUM(gift_value) AS gift_value,
			       SUM(gift_count) AS gift_count,
			       COUNT(*) AS transaction_count,
			       MAX(created_at_ms) AS last_gift_at_ms
			FROM gift_facts
			WHERE %s
			GROUP BY %s
		),
		ranked AS (
			SELECT ROW_NUMBER() OVER (ORDER BY gift_value DESC, last_gift_at_ms DESC, subject_id ASC) AS rank_no,
			       subject_id, gift_value, gift_count, transaction_count, last_gift_at_ms
			FROM aggregated
		)
		SELECT rank_no, CAST(subject_id AS CHAR), gift_value, gift_count, transaction_count, last_gift_at_ms
		FROM ranked
		ORDER BY rank_no ASC
		LIMIT ? OFFSET ?`, subject, predicate, subject)

	rows, err := s.walletDB.QueryContext(ctx, query, appCode, startMS, endMS, limit, offset)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]Item, 0, limit)
	for rows.Next() {
		item, err := scanItem(rows, boardType)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Service) rankForUser(ctx context.Context, appCode string, boardType string, startMS int64, endMS int64, userID int64) (Item, bool, error) {
	subject, predicate := leaderboardDimension(boardType)
	// 用户名次查询与列表共用真实贡献口径；只兼容读取老交易的扣费金额，不兼容读取积分字段。
	query := fmt.Sprintf(`
		WITH gift_facts AS (
			SELECT
				CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.sender_user_id')) AS SIGNED) AS sender_user_id,
				CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id,
				COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id,
				COALESCE(
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED),
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED),
					CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED),
					0
				) AS gift_value,
				COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count,
				created_at_ms
			FROM wallet_transactions
			WHERE app_code = ? AND biz_type = 'gift_debit' AND status = 'succeeded'
			  AND created_at_ms >= ? AND created_at_ms < ?
		),
		aggregated AS (
			SELECT %s AS subject_id,
			       SUM(gift_value) AS gift_value,
			       SUM(gift_count) AS gift_count,
			       COUNT(*) AS transaction_count,
			       MAX(created_at_ms) AS last_gift_at_ms
			FROM gift_facts
			WHERE %s
			GROUP BY %s
		),
		ranked AS (
			SELECT ROW_NUMBER() OVER (ORDER BY gift_value DESC, last_gift_at_ms DESC, subject_id ASC) AS rank_no,
			       subject_id, gift_value, gift_count, transaction_count, last_gift_at_ms
			FROM aggregated
		)
		SELECT rank_no, CAST(subject_id AS CHAR), gift_value, gift_count, transaction_count, last_gift_at_ms
		FROM ranked
		WHERE subject_id = ?
		LIMIT 1`, subject, predicate, subject)

	row := s.walletDB.QueryRowContext(ctx, query, appCode, startMS, endMS, userID)
	item, err := scanItem(row, boardType)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return Item{}, false, nil
		}
		return Item{}, false, err
	}
	return item, true, nil
}

type itemScanner interface {
	Scan(dest ...any) error
}

func scanItem(scanner itemScanner, boardType string) (Item, error) {
	var item Item
	var subject string
	if err := scanner.Scan(&item.Rank, &subject, &item.GiftValue, &item.GiftCount, &item.TransactionCount, &item.LastGiftAtMS); err != nil {
		return Item{}, err
	}
	if boardType == BoardTypeRoom {
		item.RoomID = subject
		item.Room = &Room{RoomID: subject}
		return item, nil
	}
	item.UserID = subject
	item.User = &User{UserID: subject}
	return item, nil
}

func leaderboardDimension(boardType string) (string, string) {
	switch boardType {
	case BoardTypeReceived:
		return "target_user_id", "target_user_id > 0"
	case BoardTypeRoom:
		return "room_id", "room_id <> ''"
	default:
		return "sender_user_id", "sender_user_id > 0"
	}
}

func (s *Service) enrich(ctx context.Context, appCode string, boardType string, items []Item, myRank *Item) error {
	if boardType == BoardTypeRoom {
		return s.enrichRooms(ctx, appCode, items)
	}

	userIDs := make([]int64, 0, len(items)+1)
	for _, item := range items {
		if id, err := strconv.ParseInt(item.UserID, 10, 64); err == nil && id > 0 {
			userIDs = append(userIDs, id)
		}
	}
	if myRank != nil {
		if id, err := strconv.ParseInt(myRank.UserID, 10, 64); err == nil && id > 0 {
			userIDs = append(userIDs, id)
		}
	}
	profiles, err := s.userProfiles(ctx, appCode, userIDs)
	if err != nil {
		return err
	}
	applyUserProfiles(items, profiles)
	if myRank != nil {
		applyUserProfile(myRank, profiles)
	}
	return nil
}

func (s *Service) userProfiles(ctx context.Context, appCode string, userIDs []int64) (map[int64]userProfile, error) {
	result := make(map[int64]userProfile, len(userIDs))
	if s == nil || s.userDB == nil || len(userIDs) == 0 {
		return result, nil
	}
	userIDs = uniqueInt64s(userIDs)
	args := make([]any, 0, len(userIDs)+1)
	args = append(args, appCode)
	for _, id := range userIDs {
		args = append(args, id)
	}
	rows, err := s.userDB.QueryContext(ctx, fmt.Sprintf(`
		SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
		FROM users
		WHERE app_code = ? AND user_id IN (%s)
	`, placeholders(len(userIDs))), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var profile userProfile
		if err := rows.Scan(&profile.UserID, &profile.DisplayUserID, &profile.Username, &profile.Avatar); err != nil {
			return nil, err
		}
		result[profile.UserID] = profile
	}
	return result, rows.Err()
}

func applyUserProfiles(items []Item, profiles map[int64]userProfile) {
	for i := range items {
		applyUserProfile(&items[i], profiles)
	}
}

func applyUserProfile(item *Item, profiles map[int64]userProfile) {
	if item == nil {
		return
	}
	id, err := strconv.ParseInt(item.UserID, 10, 64)
	if err != nil || id <= 0 {
		return
	}
	if profile, ok := profiles[id]; ok {
		item.User = &User{
			UserID:        strconv.FormatInt(profile.UserID, 10),
			DisplayUserID: profile.DisplayUserID,
			Username:      profile.Username,
			Avatar:        profile.Avatar,
		}
	}
}

func (s *Service) enrichRooms(ctx context.Context, appCode string, items []Item) error {
	roomIDs := make([]string, 0, len(items))
	for _, item := range items {
		if item.RoomID != "" {
			roomIDs = append(roomIDs, item.RoomID)
		}
	}
	rooms, err := s.roomProfiles(ctx, appCode, roomIDs)
	if err != nil {
		return err
	}
	for i := range items {
		if room, ok := rooms[items[i].RoomID]; ok {
			items[i].Room = &Room{
				RoomID:      room.RoomID,
				RoomShortID: room.RoomShortID,
				Title:       room.Title,
				CoverURL:    room.CoverURL,
				OwnerUserID: formatOptionalID(room.OwnerUserID),
				Status:      room.Status,
			}
		}
	}
	return nil
}

func (s *Service) roomProfiles(ctx context.Context, appCode string, roomIDs []string) (map[string]roomProfile, error) {
	result := make(map[string]roomProfile, len(roomIDs))
	if s == nil || s.room == nil || len(roomIDs) == 0 {
		return result, nil
	}
	roomIDs = uniqueStrings(roomIDs)
	_ = appCode
	for _, id := range roomIDs {
		room, err := s.room.GetRoom(ctx, roomclient.GetRoomRequest{RoomID: id})
		if err != nil {
			return nil, err
		}
		result[room.RoomID] = roomProfile{
			RoomID:      room.RoomID,
			RoomShortID: room.RoomShortID,
			Title:       room.Title,
			CoverURL:    room.CoverURL,
			OwnerUserID: room.OwnerUserID,
			Status:      room.Status,
		}
	}
	return result, nil
}

func offset(page int, pageSize int) int {
	if page <= 1 {
		return 0
	}
	return (page - 1) * pageSize
}

func placeholders(n int) string {
	if n <= 0 {
		return ""
	}
	return strings.TrimSuffix(strings.Repeat("?,", n), ",")
}

func uniqueInt64s(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func uniqueStrings(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func formatOptionalID(id int64) string {
	if id <= 0 {
		return ""
	}
	return strconv.FormatInt(id, 10)
}
