package orchestrator

import (
	"context"
	"strings"

	"github.com/shopspring/decimal"
	chainclient "github.com/tech/sendico/gateway/chain/client"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.uber.org/zap"
)

func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) error {
	if payment == nil {
		return merrors.InvalidArgument("payment is required")
	}
	intent := payment.Intent
	source := intent.Source.ManagedWallet
	if source == nil || strings.TrimSpace(source.ManagedWalletRef) == "" {
		return merrors.InvalidArgument("card funding: source managed wallet is required")
	}
	route, err := s.cardRoute(defaultCardGateway)
	if err != nil {
		return err
	}
	sourceWalletRef := strings.TrimSpace(source.ManagedWalletRef)
	fundingAddress := strings.TrimSpace(route.FundingAddress)
	feeWalletRef := strings.TrimSpace(route.FeeWalletRef)

	intentAmount := cloneMoney(intent.Amount)
	if intentAmount == nil || strings.TrimSpace(intentAmount.GetAmount()) == "" || strings.TrimSpace(intentAmount.GetCurrency()) == "" {
		return merrors.InvalidArgument("card funding: amount is required")
	}
	intentAmountProto := protoMoney(intentAmount)

	payoutAmount, err := cardPayoutAmount(payment)
	if err != nil {
		return err
	}

	var feeAmount *paymenttypes.Money
	if quote != nil {
		feeAmount = moneyFromProto(quote.GetExpectedFeeTotal())
	}
	if feeAmount == nil && payment.LastQuote != nil {
		feeAmount = cloneMoney(payment.LastQuote.ExpectedFeeTotal)
	}
	feeDecimal := decimal.Zero
	if feeAmount != nil && strings.TrimSpace(feeAmount.GetAmount()) != "" {
		if strings.TrimSpace(feeAmount.GetCurrency()) == "" {
			return merrors.InvalidArgument("card funding: fee currency is required")
		}
		feeDecimal, err = decimalFromMoney(feeAmount)
		if err != nil {
			return err
		}
	}
	feeRequired := feeDecimal.IsPositive()
	feeAmountProto := protoMoney(feeAmount)

	network := networkFromEndpoint(intent.Source)
	instanceID := strings.TrimSpace(intent.Source.InstanceID)
	actions := []model.RailOperation{model.RailOperationSend}
	if feeRequired {
		actions = append(actions, model.RailOperationFee)
	}
	chainClient, _, err := s.resolveChainGatewayClient(ctx, network, intentAmount, actions, instanceID, payment.PaymentRef)
	if err != nil {
		s.logger.Warn("card funding gateway resolution failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
		return err
	}

	fundingDest := &chainv1.TransferDestination{
		Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: fundingAddress},
	}
	fundingFee, err := s.estimateTransferNetworkFee(ctx, chainClient, sourceWalletRef, fundingDest, intentAmountProto)
	if err != nil {
		return err
	}

	var feeTransferFee *moneyv1.Money
	if feeRequired {
		if feeWalletRef == "" {
			return merrors.InvalidArgument("card funding: fee wallet ref is required when fee exists")
		}
		feeDest := &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: feeWalletRef},
		}
		feeTransferFee, err = s.estimateTransferNetworkFee(ctx, chainClient, sourceWalletRef, feeDest, feeAmountProto)
		if err != nil {
			return err
		}
	}

	totalFee, gasCurrency, err := sumNetworkFees(fundingFee, feeTransferFee)
	if err != nil {
		return err
	}

	var estimatedTotalFee *moneyv1.Money
	if gasCurrency != "" && !totalFee.IsNegative() {
		estimatedTotalFee = makeMoney(gasCurrency, totalFee)
	}

	var topUpMoney *moneyv1.Money
	var topUpFee *moneyv1.Money
	topUpPositive := false
	if estimatedTotalFee != nil {
		computeResp, err := chainClient.ComputeGasTopUp(ctx, &chainv1.ComputeGasTopUpRequest{
			WalletRef:         sourceWalletRef,
			EstimatedTotalFee: estimatedTotalFee,
		})
		if err != nil {
			s.logger.Warn("card funding gas top-up compute failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
			return err
		}
		if computeResp != nil {
			topUpMoney = computeResp.GetTopupAmount()
		}
		if topUpMoney != nil && strings.TrimSpace(topUpMoney.GetAmount()) != "" {
			amountDec, err := decimalFromMoney(topUpMoney)
			if err != nil {
				return err
			}
			topUpPositive = amountDec.IsPositive()
		}
		if topUpMoney != nil && topUpPositive {
			if strings.TrimSpace(topUpMoney.GetCurrency()) == "" {
				return merrors.InvalidArgument("card funding: gas top-up currency is required")
			}
			if feeWalletRef == "" {
				return merrors.InvalidArgument("card funding: fee wallet ref is required for gas top-up")
			}
			topUpDest := &chainv1.TransferDestination{
				Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
			}
			topUpFee, err = s.estimateTransferNetworkFee(ctx, chainClient, feeWalletRef, topUpDest, topUpMoney)
			if err != nil {
				return err
			}
		}
	}

	plan := ensureExecutionPlan(payment)
	var gasStep *model.ExecutionStep
	var feeStep *model.ExecutionStep
	if topUpMoney != nil && topUpPositive {
		gasStep = ensureExecutionStep(plan, stepCodeGasTopUp)
		setExecutionStepRole(gasStep, executionStepRoleSource)
		setExecutionStepStatus(gasStep, model.OperationStatePlanned)
		gasStep.Description = "Top up native gas from fee wallet"
		gasStep.Amount = moneyFromProto(topUpMoney)
		gasStep.NetworkFee = moneyFromProto(topUpFee)
		gasStep.SourceWalletRef = feeWalletRef
		gasStep.DestinationRef = sourceWalletRef
	}

	fundStep := ensureExecutionStep(plan, stepCodeFundingTransfer)
	setExecutionStepRole(fundStep, executionStepRoleSource)
	setExecutionStepStatus(fundStep, model.OperationStatePlanned)
	fundStep.Description = "Transfer payout amount to card funding wallet"
	fundStep.Amount = cloneMoney(intentAmount)
	fundStep.NetworkFee = moneyFromProto(fundingFee)
	fundStep.SourceWalletRef = sourceWalletRef
	fundStep.DestinationRef = fundingAddress

	if feeRequired {
		feeStep = ensureExecutionStep(plan, stepCodeFeeTransfer)
		setExecutionStepRole(feeStep, executionStepRoleSource)
		setExecutionStepStatus(feeStep, model.OperationStatePlanned)
		feeStep.Description = "Transfer fee to fee wallet"
		feeStep.Amount = cloneMoney(feeAmount)
		feeStep.NetworkFee = moneyFromProto(feeTransferFee)
		feeStep.SourceWalletRef = sourceWalletRef
		feeStep.DestinationRef = feeWalletRef
	}

	cardStep := ensureExecutionStep(plan, stepCodeCardPayout)
	setExecutionStepRole(cardStep, executionStepRoleConsumer)
	setExecutionStepStatus(cardStep, model.OperationStatePlanned)
	cardStep.Description = "Submit card payout"
	cardStep.Amount = cloneMoney(payoutAmount)
	if card := intent.Destination.Card; card != nil {
		if masked := strings.TrimSpace(card.MaskedPan); masked != "" {
			cardStep.DestinationRef = masked
		}
	}

	updateExecutionPlanTotalNetworkFee(plan)

	exec := payment.Execution
	if exec == nil {
		exec = &model.ExecutionRefs{}
	}

	if topUpMoney != nil && topUpPositive {
		ensureResp, gasErr := chainClient.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
			IdempotencyKey:    payment.IdempotencyKey + ":card:gas",
			OrganizationRef:   payment.OrganizationRef.Hex(),
			IntentRef:         strings.TrimSpace(payment.Intent.Ref),
			OperationRef:      strings.TrimSpace(cardStep.OperationRef),
			SourceWalletRef:   feeWalletRef,
			TargetWalletRef:   sourceWalletRef,
			EstimatedTotalFee: estimatedTotalFee,
			Metadata:          cloneMetadata(payment.Metadata),
			PaymentRef:        payment.PaymentRef,
		})
		if gasErr != nil {
			s.logger.Warn("card gas top-up transfer failed", zap.Error(gasErr), zap.String("payment_ref", payment.PaymentRef))
			return gasErr
		}
		if gasStep != nil {
			actual := (*moneyv1.Money)(nil)
			if ensureResp != nil {
				actual = ensureResp.GetTopupAmount()
				if transfer := ensureResp.GetTransfer(); transfer != nil {
					gasStep.TransferRef = strings.TrimSpace(transfer.GetTransferRef())
				}
			}
			actualPositive := false
			if actual != nil && strings.TrimSpace(actual.GetAmount()) != "" {
				actualDec, err := decimalFromMoney(actual)
				if err != nil {
					return err
				}
				actualPositive = actualDec.IsPositive()
			}
			if actual != nil && actualPositive {
				gasStep.Amount = moneyFromProto(actual)
				if strings.TrimSpace(actual.GetCurrency()) == "" {
					return merrors.InvalidArgument("card funding: gas top-up currency is required")
				}
				topUpDest := &chainv1.TransferDestination{
					Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
				}
				topUpFee, err = s.estimateTransferNetworkFee(ctx, chainClient, feeWalletRef, topUpDest, actual)
				if err != nil {
					return err
				}
				gasStep.NetworkFee = moneyFromProto(topUpFee)
				setExecutionStepStatus(gasStep, model.OperationStateWaiting)
			} else {
				gasStep.Amount = nil
				gasStep.NetworkFee = nil
				gasStep.TransferRef = ""
				setExecutionStepStatus(gasStep, model.OperationStateSkipped)
			}
		}
		if gasStep != nil {
			s.logger.Info("card gas top-up transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", gasStep.TransferRef))
		}
		updateExecutionPlanTotalNetworkFee(plan)
	}

	fundResp, err := chainClient.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
		IdempotencyKey:  payment.IdempotencyKey + ":card:fund",
		OrganizationRef: payment.OrganizationRef.Hex(),
		SourceWalletRef: sourceWalletRef,
		Destination:     fundingDest,
		Amount:          cloneProtoMoney(intentAmountProto),
		Metadata:        cloneMetadata(payment.Metadata),
		PaymentRef:      payment.PaymentRef,
		IntentRef:       strings.TrimSpace(intent.Ref),
		OperationRef:    strings.TrimSpace(cardStep.OperationRef),
	})
	if err != nil {
		return err
	}
	if fundResp != nil && fundResp.GetTransfer() != nil {
		exec.ChainTransferRef = strings.TrimSpace(fundResp.GetTransfer().GetTransferRef())
		fundStep.TransferRef = exec.ChainTransferRef
	}
	setExecutionStepStatus(fundStep, model.OperationStateWaiting)
	updateExecutionPlanTotalNetworkFee(plan)

	if feeRequired {
		feeResp, err := chainClient.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
			IntentRef:       intent.Ref,
			OperationRef:    feeStep.OperationRef,
			IdempotencyKey:  payment.IdempotencyKey + ":card:fee",
			OrganizationRef: payment.OrganizationRef.Hex(),
			SourceWalletRef: sourceWalletRef,
			Destination: &chainv1.TransferDestination{
				Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: feeWalletRef},
			},
			Amount:     cloneProtoMoney(feeAmountProto),
			Metadata:   cloneMetadata(payment.Metadata),
			PaymentRef: payment.PaymentRef,
		})
		if err != nil {
			return err
		}
		if feeResp != nil && feeResp.GetTransfer() != nil {
			exec.FeeTransferRef = strings.TrimSpace(feeResp.GetTransfer().GetTransferRef())
			feeStep.TransferRef = exec.FeeTransferRef
		}
		setExecutionStepStatus(feeStep, model.OperationStateWaiting)
		s.logger.Info("card fee transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", exec.FeeTransferRef))
	}

	payment.Execution = exec
	return nil
}

func (s *Service) estimateTransferNetworkFee(ctx context.Context, client chainclient.Client, sourceWalletRef string, destination *chainv1.TransferDestination, amount *moneyv1.Money) (*moneyv1.Money, error) {
	if client == nil {
		return nil, merrors.InvalidArgument("chain gateway unavailable")
	}
	sourceWalletRef = strings.TrimSpace(sourceWalletRef)
	if sourceWalletRef == "" {
		return nil, merrors.InvalidArgument("source wallet ref is required")
	}
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return nil, merrors.InvalidArgument("amount is required")
	}

	resp, err := client.EstimateTransferFee(ctx, &chainv1.EstimateTransferFeeRequest{
		SourceWalletRef: sourceWalletRef,
		Destination:     destination,
		Amount:          cloneProtoMoney(amount),
	})
	if err != nil {
		s.logger.Warn("chain gateway fee estimation failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
		return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
	}
	if resp == nil {
		s.logger.Warn("chain gateway fee estimation returned empty response", zap.String("source_wallet_ref", sourceWalletRef))
		return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
	}
	fee := resp.GetNetworkFee()
	if fee == nil || strings.TrimSpace(fee.GetAmount()) == "" || strings.TrimSpace(fee.GetCurrency()) == "" {
		s.logger.Warn("chain gateway fee estimation missing network fee", zap.String("source_wallet_ref", sourceWalletRef))
		return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
	}
	return cloneProtoMoney(fee), nil
}

func sumNetworkFees(fees ...*moneyv1.Money) (decimal.Decimal, string, error) {
	total := decimal.Zero
	currency := ""
	for _, fee := range fees {
		if fee == nil {
			continue
		}
		amount := strings.TrimSpace(fee.GetAmount())
		feeCurrency := strings.TrimSpace(fee.GetCurrency())
		if amount == "" || feeCurrency == "" {
			return decimal.Zero, "", merrors.InvalidArgument("network fee is required")
		}
		value, err := decimalFromMoney(fee)
		if err != nil {
			return decimal.Zero, "", err
		}
		if currency == "" {
			currency = feeCurrency
		} else if !strings.EqualFold(currency, feeCurrency) {
			return decimal.Zero, "", merrors.InvalidArgument("network fee currency mismatch")
		}
		total = total.Add(value)
	}
	return total, currency, nil
}
