package orchestrator

import (
	"context"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/mutil/mzap"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.uber.org/zap"
)

func (b *defaultPlanBuilder) buildPlanFromTemplate(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, template *model.PaymentPlanTemplate, sourceRail, destRail model.Rail, sourceNetwork, destNetwork string, gateways GatewayRegistry) (*model.PaymentPlan, error) {
	if template == nil {
		return nil, merrors.InvalidArgument("plan builder: plan template is required")
	}

	logger := b.logger.With(
		zap.String("payment_ref", payment.PaymentRef),
		mzap.ObjRef("template_id", template.ID),
		zap.String("source_rail", string(sourceRail)),
		zap.String("dest_rail", string(destRail)),
	)
	logger.Debug("Building plan from template", zap.Int("template_steps", len(template.Steps)))

	intentAmount, err := requireMoney(cloneMoney(payment.Intent.Amount), "amount")
	if err != nil {
		logger.Warn("Invalid intent amount", zap.Error(err))
		return nil, err
	}
	sourceAmount, err := requireMoney(resolveDebitAmount(payment, quote, intentAmount), "debit amount")
	if err != nil {
		logger.Warn("Failed to resolve debit amount", zap.Error(err))
		return nil, err
	}
	settlementAmount, err := requireMoney(resolveSettlementAmount(payment, quote, sourceAmount), "settlement amount")
	if err != nil {
		logger.Warn("Failed to resolve settlement amount", zap.Error(err))
		return nil, err
	}
	feeAmount := resolveFeeAmount(payment, quote)
	feeRequired := isPositiveMoney(feeAmount)
	sourceSendAmount, err := netSourceAmount(sourceAmount, feeAmount, quote)
	if err != nil {
		logger.Warn("Failed to calculate net source amount", zap.Error(err))
		return nil, err
	}
	providerSettlementAmount := settlementAmount
	if payment.Intent.SettlementMode == model.SettlementModeFixReceived && feeRequired {
		providerSettlementAmount, err = netSettlementAmount(settlementAmount, feeAmount, quote)
		if err != nil {
			logger.Warn("Failed to calculate net settlement amount", zap.Error(err))
			return nil, err
		}
	}

	logger.Debug("Amounts calculated",
		zap.String("intent_amount", moneyString(intentAmount)),
		zap.String("source_amount", moneyString(sourceAmount)),
		zap.String("settlement_amount", moneyString(settlementAmount)),
		zap.String("fee_amount", moneyString(feeAmount)),
		zap.Bool("fee_required", feeRequired),
	)

	payoutAmount := settlementAmount
	if destRail == model.RailCardPayout {
		payoutAmount, err = cardPayoutAmount(payment)
		if err != nil {
			logger.Warn("Failed to calculate card payout amount", zap.Error(err))
			return nil, err
		}
	}

	ledgerCreditAmount := settlementAmount
	ledgerDebitAmount := settlementAmount
	if destRail == model.RailCardPayout && payoutAmount != nil {
		ledgerDebitAmount = payoutAmount
	}

	steps := make([]*model.PaymentStep, 0, len(template.Steps))
	gatewaysByRail := map[model.Rail]*model.GatewayInstanceDescriptor{}
	stepIDs := map[string]bool{}
	sourceManagedWalletNetwork := ""
	destManagedWalletNetwork := ""
	if payment.Intent.Source.Type == model.EndpointTypeManagedWallet {
		sourceManagedWalletNetwork = networkFromEndpoint(payment.Intent.Source)
	}
	if payment.Intent.Destination.Type == model.EndpointTypeManagedWallet {
		destManagedWalletNetwork = networkFromEndpoint(payment.Intent.Destination)
	}

	for _, tpl := range template.Steps {
		stepID := strings.TrimSpace(tpl.StepID)
		if stepID == "" {
			return nil, merrors.InvalidArgument("plan builder: plan template step id is required")
		}
		if stepIDs[stepID] {
			return nil, merrors.InvalidArgument("plan builder: plan template step id must be unique")
		}
		stepIDs[stepID] = true

		action, err := actionForOperation(tpl.Operation)
		if err != nil {
			b.logger.Warn("plan builder: unsupported operation in plan template step", zap.String("step_id", stepID), zap.String("operation", tpl.Operation), zap.Error(err))
			return nil, err
		}

		amount, err := stepAmountForAction(action, tpl.Rail, sourceRail, destRail, sourceSendAmount, settlementAmount, payoutAmount, feeAmount, ledgerDebitAmount, ledgerCreditAmount, feeRequired)
		if err != nil {
			return nil, err
		}
		if action == model.RailOperationSend && tpl.Rail == model.RailProviderSettlement {
			amount = cloneMoney(providerSettlementAmount)
		}
		if amount == nil &&
			action != model.RailOperationObserveConfirm &&
			action != model.RailOperationFee {
			logger.Warn("Plan template step has no amount for action, skipping",
				zap.String("step_id", stepID), zap.String("action", string(action)))
			continue
		}

		policy := tpl.CommitPolicy
		if strings.TrimSpace(string(policy)) == "" {
			policy = model.CommitPolicyImmediate
		}
		step := &model.PaymentStep{
			StepID:       stepID,
			Rail:         tpl.Rail,
			Action:       action,
			DependsOn:    cloneStringList(tpl.DependsOn),
			CommitPolicy: policy,
			CommitAfter:  cloneStringList(tpl.CommitAfter),
			Amount:       cloneMoney(amount),
			FromRole:     cloneAccountRole(tpl.FromRole),
			ToRole:       cloneAccountRole(tpl.ToRole),
		}

		needsGateway := action == model.RailOperationSend || action == model.RailOperationFee || action == model.RailOperationObserveConfirm
		if (action == model.RailOperationBlock || action == model.RailOperationRelease) && tpl.Rail != model.RailLedger {
			needsGateway = true
		}
		if needsGateway {
			network := gatewayNetworkForRail(tpl.Rail, sourceRail, destRail, sourceNetwork, destNetwork)
			managedWalletNetwork := ""
			if tpl.Rail == sourceRail && sourceManagedWalletNetwork != "" {
				managedWalletNetwork = sourceManagedWalletNetwork
			} else if tpl.Rail == destRail && destManagedWalletNetwork != "" {
				managedWalletNetwork = destManagedWalletNetwork
			}
			if managedWalletNetwork != "" {
				logger.Debug("Managed wallet network resolved for gateway selection",
					zap.String("step_id", stepID),
					zap.String("rail", string(tpl.Rail)),
					zap.String("managed_wallet_network", managedWalletNetwork),
					zap.String("gateway_network", network),
				)
			}
			instanceID := stepInstanceIDForRail(payment.Intent, tpl.Rail, sourceRail, destRail)
			checkAmount := amount
			if action == model.RailOperationObserveConfirm {
				checkAmount = observeAmountForRail(tpl.Rail, sourceSendAmount, settlementAmount, payoutAmount)
				if tpl.Rail == model.RailProviderSettlement {
					checkAmount = cloneMoney(providerSettlementAmount)
				}
			}
			gw, err := ensureGatewayForAction(ctx, b.logger, gateways, gatewaysByRail, tpl.Rail, network, checkAmount, action, instanceID, sendDirectionForRail(tpl.Rail))
			if err != nil {
				logger.Warn("Failed to ensure gateway for plan step", zap.Error(err),
					zap.String("step_id", stepID), zap.String("rail", string(tpl.Rail)),
					zap.String("gateway_network", network), zap.String("managed_wallet_network", managedWalletNetwork),
					zap.Int("gateways_by_rail_count", len(gatewaysByRail)),
				)
				return nil, err
			}
			step.GatewayID = strings.TrimSpace(gw.ID)
			step.InstanceID = strings.TrimSpace(gw.InstanceID)
		}

		logger.Debug("Plan step added",
			zap.String("step_id", step.StepID),
			zap.String("rail", string(step.Rail)),
			zap.String("action", string(step.Action)),
			zap.String("commit_policy", string(step.CommitPolicy)),
			zap.String("amount", moneyString(step.Amount)),
			zap.Strings("depends_on", step.DependsOn),
		)
		steps = append(steps, step)
	}

	if len(steps) == 0 {
		logger.Warn("Empty payment plan after processing template")
		return nil, merrors.InvalidArgument("plan builder: empty payment plan")
	}

	execQuote := executionQuote(payment, quote)
	plan := &model.PaymentPlan{
		ID:             payment.PaymentRef,
		FXQuote:        fxQuoteFromProto(execQuote.GetFxQuote()),
		Fees:           feeLinesFromProto(execQuote.GetFeeLines()),
		Steps:          steps,
		IdempotencyKey: payment.IdempotencyKey,
		CreatedAt:      planTimestamp(payment),
	}

	logger.Info("Payment plan built", zap.Int("steps", len(plan.Steps)),
		zap.Int("fees", len(plan.Fees)), zap.Bool("has_fx_quote", plan.FXQuote != nil),
	)
	return plan, nil
}

func moneyString(m *paymenttypes.Money) string {
	if m == nil {
		return "nil"
	}
	return m.Amount + " " + m.Currency
}

func actionForOperation(operation string) (model.RailOperation, error) {
	op := strings.ToLower(strings.TrimSpace(operation))
	if op == "ledger.block" || op == "ledger.release" {
		return model.RailOperationUnspecified, merrors.InvalidArgument("unsupported legacy ledger operation, use ledger.move with roles")
	}
	switch op {
	case "ledger.move":
		return model.RailOperationMove, nil
	case "external.debit":
		return model.RailOperationExternalDebit, nil
	case "external.credit":
		return model.RailOperationExternalCredit, nil
	case "debit", "wallet.debit":
		return model.RailOperationExternalDebit, nil
	case "credit", "wallet.credit":
		return model.RailOperationExternalCredit, nil
	case "fx.convert", "fx_conversion", "fx.converted":
		return model.RailOperationFXConvert, nil
	case "observe", "observe.confirm", "observe.confirmation", "observe.crypto", "observe.card":
		return model.RailOperationObserveConfirm, nil
	case "fee", "fee.send":
		return model.RailOperationFee, nil
	case "send", "payout.card", "payout.crypto", "payout.fiat", "payin.crypto", "payin.fiat", "fund.crypto", "fund.card":
		return model.RailOperationSend, nil
	case "block", "hold", "reserve":
		return model.RailOperationBlock, nil
	case "release", "unblock":
		return model.RailOperationRelease, nil
	}

	switch strings.ToUpper(strings.TrimSpace(operation)) {
	case string(model.RailOperationExternalDebit), string(model.RailOperationDebit):
		return model.RailOperationExternalDebit, nil
	case string(model.RailOperationExternalCredit), string(model.RailOperationCredit):
		return model.RailOperationExternalCredit, nil
	case string(model.RailOperationMove):
		return model.RailOperationMove, nil
	case string(model.RailOperationSend):
		return model.RailOperationSend, nil
	case string(model.RailOperationFee):
		return model.RailOperationFee, nil
	case string(model.RailOperationObserveConfirm):
		return model.RailOperationObserveConfirm, nil
	case string(model.RailOperationFXConvert):
		return model.RailOperationFXConvert, nil
	case string(model.RailOperationBlock):
		return model.RailOperationBlock, nil
	case string(model.RailOperationRelease):
		return model.RailOperationRelease, nil
	}

	return model.RailOperationUnspecified, merrors.InvalidArgument("plan builder: unsupported operation")
}

func stepAmountForAction(action model.RailOperation, rail, sourceRail, destRail model.Rail, sourceSendAmount, settlementAmount, payoutAmount, feeAmount, ledgerDebitAmount, ledgerCreditAmount *paymenttypes.Money, feeRequired bool) (*paymenttypes.Money, error) {
	switch action {
	case model.RailOperationDebit, model.RailOperationExternalDebit:
		if rail == model.RailLedger {
			return cloneMoney(ledgerDebitAmount), nil
		}
		return cloneMoney(settlementAmount), nil
	case model.RailOperationCredit, model.RailOperationExternalCredit:
		if rail == model.RailLedger {
			return cloneMoney(ledgerCreditAmount), nil
		}
		return cloneMoney(settlementAmount), nil
	case model.RailOperationMove:
		if rail == model.RailLedger {
			return cloneMoney(ledgerDebitAmount), nil
		}
		return cloneMoney(settlementAmount), nil
	case model.RailOperationSend:
		switch rail {
		case sourceRail:
			return cloneMoney(sourceSendAmount), nil
		case destRail:
			return cloneMoney(payoutAmount), nil
		default:
			return cloneMoney(settlementAmount), nil
		}
	case model.RailOperationBlock, model.RailOperationRelease:
		if rail == model.RailLedger {
			return cloneMoney(ledgerDebitAmount), nil
		}
		return cloneMoney(settlementAmount), nil
	case model.RailOperationFee:
		if !feeRequired {
			return nil, nil
		}
		return cloneMoney(feeAmount), nil
	case model.RailOperationObserveConfirm:
		return nil, nil
	case model.RailOperationFXConvert:
		return cloneMoney(settlementAmount), nil
	default:
		return nil, merrors.InvalidArgument("plan builder: unsupported action")
	}
}

func stepInstanceIDForRail(intent model.PaymentIntent, rail, sourceRail, destRail model.Rail) string {
	if rail == sourceRail {
		return strings.TrimSpace(intent.Source.InstanceID)
	}
	if rail == destRail {
		return strings.TrimSpace(intent.Destination.InstanceID)
	}
	return ""
}

func observeAmountForRail(rail model.Rail, source, settlement, payout *paymenttypes.Money) *paymenttypes.Money {
	switch rail {
	case model.RailCrypto, model.RailFiatOnRamp:
		if source != nil {
			return source
		}
		if settlement != nil {
			return settlement
		}
	case model.RailProviderSettlement:
		if settlement != nil {
			return settlement
		}
	case model.RailCardPayout:
		if payout != nil {
			return payout
		}
	}
	if settlement != nil {
		return settlement
	}
	return source
}

func cloneAccountRole(role *account_role.AccountRole) *account_role.AccountRole {
	if role == nil {
		return nil
	}
	cloned := *role
	return &cloned
}

func netSourceAmount(sourceAmount, feeAmount *paymenttypes.Money, quote *orchestratorv1.PaymentQuote) (*paymenttypes.Money, error) {
	if sourceAmount == nil {
		return nil, merrors.InvalidArgument("plan builder: source amount is required")
	}
	netAmount := cloneMoney(sourceAmount)
	if !isPositiveMoney(feeAmount) {
		return netAmount, nil
	}

	currency := strings.TrimSpace(sourceAmount.GetCurrency())
	if currency == "" {
		return netAmount, nil
	}
	var fxQuote *oraclev1.Quote
	if quote != nil {
		fxQuote = quote.GetFxQuote()
	}
	convertedFee, err := ensureCurrency(protoMoney(feeAmount), currency, fxQuote)
	if err != nil {
		return nil, err
	}
	if convertedFee == nil {
		return netAmount, nil
	}
	sourceValue, err := decimalFromMoney(sourceAmount)
	if err != nil {
		return nil, err
	}
	feeValue, err := decimalFromMoney(convertedFee)
	if err != nil {
		return nil, err
	}
	netValue := sourceValue.Sub(feeValue)
	if netValue.IsNegative() {
		return nil, merrors.InvalidArgument("plan builder: fee exceeds source amount")
	}
	return &paymenttypes.Money{
		Currency: currency,
		Amount:   netValue.String(),
	}, nil
}

func netSettlementAmount(settlementAmount, feeAmount *paymenttypes.Money, quote *orchestratorv1.PaymentQuote) (*paymenttypes.Money, error) {
	if settlementAmount == nil {
		return nil, merrors.InvalidArgument("plan builder: settlement amount is required")
	}
	netAmount := cloneMoney(settlementAmount)
	if !isPositiveMoney(feeAmount) {
		return netAmount, nil
	}

	currency := strings.TrimSpace(settlementAmount.GetCurrency())
	if currency == "" {
		return netAmount, nil
	}
	var fxQuote *oraclev1.Quote
	if quote != nil {
		fxQuote = quote.GetFxQuote()
	}
	convertedFee, err := ensureCurrency(protoMoney(feeAmount), currency, fxQuote)
	if err != nil {
		return nil, err
	}
	if convertedFee == nil {
		return netAmount, nil
	}
	settlementValue, err := decimalFromMoney(settlementAmount)
	if err != nil {
		return nil, err
	}
	feeValue, err := decimalFromMoney(convertedFee)
	if err != nil {
		return nil, err
	}
	netValue := settlementValue.Sub(feeValue)
	if netValue.IsNegative() {
		return nil, merrors.InvalidArgument("plan builder: fee exceeds settlement amount")
	}
	return &paymenttypes.Money{
		Currency: currency,
		Amount:   netValue.String(),
	}, nil
}

func requireMoney(amount *paymenttypes.Money, label string) (*paymenttypes.Money, error) {
	if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
		return nil, merrors.InvalidArgument("plan builder: " + label + " is required")
	}
	return amount, nil
}
