package repository

import (
	"errors"
	"math/big"
	"strings"

	"hyapp-admin-server/internal/model"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

var (
	ErrWithdrawalApplicationAlreadyAudited     = errors.New("withdrawal application already audited")
	ErrWithdrawalApplicationStageNotReviewable = errors.New("withdrawal application is not ready for this review stage")
)

type WithdrawalApplicationReviewStage string

const (
	WithdrawalApplicationReviewStageFinance    WithdrawalApplicationReviewStage = "finance"
	WithdrawalApplicationReviewStageOperations WithdrawalApplicationReviewStage = "operations"
)

type WithdrawalApplicationListOptions struct {
	Page             int
	PageSize         int
	AppCode          string
	AllowedAppCodes  []string
	RestrictAppCodes bool
	Keyword          string
	Stage            WithdrawalApplicationReviewStage
}

type WithdrawalApplicationAuditInput struct {
	Stage              WithdrawalApplicationReviewStage
	Decision           string
	ApproverUserID     uint
	ApproverName       string
	AuditRemark        string
	AuditImageURL      string
	AuditCommandID     string
	AuditTransactionID string
	ApprovedAtMS       int64
}

// WithdrawalApplicationAuditEffect 是持行锁期间完成的钱包命令结果。
// 运营通过没有资金副作用，因此允许返回空 command/transaction；任一拒绝和财务通过都会保存可追溯的账务事实。
type WithdrawalApplicationAuditEffect struct {
	CommandID     string
	TransactionID string
}

type WithdrawalApplicationAuditAction func(application model.UserWithdrawalApplication) (WithdrawalApplicationAuditEffect, error)

// WithdrawalApplicationClaimValidator 在拒绝 claim 提交前校验钱包释放所需的冻结快照。
// 校验必须留在持行锁的短事务内，避免坏数据先进入 rejecting 后再永久卡住人工流程。
type WithdrawalApplicationClaimValidator func(application model.UserWithdrawalApplication) error

// CreateExternalWithdrawalApplication 以 App、来源系统、来源申请号作为业务幂等键。
// 来源钱包采用持久重试投递；重复提交必须返回同一 admin 申请，不能制造两张可独立审核的资金单。
func (s *Store) CreateExternalWithdrawalApplication(application model.UserWithdrawalApplication) (*model.UserWithdrawalApplication, bool, error) {
	application.AppCode = strings.TrimSpace(application.AppCode)
	application.SourceSystem = strings.TrimSpace(application.SourceSystem)
	application.SourceApplicationID = strings.TrimSpace(application.SourceApplicationID)
	if application.AppCode == "" || application.SourceSystem == "" || application.SourceApplicationID == "" {
		return nil, false, errors.New("external withdrawal source identity is incomplete")
	}

	var existing model.UserWithdrawalApplication
	lookup := s.db.Where(
		"app_code = ? AND source_system = ? AND source_application_id = ?",
		application.AppCode,
		application.SourceSystem,
		application.SourceApplicationID,
	).First(&existing)
	if lookup.Error == nil {
		if err := validateExternalWithdrawalReplay(existing, application); err != nil {
			return nil, false, err
		}
		return &existing, false, nil
	}
	if !errors.Is(lookup.Error, gorm.ErrRecordNotFound) {
		return nil, false, lookup.Error
	}

	result := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&application)
	if result.Error != nil {
		return nil, false, result.Error
	}
	// 首次创建才通知运营。顺序重放会在上面的查询直接返回；并发竞争时 MySQL 的 no-op conflict 通常 RowsAffected=0，避免两次提醒。
	created := result.RowsAffected == 1
	// MySQL 对 ON DUPLICATE KEY 的 RowsAffected 会受连接参数影响；无论本次创建还是并发重放，都按业务键回读唯一事实。
	if err := s.db.Where(
		"app_code = ? AND source_system = ? AND source_application_id = ?",
		application.AppCode,
		application.SourceSystem,
		application.SourceApplicationID,
	).First(&existing).Error; err != nil {
		return nil, false, err
	}
	if err := validateExternalWithdrawalReplay(existing, application); err != nil {
		return nil, false, err
	}
	return &existing, created, nil
}

func validateExternalWithdrawalReplay(existing model.UserWithdrawalApplication, replay model.UserWithdrawalApplication) error {
	if existing.UserID != replay.UserID ||
		existing.SalaryAssetType != replay.SalaryAssetType ||
		existing.WithdrawAmountMinor != replay.WithdrawAmountMinor ||
		!sameOptionalDecimal(existing.BalanceBefore, replay.BalanceBefore) ||
		!sameOptionalDecimal(existing.ServiceChargeRatio, replay.ServiceChargeRatio) ||
		!sameOptionalDecimal(existing.ServiceCharge, replay.ServiceCharge) ||
		!sameOptionalDecimal(existing.ActualAmount, replay.ActualAmount) ||
		!sameOptionalInt64(existing.ActualAmountMinor, replay.ActualAmountMinor) ||
		existing.WithdrawMethod != replay.WithdrawMethod ||
		existing.WithdrawAddress != replay.WithdrawAddress {
		// 相同来源单号只能重放完全一致的资金事实，不能用幂等接口覆盖已进入人工审核的金额或收款地址。
		return errors.New("external withdrawal replay payload conflicts with existing application")
	}
	return nil
}

func sameOptionalDecimal(existing *string, replay *string) bool {
	// 快照字段上线前的旧单没有可信值；旧单重试不能因新增字段被卡死，但已落库的快照禁止被丢弃或篡改。
	if existing == nil {
		return true
	}
	if replay == nil {
		return false
	}
	// DECIMAL(18,2) 回读会把来源的 84.5 规范化成 84.50；资金幂等比较必须按精确十进制数值，而非展示字符串。
	existingValue, existingOK := new(big.Rat).SetString(strings.TrimSpace(*existing))
	replayValue, replayOK := new(big.Rat).SetString(strings.TrimSpace(*replay))
	return existingOK && replayOK && existingValue.Cmp(replayValue) == 0
}

func sameOptionalInt64(existing *int64, replay *int64) bool {
	// 与可选 decimal 快照保持相同的旧单兼容边界；一旦存在最小单位事实，幂等重放必须完全一致。
	if existing == nil {
		return true
	}
	return replay != nil && *existing == *replay
}

func (s *Store) GetWithdrawalApplication(id uint) (*model.UserWithdrawalApplication, error) {
	var application model.UserWithdrawalApplication
	if err := s.db.First(&application, id).Error; err != nil {
		return nil, err
	}
	return &application, nil
}

func (s *Store) GetWithdrawalApplicationForApp(appCode string, id uint) (*model.UserWithdrawalApplication, error) {
	var application model.UserWithdrawalApplication
	if err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&application).Error; err != nil {
		return nil, err
	}
	return &application, nil
}

func (s *Store) ListWithdrawalApplications(options WithdrawalApplicationListOptions) ([]model.UserWithdrawalApplication, int64, error) {
	page, pageSize := normalizePage(options.Page, options.PageSize)
	query := s.db.Model(&model.UserWithdrawalApplication{})
	query = applyWithdrawalApplicationFilters(query, options)

	var total int64
	if err := query.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	var applications []model.UserWithdrawalApplication
	// 提现申请用于运营初审和财务终审，默认按最新申请优先展示；id 作为同毫秒写入时的稳定次序，避免分页翻页时记录抖动。
	err := query.
		Order("created_at_ms DESC, id DESC").
		Limit(pageSize).
		Offset((page - 1) * pageSize).
		Find(&applications).Error
	return applications, total, err
}

func (s *Store) AuditWithdrawalApplication(id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) {
	return s.ReviewWithdrawalApplicationForApp("", id, input, nil)
}

func (s *Store) AuditWithdrawalApplicationForApp(appCode string, id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) {
	return s.ReviewWithdrawalApplicationForApp(appCode, id, input, nil)
}

// ClaimWithdrawalOperationsRejectionForApp 先独立提交 rejecting，再允许调用 wallet/inbox。
// 这个持久门禁覆盖“钱包释放成功、但通知或 admin 最终提交失败”的窗口：后续运营通过和财务审核都会被 rejecting 拒绝，
// 只有相同阶段 command 的拒绝重试可以继续，且审核人、拒绝原因以第一次 claim 的快照为准。
func (s *Store) ClaimWithdrawalOperationsRejectionForApp(appCode string, id uint, input WithdrawalApplicationAuditInput, validate WithdrawalApplicationClaimValidator) (*model.UserWithdrawalApplication, error) {
	if id == 0 {
		return nil, errors.New("withdrawal application id is required")
	}
	if input.Stage != WithdrawalApplicationReviewStageOperations || input.Decision != model.WithdrawalApplicationStatusRejected {
		return nil, errors.New("withdrawal application rejection claim is invalid")
	}
	input.AuditCommandID = strings.TrimSpace(input.AuditCommandID)
	if input.AuditCommandID == "" || input.ApproverUserID == 0 || strings.TrimSpace(input.AuditRemark) == "" || input.ApprovedAtMS <= 0 {
		return nil, errors.New("withdrawal application rejection claim is incomplete")
	}

	var application model.UserWithdrawalApplication
	err := s.db.Transaction(func(tx *gorm.DB) error {
		query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
		if strings.TrimSpace(appCode) != "" {
			query = query.Where("app_code = ?", strings.TrimSpace(appCode))
		}
		if err := query.First(&application, id).Error; err != nil {
			return err
		}

		operationsStatus := strings.TrimSpace(application.OperationsStatus)
		if application.Status == model.WithdrawalApplicationStatusRejected && operationsStatus == model.WithdrawalOperationsStatusRejected {
			// 已完成拒绝是终态幂等响应；不能再次触发 wallet 或 notice。
			return nil
		}
		if application.Status != model.WithdrawalApplicationStatusPending {
			return ErrWithdrawalApplicationAlreadyAudited
		}
		switch operationsStatus {
		case model.WithdrawalOperationsStatusPending:
			if validate != nil {
				if err := validate(application); err != nil {
					return err
				}
			}
			updates := map[string]any{
				"operations_status":           model.WithdrawalOperationsStatusRejecting,
				"operations_reviewer_user_id": input.ApproverUserID,
				"operations_reviewer_name":    strings.TrimSpace(input.ApproverName),
				"operations_audit_remark":     strings.TrimSpace(input.AuditRemark),
				"operations_audit_command_id": input.AuditCommandID,
				"operations_reviewed_at_ms":   input.ApprovedAtMS,
				"updated_at_ms":               input.ApprovedAtMS,
			}
			if err := tx.Model(&application).Updates(updates).Error; err != nil {
				return err
			}
			application.OperationsStatus = model.WithdrawalOperationsStatusRejecting
			application.OperationsReviewerUserID = &input.ApproverUserID
			application.OperationsReviewerName = strings.TrimSpace(input.ApproverName)
			application.OperationsAuditRemark = strings.TrimSpace(input.AuditRemark)
			application.OperationsAuditCommandID = input.AuditCommandID
			application.OperationsReviewedAtMS = &input.ApprovedAtMS
			application.UpdatedAtMS = input.ApprovedAtMS
			return nil
		case model.WithdrawalOperationsStatusRejecting:
			// 重试不能改写第一次审核的上下文，否则钱包 metadata、通知原因和后台审计记录会彼此矛盾。
			if strings.TrimSpace(application.OperationsAuditCommandID) != input.AuditCommandID {
				return ErrWithdrawalApplicationStageNotReviewable
			}
			if validate != nil {
				return validate(application)
			}
			return nil
		default:
			return ErrWithdrawalApplicationStageNotReviewable
		}
	})
	if err != nil {
		return nil, err
	}
	return &application, nil
}

// FinalizeWithdrawalOperationsRejectionForApp 只把已持久 claim 的 rejecting 收敛到双重 rejected 终态。
// wallet transaction id 非空且 stage command 必须与 claim 一致，防止其他流程借最终更新覆盖资金事实。
func (s *Store) FinalizeWithdrawalOperationsRejectionForApp(appCode string, id uint, commandID string, transactionID string, completedAtMS int64) (*model.UserWithdrawalApplication, error) {
	commandID = strings.TrimSpace(commandID)
	transactionID = strings.TrimSpace(transactionID)
	if id == 0 || commandID == "" || transactionID == "" || completedAtMS <= 0 {
		return nil, errors.New("withdrawal application rejection finalization is incomplete")
	}

	var application model.UserWithdrawalApplication
	err := s.db.Transaction(func(tx *gorm.DB) error {
		query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
		if strings.TrimSpace(appCode) != "" {
			query = query.Where("app_code = ?", strings.TrimSpace(appCode))
		}
		if err := query.First(&application, id).Error; err != nil {
			return err
		}

		operationsStatus := strings.TrimSpace(application.OperationsStatus)
		if application.Status == model.WithdrawalApplicationStatusRejected && operationsStatus == model.WithdrawalOperationsStatusRejected {
			if strings.TrimSpace(application.OperationsAuditCommandID) != commandID || strings.TrimSpace(application.OperationsAuditTransactionID) != transactionID {
				return ErrWithdrawalApplicationStageNotReviewable
			}
			return nil
		}
		if application.Status != model.WithdrawalApplicationStatusPending || operationsStatus != model.WithdrawalOperationsStatusRejecting || strings.TrimSpace(application.OperationsAuditCommandID) != commandID {
			return ErrWithdrawalApplicationStageNotReviewable
		}
		updates := map[string]any{
			"status":                          model.WithdrawalApplicationStatusRejected,
			"operations_status":               model.WithdrawalOperationsStatusRejected,
			"operations_audit_transaction_id": transactionID,
			"updated_at_ms":                   completedAtMS,
		}
		if err := tx.Model(&application).Updates(updates).Error; err != nil {
			return err
		}
		application.Status = model.WithdrawalApplicationStatusRejected
		application.OperationsStatus = model.WithdrawalOperationsStatusRejected
		application.OperationsAuditTransactionID = transactionID
		application.UpdatedAtMS = completedAtMS
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &application, nil
}

// ClaimExternalWithdrawalFinanceDecisionForApp 在调用来源钱包前独立提交首次财务决策。
// 调用方重试可以更换登录人，但只能沿用已持久的 decision/actor/remark/image/command/time；
// 这使“来源回调 2xx、Admin finalize 失败”后的再次回调仍使用完全相同的幂等请求。
func (s *Store) ClaimExternalWithdrawalFinanceDecisionForApp(appCode string, id uint, input WithdrawalApplicationAuditInput, validate WithdrawalApplicationClaimValidator) (*model.UserWithdrawalApplication, error) {
	input.AuditCommandID = strings.TrimSpace(input.AuditCommandID)
	input.ApproverName = strings.TrimSpace(input.ApproverName)
	input.AuditRemark = strings.TrimSpace(input.AuditRemark)
	input.AuditImageURL = strings.TrimSpace(input.AuditImageURL)
	if id == 0 || input.Stage != WithdrawalApplicationReviewStageFinance ||
		(input.Decision != model.WithdrawalApplicationStatusApproved && input.Decision != model.WithdrawalApplicationStatusRejected) ||
		input.ApproverUserID == 0 || input.ApproverName == "" || input.AuditCommandID == "" || input.ApprovedAtMS <= 0 {
		return nil, errors.New("external withdrawal finance claim is incomplete")
	}
	if input.Decision == model.WithdrawalApplicationStatusApproved && input.AuditImageURL == "" {
		return nil, errors.New("external withdrawal approval claim credential is required")
	}
	if input.Decision == model.WithdrawalApplicationStatusRejected && input.AuditRemark == "" {
		return nil, errors.New("external withdrawal rejection claim remark is required")
	}

	var application model.UserWithdrawalApplication
	err := s.db.Transaction(func(tx *gorm.DB) error {
		query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
		if strings.TrimSpace(appCode) != "" {
			query = query.Where("app_code = ?", strings.TrimSpace(appCode))
		}
		if err := query.First(&application, id).Error; err != nil {
			return err
		}
		if !isExternalWithdrawalSource(application) {
			return errors.New("durable external finance claim only supports external withdrawals")
		}

		claimDecision := optionalWithdrawalString(application.ExternalFinanceClaimDecision)
		claimStatus := optionalWithdrawalString(application.ExternalFinanceClaimStatus)
		if claimStatus == model.WithdrawalExternalFinanceClaimStatusFinalized {
			// 本地 commit 结果不确定时，相同决策重试直接返回终态；相反决策不得借重试改写。
			if application.Status != claimDecision || input.Decision != claimDecision || strings.TrimSpace(application.AuditCommandID) != input.AuditCommandID {
				return ErrWithdrawalApplicationStageNotReviewable
			}
			return validateExternalFinanceClaimSnapshot(application)
		}
		if application.Status != model.WithdrawalApplicationStatusPending {
			return ErrWithdrawalApplicationAlreadyAudited
		}
		if reviewErr := withdrawalApplicationReviewError(application, WithdrawalApplicationReviewStageFinance); reviewErr != nil {
			return reviewErr
		}

		switch claimStatus {
		case "":
			if claimDecision != "" {
				return ErrWithdrawalApplicationStageNotReviewable
			}
			if validate != nil {
				if err := validate(application); err != nil {
					return err
				}
			}
			claimStatusValue := model.WithdrawalExternalFinanceClaimStatusClaimed
			claimDecisionValue := input.Decision
			updates := map[string]any{
				"external_finance_claim_decision": input.Decision,
				"external_finance_claim_status":   claimStatusValue,
				"approver_user_id":                input.ApproverUserID,
				"approver_name":                   input.ApproverName,
				"audit_remark":                    input.AuditRemark,
				"audit_image_url":                 input.AuditImageURL,
				"audit_command_id":                input.AuditCommandID,
				"approved_at_ms":                  input.ApprovedAtMS,
				"updated_at_ms":                   input.ApprovedAtMS,
			}
			if err := tx.Model(&application).Updates(updates).Error; err != nil {
				return err
			}
			application.ExternalFinanceClaimDecision = &claimDecisionValue
			application.ExternalFinanceClaimStatus = &claimStatusValue
			application.ApproverUserID = &input.ApproverUserID
			application.ApproverName = input.ApproverName
			application.AuditRemark = input.AuditRemark
			application.AuditImageURL = input.AuditImageURL
			application.AuditCommandID = input.AuditCommandID
			application.ApprovedAtMS = &input.ApprovedAtMS
			application.UpdatedAtMS = input.ApprovedAtMS
			return nil
		case model.WithdrawalExternalFinanceClaimStatusClaimed:
			if claimDecision != input.Decision || strings.TrimSpace(application.AuditCommandID) != input.AuditCommandID {
				return ErrWithdrawalApplicationStageNotReviewable
			}
			if err := validateExternalFinanceClaimSnapshot(application); err != nil {
				return err
			}
			if validate != nil {
				return validate(application)
			}
			return nil
		default:
			return ErrWithdrawalApplicationStageNotReviewable
		}
	})
	if err != nil {
		return nil, err
	}
	return &application, nil
}

// FinalizeExternalWithdrawalFinanceDecisionForApp 只把已持久 claimed 的外部财务决策收敛到本地终态。
// transaction id 来自已成功的来源回调；终态重试必须与首次 claim 的决策、command 和回调事务完全一致。
func (s *Store) FinalizeExternalWithdrawalFinanceDecisionForApp(appCode string, id uint, decision string, commandID string, transactionID string, completedAtMS int64) (*model.UserWithdrawalApplication, error) {
	decision = strings.TrimSpace(decision)
	commandID = strings.TrimSpace(commandID)
	transactionID = strings.TrimSpace(transactionID)
	if id == 0 || (decision != model.WithdrawalApplicationStatusApproved && decision != model.WithdrawalApplicationStatusRejected) || commandID == "" || transactionID == "" || completedAtMS <= 0 {
		return nil, errors.New("external withdrawal finance finalization is incomplete")
	}

	var application model.UserWithdrawalApplication
	err := s.db.Transaction(func(tx *gorm.DB) error {
		query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
		if strings.TrimSpace(appCode) != "" {
			query = query.Where("app_code = ?", strings.TrimSpace(appCode))
		}
		if err := query.First(&application, id).Error; err != nil {
			return err
		}
		if !isExternalWithdrawalSource(application) {
			return errors.New("external withdrawal finance finalization only supports external withdrawals")
		}
		claimDecision := optionalWithdrawalString(application.ExternalFinanceClaimDecision)
		claimStatus := optionalWithdrawalString(application.ExternalFinanceClaimStatus)
		if claimStatus == model.WithdrawalExternalFinanceClaimStatusFinalized {
			if application.Status != decision || claimDecision != decision || strings.TrimSpace(application.AuditCommandID) != commandID || strings.TrimSpace(application.AuditTransactionID) != transactionID {
				return ErrWithdrawalApplicationStageNotReviewable
			}
			return validateExternalFinanceClaimSnapshot(application)
		}
		if application.Status != model.WithdrawalApplicationStatusPending || claimStatus != model.WithdrawalExternalFinanceClaimStatusClaimed || claimDecision != decision || strings.TrimSpace(application.AuditCommandID) != commandID {
			return ErrWithdrawalApplicationStageNotReviewable
		}
		if reviewErr := withdrawalApplicationReviewError(application, WithdrawalApplicationReviewStageFinance); reviewErr != nil {
			return reviewErr
		}
		if err := validateExternalFinanceClaimSnapshot(application); err != nil {
			return err
		}
		finalizedStatus := model.WithdrawalExternalFinanceClaimStatusFinalized
		updates := map[string]any{
			"status":                        decision,
			"external_finance_claim_status": finalizedStatus,
			"audit_transaction_id":          transactionID,
			"updated_at_ms":                 completedAtMS,
		}
		if err := tx.Model(&application).Updates(updates).Error; err != nil {
			return err
		}
		application.Status = decision
		application.ExternalFinanceClaimStatus = &finalizedStatus
		application.AuditTransactionID = transactionID
		application.UpdatedAtMS = completedAtMS
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &application, nil
}

func validateExternalFinanceClaimSnapshot(application model.UserWithdrawalApplication) error {
	decision := optionalWithdrawalString(application.ExternalFinanceClaimDecision)
	if application.ApproverUserID == nil || *application.ApproverUserID == 0 || strings.TrimSpace(application.ApproverName) == "" || application.ApprovedAtMS == nil || *application.ApprovedAtMS <= 0 || strings.TrimSpace(application.AuditCommandID) == "" {
		return errors.New("external withdrawal finance claim snapshot is incomplete")
	}
	if decision == model.WithdrawalApplicationStatusApproved && strings.TrimSpace(application.AuditImageURL) == "" {
		return errors.New("external withdrawal finance approval claim snapshot is incomplete")
	}
	if decision == model.WithdrawalApplicationStatusRejected && strings.TrimSpace(application.AuditRemark) == "" {
		return errors.New("external withdrawal finance rejection claim snapshot is incomplete")
	}
	if decision != model.WithdrawalApplicationStatusApproved && decision != model.WithdrawalApplicationStatusRejected {
		return errors.New("external withdrawal finance claim decision is invalid")
	}
	return nil
}

func optionalWithdrawalString(value *string) string {
	if value == nil {
		return ""
	}
	return strings.TrimSpace(*value)
}

func isExternalWithdrawalSource(application model.UserWithdrawalApplication) bool {
	sourceSystem := strings.ToLower(strings.TrimSpace(application.SourceSystem))
	return sourceSystem != "" && sourceSystem != "hyapp_wallet"
}

// ReviewWithdrawalApplicationForApp 把普通阶段校验、跨服务资金/通知动作和本地状态更新收敛在同一把申请行锁内。
// 提现审核是低 QPS、高价值资金边界；这里有意在钱包和 inbox 网络调用期间持有行锁，让运营通过及财务通过/拒绝严格串行。
// 外部动作必须使用阶段固定的幂等 command/event id：事务回滚后重试不会重复扣款或通知，而相反决策会以同一 command id 的请求哈希冲突失败。
// 运营拒绝不能走本函数：它必须先用 Claim... 独立提交 rejecting，再在外部动作成功后用 Finalize... 收敛终态。
func (s *Store) ReviewWithdrawalApplicationForApp(appCode string, id uint, input WithdrawalApplicationAuditInput, action WithdrawalApplicationAuditAction) (*model.UserWithdrawalApplication, error) {
	if id == 0 {
		return nil, errors.New("withdrawal application id is required")
	}
	if input.Stage == "" {
		// 保留 repository 旧调用者的财务审核语义；新业务入口必须显式传阶段。
		input.Stage = WithdrawalApplicationReviewStageFinance
	}
	if input.Stage != WithdrawalApplicationReviewStageFinance && input.Stage != WithdrawalApplicationReviewStageOperations {
		return nil, errors.New("withdrawal application review stage is invalid")
	}
	if input.Decision != model.WithdrawalApplicationStatusApproved && input.Decision != model.WithdrawalApplicationStatusRejected {
		return nil, errors.New("withdrawal application decision is invalid")
	}
	if input.Stage == WithdrawalApplicationReviewStageOperations && input.Decision == model.WithdrawalApplicationStatusRejected {
		// 运营拒绝必须先提交 rejecting claim；禁止旧调用者继续把钱包释放和本地终态放在一个可整体回滚的事务里。
		return nil, errors.New("operations withdrawal rejection must use durable claim")
	}

	var application model.UserWithdrawalApplication
	err := s.db.Transaction(func(tx *gorm.DB) error {
		// 行锁保护当前阶段门禁：同一申请的运营通过/拒绝与后续财务通过/拒绝按提交顺序串行，不能跨阶段覆盖审核结果。
		query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
		if strings.TrimSpace(appCode) != "" {
			// 后台按当前 App 上下文审核，不能只凭全局自增 ID 跨 App 锁定和审核提现单。
			query = query.Where("app_code = ?", strings.TrimSpace(appCode))
		}
		if err := query.First(&application, id).Error; err != nil {
			return err
		}
		if input.Stage == WithdrawalApplicationReviewStageFinance && isExternalWithdrawalSource(application) {
			// 外部回调无法参与 Admin DB 事务，禁止绕过持久 claim 重新引入“回调成功、本地回滚”窗口。
			return errors.New("external withdrawal finance review must use durable claim")
		}
		if reviewErr := withdrawalApplicationReviewError(application, input.Stage); reviewErr != nil {
			return reviewErr
		}
		effect := WithdrawalApplicationAuditEffect{
			CommandID:     strings.TrimSpace(input.AuditCommandID),
			TransactionID: strings.TrimSpace(input.AuditTransactionID),
		}
		if action != nil {
			var err error
			effect, err = action(application)
			if err != nil {
				return err
			}
		}
		updates := withdrawalApplicationReviewUpdates(input, effect)
		if err := tx.Model(&application).Updates(updates).Error; err != nil {
			return err
		}
		applyWithdrawalApplicationReview(&application, input, effect)
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &application, nil
}

func withdrawalApplicationReviewError(application model.UserWithdrawalApplication, stage WithdrawalApplicationReviewStage) error {
	if application.Status != model.WithdrawalApplicationStatusPending {
		return ErrWithdrawalApplicationAlreadyAudited
	}
	operationsStatus := strings.TrimSpace(application.OperationsStatus)
	if operationsStatus == "" {
		// 数据库迁移只把历史终态行填成 skipped；空值只兼容迁移前构造的模型和旧测试夹具。
		operationsStatus = model.WithdrawalOperationsStatusSkipped
	}
	switch stage {
	case WithdrawalApplicationReviewStageOperations:
		if operationsStatus == model.WithdrawalOperationsStatusPending {
			return nil
		}
	case WithdrawalApplicationReviewStageFinance:
		if operationsStatus == model.WithdrawalOperationsStatusApproved || operationsStatus == model.WithdrawalOperationsStatusSkipped {
			return nil
		}
	}
	return ErrWithdrawalApplicationStageNotReviewable
}

func withdrawalApplicationReviewUpdates(input WithdrawalApplicationAuditInput, effect WithdrawalApplicationAuditEffect) map[string]any {
	updates := map[string]any{"updated_at_ms": input.ApprovedAtMS}
	if input.Stage == WithdrawalApplicationReviewStageOperations {
		updates["operations_status"] = input.Decision
		updates["operations_reviewer_user_id"] = input.ApproverUserID
		updates["operations_reviewer_name"] = strings.TrimSpace(input.ApproverName)
		updates["operations_audit_remark"] = strings.TrimSpace(input.AuditRemark)
		updates["operations_audit_command_id"] = strings.TrimSpace(effect.CommandID)
		updates["operations_audit_transaction_id"] = strings.TrimSpace(effect.TransactionID)
		updates["operations_reviewed_at_ms"] = input.ApprovedAtMS
		if input.Decision == model.WithdrawalApplicationStatusRejected {
			updates["status"] = model.WithdrawalApplicationStatusRejected
		}
		return updates
	}
	updates["status"] = input.Decision
	updates["approver_user_id"] = input.ApproverUserID
	updates["approver_name"] = strings.TrimSpace(input.ApproverName)
	updates["audit_remark"] = strings.TrimSpace(input.AuditRemark)
	updates["audit_image_url"] = strings.TrimSpace(input.AuditImageURL)
	updates["audit_command_id"] = strings.TrimSpace(effect.CommandID)
	updates["audit_transaction_id"] = strings.TrimSpace(effect.TransactionID)
	updates["approved_at_ms"] = input.ApprovedAtMS
	return updates
}

func applyWithdrawalApplicationReview(application *model.UserWithdrawalApplication, input WithdrawalApplicationAuditInput, effect WithdrawalApplicationAuditEffect) {
	if application == nil {
		return
	}
	application.UpdatedAtMS = input.ApprovedAtMS
	if input.Stage == WithdrawalApplicationReviewStageOperations {
		application.OperationsStatus = input.Decision
		application.OperationsReviewerUserID = &input.ApproverUserID
		application.OperationsReviewerName = strings.TrimSpace(input.ApproverName)
		application.OperationsAuditRemark = strings.TrimSpace(input.AuditRemark)
		application.OperationsAuditCommandID = strings.TrimSpace(effect.CommandID)
		application.OperationsAuditTransactionID = strings.TrimSpace(effect.TransactionID)
		application.OperationsReviewedAtMS = &input.ApprovedAtMS
		if input.Decision == model.WithdrawalApplicationStatusRejected {
			application.Status = model.WithdrawalApplicationStatusRejected
		}
		return
	}
	application.Status = input.Decision
	application.ApproverUserID = &input.ApproverUserID
	application.ApproverName = strings.TrimSpace(input.ApproverName)
	application.AuditRemark = strings.TrimSpace(input.AuditRemark)
	application.AuditImageURL = strings.TrimSpace(input.AuditImageURL)
	application.AuditCommandID = strings.TrimSpace(effect.CommandID)
	application.AuditTransactionID = strings.TrimSpace(effect.TransactionID)
	application.ApprovedAtMS = &input.ApprovedAtMS
}

// ApprovedWithdrawalStats 按审批通过时间聚合用户提现的实际 USDT 打款金额。
// 外部来源优先使用申请时固化的 actual_amount_minor；无快照旧单才回退 gross 口径，POINT 旧单仍需从 USD 展示值折算。
type ApprovedWithdrawalStats struct {
	ApprovedCount    int64
	ApprovedUSDMinor int64
}

func (s *Store) ApprovedWithdrawalStats(appCode string, startAtMS int64, endAtMS int64) (ApprovedWithdrawalStats, error) {
	query := s.db.Model(&model.UserWithdrawalApplication{}).
		Where("status = ?", model.WithdrawalApplicationStatusApproved)
	if appCode = strings.TrimSpace(appCode); appCode != "" {
		query = query.Where("app_code = ?", appCode)
	}
	if startAtMS > 0 {
		query = query.Where("approved_at_ms >= ?", startAtMS)
	}
	if endAtMS > 0 {
		query = query.Where("approved_at_ms < ?", endAtMS)
	}
	var stats ApprovedWithdrawalStats
	row := query.Select(`
		COUNT(*),
		COALESCE(SUM(CASE
			WHEN actual_amount_minor IS NOT NULL THEN actual_amount_minor
			WHEN salary_asset_type IN ('POINT', 'POINT_DIAMOND', 'COIN_SELLER_POINT') THEN CAST(withdraw_amount * 100 AS SIGNED)
			ELSE withdraw_amount_minor
		END), 0)`).Row()
	if err := row.Scan(&stats.ApprovedCount, &stats.ApprovedUSDMinor); err != nil {
		return ApprovedWithdrawalStats{}, err
	}
	return stats, nil
}

func applyWithdrawalApplicationFilters(query *gorm.DB, options WithdrawalApplicationListOptions) *gorm.DB {
	if appCode := strings.TrimSpace(options.AppCode); appCode != "" {
		query = query.Where("app_code = ?", appCode)
	} else if options.RestrictAppCodes {
		if len(options.AllowedAppCodes) == 0 {
			// 有财务权限但未授予任何资金数据范围时必须返回空集，不能把空 IN 退化成全 App 查询。
			query = query.Where("1 = 0")
		} else {
			query = query.Where("app_code IN ?", options.AllowedAppCodes)
		}
	}
	if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
		like := "%" + keyword + "%"
		// 关键词面向两级审核排查，覆盖用户、收款方式、收款地址和两阶段审核人；不做金额模糊，避免 decimal 隐式转换拖慢常规查询。
		query = query.Where("user_id LIKE ? OR withdraw_method LIKE ? OR withdraw_address LIKE ? OR approver_name LIKE ? OR operations_reviewer_name LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like, like)
	}
	switch options.Stage {
	case WithdrawalApplicationReviewStageFinance:
		// skipped 是 102 迁移前的历史申请；它们保持原财务队列，不倒退补做运营初审。
		query = query.Where("operations_status IN ?", []string{model.WithdrawalOperationsStatusApproved, model.WithdrawalOperationsStatusSkipped})
	case WithdrawalApplicationReviewStageOperations:
		// 运营页保留已审核历史，但永远不展示 skipped 旧单，避免把历史财务申请伪装成运营已审。
		query = query.Where("operations_status <> ?", model.WithdrawalOperationsStatusSkipped)
	}
	return query
}
