package quote_computation_service

import (
	"fmt"
	"github.com/tech/sendico/pkg/discovery"
	"strings"

	"github.com/tech/sendico/payments/storage/model"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)

func buildComputationSteps(
	index int,
	intent model.PaymentIntent,
	destination model.PaymentEndpoint,
	routeRails []model.Rail,
) []*QuoteComputationStep {
	if intent.Amount == nil {
		return nil
	}

	attrs := intent.Attributes
	amount := protoMoneyFromModel(intent.Amount)
	sourceAmount := sourceStepAmount(intent, amount)
	destinationAmount := destinationStepAmount(intent, amount)
	sourceRail := sourceRailForIntent(intent)
	destinationRail := destinationRailForIntent(intent)
	rails := normalizeRouteRails(sourceRail, destinationRail, routeRails)
	sourceGatewayID := strings.TrimSpace(lookupAttr(attrs,
		"source_gateway",
		"sourceGateway",
		"source_gateway_id",
		"sourceGatewayId",
	))
	sourceInstanceID := strings.TrimSpace(lookupAttr(attrs, "source_instance_id", "sourceInstanceId"))
	destinationGatewayID := gatewayKeyForFunding(attrs, destination)
	destinationInstanceID := firstNonEmpty(
		strings.TrimSpace(lookupAttr(attrs, "destination_instance_id", "destinationInstanceId")),
		strings.TrimSpace(lookupAttr(attrs, "instance_id", "instanceId")),
	)

	sourceStepID := fmt.Sprintf("i%d.source", index)
	steps := []*QuoteComputationStep{
		{
			StepID:             sourceStepID,
			Rail:               rails[0],
			Operation:          sourceOperationForRail(rails[0]),
			GatewayID:          sourceGatewayID,
			InstanceID:         sourceInstanceID,
			Amount:             cloneProtoMoney(sourceAmount),
			Optional:           false,
			IncludeInAggregate: false,
		},
	}

	lastStepID := sourceStepID
	fxAssigned := false
	if intent.RequiresFX {
		if len(rails) > 1 && rails[1] == discovery.RailProviderSettlement {
			fxAssigned = true
		} else {
			fxStepID := fmt.Sprintf("i%d.fx", index)
			steps = append(steps, &QuoteComputationStep{
				StepID:             fxStepID,
				Rail:               discovery.RailProviderSettlement,
				Operation:          discovery.RailOperationFXConvert,
				DependsOn:          []string{sourceStepID},
				Amount:             cloneProtoMoney(sourceAmount),
				Optional:           false,
				IncludeInAggregate: false,
			})
			lastStepID = fxStepID
			fxAssigned = true
		}
	}

	transitIndex := 1
	for i := 1; i < len(rails)-1; i++ {
		rail := rails[i]
		stepID := fmt.Sprintf("i%d.transit%d", index, transitIndex)
		var operation model.RailOperation = discovery.RailOperationMove
		if intent.RequiresFX && !fxAssigned && rail == discovery.RailProviderSettlement {
			stepID = fmt.Sprintf("i%d.fx", index)
			operation = discovery.RailOperationFXConvert
			fxAssigned = true
		}
		stepAmount := amount
		if operation == discovery.RailOperationFXConvert {
			stepAmount = sourceAmount
		}
		steps = append(steps, &QuoteComputationStep{
			StepID:             stepID,
			Rail:               rail,
			Operation:          operation,
			DependsOn:          []string{lastStepID},
			Amount:             cloneProtoMoney(stepAmount),
			Optional:           false,
			IncludeInAggregate: false,
		})
		lastStepID = stepID
		transitIndex++
	}

	destinationStepID := fmt.Sprintf("i%d.destination", index)
	steps = append(steps, &QuoteComputationStep{
		StepID:             destinationStepID,
		Rail:               rails[len(rails)-1],
		Operation:          destinationOperationForRail(rails[len(rails)-1]),
		GatewayID:          destinationGatewayID,
		InstanceID:         destinationInstanceID,
		DependsOn:          []string{lastStepID},
		Amount:             destinationAmount,
		Optional:           false,
		IncludeInAggregate: true,
	})

	return steps
}

func normalizeRouteRails(sourceRail, destinationRail model.Rail, routeRails []model.Rail) []model.Rail {
	if len(routeRails) == 0 {
		if requiresTransitBridgeStep(sourceRail, destinationRail) {
			return []model.Rail{sourceRail, discovery.RailLedger, destinationRail}
		}
		return []model.Rail{sourceRail, destinationRail}
	}

	result := make([]model.Rail, 0, len(routeRails))
	for _, rail := range routeRails {
		if rail == discovery.RailUnspecified {
			continue
		}
		if len(result) > 0 && result[len(result)-1] == rail {
			continue
		}
		result = append(result, rail)
	}

	if len(result) == 0 {
		return []model.Rail{sourceRail, destinationRail}
	}
	if result[0] != sourceRail {
		result = append([]model.Rail{sourceRail}, result...)
	}
	if result[len(result)-1] != destinationRail {
		result = append(result, destinationRail)
	}
	if len(result) == 1 {
		result = append(result, destinationRail)
	}
	return result
}

func requiresTransitBridgeStep(sourceRail, destinationRail model.Rail) bool {
	if sourceRail == discovery.RailUnspecified || destinationRail == discovery.RailUnspecified {
		return false
	}
	if sourceRail == destinationRail {
		return false
	}
	if sourceRail == discovery.RailLedger || destinationRail == discovery.RailLedger {
		return false
	}
	return true
}

func sourceRailForIntent(intent model.PaymentIntent) model.Rail {
	if intent.Source.Type == model.EndpointTypeLedger {
		return discovery.RailLedger
	}
	if intent.Source.Type == model.EndpointTypeManagedWallet || intent.Source.Type == model.EndpointTypeExternalChain {
		return discovery.RailCrypto
	}
	return discovery.RailLedger
}

func destinationRailForIntent(intent model.PaymentIntent) model.Rail {
	switch intent.Destination.Type {
	case model.EndpointTypeCard:
		return discovery.RailCardPayout
	case model.EndpointTypeManagedWallet, model.EndpointTypeExternalChain:
		return discovery.RailCrypto
	case model.EndpointTypeLedger:
		return discovery.RailLedger
	default:
		return discovery.RailProviderSettlement
	}
}

func sourceOperationForRail(rail model.Rail) model.RailOperation {
	if rail == discovery.RailLedger {
		return discovery.RailOperationMove
	}
	return discovery.RailOperationExternalDebit
}

func destinationOperationForRail(rail model.Rail) model.RailOperation {
	switch rail {
	case discovery.RailLedger:
		return discovery.RailOperationMove
	case discovery.RailCardPayout:
		return discovery.RailOperationSend
	default:
		return discovery.RailOperationExternalCredit
	}
}

func destinationStepAmount(intent model.PaymentIntent, sourceAmount *moneyv1.Money) *moneyv1.Money {
	amount := cloneProtoMoney(sourceAmount)
	if amount == nil {
		return nil
	}
	if !intent.RequiresFX {
		return amount
	}

	settlementCurrency := strings.ToUpper(strings.TrimSpace(intent.SettlementCurrency))
	if settlementCurrency == "" {
		settlementCurrency = settlementCurrencyFromFX(intent.FX)
	}
	if settlementCurrency != "" {
		amount.Currency = settlementCurrency
	}
	return amount
}

func settlementCurrencyFromFX(fx *model.FXIntent) string {
	if fx == nil || fx.Pair == nil {
		return ""
	}
	base := normalizeAsset(fx.Pair.GetBase())
	quote := normalizeAsset(fx.Pair.GetQuote())
	switch fx.Side {
	case paymenttypes.FXSideBuyBaseSellQuote:
		return firstNonEmpty(base, quote)
	case paymenttypes.FXSideSellBaseBuyQuote:
		return firstNonEmpty(quote, base)
	default:
		return firstNonEmpty(quote, base)
	}
}

func sourceStepAmount(intent model.PaymentIntent, amount *moneyv1.Money) *moneyv1.Money {
	result := cloneProtoMoney(amount)
	if result == nil {
		return nil
	}
	if currency := sourceStepCurrency(intent, result.GetCurrency()); currency != "" {
		result.Currency = currency
	}
	return result
}

func sourceStepCurrency(intent model.PaymentIntent, fallback string) string {
	if currency := sourceCurrencyFromFX(intent.FX); currency != "" {
		return currency
	}
	if currency := sourceAssetToken(intent.Source); currency != "" {
		return currency
	}
	return normalizeAsset(fallback)
}

func sourceCurrencyFromFX(fx *model.FXIntent) string {
	if fx == nil || fx.Pair == nil {
		return ""
	}
	base := normalizeAsset(fx.Pair.GetBase())
	quote := normalizeAsset(fx.Pair.GetQuote())
	switch fx.Side {
	case paymenttypes.FXSideBuyBaseSellQuote:
		return quote
	case paymenttypes.FXSideSellBaseBuyQuote:
		return base
	default:
		return firstNonEmpty(base, quote)
	}
}

func sourceAssetToken(endpoint model.PaymentEndpoint) string {
	if endpoint.ManagedWallet != nil && endpoint.ManagedWallet.Asset != nil {
		if token := normalizeAsset(endpoint.ManagedWallet.Asset.GetTokenSymbol()); token != "" {
			return token
		}
	}
	if endpoint.ExternalChain != nil && endpoint.ExternalChain.Asset != nil {
		if token := normalizeAsset(endpoint.ExternalChain.Asset.GetTokenSymbol()); token != "" {
			return token
		}
	}
	return ""
}
