package tron

import (
	"fmt"
	"strings"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/gateway/tron/shared"
	"github.com/tech/sendico/gateway/tron/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)

var tronBaseUnitFactor = decimal.NewFromInt(1_000_000)

// GasTopUpDecision captures the applied policy inputs and outputs (in TRX units).
type GasTopUpDecision struct {
	CurrentBalanceTRX   decimal.Decimal
	EstimatedFeeTRX     decimal.Decimal
	RequiredTRX         decimal.Decimal
	BufferedRequiredTRX decimal.Decimal
	MinBalanceTopUpTRX  decimal.Decimal
	RawTopUpTRX         decimal.Decimal
	RoundedTopUpTRX     decimal.Decimal
	TopUpTRX            decimal.Decimal
	CapHit              bool
	OperationType       string
}

// ComputeGasTopUp applies the network policy to decide a TRX top-up amount.
func ComputeGasTopUp(network shared.Network, wallet *model.ManagedWallet, estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) (*moneyv1.Money, GasTopUpDecision, error) {
	decision := GasTopUpDecision{}
	if wallet == nil {
		return nil, decision, merrors.InvalidArgument("wallet is required")
	}
	if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
		return nil, decision, merrors.InvalidArgument("estimated fee is required")
	}
	if currentBalance == nil || strings.TrimSpace(currentBalance.GetAmount()) == "" || strings.TrimSpace(currentBalance.GetCurrency()) == "" {
		return nil, decision, merrors.InvalidArgument("current native balance is required")
	}
	if network.GasTopUpPolicy == nil {
		return nil, decision, merrors.InvalidArgument("gas top-up policy is not configured")
	}

	nativeCurrency := strings.TrimSpace(network.NativeToken)
	if nativeCurrency == "" {
		nativeCurrency = strings.ToUpper(network.Name.String())
	}
	if !strings.EqualFold(nativeCurrency, estimatedFee.GetCurrency()) {
		return nil, decision, merrors.InvalidArgument(fmt.Sprintf("estimated fee currency mismatch (expected %s)", nativeCurrency))
	}
	if !strings.EqualFold(nativeCurrency, currentBalance.GetCurrency()) {
		return nil, decision, merrors.InvalidArgument(fmt.Sprintf("native balance currency mismatch (expected %s)", nativeCurrency))
	}

	estimatedTRX, err := tronToTRX(estimatedFee)
	if err != nil {
		return nil, decision, err
	}
	currentTRX, err := tronToTRX(currentBalance)
	if err != nil {
		return nil, decision, err
	}

	isContract := strings.TrimSpace(wallet.ContractAddress) != ""
	rule, ok := network.GasTopUpPolicy.Rule(isContract)
	if !ok {
		return nil, decision, merrors.InvalidArgument("gas top-up policy is not configured")
	}
	if rule.RoundingUnit.LessThanOrEqual(decimal.Zero) {
		return nil, decision, merrors.InvalidArgument("gas top-up rounding unit must be > 0")
	}
	if rule.MaxTopUp.LessThanOrEqual(decimal.Zero) {
		return nil, decision, merrors.InvalidArgument("gas top-up max top-up must be > 0")
	}

	required := estimatedTRX.Sub(currentTRX)
	if required.IsNegative() {
		required = decimal.Zero
	}
	bufferedRequired := required.Mul(decimal.NewFromInt(1).Add(rule.BufferPercent))

	minBalanceTopUp := rule.MinNativeBalance.Sub(currentTRX)
	if minBalanceTopUp.IsNegative() {
		minBalanceTopUp = decimal.Zero
	}

	rawTopUp := bufferedRequired
	if minBalanceTopUp.GreaterThan(rawTopUp) {
		rawTopUp = minBalanceTopUp
	}

	roundedTopUp := decimal.Zero
	if rawTopUp.IsPositive() {
		roundedTopUp = rawTopUp.Div(rule.RoundingUnit).Ceil().Mul(rule.RoundingUnit)
	}

	topUp := roundedTopUp
	capHit := false
	if topUp.GreaterThan(rule.MaxTopUp) {
		topUp = rule.MaxTopUp
		capHit = true
	}

	decision = GasTopUpDecision{
		CurrentBalanceTRX:   currentTRX,
		EstimatedFeeTRX:     estimatedTRX,
		RequiredTRX:         required,
		BufferedRequiredTRX: bufferedRequired,
		MinBalanceTopUpTRX:  minBalanceTopUp,
		RawTopUpTRX:         rawTopUp,
		RoundedTopUpTRX:     roundedTopUp,
		TopUpTRX:            topUp,
		CapHit:              capHit,
		OperationType:       operationType(isContract),
	}

	if !topUp.IsPositive() {
		return nil, decision, nil
	}

	baseUnits := topUp.Mul(tronBaseUnitFactor).Ceil().Truncate(0)
	return &moneyv1.Money{
		Currency: strings.ToUpper(nativeCurrency),
		Amount:   baseUnits.StringFixed(0),
	}, decision, nil
}

func tronToTRX(amount *moneyv1.Money) (decimal.Decimal, error) {
	value, err := decimal.NewFromString(strings.TrimSpace(amount.GetAmount()))
	if err != nil {
		return decimal.Zero, err
	}
	return value.Div(tronBaseUnitFactor), nil
}

func operationType(contract bool) string {
	if contract {
		return "trc20"
	}
	return "native"
}
