package activityapi

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

	"hyapp/pkg/appcode"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"

	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
)

const (
	leaderboardTypeSent     = "sent"
	leaderboardTypeReceived = "received"
	leaderboardTypeRoom     = "room"

	leaderboardPeriodToday = "today"
	leaderboardPeriodWeek  = "week"
	leaderboardPeriodMonth = "month"

	userLeaderboardMaxPageSize = 100
)

type userLeaderboardResponse struct {
	Items        []userLeaderboardItem `json:"items"`
	Total        int64                 `json:"total"`
	Page         int32                 `json:"page"`
	PageSize     int32                 `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       *userLeaderboardItem  `json:"my_rank,omitempty"`
}

type userLeaderboardItem 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             *userLeaderboardUser `json:"user,omitempty"`
	Room             *userLeaderboardRoom `json:"room,omitempty"`
}

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

type userLeaderboardRoom struct {
	RoomID      string `json:"room_id"`
	RoomShortID string `json:"room_short_id,omitempty"`
	Title       string `json:"title,omitempty"`
	CoverURL    string `json:"cover_url,omitempty"`
	RoomAvatar  string `json:"room_avatar,omitempty"`
}

func (h *Handler) listUserLeaderboards(writer http.ResponseWriter, request *http.Request) {
	boardType := normalizeUserLeaderboardType(request.URL.Query().Get("board_type"))
	if boardType == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	period := normalizeUserLeaderboardPeriod(request.URL.Query().Get("period"))
	if period == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if pageSize > userLeaderboardMaxPageSize {
		pageSize = userLeaderboardMaxPageSize
	}
	if boardType == leaderboardTypeRoom {
		h.listRoomGiftLeaderboards(writer, request, period, page, pageSize)
		return
	}
	if h.walletDB == nil {
		httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	now := time.Now().UTC()
	start, end := userLeaderboardPeriodWindow(period, now)
	startMS := start.UnixMilli()
	endMS := end.UnixMilli()
	app := appcode.FromContext(request.Context())
	total, err := countUserLeaderboard(request.Context(), h.walletDB, app, boardType, startMS, endMS)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	items, err := listUserLeaderboardItems(request.Context(), h.walletDB, app, boardType, startMS, endMS, int(pageSize), int((page-1)*pageSize))
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	var myRank *userLeaderboardItem
	viewerUserID := auth.UserIDFromContext(request.Context())
	if viewerUserID > 0 && boardType != leaderboardTypeRoom {
		item, found, err := userLeaderboardRankForUser(request.Context(), h.walletDB, app, boardType, startMS, endMS, viewerUserID)
		if err != nil {
			httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		if found {
			myRank = &item
		}
	}
	if err := h.enrichUserLeaderboardItems(request, boardType, items, myRank); err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	httpkit.WriteOK(writer, request, userLeaderboardResponse{
		Items:        items,
		Total:        total,
		Page:         page,
		PageSize:     pageSize,
		BoardType:    boardType,
		Period:       period,
		StartAtMS:    startMS,
		EndAtMS:      endMS,
		ServerTimeMS: now.UnixMilli(),
		MyRank:       myRank,
	})
}

func (h *Handler) listRoomGiftLeaderboards(writer http.ResponseWriter, request *http.Request, period string, page int32, pageSize int32) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resp, err := h.roomQueryClient.ListRoomGiftLeaderboard(request.Context(), &roomv1.ListRoomGiftLeaderboardRequest{
		Meta:     httpkit.RoomMeta(request, "", ""),
		Period:   period,
		Page:     page,
		PageSize: pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, userLeaderboardResponse{
		Items:        userLeaderboardItemsFromRoomGiftLeaderboard(resp.GetItems()),
		Total:        resp.GetTotal(),
		Page:         page,
		PageSize:     pageSize,
		BoardType:    leaderboardTypeRoom,
		Period:       resp.GetPeriod(),
		StartAtMS:    resp.GetStartAtMs(),
		EndAtMS:      resp.GetEndAtMs(),
		ServerTimeMS: resp.GetServerTimeMs(),
	})
}

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

func normalizeUserLeaderboardPeriod(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "today", "day", "daily":
		return leaderboardPeriodToday
	case "week", "weekly":
		return leaderboardPeriodWeek
	case "month", "monthly":
		return leaderboardPeriodMonth
	default:
		return ""
	}
}

func userLeaderboardPeriodWindow(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 leaderboardPeriodWeek:
		weekday := int(dayStart.Weekday())
		if weekday == 0 {
			weekday = 7
		}
		return dayStart.AddDate(0, 0, 1-weekday), utcNow
	case leaderboardPeriodMonth:
		return time.Date(utcNow.Year(), utcNow.Month(), 1, 0, 0, 0, 0, time.UTC), utcNow
	default:
		return dayStart, utcNow
	}
}

func countUserLeaderboard(ctx context.Context, db *sql.DB, appCode string, boardType string, startMS int64, endMS int64) (int64, error) {
	subject, predicate := userLeaderboardDimension(boardType)
	// App 排行榜价值优先使用真实送礼贡献 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
			FROM gift_facts
			WHERE %s
			GROUP BY %s
		)
		SELECT COUNT(*) FROM aggregated`, subject, predicate, subject)

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

func listUserLeaderboardItems(ctx context.Context, db *sql.DB, appCode string, boardType string, startMS int64, endMS int64, limit int, offset int) ([]userLeaderboardItem, error) {
	subject, predicate := userLeaderboardDimension(boardType)
	// 列表排序和值统计统一走 heat_value，保证 App 和后台排行榜不被历史积分字段污染。
	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 := db.QueryContext(ctx, query, appCode, startMS, endMS, limit, offset)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

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

func userLeaderboardRankForUser(ctx context.Context, db *sql.DB, appCode string, boardType string, startMS int64, endMS int64, userID int64) (userLeaderboardItem, bool, error) {
	subject, predicate := userLeaderboardDimension(boardType)
	// 当前用户名次查询只按真实贡献或扣费兜底统计，不再兼容 gift_point_added/gift_point_amount。
	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 := db.QueryRowContext(ctx, query, appCode, startMS, endMS, userID)
	item, err := scanUserLeaderboardItem(row, boardType)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return userLeaderboardItem{}, false, nil
		}
		return userLeaderboardItem{}, false, err
	}
	return item, true, nil
}

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

func scanUserLeaderboardItem(scanner userLeaderboardScanner, boardType string) (userLeaderboardItem, error) {
	var item userLeaderboardItem
	var subject string
	if err := scanner.Scan(&item.Rank, &subject, &item.GiftValue, &item.GiftCount, &item.TransactionCount, &item.LastGiftAtMS); err != nil {
		return userLeaderboardItem{}, err
	}
	if boardType == leaderboardTypeRoom {
		item.RoomID = subject
		item.Room = &userLeaderboardRoom{RoomID: subject}
		return item, nil
	}
	item.UserID = subject
	item.User = &userLeaderboardUser{UserID: subject}
	return item, nil
}

func userLeaderboardDimension(boardType string) (string, string) {
	switch boardType {
	case leaderboardTypeReceived:
		return "target_user_id", "target_user_id > 0"
	case leaderboardTypeRoom:
		return "room_id", "room_id <> ''"
	default:
		return "sender_user_id", "sender_user_id > 0"
	}
}

func (h *Handler) enrichUserLeaderboardItems(request *http.Request, boardType string, items []userLeaderboardItem, myRank *userLeaderboardItem) error {
	if boardType == leaderboardTypeRoom {
		// room 榜已经由 room-service 的 zset + room_list_entries 返回卡片，不再读取完整 RoomSnapshot。
		return nil
	}
	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 := h.userLeaderboardProfiles(request, userIDs)
	if err != nil {
		return err
	}
	for i := range items {
		applyUserLeaderboardProfile(&items[i], profiles)
	}
	if myRank != nil {
		applyUserLeaderboardProfile(myRank, profiles)
	}
	return nil
}

func userLeaderboardItemsFromRoomGiftLeaderboard(items []*roomv1.RoomGiftLeaderboardItem) []userLeaderboardItem {
	out := make([]userLeaderboardItem, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		roomID := item.GetRoomId()
		room := userLeaderboardRoomFromListItem(item.GetRoom())
		if room == nil {
			room = &userLeaderboardRoom{RoomID: roomID}
		}
		out = append(out, userLeaderboardItem{
			Rank:      item.GetRank(),
			RoomID:    roomID,
			GiftValue: item.GetCoinSpent(),
			Room:      room,
		})
	}
	return out
}

func userLeaderboardRoomFromListItem(room *roomv1.RoomListItem) *userLeaderboardRoom {
	if room == nil {
		return nil
	}
	roomID := room.GetRoomId()
	if roomID == "" {
		return nil
	}
	return &userLeaderboardRoom{
		RoomID:      roomID,
		RoomShortID: room.GetRoomShortId(),
		Title:       room.GetTitle(),
		CoverURL:    room.GetCoverUrl(),
		RoomAvatar:  room.GetCoverUrl(),
	}
}

func roomLeaderboardRoomFromSnapshot(roomID string, snapshot *roomv1.RoomSnapshot) *userLeaderboardRoom {
	room := &userLeaderboardRoom{RoomID: roomID}
	if snapshot == nil {
		return room
	}
	if snapshot.GetRoomId() != "" {
		room.RoomID = snapshot.GetRoomId()
	}
	ext := snapshot.GetRoomExt()
	room.RoomShortID = snapshot.GetRoomShortId()
	if room.RoomShortID == "" {
		room.RoomShortID = ext["room_short_id"]
	}
	room.Title = ext["title"]
	room.CoverURL = ext["cover_url"]
	room.RoomAvatar = room.CoverURL
	return room
}

func (h *Handler) userLeaderboardProfiles(request *http.Request, userIDs []int64) (map[int64]*userv1.User, error) {
	result := make(map[int64]*userv1.User)
	if h.userProfileClient == nil || len(userIDs) == 0 {
		return result, nil
	}
	userIDs = uniqueUserLeaderboardInt64s(userIDs)
	resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
		Meta:    httpkit.UserMeta(request, ""),
		UserIds: userIDs,
	})
	if err != nil {
		return nil, err
	}
	return resp.GetUsers(), nil
}

func applyUserLeaderboardProfile(item *userLeaderboardItem, profiles map[int64]*userv1.User) {
	if item == nil {
		return
	}
	id, err := strconv.ParseInt(item.UserID, 10, 64)
	if err != nil || id <= 0 {
		return
	}
	profile := profiles[id]
	if profile == nil {
		return
	}
	item.User = &userLeaderboardUser{
		UserID:        strconv.FormatInt(profile.GetUserId(), 10),
		DisplayUserID: profile.GetDisplayUserId(),
		Username:      profile.GetUsername(),
		Avatar:        profile.GetAvatar(),
	}
}

func uniqueUserLeaderboardInt64s(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
}
