package orchestrator

import (
	"context"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	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"
)

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")
	}

	intentAmount, err := requireMoney(cloneMoney(payment.Intent.Amount), "amount")
	if err != nil {
		return nil, err
	}
	sourceAmount, err := requireMoney(resolveDebitAmount(payment, quote, intentAmount), "debit amount")
	if err != nil {
		return nil, err
	}
	settlementAmount, err := requireMoney(resolveSettlementAmount(payment, quote, sourceAmount), "settlement amount")
	if err != nil {
		return nil, err
	}
	feeAmount := resolveFeeAmount(payment, quote)
	feeRequired := isPositiveMoney(feeAmount)
	sourceSendAmount, err := netSourceAmount(sourceAmount, feeAmount, quote)
	if err != nil {
		return nil, err
	}

	payoutAmount := settlementAmount
	if destRail == model.RailCardPayout {
		payoutAmount, err = cardPayoutAmount(payment)
		if err != nil {
			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{}

	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 {
			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 amount == nil && action != model.RailOperationObserveConfirm {
			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),
		}

		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)
			instanceID := stepInstanceIDForRail(payment.Intent, tpl.Rail, sourceRail, destRail)
			checkAmount := amount
			if action == model.RailOperationObserveConfirm {
				checkAmount = observeAmountForRail(tpl.Rail, sourceSendAmount, settlementAmount, payoutAmount)
			}
			gw, err := ensureGatewayForAction(ctx, gateways, gatewaysByRail, tpl.Rail, network, checkAmount, action, instanceID, sendDirectionForRail(tpl.Rail))
			if err != nil {
				return nil, err
			}
			step.GatewayID = strings.TrimSpace(gw.ID)
			step.InstanceID = strings.TrimSpace(gw.InstanceID)
		}

		steps = append(steps, step)
	}

	if len(steps) == 0 {
		return nil, merrors.InvalidArgument("plan builder: empty payment plan")
	}

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

func actionForOperation(operation string) (model.RailOperation, error) {
	op := strings.ToLower(strings.TrimSpace(operation))
	switch op {
	case "debit", "ledger.debit", "wallet.debit":
		return model.RailOperationDebit, nil
	case "credit", "ledger.credit", "wallet.credit":
		return model.RailOperationCredit, 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", "ledger.block", "ledger.hold", "ledger.reserve":
		return model.RailOperationBlock, nil
	case "release", "unblock", "ledger.release":
		return model.RailOperationRelease, nil
	}

	switch strings.ToUpper(strings.TrimSpace(operation)) {
	case string(model.RailOperationDebit):
		return model.RailOperationDebit, nil
	case string(model.RailOperationCredit):
		return model.RailOperationCredit, 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:
		if rail == model.RailLedger {
			return cloneMoney(ledgerDebitAmount), nil
		}
		return cloneMoney(settlementAmount), nil
	case model.RailOperationCredit:
		if rail == model.RailLedger {
			return cloneMoney(ledgerCreditAmount), 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 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 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
}
