package mysql

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
)

func (r *Repository) GetCPWeeklyRankConfig(ctx context.Context) (domain.Config, bool, error) {
	if r == nil || r.db == nil {
		return domain.Config{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	config, err := scanCPWeeklyRankConfig(r.db.QueryRowContext(ctx, `
		SELECT app_code, activity_code, enabled, relation_type, top_count, updated_by_admin_id, created_at_ms, updated_at_ms
		FROM cp_weekly_rank_configs
		WHERE app_code = ?`, appCode))
	if err == sql.ErrNoRows {
		return domain.Config{}, false, nil
	}
	if err != nil {
		return domain.Config{}, false, err
	}
	rewards, err := r.listCPWeeklyRankRewards(ctx, appCode)
	if err != nil {
		return domain.Config{}, false, err
	}
	config.Rewards = rewards
	return config, true, nil
}

func (r *Repository) UpdateCPWeeklyRankConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error) {
	if r == nil || r.db == nil {
		return domain.Config{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Config{}, err
	}
	defer func() { _ = tx.Rollback() }()
	// 配置行和奖励行必须一起替换；否则后台保存成功但 cron 读到半套奖励会导致榜单部分名次无法发奖。
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO cp_weekly_rank_configs (
			app_code, activity_code, enabled, relation_type, top_count, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			activity_code = VALUES(activity_code),
			enabled = VALUES(enabled),
			relation_type = VALUES(relation_type),
			top_count = VALUES(top_count),
			updated_by_admin_id = VALUES(updated_by_admin_id),
			updated_at_ms = VALUES(updated_at_ms)`,
		appCode, domain.ActivityCode, config.Enabled, "cp", config.TopCount, config.UpdatedByAdminID, nowMS, nowMS,
	); err != nil {
		return domain.Config{}, err
	}
	if _, err := tx.ExecContext(ctx, `DELETE FROM cp_weekly_rank_rewards WHERE app_code = ?`, appCode); err != nil {
		return domain.Config{}, err
	}
	for _, reward := range config.Rewards {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO cp_weekly_rank_rewards (
				app_code, rank_no, resource_group_id, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?)`,
			appCode, reward.RankNo, reward.ResourceGroupID, nowMS, nowMS,
		); err != nil {
			return domain.Config{}, err
		}
	}
	if err := tx.Commit(); err != nil {
		return domain.Config{}, err
	}
	stored, _, err := r.GetCPWeeklyRankConfig(ctx)
	return stored, err
}

func (r *Repository) ListCPWeeklyRankSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64) ([]domain.Settlement, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	where := []string{"app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	if periodStartMS > 0 {
		where = append(where, "period_start_ms = ?")
		args = append(args, periodStartMS)
	}
	if periodEndMS > 0 {
		where = append(where, "period_end_ms = ?")
		args = append(args, periodEndMS)
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, settlement_id, period_start_ms, period_end_ms, rank_no, relationship_id, user_id, score,
		       resource_group_id, wallet_command_id, wallet_grant_id, status, failure_reason, attempt_count, created_at_ms, updated_at_ms
		FROM cp_weekly_rank_settlements
		WHERE `+strings.Join(where, " AND ")+`
		ORDER BY period_start_ms DESC, rank_no ASC, relationship_id ASC, user_id ASC`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCPWeeklyRankSettlements(rows)
}

func (r *Repository) PrepareCPWeeklyRankSettlements(ctx context.Context, config domain.Config, entries []domain.Entry, periodStartMS int64, periodEndMS int64, nowMS int64) ([]domain.Settlement, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()
	var existing int
	if err := tx.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM cp_weekly_rank_settlements
		WHERE app_code = ? AND period_start_ms = ? AND period_end_ms = ?`,
		appCode, periodStartMS, periodEndMS,
	).Scan(&existing); err != nil {
		return nil, err
	}
	if existing == 0 {
		rewardsByRank := make(map[int32]int64, len(config.Rewards))
		for _, reward := range config.Rewards {
			rewardsByRank[reward.RankNo] = reward.ResourceGroupID
		}
		for _, entry := range entries {
			rankNo := int32(entry.Rank)
			resourceGroupID := rewardsByRank[rankNo]
			if resourceGroupID <= 0 {
				continue
			}
			for _, userID := range []int64{entry.UserA.UserID, entry.UserB.UserID} {
				if userID <= 0 {
					continue
				}
				commandID := fmt.Sprintf("cp_weekly_rank:%d:%d:%s:%d", periodStartMS, rankNo, entry.RelationshipID, userID)
				// 每个获奖关系拆成两个用户发奖记录；INSERT IGNORE 配合唯一键保证同一周期并发 cron 不会重复固化。
				if _, err := tx.ExecContext(ctx, `
					INSERT IGNORE INTO cp_weekly_rank_settlements (
						app_code, settlement_id, period_start_ms, period_end_ms, rank_no, relationship_id, user_id, score,
						resource_group_id, wallet_command_id, status, created_at_ms, updated_at_ms
					) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
					appCode, idgen.New("cpwrs"), periodStartMS, periodEndMS, rankNo, entry.RelationshipID, userID, entry.Score,
					resourceGroupID, commandID, domain.SettlementStatusPending, nowMS, nowMS,
				); err != nil {
					return nil, err
				}
			}
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return r.ListPendingCPWeeklyRankSettlements(ctx, periodStartMS, periodEndMS, 1000)
}

func (r *Repository) ListPendingCPWeeklyRankSettlements(ctx context.Context, periodStartMS int64, periodEndMS int64, limit int) ([]domain.Settlement, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if limit <= 0 {
		limit = 50
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, settlement_id, period_start_ms, period_end_ms, rank_no, relationship_id, user_id, score,
		       resource_group_id, wallet_command_id, wallet_grant_id, status, failure_reason, attempt_count, created_at_ms, updated_at_ms
		FROM cp_weekly_rank_settlements
		WHERE app_code = ? AND period_start_ms = ? AND period_end_ms = ? AND status IN (?, ?)
		ORDER BY rank_no ASC, relationship_id ASC, user_id ASC
		LIMIT ?`,
		appcode.FromContext(ctx), periodStartMS, periodEndMS, domain.SettlementStatusPending, domain.SettlementStatusFailed, limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCPWeeklyRankSettlements(rows)
}

func (r *Repository) MarkCPWeeklyRankSettlementGranted(ctx context.Context, settlementID string, walletGrantID string, nowMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE cp_weekly_rank_settlements
		SET status = ?, wallet_grant_id = ?, failure_reason = '', updated_at_ms = ?
		WHERE app_code = ? AND settlement_id = ?`,
		domain.SettlementStatusGranted, walletGrantID, nowMS, appcode.FromContext(ctx), settlementID,
	)
	return err
}

func (r *Repository) MarkCPWeeklyRankSettlementFailed(ctx context.Context, settlementID string, reason string, nowMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE cp_weekly_rank_settlements
		SET status = ?, failure_reason = ?, attempt_count = attempt_count + 1, updated_at_ms = ?
		WHERE app_code = ? AND settlement_id = ?`,
		domain.SettlementStatusFailed, truncateCPWeeklyRankFailure(reason), nowMS, appcode.FromContext(ctx), settlementID,
	)
	return err
}

func (r *Repository) listCPWeeklyRankRewards(ctx context.Context, appCode string) ([]domain.Reward, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, rank_no, resource_group_id, created_at_ms, updated_at_ms
		FROM cp_weekly_rank_rewards
		WHERE app_code = ?
		ORDER BY rank_no ASC`, appCode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	rewards := make([]domain.Reward, 0)
	for rows.Next() {
		var reward domain.Reward
		if err := rows.Scan(&reward.AppCode, &reward.RankNo, &reward.ResourceGroupID, &reward.CreatedAtMS, &reward.UpdatedAtMS); err != nil {
			return nil, err
		}
		rewards = append(rewards, reward)
	}
	return rewards, rows.Err()
}

func scanCPWeeklyRankConfig(row interface{ Scan(dest ...any) error }) (domain.Config, error) {
	var config domain.Config
	var enabled bool
	err := row.Scan(&config.AppCode, &config.ActivityCode, &enabled, &config.RelationType, &config.TopCount, &config.UpdatedByAdminID, &config.CreatedAtMS, &config.UpdatedAtMS)
	config.Enabled = enabled
	return config, err
}

func scanCPWeeklyRankSettlements(rows *sql.Rows) ([]domain.Settlement, error) {
	items := make([]domain.Settlement, 0)
	for rows.Next() {
		var item domain.Settlement
		if err := rows.Scan(
			&item.AppCode, &item.SettlementID, &item.PeriodStartMS, &item.PeriodEndMS, &item.RankNo, &item.RelationshipID, &item.UserID, &item.Score,
			&item.ResourceGroupID, &item.WalletCommandID, &item.WalletGrantID, &item.Status, &item.FailureReason, &item.AttemptCount, &item.CreatedAtMS, &item.UpdatedAtMS,
		); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func truncateCPWeeklyRankFailure(value string) string {
	value = strings.TrimSpace(value)
	if len(value) > 512 {
		return value[:512]
	}
	return value
}
