package wallet

import (
	"context"
	"strings"

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

// FreezePointWithdrawal 把 POINT 从 available 冻结到 frozen；repository 会在账变事务内
// 重新解析已发布 wallet 政策，不能信任 gateway 预览时携带的比例与手续费。
func (s *Service) FreezePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
	if err := normalizePointWithdrawalCommand(&command, false, true, false); err != nil {
		return ledger.PointWithdrawalReceipt{}, err
	}
	if command.Reason == "" {
		command.Reason = "point withdrawal freeze"
	}
	if s.repository == nil {
		return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	receipt, err := s.repository.FreezeSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command))
	if err != nil {
		return ledger.PointWithdrawalReceipt{}, err
	}
	return pointWithdrawalReceiptFromSalary(receipt), nil
}

// SettlePointWithdrawal 在财务审核通过时扣掉 frozen 积分；冻结时已扣 available，因此这里不能再次扣 available。
func (s *Service) SettlePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
	if err := normalizePointWithdrawalCommand(&command, true, false, true); err != nil {
		return ledger.PointWithdrawalReceipt{}, err
	}
	if command.Reason == "" {
		command.Reason = "point withdrawal approved"
	}
	if s.repository == nil {
		return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	receipt, err := s.repository.SettleSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command))
	if err != nil {
		return ledger.PointWithdrawalReceipt{}, err
	}
	return pointWithdrawalReceiptFromSalary(receipt), nil
}

// ReleasePointWithdrawal 在审核拒绝或申请落库失败时把 frozen 积分返还 available，保证用户不会被永久冻结。
func (s *Service) ReleasePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
	if err := normalizePointWithdrawalCommand(&command, false, false, true); err != nil {
		return ledger.PointWithdrawalReceipt{}, err
	}
	if command.Reason == "" {
		command.Reason = "point withdrawal released"
	}
	if s.repository == nil {
		return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	ctx = appcode.WithContext(ctx, command.AppCode)
	receipt, err := s.repository.ReleaseSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command))
	if err != nil {
		return ledger.PointWithdrawalReceipt{}, err
	}
	return pointWithdrawalReceiptFromSalary(receipt), nil
}

func normalizePointWithdrawalCommand(command *ledger.PointWithdrawalCommand, requireOperator bool, requireRegion bool, validateFrozenSnapshot bool) error {
	if command == nil || strings.TrimSpace(command.CommandID) == "" || command.UserID <= 0 {
		return xerr.New(xerr.InvalidArgument, "point withdrawal command is incomplete")
	}
	if requireOperator && command.OperatorUserID <= 0 {
		return xerr.New(xerr.InvalidArgument, "operator_user_id is required")
	}
	if requireRegion && command.RegionID <= 0 {
		return xerr.New(xerr.InvalidArgument, "region_id is required")
	}
	command.CommandID = strings.TrimSpace(command.CommandID)
	if strings.TrimSpace(command.AppCode) == "" {
		return xerr.New(xerr.InvalidArgument, "app_code is required")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType))
	if !ledger.ValidPointWithdrawalAssetType(command.AssetType) {
		return xerr.New(xerr.InvalidArgument, "point asset_type is invalid")
	}
	if command.GrossPointAmount <= 0 && !(command.AssetType == ledger.AssetPointDiamond && !validateFrozenSnapshot && command.GrossUSDMinor > 0) {
		return xerr.New(xerr.InvalidArgument, "point withdrawal amount is invalid")
	}
	// 首次冻结的比例、门槛、手续费和净额由 repository 在同一数据库事务内覆盖；
	// 审核与回滚则必须严格复用申请落库时的首次冻结快照。
	if !validateFrozenSnapshot {
		command.Reason = strings.TrimSpace(command.Reason)
		command.WithdrawalRef = strings.TrimSpace(command.WithdrawalRef)
		command.WithdrawalApplicationID = strings.TrimSpace(command.WithdrawalApplicationID)
		command.FreezeTransactionID = strings.TrimSpace(command.FreezeTransactionID)
		if len(command.CommandID) > 128 || len(command.Reason) > 512 || len(command.WithdrawalRef) > 128 || len(command.WithdrawalApplicationID) > 64 {
			return xerr.New(xerr.InvalidArgument, "point withdrawal text fields are too long")
		}
		return nil
	}
	if command.PointsPerUSD <= 0 {
		return xerr.New(xerr.InvalidArgument, "point withdrawal points_per_usd is invalid")
	}
	if command.FeeBPS < 0 || command.FeeBPS > 10000 {
		return xerr.New(xerr.InvalidArgument, "point withdrawal fee_bps is invalid")
	}
	expectedFee := command.GrossPointAmount * command.FeeBPS / 10000
	if command.AssetType == ledger.AssetPointDiamond {
		// 永久积分冻结按美元分先 floor 手续费；审核/回滚只允许复用首次冻结保存的 fee/net 快照。
		expectedFee = command.FeePointAmount
	}
	if command.FeePointAmount == 0 {
		command.FeePointAmount = expectedFee
	}
	if command.FeePointAmount != expectedFee {
		return xerr.New(xerr.InvalidArgument, "point withdrawal fee is invalid")
	}
	expectedNet := command.GrossPointAmount - command.FeePointAmount
	if command.NetPointAmount == 0 {
		command.NetPointAmount = expectedNet
	}
	if command.NetPointAmount != expectedNet || command.NetPointAmount < 0 {
		return xerr.New(xerr.InvalidArgument, "point withdrawal net amount is invalid")
	}
	command.Reason = strings.TrimSpace(command.Reason)
	command.WithdrawalRef = strings.TrimSpace(command.WithdrawalRef)
	command.WithdrawalApplicationID = strings.TrimSpace(command.WithdrawalApplicationID)
	command.FreezeTransactionID = strings.TrimSpace(command.FreezeTransactionID)
	if command.AssetType == ledger.AssetPointDiamond && command.FreezeTransactionID == "" {
		return xerr.New(xerr.InvalidArgument, "freeze_transaction_id is required for point diamond terminal mutation")
	}
	if len(command.CommandID) > 128 || len(command.Reason) > 512 || len(command.WithdrawalRef) > 128 || len(command.WithdrawalApplicationID) > 64 || len(command.FreezeTransactionID) > 128 {
		return xerr.New(xerr.InvalidArgument, "point withdrawal text fields are too long")
	}
	return nil
}

func pointWithdrawalAsSalaryCommand(command ledger.PointWithdrawalCommand) ledger.SalaryWithdrawalCommand {
	return ledger.SalaryWithdrawalCommand{
		AppCode:                 command.AppCode,
		CommandID:               command.CommandID,
		UserID:                  command.UserID,
		SalaryAssetType:         command.AssetType,
		SalaryUSDMinor:          command.GrossPointAmount,
		GrossUSDMinor:           command.GrossUSDMinor,
		PointFeeAmount:          command.FeePointAmount,
		PointNetAmount:          command.NetPointAmount,
		PointsPerUSD:            command.PointsPerUSD,
		PointWithdrawFeeBPS:     command.FeeBPS,
		OperatorUserID:          command.OperatorUserID,
		Reason:                  command.Reason,
		WithdrawalRef:           command.WithdrawalRef,
		WithdrawalApplicationID: command.WithdrawalApplicationID,
		FreezeTransactionID:     command.FreezeTransactionID,
		RegionID:                command.RegionID,
	}
}

func pointWithdrawalReceiptFromSalary(receipt ledger.SalaryWithdrawalReceipt) ledger.PointWithdrawalReceipt {
	return ledger.PointWithdrawalReceipt{
		TransactionID:           receipt.TransactionID,
		UserID:                  receipt.UserID,
		AssetType:               receipt.SalaryAssetType,
		GrossPointAmount:        receipt.SalaryUSDMinor,
		FeePointAmount:          receipt.PointFeeAmount,
		NetPointAmount:          receipt.PointNetAmount,
		PointsPerUSD:            receipt.PointsPerUSD,
		FeeBPS:                  receipt.PointWithdrawFeeBPS,
		PointPolicyInstanceCode: receipt.PointPolicyInstanceCode,
		GrossUSDMinor:           receipt.GrossUSDMinor,
		FeeUSDMinor:             receipt.FeeUSDMinor,
		NetUSDMinor:             receipt.NetUSDMinor,
		PointPolicyID:           receipt.PointPolicyID,
		PointPolicyVersion:      receipt.PointPolicyVersion,
		CoinsPerUSD:             receipt.CoinsPerUSD,
		AvailableAfter:          receipt.AvailableAfter,
		FrozenAfter:             receipt.FrozenAfter,
		Version:                 receipt.Version,
		CreatedAtMS:             receipt.CreatedAtMS,
	}
}
