package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"math"
	"math/big"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
)

type hostSalaryCandidate struct {
	// UserID 与 CycleKey 唯一定位主播当月工资周期账户；后续所有结算进度都挂在这组键上。
	UserID            int64
	CycleKey          string
	RegionID          int64
	AgencyOwnerUserID int64
	TotalDiamonds     int64
	GiftDiamondTotal  int64
}

type hostSalaryProgress struct {
	// 这些 settled_* 字段保存“已发累计权益”，结算时只对比当前等级累计值来补差额，天然防重复发放。
	SettledLevelNo              int
	SettledHostSalaryUSDMinor   int64
	SettledHostCoinReward       int64
	SettledAgencySalaryUSDMinor int64
	LastSettledTotalDiamonds    int64
	ResidualUSDMinor            int64
	MonthEndClearedAtMS         int64
	LastPolicyID                uint64
	LastPolicyVersion           uint64
	Version                     int64
}

type hostSalarySettlementMetadata struct {
	// metadata 是审计快照：钱包交易、outbox、结算记录共享同一份口径，方便后续从任一链路追账。
	AppCode                     string `json:"app_code"`
	SettlementType              string `json:"settlement_type"`
	TriggerMode                 string `json:"trigger_mode"`
	SettlementRole              string `json:"settlement_role"`
	HostUserID                  int64  `json:"host_user_id"`
	AgencyOwnerUserID           int64  `json:"agency_owner_user_id"`
	CycleKey                    string `json:"cycle_key"`
	RegionID                    int64  `json:"region_id"`
	PolicyID                    uint64 `json:"policy_id"`
	PolicyVersion               uint64 `json:"policy_version"`
	PolicyName                  string `json:"policy_name"`
	LevelNo                     int    `json:"level_no"`
	RequiredDiamonds            int64  `json:"required_diamonds"`
	TotalDiamonds               int64  `json:"total_diamonds"`
	HostSalaryUSDMinorDelta     int64  `json:"host_salary_usd_minor_delta"`
	HostCoinRewardDelta         int64  `json:"host_coin_reward_delta"`
	AgencySalaryUSDMinorDelta   int64  `json:"agency_salary_usd_minor_delta"`
	ResidualUSDMinorDelta       int64  `json:"residual_usd_minor_delta"`
	HostUSDBalanceAfter         int64  `json:"host_usd_balance_after"`
	HostCoinBalanceAfter        int64  `json:"host_coin_balance_after"`
	AgencyUSDBalanceAfter       int64  `json:"agency_usd_balance_after"`
	HostSalaryAssetType         string `json:"host_salary_asset_type"`
	AgencySalaryAssetType       string `json:"agency_salary_asset_type"`
	SettledLevelNo              int    `json:"settled_level_no"`
	SettledHostSalaryUSDMinor   int64  `json:"settled_host_salary_usd_minor"`
	SettledHostCoinReward       int64  `json:"settled_host_coin_reward"`
	SettledAgencySalaryUSDMinor int64  `json:"settled_agency_salary_usd_minor"`
	ResidualUSDMinor            int64  `json:"residual_usd_minor"`
	MonthEndClearedAtMS         int64  `json:"month_end_cleared_at_ms"`
	ProcessedAtMS               int64  `json:"processed_at_ms"`
}

type hostSalaryPolicyQuerier interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

// ProcessHostSalarySettlementBatch 按周期账户扫描并结算主播美元、主播金币奖励和代理美元工资。
// 每个主播单独开事务，避免某个主播配置异常拖垮整个批次，也让 cron-service 可按 has_more 继续拉下一页。
func (r *Repository) ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error) {
	if r == nil || r.db == nil {
		return ledger.HostSalarySettlementBatchResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command.SettlementType = normalizeHostSalarySettlementType(command.SettlementType)
	if command.SettlementType == "" {
		return ledger.HostSalarySettlementBatchResult{}, xerr.New(xerr.InvalidArgument, "settlement_type is invalid")
	}
	command.TriggerMode = normalizeHostSalarySettlementTriggerMode(command.TriggerMode)
	if command.TriggerMode == "" {
		return ledger.HostSalarySettlementBatchResult{}, xerr.New(xerr.InvalidArgument, "trigger_mode is invalid")
	}
	command.SettlementRole = normalizeHostSalarySettlementRole(command.SettlementRole)
	if command.SettlementRole == "" {
		return ledger.HostSalarySettlementBatchResult{}, xerr.New(xerr.InvalidArgument, "settlement_role is invalid")
	}
	if command.NowMs <= 0 {
		return ledger.HostSalarySettlementBatchResult{}, xerr.New(xerr.InvalidArgument, "now_ms is required")
	}
	if command.BatchSize <= 0 {
		command.BatchSize = 100
	}
	limit := command.BatchSize + 1
	candidates, err := r.listHostSalaryCandidates(ctx, command, limit)
	if err != nil {
		return ledger.HostSalarySettlementBatchResult{}, err
	}
	hasMore := len(candidates) > command.BatchSize
	if hasMore {
		candidates = candidates[:command.BatchSize]
	}

	result := ledger.HostSalarySettlementBatchResult{ClaimedCount: len(candidates), HasMore: hasMore}
	var firstErr error
	for _, candidate := range candidates {
		processed, err := r.processHostSalaryCandidate(ctx, command, candidate)
		result.ProcessedCount++
		if err != nil {
			if firstErr == nil {
				firstErr = err
			}
			result.FailureCount++
			continue
		}
		if processed {
			result.SuccessCount++
		}
	}
	if result.ClaimedCount > 0 && result.SuccessCount == 0 && firstErr != nil {
		return result, firstErr
	}
	return result, nil
}

func (r *Repository) listHostSalaryCandidates(ctx context.Context, command ledger.HostSalarySettlementBatchCommand, limit int) ([]hostSalaryCandidate, error) {
	policyModeClause := ""
	userFilterClause := ""
	args := []any{appcode.FromContext(ctx), command.CycleKey}
	previousCycleKey, err := previousHostSalaryCycleKey(command.CycleKey)
	if err != nil {
		return nil, xerr.New(xerr.InvalidArgument, "cycle_key is invalid")
	}
	var policyModeArgs []any
	if command.SettlementType != ledger.HostSalarySettlementTypeMonthEnd {
		// 日结和半月结必须精确匹配政策配置的结算模式；月底清算复用有效政策处理剩余钻石和周期关闭。
		policyModeClause = "AND p.settlement_mode = ?"
		policyModeArgs = append(policyModeArgs, command.SettlementType)
	}
	userIDs := uniquePositiveInt64(command.UserIDs)
	if len(userIDs) > 0 {
		// 后台手动结算只允许处理运营勾选的主播；过滤下推到候选 SQL，避免锁定无关周期账户。
		userFilterClause = "AND a.user_id IN (" + placeholders(len(userIDs)) + ")"
		for _, id := range userIDs {
			args = append(args, id)
		}
	}
	// 候选扫描也接受紧邻上月的绑定；具体结算事务会把它复制到当前周期，避免扫描阶段漏掉应继承的主播。
	args = append(args, previousCycleKey, command.TriggerMode, ledger.HostPolicyTypeSalaryDiamond)
	args = append(args, policyModeArgs...)
	// 日结/半月结只扫描“本周期钻石比上次结算更多”的主播，避免 cron 重跑时反复锁定无新增收益的账户。
	// 月底批次必须扫描全部未关闭周期账户，因为它还负责剩余钻石折美元和周期逻辑关闭。
	progressClause := "AND a.total_diamonds > COALESCE(progress.last_settled_total_diamonds, 0)"
	if command.SettlementType == ledger.HostSalarySettlementTypeMonthEnd {
		progressClause = ""
	}
	args = append(args, limit)
	query := fmt.Sprintf(`
		SELECT a.user_id, a.cycle_key, a.region_id, a.agency_owner_user_id, a.total_diamonds, a.gift_diamond_total
		FROM host_period_diamond_accounts a
		LEFT JOIN host_salary_settlement_progress progress
		  ON progress.app_code = a.app_code AND progress.user_id = a.user_id AND progress.cycle_key = a.cycle_key
		WHERE a.app_code = ?
		  AND a.cycle_key = ?
		  AND a.total_diamonds > 0
		  AND COALESCE(progress.month_end_cleared_at_ms, 0) = 0
		  %s
		  %s
		  AND EXISTS (
		    SELECT 1
		    FROM host_salary_policy_cycle_bindings binding
		    JOIN host_agency_salary_policies p
		      ON p.app_code = binding.app_code
		     AND p.policy_id = binding.policy_id
		     AND p.policy_version = binding.policy_version
		    WHERE binding.app_code = a.app_code
		      AND binding.region_id = a.region_id
		      AND binding.cycle_key IN (a.cycle_key, ?)
		      AND p.status = 'active'
		      AND p.settlement_trigger_mode = ?
		      AND p.policy_type = ?
		      %s
		  )
		ORDER BY a.updated_at_ms ASC, a.user_id ASC
		LIMIT ?`, progressClause, userFilterClause, policyModeClause)
	rows, err := r.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	candidates := make([]hostSalaryCandidate, 0, limit)
	for rows.Next() {
		var candidate hostSalaryCandidate
		if err := rows.Scan(&candidate.UserID, &candidate.CycleKey, &candidate.RegionID, &candidate.AgencyOwnerUserID, &candidate.TotalDiamonds, &candidate.GiftDiamondTotal); err != nil {
			return nil, err
		}
		candidates = append(candidates, candidate)
	}
	return candidates, rows.Err()
}

func (r *Repository) processHostSalaryCandidate(ctx context.Context, command ledger.HostSalarySettlementBatchCommand, selected hostSalaryCandidate) (bool, error) {
	// 账务、进度和 outbox 必须在同一个主播级事务中提交；批次层只负责继续处理下一位主播。
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return false, err
	}
	defer func() { _ = tx.Rollback() }()

	account, exists, err := r.lockHostSalaryCandidate(ctx, tx, selected.UserID, selected.CycleKey)
	if err != nil || !exists {
		return false, err
	}
	progress, err := r.lockHostSalaryProgress(ctx, tx, account.UserID, account.CycleKey, command.NowMs)
	if err != nil {
		return false, err
	}
	if progress.MonthEndClearedAtMS > 0 {
		return true, tx.Commit()
	}
	// 非月底批次不关闭周期，若同一周期已结算到当前钻石数，则本次只作为幂等空跑通过。
	if command.SettlementType != ledger.HostSalarySettlementTypeMonthEnd && account.TotalDiamonds <= progress.LastSettledTotalDiamonds {
		return true, tx.Commit()
	}

	// 区域来自周期账户快照，政策则沿 (app, region, cycle_key) 绑定读取；执行时间不参与版本选择。
	policy, ok, err := r.resolveHostSalaryPolicy(ctx, tx, account.RegionID, account.CycleKey, command.SettlementType, command.TriggerMode)
	if err != nil || !ok {
		return false, err
	}
	shouldSettleHost := command.SettlementRole == "all" || command.SettlementRole == "host"
	shouldSettleAgency := command.SettlementRole == "all" || command.SettlementRole == "agency"
	// 工资表是累计值，因此每次只发“目标累计值 - 已发累计值”；这对应产品规则里的 2 级只补 2 级减 1 级。
	level := highestHostSalaryLevel(policy.Levels, account.TotalDiamonds)
	residualTarget := progress.ResidualUSDMinor
	residualDelta := int64(0)
	if command.SettlementType == ledger.HostSalarySettlementTypeMonthEnd {
		// 月底不物理清零历史钻石，改为把剩余钻石折算入账并写 month_end_cleared_at_ms，保留审计和重放依据。
		residualDiamonds := residualDiamondRemainder(account.TotalDiamonds, level)
		residualTarget, err = usdMinorFromDiamondRate(residualDiamonds, policy.ResidualDiamondToUSDRate)
		if err != nil {
			return false, err
		}
		residualDelta = positiveDelta(residualTarget, progress.ResidualUSDMinor)
	}
	if !shouldSettleHost {
		// Agency 人工结算只处理代理工资钱包；月底剩余钻石折美元仍属于 Host 工资钱包，不能在 Agency 行触发。
		residualDelta = 0
	}

	hostSalaryDelta := int64(0)
	hostCoinDelta := int64(0)
	if shouldSettleHost {
		hostSalaryDelta = positiveDelta(level.HostSalaryUSDMinor, progress.SettledHostSalaryUSDMinor)
		hostCoinDelta = positiveDelta(level.HostCoinReward, progress.SettledHostCoinReward)
	}
	agencyEntitlementDelta := int64(0)
	if shouldSettleAgency {
		agencyEntitlementDelta = positiveDelta(level.AgencySalaryUSDMinor, progress.SettledAgencySalaryUSDMinor)
	}
	agencyPaidDelta := agencyEntitlementDelta
	if account.AgencyOwnerUserID <= 0 {
		// 没有代理收款人时只把该等级代理权益标记为已处理，不在未来组织绑定后补发历史月份工资。
		agencyPaidDelta = 0
	}

	nextProgress := progress
	nextProgress.LastPolicyID = policy.PolicyID
	nextProgress.LastPolicyVersion = policy.PolicyVersion
	if shouldSettleHost && (hostSalaryDelta > 0 || hostCoinDelta > 0 || residualDelta > 0) {
		nextProgress.SettledLevelNo = maxInt(nextProgress.SettledLevelNo, level.LevelNo)
		nextProgress.SettledHostSalaryUSDMinor = maxInt64(nextProgress.SettledHostSalaryUSDMinor, level.HostSalaryUSDMinor)
		nextProgress.SettledHostCoinReward = maxInt64(nextProgress.SettledHostCoinReward, level.HostCoinReward)
		nextProgress.ResidualUSDMinor = maxInt64(nextProgress.ResidualUSDMinor, residualTarget)
	}
	if shouldSettleAgency && agencyEntitlementDelta > 0 {
		nextProgress.SettledLevelNo = maxInt(nextProgress.SettledLevelNo, level.LevelNo)
		nextProgress.SettledAgencySalaryUSDMinor = maxInt64(nextProgress.SettledAgencySalaryUSDMinor, level.AgencySalaryUSDMinor)
	}
	if account.AgencyOwnerUserID <= 0 {
		// 当前周期没有代理收款人时，代理权益视为已处理，避免后续绑定代理后追发旧周期代理工资。
		nextProgress.SettledAgencySalaryUSDMinor = maxInt64(nextProgress.SettledAgencySalaryUSDMinor, level.AgencySalaryUSDMinor)
	}
	if hostSalaryProgressCoversLevel(nextProgress, level, account, command.SettlementType, residualTarget) {
		nextProgress.SettledLevelNo = maxInt(nextProgress.SettledLevelNo, level.LevelNo)
		nextProgress.LastSettledTotalDiamonds = account.TotalDiamonds
		if command.SettlementType == ledger.HostSalarySettlementTypeMonthEnd {
			nextProgress.MonthEndClearedAtMS = command.NowMs
		}
	}

	paidHostUSDDelta, err := checkedAdd(hostSalaryDelta, residualDelta)
	if err != nil {
		return false, err
	}
	metadata := hostSalarySettlementMetadata{
		AppCode:                     appcode.FromContext(ctx),
		SettlementType:              command.SettlementType,
		TriggerMode:                 command.TriggerMode,
		SettlementRole:              command.SettlementRole,
		HostUserID:                  account.UserID,
		AgencyOwnerUserID:           account.AgencyOwnerUserID,
		CycleKey:                    account.CycleKey,
		RegionID:                    account.RegionID,
		PolicyID:                    policy.PolicyID,
		PolicyVersion:               policy.PolicyVersion,
		PolicyName:                  policy.Name,
		LevelNo:                     level.LevelNo,
		RequiredDiamonds:            level.RequiredDiamonds,
		TotalDiamonds:               account.TotalDiamonds,
		HostSalaryUSDMinorDelta:     hostSalaryDelta,
		HostCoinRewardDelta:         hostCoinDelta,
		AgencySalaryUSDMinorDelta:   agencyPaidDelta,
		ResidualUSDMinorDelta:       residualDelta,
		HostSalaryAssetType:         ledger.AssetHostSalaryUSD,
		AgencySalaryAssetType:       ledger.AssetAgencySalaryUSD,
		SettledLevelNo:              nextProgress.SettledLevelNo,
		SettledHostSalaryUSDMinor:   nextProgress.SettledHostSalaryUSDMinor,
		SettledHostCoinReward:       nextProgress.SettledHostCoinReward,
		SettledAgencySalaryUSDMinor: nextProgress.SettledAgencySalaryUSDMinor,
		ResidualUSDMinor:            nextProgress.ResidualUSDMinor,
		MonthEndClearedAtMS:         nextProgress.MonthEndClearedAtMS,
		ProcessedAtMS:               command.NowMs,
	}

	if paidHostUSDDelta > 0 || hostCoinDelta > 0 || agencyPaidDelta > 0 {
		// settlement commandID 带上已结算等级和当前钻石，确保同一增量重复执行会命中交易幂等键。
		commandID := hostSalarySettlementCommandID(command, account, policy.PolicyID, nextProgress)
		transactionID := transactionID(appcode.FromContext(ctx), commandID)
		if err := r.insertTransaction(ctx, tx, transactionID, commandID, bizTypeHostSalarySettlement, hostSalarySettlementRequestHash(metadata), fmt.Sprintf("%s:%d", account.CycleKey, account.UserID), metadata, command.NowMs); err != nil {
			return false, err
		}
		events, err := r.applyHostSalaryCredits(ctx, tx, transactionID, commandID, metadata, paidHostUSDDelta, hostCoinDelta, agencyPaidDelta, command.NowMs)
		if err != nil {
			return false, err
		}
		if err := r.insertWalletOutbox(ctx, tx, events); err != nil {
			return false, err
		}
		if err := r.insertHostSalarySettlementRecord(ctx, tx, transactionID, commandID, metadata, command.NowMs); err != nil {
			return false, err
		}
	}
	if err := r.updateHostSalaryProgress(ctx, tx, account.UserID, account.CycleKey, nextProgress, command.NowMs); err != nil {
		return false, err
	}
	if err := tx.Commit(); err != nil {
		return false, err
	}
	return true, nil
}

func (r *Repository) lockHostSalaryCandidate(ctx context.Context, tx *sql.Tx, userID int64, cycleKey string) (hostSalaryCandidate, bool, error) {
	// 锁定周期账户后再读取总钻石，防止送礼入账与结算批次并发时结算到半更新状态。
	row := tx.QueryRowContext(ctx, `
		SELECT user_id, cycle_key, region_id, agency_owner_user_id, total_diamonds, gift_diamond_total
		FROM host_period_diamond_accounts
		WHERE app_code = ? AND user_id = ? AND cycle_key = ?
		FOR UPDATE`, appcode.FromContext(ctx), userID, cycleKey)
	var account hostSalaryCandidate
	if err := row.Scan(&account.UserID, &account.CycleKey, &account.RegionID, &account.AgencyOwnerUserID, &account.TotalDiamonds, &account.GiftDiamondTotal); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return hostSalaryCandidate{}, false, nil
		}
		return hostSalaryCandidate{}, false, err
	}
	return account, true, nil
}

func (r *Repository) lockHostSalaryProgress(ctx context.Context, tx *sql.Tx, userID int64, cycleKey string, nowMs int64) (hostSalaryProgress, error) {
	// INSERT IGNORE 让第一次结算自动创建进度行；随后 FOR UPDATE 串行化同主播同周期的所有结算请求。
	if _, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO host_salary_settlement_progress (
		  app_code, user_id, cycle_key, settled_level_no, settled_host_salary_usd_minor,
		  settled_host_coin_reward, settled_agency_salary_usd_minor, last_settled_total_diamonds,
		  residual_usd_minor, month_end_cleared_at_ms, last_policy_id, last_policy_version,
		  version, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, ?, ?)`,
		appcode.FromContext(ctx), userID, cycleKey, nowMs, nowMs,
	); err != nil {
		return hostSalaryProgress{}, err
	}
	row := tx.QueryRowContext(ctx, `
		SELECT settled_level_no, settled_host_salary_usd_minor, settled_host_coin_reward,
		       settled_agency_salary_usd_minor, last_settled_total_diamonds, residual_usd_minor,
		       month_end_cleared_at_ms, last_policy_id, last_policy_version, version
		FROM host_salary_settlement_progress
		WHERE app_code = ? AND user_id = ? AND cycle_key = ?
		FOR UPDATE`, appcode.FromContext(ctx), userID, cycleKey)
	var progress hostSalaryProgress
	if err := row.Scan(&progress.SettledLevelNo, &progress.SettledHostSalaryUSDMinor, &progress.SettledHostCoinReward, &progress.SettledAgencySalaryUSDMinor, &progress.LastSettledTotalDiamonds, &progress.ResidualUSDMinor, &progress.MonthEndClearedAtMS, &progress.LastPolicyID, &progress.LastPolicyVersion, &progress.Version); err != nil {
		return hostSalaryProgress{}, err
	}
	return progress, nil
}

// GetActiveHostSalaryPolicy 返回 app、区域和工资周期已绑定的不可变政策版本；这是 H5 展示平台政策的只读入口。
func (r *Repository) GetActiveHostSalaryPolicy(ctx context.Context, regionID int64, settlementMode string, triggerMode string, cycleKey string) (ledger.HostSalaryPolicy, bool, error) {
	if r == nil || r.db == nil {
		return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	return r.queryHostSalaryPolicyByCycle(ctx, r.db, regionID, cycleKey, settlementMode, triggerMode)
}

// GetHostSalaryProgress 读取主播工资周期钻石账户；没有任何收礼时返回空累计，前端据此展示距离首档还差多少钻石。
func (r *Repository) GetHostSalaryProgress(ctx context.Context, userID int64, cycleKey string) (ledger.HostSalaryProgress, error) {
	if r == nil || r.db == nil {
		return ledger.HostSalaryProgress{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	progress := ledger.HostSalaryProgress{
		HostUserID: userID,
		CycleKey:   cycleKey,
	}
	row := r.db.QueryRowContext(ctx, `
		SELECT user_id, cycle_key, region_id, agency_owner_user_id, total_diamonds,
		       gift_diamond_total, updated_at_ms
		FROM host_period_diamond_accounts
		WHERE app_code = ? AND user_id = ? AND cycle_key = ?`,
		appcode.FromContext(ctx), userID, cycleKey)
	if err := row.Scan(&progress.HostUserID, &progress.CycleKey, &progress.RegionID, &progress.AgencyOwnerUserID, &progress.TotalDiamonds, &progress.GiftDiamondTotal, &progress.UpdatedAtMS); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return progress, nil
		}
		return ledger.HostSalaryProgress{}, err
	}
	return progress, nil
}

func (r *Repository) resolveHostSalaryPolicy(ctx context.Context, tx *sql.Tx, regionID int64, cycleKey string, settlementType string, triggerMode string) (ledger.HostSalaryPolicy, bool, error) {
	settlementMode := ""
	if settlementType != ledger.HostSalarySettlementTypeMonthEnd {
		settlementMode = settlementType
	}
	policy, found, err := r.queryHostSalaryPolicyByCycle(ctx, tx, regionID, cycleKey, settlementMode, triggerMode)
	if err != nil || !found {
		return policy, found, err
	}
	if policy.PolicyType != ledger.HostPolicyTypeSalaryDiamond {
		// POINT_DIAMOND 永久钱包没有工资等级和月底清算语义，结算任务必须将其视为无工资政策。
		return ledger.HostSalaryPolicy{}, false, nil
	}
	return policy, true, nil
}

func (r *Repository) queryHostSalaryPolicyByCycle(ctx context.Context, q hostSalaryPolicyQuerier, regionID int64, cycleKey string, settlementMode string, triggerMode string) (ledger.HostSalaryPolicy, bool, error) {
	policy, found, err := r.queryBoundHostSalaryPolicy(ctx, q, regionID, cycleKey, settlementMode, triggerMode)
	if err != nil || found {
		return policy, found, err
	}
	// 当前周期无绑定时，仅复制紧邻上月同条件的不可变版本；已有周期绑定从不覆盖，历史工资口径保持可追溯。
	previousCycleKey, err := previousHostSalaryCycleKey(cycleKey)
	if err != nil {
		return ledger.HostSalaryPolicy{}, false, nil
	}
	if err := r.inheritHostSalaryPolicyBinding(ctx, q, regionID, previousCycleKey, cycleKey, settlementMode, triggerMode); err != nil {
		return ledger.HostSalaryPolicy{}, false, err
	}
	return r.queryBoundHostSalaryPolicy(ctx, q, regionID, cycleKey, settlementMode, triggerMode)
}

func (r *Repository) queryBoundHostSalaryPolicy(ctx context.Context, q hostSalaryPolicyQuerier, regionID int64, cycleKey string, settlementMode string, triggerMode string) (ledger.HostSalaryPolicy, bool, error) {
	modeClause := ""
	triggerClause := ""
	args := []any{appcode.FromContext(ctx), regionID, cycleKey}
	if strings.TrimSpace(triggerMode) != "" {
		// H5 展示不传 trigger 时 automatic/manual 都可展示；结算任务传 trigger 时仍精确匹配。
		triggerClause = "AND p.settlement_trigger_mode = ?"
		args = append(args, triggerMode)
	}
	if strings.TrimSpace(settlementMode) != "" {
		// SQL 里 trigger 条件排在 mode 前面，所以参数也按 trigger -> mode 追加；否则 manual half-month 会被查成 trigger=half_month。
		modeClause = "AND p.settlement_mode = ?"
		args = append(args, settlementMode)
	}
	query := fmt.Sprintf(`
		SELECT p.policy_id, binding.cycle_key, p.policy_version, p.name, p.policy_type, binding.region_id, p.status,
		       p.settlement_mode, p.settlement_trigger_mode,
		       CAST(p.gift_coin_to_diamond_ratio AS CHAR),
		       p.point_diamonds_per_usd, p.coins_per_usd, p.minimum_withdraw_usd_minor,
		       p.coin_seller_minimum_withdraw_usd_minor, p.platform_minimum_withdraw_usd_minor, p.diamond_exchange_enabled,
		       p.withdraw_fee_bps, p.agency_point_share_bps,
		       CAST(p.residual_diamond_to_usd_rate AS CHAR),
		       p.coin_seller_withdrawal_limit_period, p.coin_seller_withdrawal_limit_count,
		       p.platform_withdrawal_limit_period, p.platform_withdrawal_limit_count, p.platform_withdrawal_allowed_days,
		       p.effective_from_ms, p.effective_to_ms
		FROM host_salary_policy_cycle_bindings binding
		JOIN host_agency_salary_policies p
		  ON p.app_code = binding.app_code
		 AND p.policy_id = binding.policy_id
		 AND p.policy_version = binding.policy_version
		WHERE binding.app_code = ?
		  AND binding.region_id = ?
		  AND binding.cycle_key = ?
		  AND p.status = 'active'
		  %s
		  %s
		LIMIT 1`, triggerClause, modeClause)
	var policy ledger.HostSalaryPolicy
	err := q.QueryRowContext(ctx, query, args...).Scan(
		&policy.PolicyID, &policy.CycleKey, &policy.PolicyVersion, &policy.Name, &policy.PolicyType, &policy.RegionID, &policy.Status,
		&policy.SettlementMode, &policy.SettlementTriggerMode, &policy.GiftCoinToDiamondRatio,
		&policy.PointDiamondsPerUSD, &policy.CoinsPerUSD, &policy.MinimumWithdrawUSDMinor,
		&policy.CoinSellerMinimumWithdrawUSDMinor, &policy.PlatformMinimumWithdrawUSDMinor, &policy.DiamondExchangeEnabled,
		&policy.WithdrawFeeBPS,
		&policy.AgencyPointShareBPS,
		&policy.ResidualDiamondToUSDRate,
		&policy.CoinSellerWithdrawalLimitPeriod, &policy.CoinSellerWithdrawalLimitCount,
		&policy.PlatformWithdrawalLimitPeriod, &policy.PlatformWithdrawalLimitCount,
		&policy.PlatformWithdrawalAllowedDays,
		&policy.EffectiveFromMs, &policy.EffectiveToMs,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return ledger.HostSalaryPolicy{}, false, nil
		}
		return ledger.HostSalaryPolicy{}, false, err
	}
	policy.PolicyType = strings.ToUpper(strings.TrimSpace(policy.PolicyType))
	if policy.PolicyType != ledger.HostPolicyTypeSalaryDiamond && policy.PolicyType != ledger.HostPolicyTypePointDiamond {
		return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.Internal, "host policy type is invalid")
	}
	normalizeHostPolicyWithdrawalMinimums(&policy)
	levels, err := r.listHostSalaryPolicyLevels(ctx, q, policy.PolicyID, policy.PolicyVersion)
	if err != nil {
		return ledger.HostSalaryPolicy{}, false, err
	}
	policy.Levels = levels
	return policy, true, nil
}

func (r *Repository) inheritHostSalaryPolicyBinding(ctx context.Context, q hostSalaryPolicyQuerier, regionID int64, previousCycleKey string, cycleKey string, settlementMode string, triggerMode string) error {
	modeClause := ""
	triggerClause := ""
	args := []any{appcode.FromContext(ctx), regionID, previousCycleKey}
	if strings.TrimSpace(triggerMode) != "" {
		triggerClause = "AND p.settlement_trigger_mode = ?"
		args = append(args, triggerMode)
	}
	if strings.TrimSpace(settlementMode) != "" {
		modeClause = "AND p.settlement_mode = ?"
		args = append(args, settlementMode)
	}
	var policyID, policyVersion uint64
	err := q.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT binding.policy_id, binding.policy_version
		FROM host_salary_policy_cycle_bindings binding
		JOIN host_agency_salary_policies p
		  ON p.app_code = binding.app_code
		 AND p.policy_id = binding.policy_id
		 AND p.policy_version = binding.policy_version
		WHERE binding.app_code = ?
		  AND binding.region_id = ?
		  AND binding.cycle_key = ?
		  AND p.status = 'active'
		  %s
		  %s
		LIMIT 1`, triggerClause, modeClause), args...).Scan(&policyID, &policyVersion)
	if errors.Is(err, sql.ErrNoRows) {
		return nil
	}
	if err != nil {
		return err
	}
	// INSERT IGNORE 只吸收并发继承；它绝不改写已经存在的当前周期绑定。
	_, err = q.ExecContext(ctx, `
		INSERT IGNORE INTO host_salary_policy_cycle_bindings (
		  app_code, region_id, cycle_key, policy_id, policy_version, source_cycle_key, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), regionID, cycleKey, policyID, policyVersion, previousCycleKey, time.Now().UTC().UnixMilli())
	return err
}

func previousHostSalaryCycleKey(cycleKey string) (string, error) {
	trimmed := strings.TrimSpace(cycleKey)
	parsed, err := time.Parse("2006-01", trimmed)
	if err != nil || parsed.UTC().Format("2006-01") != trimmed {
		return "", errors.New("cycle_key is invalid")
	}
	return parsed.AddDate(0, -1, 0).Format("2006-01"), nil
}

func (r *Repository) listHostSalaryPolicyLevels(ctx context.Context, q hostSalaryPolicyQuerier, policyID uint64, policyVersion uint64) ([]ledger.HostSalaryPolicyLevel, error) {
	// 等级必须按 required_diamonds 升序返回，highestHostSalaryLevel 依赖这个顺序找到最高已达档位。
	rows, err := q.QueryContext(ctx, `
		SELECT level_no, required_diamonds, host_salary_usd_minor, host_coin_reward,
		       agency_salary_usd_minor, status, sort_order
		FROM host_agency_salary_policy_levels
		WHERE app_code = ? AND policy_id = ? AND policy_version = ? AND status = 'active'
		ORDER BY required_diamonds ASC, level_no ASC`, appcode.FromContext(ctx), policyID, policyVersion)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var levels []ledger.HostSalaryPolicyLevel
	for rows.Next() {
		var level ledger.HostSalaryPolicyLevel
		if err := rows.Scan(&level.LevelNo, &level.RequiredDiamonds, &level.HostSalaryUSDMinor, &level.HostCoinReward, &level.AgencySalaryUSDMinor, &level.Status, &level.SortOrder); err != nil {
			return nil, err
		}
		levels = append(levels, level)
	}
	return levels, rows.Err()
}

func (r *Repository) applyHostSalaryCredits(ctx context.Context, tx *sql.Tx, transactionID string, commandID string, metadata hostSalarySettlementMetadata, hostUSDDelta int64, hostCoinDelta int64, agencyUSDDelta int64, nowMs int64) ([]walletOutboxEvent, error) {
	// 主播美元、主播金币、代理美元共用同一 transaction_id，确保账务上可视为一次工资结算。
	events := make([]walletOutboxEvent, 0, 3)
	if hostUSDDelta > 0 {
		// 主播工资和月底剩余钻石折美元只进入主播工资钱包，不混入代理或团队工资钱包。
		balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.HostUserID, ledger.AssetHostSalaryUSD, hostUSDDelta, 0, nowMs)
		if err != nil {
			return nil, err
		}
		metadata.HostUSDBalanceAfter = balanceAfter
		events = append(events, balanceChangedEvent(transactionID, commandID, metadata.HostUserID, ledger.AssetHostSalaryUSD, hostUSDDelta, 0, balanceAfter, 0, version, metadata, nowMs, bizTypeHostSalarySettlement))
	}
	if hostCoinDelta > 0 {
		balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.HostUserID, ledger.AssetCoin, hostCoinDelta, 0, nowMs)
		if err != nil {
			return nil, err
		}
		metadata.HostCoinBalanceAfter = balanceAfter
		events = append(events, balanceChangedEvent(transactionID, commandID, metadata.HostUserID, ledger.AssetCoin, hostCoinDelta, 0, balanceAfter, 0, version, metadata, nowMs, bizTypeHostSalarySettlement))
	}
	if agencyUSDDelta > 0 {
		// 代理工资进入代理工资钱包，同一用户若同时是主播也不会和主播工资钱包合并。
		balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.AgencyOwnerUserID, ledger.AssetAgencySalaryUSD, agencyUSDDelta, metadata.HostUserID, nowMs)
		if err != nil {
			return nil, err
		}
		metadata.AgencyUSDBalanceAfter = balanceAfter
		events = append(events, balanceChangedEvent(transactionID, commandID, metadata.AgencyOwnerUserID, ledger.AssetAgencySalaryUSD, agencyUSDDelta, 0, balanceAfter, 0, version, metadata, nowMs, bizTypeHostSalarySettlement))
	}
	return events, nil
}

func (r *Repository) creditSettlementAsset(ctx context.Context, tx *sql.Tx, transactionID string, userID int64, assetType string, amount int64, counterpartyUserID int64, nowMs int64) (int64, int64, error) {
	// 工资入账只增加可用余额，不走冻结；counterparty 仅用于代理工资反查来源主播。
	account, err := r.lockAccount(ctx, tx, userID, assetType, true, nowMs)
	if err != nil {
		return 0, 0, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, amount, 0, nowMs); err != nil {
		return 0, 0, err
	}
	after := account.AvailableAmount + amount
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             userID,
		AssetType:          assetType,
		AvailableDelta:     amount,
		FrozenDelta:        0,
		AvailableAfter:     after,
		FrozenAfter:        account.FrozenAmount,
		CounterpartyUserID: counterpartyUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return 0, 0, err
	}
	return after, account.Version + 1, nil
}

func (r *Repository) insertHostSalarySettlementRecord(ctx context.Context, tx *sql.Tx, transactionID string, commandID string, metadata hostSalarySettlementMetadata, nowMs int64) error {
	// 结算记录保存每次实际发放的增量；BD 结算后续从这里汇总主播工资和剩余钻石折美元。
	_, err := tx.ExecContext(ctx, `
		INSERT INTO host_salary_settlement_records (
		  app_code, settlement_id, command_id, transaction_id, settlement_type, trigger_mode, settlement_role, user_id,
		  agency_owner_user_id, cycle_key, policy_id, policy_version, level_no, total_diamonds,
		  host_salary_usd_minor_delta, host_coin_reward_delta, agency_salary_usd_minor_delta,
		  residual_usd_minor_delta, status, reason, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'succeeded', '', ?)`,
		appcode.FromContext(ctx),
		hostSalarySettlementID(metadata.AppCode, commandID),
		commandID,
		transactionID,
		metadata.SettlementType,
		metadata.TriggerMode,
		metadata.SettlementRole,
		metadata.HostUserID,
		metadata.AgencyOwnerUserID,
		metadata.CycleKey,
		metadata.PolicyID,
		metadata.PolicyVersion,
		metadata.LevelNo,
		metadata.TotalDiamonds,
		metadata.HostSalaryUSDMinorDelta,
		metadata.HostCoinRewardDelta,
		metadata.AgencySalaryUSDMinorDelta,
		metadata.ResidualUSDMinorDelta,
		nowMs,
	)
	return err
}

func (r *Repository) updateHostSalaryProgress(ctx context.Context, tx *sql.Tx, userID int64, cycleKey string, progress hostSalaryProgress, nowMs int64) error {
	// version 乐观锁补充 FOR UPDATE 之外的防线：异常重入不会悄悄覆盖已推进的进度。
	result, err := tx.ExecContext(ctx, `
		UPDATE host_salary_settlement_progress
		SET settled_level_no = ?,
		    settled_host_salary_usd_minor = ?,
		    settled_host_coin_reward = ?,
		    settled_agency_salary_usd_minor = ?,
		    last_settled_total_diamonds = ?,
		    residual_usd_minor = ?,
		    month_end_cleared_at_ms = ?,
		    last_policy_id = ?,
		    last_policy_version = ?,
		    version = version + 1,
		    updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND cycle_key = ? AND version = ?`,
		progress.SettledLevelNo,
		progress.SettledHostSalaryUSDMinor,
		progress.SettledHostCoinReward,
		progress.SettledAgencySalaryUSDMinor,
		progress.LastSettledTotalDiamonds,
		progress.ResidualUSDMinor,
		progress.MonthEndClearedAtMS,
		progress.LastPolicyID,
		progress.LastPolicyVersion,
		nowMs,
		appcode.FromContext(ctx),
		userID,
		cycleKey,
		progress.Version,
	)
	if err != nil {
		return err
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return err
	}
	if rows != 1 {
		return xerr.New(xerr.LedgerConflict, "host salary settlement progress version conflict")
	}
	return nil
}

func highestHostSalaryLevel(levels []ledger.HostSalaryPolicyLevel, totalDiamonds int64) ledger.HostSalaryPolicyLevel {
	// 未达到任何等级时返回零值，后续 delta 都为 0；仍可在月底用于只折算剩余钻石。
	var selected ledger.HostSalaryPolicyLevel
	for _, level := range levels {
		if level.RequiredDiamonds <= totalDiamonds {
			selected = level
		}
	}
	return selected
}

func residualDiamondRemainder(totalDiamonds int64, level ledger.HostSalaryPolicyLevel) int64 {
	// 剩余钻石只计算最高已达等级以上的部分，避免把已获得等级工资的门槛钻石重复折美元。
	if level.LevelNo <= 0 || totalDiamonds <= level.RequiredDiamonds {
		return 0
	}
	return totalDiamonds - level.RequiredDiamonds
}

func usdMinorFromDiamondRate(diamonds int64, rate string) (int64, error) {
	// 使用 big.Rat 做精确十进制换算，避免 residual_diamond_to_usd_rate 走 float 后出现分位误差。
	if diamonds <= 0 || strings.TrimSpace(rate) == "" {
		return 0, nil
	}
	rat, ok := new(big.Rat).SetString(strings.TrimSpace(rate))
	if !ok || rat.Sign() < 0 {
		return 0, xerr.New(xerr.InvalidArgument, "residual diamond rate is invalid")
	}
	value := new(big.Rat).Mul(new(big.Rat).SetInt64(diamonds), rat)
	value.Mul(value, big.NewRat(100, 1))
	result := new(big.Int).Quo(value.Num(), value.Denom())
	if !result.IsInt64() || result.Int64() > math.MaxInt64 {
		return 0, xerr.New(xerr.InvalidArgument, "residual diamond amount overflow")
	}
	return result.Int64(), nil
}

func normalizeHostSalarySettlementType(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case ledger.HostSalarySettlementModeDaily:
		return ledger.HostSalarySettlementModeDaily
	case ledger.HostSalarySettlementModeHalfMonth:
		return ledger.HostSalarySettlementModeHalfMonth
	case ledger.HostSalarySettlementTypeMonthEnd:
		return ledger.HostSalarySettlementTypeMonthEnd
	default:
		return ""
	}
}

func normalizeHostSalarySettlementTriggerMode(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", ledger.HostSalarySettlementTriggerAutomatic:
		// 空值按自动处理，保证已有 cron 调用不需要同步改所有入参。
		return ledger.HostSalarySettlementTriggerAutomatic
	case ledger.HostSalarySettlementTriggerManual:
		return ledger.HostSalarySettlementTriggerManual
	default:
		return ""
	}
}

func normalizeHostSalarySettlementRole(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", "all":
		// 空值按完整结算处理，保证已有定时任务继续同时处理主播和代理权益。
		return "all"
	case "host":
		return "host"
	case "agency":
		return "agency"
	default:
		return ""
	}
}

func uniquePositiveInt64(values []int64) []int64 {
	seen := map[int64]struct{}{}
	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 hostSalarySettlementCommandID(command ledger.HostSalarySettlementBatchCommand, account hostSalaryCandidate, policyID uint64, progress hostSalaryProgress) string {
	// commandID 包含触发方式和政策，避免 automatic/manual 共存或政策重发时把不同来源的结算误判为同一交易。
	return fmt.Sprintf("host_salary:%s:%s:%s:%s:%d:%d:%d:%d", command.SettlementType, command.TriggerMode, command.SettlementRole, account.CycleKey, account.UserID, policyID, progress.SettledLevelNo, account.TotalDiamonds)
}

func hostSalarySettlementID(appCode string, commandID string) string {
	// settlement_id 对外稳定且不暴露原始 command_id，方便后台查账和防重复记录。
	return fmt.Sprintf("hsset_%s_%s", appcode.Normalize(appCode), stableHash(commandID))
}

func hostSalarySettlementRequestHash(metadata hostSalarySettlementMetadata) string {
	// 请求哈希覆盖金额、等级和周期口径；同 commandID 但 payload 改变会被交易层识别为冲突。
	return stableHash(fmt.Sprintf("host_salary|%s|%s|%s|%s|%d|%d|%s|%d|%d|%d|%d|%d|%d|%d|%d",
		appcode.Normalize(metadata.AppCode),
		metadata.SettlementType,
		metadata.TriggerMode,
		metadata.SettlementRole,
		metadata.HostUserID,
		metadata.AgencyOwnerUserID,
		metadata.CycleKey,
		metadata.PolicyID,
		metadata.PolicyVersion,
		metadata.LevelNo,
		metadata.TotalDiamonds,
		metadata.HostSalaryUSDMinorDelta,
		metadata.HostCoinRewardDelta,
		metadata.AgencySalaryUSDMinorDelta,
		metadata.ResidualUSDMinorDelta,
	))
}

func hostSalaryProgressCoversLevel(progress hostSalaryProgress, level ledger.HostSalaryPolicyLevel, account hostSalaryCandidate, settlementType string, residualTarget int64) bool {
	hostCovered := progress.SettledHostSalaryUSDMinor >= level.HostSalaryUSDMinor && progress.SettledHostCoinReward >= level.HostCoinReward
	agencyCovered := progress.SettledAgencySalaryUSDMinor >= level.AgencySalaryUSDMinor || account.AgencyOwnerUserID <= 0
	residualCovered := settlementType != ledger.HostSalarySettlementTypeMonthEnd || progress.ResidualUSDMinor >= residualTarget
	return hostCovered && agencyCovered && residualCovered
}

func positiveDelta(target int64, current int64) int64 {
	if target <= current {
		return 0
	}
	return target - current
}

func maxInt(left int, right int) int {
	if right > left {
		return right
	}
	return left
}

func maxInt64(left int64, right int64) int64 {
	if right > left {
		return right
	}
	return left
}
