package transfer

import (
	"context"
	"strings"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
	"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/evm"
	"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
	"github.com/tech/sendico/gateway/chain/storage/model"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"go.uber.org/zap"
)

type computeGasTopUpCommand struct {
	deps Deps
}

func NewComputeGasTopUp(deps Deps) *computeGasTopUpCommand {
	return &computeGasTopUpCommand{deps: deps}
}

func (c *computeGasTopUpCommand) Execute(ctx context.Context, req *chainv1.ComputeGasTopUpRequest) gsresponse.Responder[chainv1.ComputeGasTopUpResponse] {
	if err := c.deps.EnsureRepository(ctx); err != nil {
		c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
		return gsresponse.Unavailable[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
	}
	if req == nil {
		c.deps.Logger.Warn("Nil request")
		return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
	}

	walletRef := strings.TrimSpace(req.GetWalletRef())
	if walletRef == "" {
		c.deps.Logger.Warn("Wallet ref missing")
		return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
	}
	estimatedFee := req.GetEstimatedTotalFee()
	if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
		c.deps.Logger.Warn("Estimated fee missing")
		return gsresponse.InvalidArgument[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required"))
	}

	topUp, capHit, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, walletRef, estimatedFee)
	if err != nil {
		return gsresponse.Auto[chainv1.ComputeGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
	}

	logDecision(c.deps.Logger, walletRef, estimatedFee, nativeBalance, topUp, capHit, walletModel)

	return gsresponse.Success(&chainv1.ComputeGasTopUpResponse{
		TopupAmount: topUp,
		CapHit:      capHit,
	})
}

type ensureGasTopUpCommand struct {
	deps Deps
}

func NewEnsureGasTopUp(deps Deps) *ensureGasTopUpCommand {
	return &ensureGasTopUpCommand{deps: deps}
}

func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.EnsureGasTopUpRequest) gsresponse.Responder[chainv1.EnsureGasTopUpResponse] {
	if err := c.deps.EnsureRepository(ctx); err != nil {
		c.deps.Logger.Warn("Repository unavailable", zap.Error(err))
		return gsresponse.Unavailable[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
	}
	if req == nil {
		c.deps.Logger.Warn("Nil request")
		return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
	}

	idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
	if idempotencyKey == "" {
		c.deps.Logger.Warn("Idempotency key missing")
		return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
	}
	organizationRef := strings.TrimSpace(req.GetOrganizationRef())
	if organizationRef == "" {
		c.deps.Logger.Warn("Organization ref missing")
		return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
	}
	sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
	if sourceWalletRef == "" {
		c.deps.Logger.Warn("Source wallet ref missing")
		return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
	}
	targetWalletRef := strings.TrimSpace(req.GetTargetWalletRef())
	if targetWalletRef == "" {
		c.deps.Logger.Warn("Target wallet ref missing")
		return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("target_wallet_ref is required"))
	}
	estimatedFee := req.GetEstimatedTotalFee()
	if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
		c.deps.Logger.Warn("Estimated fee missing")
		return gsresponse.InvalidArgument[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("estimated_total_fee is required"))
	}

	topUp, capHit, nativeBalance, walletModel, err := computeGasTopUp(ctx, c.deps, targetWalletRef, estimatedFee)
	if err != nil {
		return gsresponse.Auto[chainv1.EnsureGasTopUpResponse](c.deps.Logger, mservice.ChainGateway, err)
	}

	logDecision(c.deps.Logger, targetWalletRef, estimatedFee, nativeBalance, topUp, capHit, walletModel)

	if topUp == nil || strings.TrimSpace(topUp.GetAmount()) == "" {
		return gsresponse.Success(&chainv1.EnsureGasTopUpResponse{
			TopupAmount: nil,
			CapHit:      capHit,
		})
	}

	submitReq := &chainv1.SubmitTransferRequest{
		IdempotencyKey:  idempotencyKey,
		OrganizationRef: organizationRef,
		SourceWalletRef: sourceWalletRef,
		Destination: &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
		},
		Amount:     topUp,
		Metadata:   shared.CloneMetadata(req.GetMetadata()),
		PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
	}

	submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
	return func(ctx context.Context) (*chainv1.EnsureGasTopUpResponse, error) {
		submitResp, err := submitResponder(ctx)
		if err != nil {
			return nil, err
		}
		return &chainv1.EnsureGasTopUpResponse{
			TopupAmount: topUp,
			CapHit:      capHit,
			Transfer:    submitResp.GetTransfer(),
		}, nil
	}
}

func computeGasTopUp(ctx context.Context, deps Deps, walletRef string, estimatedFee *moneyv1.Money) (*moneyv1.Money, bool, *moneyv1.Money, *model.ManagedWallet, error) {
	walletRef = strings.TrimSpace(walletRef)
	estimatedFee = shared.CloneMoney(estimatedFee)
	walletModel, err := deps.Storage.Wallets().Get(ctx, walletRef)
	if err != nil {
		return nil, false, nil, nil, err
	}

	networkCfg, ok := deps.Networks.Network(walletModel.Network)
	if !ok {
		return nil, false, nil, nil, merrors.InvalidArgument("unsupported chain for wallet")
	}

	nativeBalance, err := nativeBalanceForWallet(ctx, deps, walletModel)
	if err != nil {
		return nil, false, nil, nil, err
	}

	if networkCfg.GasTopUpPolicy != nil {
		topUp, capHit, err := evm.ComputeGasTopUp(networkCfg, walletModel, estimatedFee, nativeBalance)
		if err != nil {
			return nil, false, nil, nil, err
		}
		return topUp, capHit, nativeBalance, walletModel, nil
	}

	topUp, err := defaultGasTopUp(estimatedFee, nativeBalance)
	if err != nil {
		return nil, false, nil, nil, err
	}
	return topUp, false, nativeBalance, walletModel, nil
}

func nativeBalanceForWallet(ctx context.Context, deps Deps, walletModel *model.ManagedWallet) (*moneyv1.Money, error) {
	if walletModel == nil {
		return nil, merrors.InvalidArgument("wallet is required")
	}
	walletDeps := wallet.Deps{
		Logger:           deps.Logger.Named("wallet"),
		Drivers:          deps.Drivers,
		Networks:         deps.Networks,
		KeyManager:       nil,
		Storage:          deps.Storage,
		Clock:            deps.Clock,
		BalanceCacheTTL:  0,
		RPCTimeout:       deps.RPCTimeout,
		EnsureRepository: deps.EnsureRepository,
	}
	_, nativeBalance, err := wallet.OnChainWalletBalances(ctx, walletDeps, walletModel)
	if err != nil {
		return nil, err
	}
	if nativeBalance == nil || strings.TrimSpace(nativeBalance.GetAmount()) == "" || strings.TrimSpace(nativeBalance.GetCurrency()) == "" {
		return nil, merrors.InvalidArgument("native balance is unavailable")
	}
	return nativeBalance, nil
}

func defaultGasTopUp(estimatedFee *moneyv1.Money, currentBalance *moneyv1.Money) (*moneyv1.Money, error) {
	if estimatedFee == nil || strings.TrimSpace(estimatedFee.GetAmount()) == "" || strings.TrimSpace(estimatedFee.GetCurrency()) == "" {
		return nil, merrors.InvalidArgument("estimated fee is required")
	}
	if currentBalance == nil || strings.TrimSpace(currentBalance.GetAmount()) == "" || strings.TrimSpace(currentBalance.GetCurrency()) == "" {
		return nil, merrors.InvalidArgument("native balance is required")
	}
	if !strings.EqualFold(estimatedFee.GetCurrency(), currentBalance.GetCurrency()) {
		return nil, merrors.InvalidArgument("native balance currency mismatch")
	}

	estimated, err := decimal.NewFromString(strings.TrimSpace(estimatedFee.GetAmount()))
	if err != nil {
		return nil, err
	}
	current, err := decimal.NewFromString(strings.TrimSpace(currentBalance.GetAmount()))
	if err != nil {
		return nil, err
	}
	required := estimated.Sub(current)
	if !required.IsPositive() {
		//nolint:nilnil // No top-up required is represented as (nil, nil).
		return nil, nil
	}
	return &moneyv1.Money{
		Currency: strings.ToUpper(strings.TrimSpace(estimatedFee.GetCurrency())),
		Amount:   required.String(),
	}, nil
}

func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.Money, nativeBalance *moneyv1.Money, topUp *moneyv1.Money, capHit bool, walletModel *model.ManagedWallet) {
	if logger == nil {
		return
	}
	fields := []zap.Field{
		zap.String("wallet_ref", walletRef),
		zap.String("estimated_total_fee", amountString(estimatedFee)),
		zap.String("current_native_balance", amountString(nativeBalance)),
		zap.String("topup_amount", amountString(topUp)),
		zap.Bool("cap_hit", capHit),
	}
	if walletModel != nil {
		fields = append(fields, zap.String("network", string(walletModel.Network)))
	}
	logger.Info("Gas top-up decision", fields...)
}

func amountString(m *moneyv1.Money) string {
	if m == nil {
		return ""
	}
	amount := strings.TrimSpace(m.GetAmount())
	currency := strings.TrimSpace(m.GetCurrency())
	if amount == "" && currency == "" {
		return ""
	}
	if currency == "" {
		return amount
	}
	if amount == "" {
		return currency
	}
	return amount + " " + currency
}
