package quotation

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

	oracleclient "github.com/tech/sendico/fx/oracle/client"
	"github.com/tech/sendico/payments/storage/model"
	chainpkg "github.com/tech/sendico/pkg/chain"
	"github.com/tech/sendico/pkg/merrors"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type quoteRequest struct {
	Meta           *sharedv1.RequestMeta
	IdempotencyKey string
	Intent         *sharedv1.PaymentIntent
}

func (r *quoteRequest) GetMeta() *sharedv1.RequestMeta {
	if r == nil {
		return nil
	}
	return r.Meta
}

func (r *quoteRequest) GetIdempotencyKey() string {
	if r == nil {
		return ""
	}
	return r.IdempotencyKey
}

func (r *quoteRequest) GetIntent() *sharedv1.PaymentIntent {
	if r == nil {
		return nil
	}
	return r.Intent
}

func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *quoteRequest) (*sharedv1.PaymentQuote, time.Time, error) {
	intent := req.GetIntent()
	amount := intent.GetAmount()
	fxSide := fxv1.Side_SIDE_UNSPECIFIED
	if fxIntent := fxIntentForQuote(intent); fxIntent != nil {
		fxSide = fxIntent.GetSide()
	}

	var fxQuote *oraclev1.Quote
	var err error
	if shouldRequestFX(intent) {
		fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
		if err != nil {
			return nil, time.Time{}, err
		}
		s.logger.Debug("Fx quote attached to payment quote", zap.String("org_ref", orgRef))
	}

	payAmount, settlementAmountBeforeFees := resolveTradeAmounts(amount, fxQuote, fxSide)

	feeBaseAmount := payAmount
	if feeBaseAmount == nil {
		feeBaseAmount = cloneProtoMoney(amount)
	}

	intentModel := intentFromProto(intent)
	sourceRail, _, err := railFromEndpoint(intentModel.Source, intentModel.Attributes, true)
	if err != nil {
		return nil, time.Time{}, err
	}
	destRail, _, err := railFromEndpoint(intentModel.Destination, intentModel.Attributes, false)
	if err != nil {
		return nil, time.Time{}, err
	}
	feeRequired := feesRequiredForRails(sourceRail, destRail)
	feeQuote := &feesv1.PrecomputeFeesResponse{}
	if feeRequired {
		feeQuote, err = s.quoteFees(ctx, orgRef, req, feeBaseAmount)
		if err != nil {
			return nil, time.Time{}, err
		}
	}
	conversionFeeQuote := &feesv1.PrecomputeFeesResponse{}
	if s.shouldQuoteConversionFee(req.GetIntent()) {
		conversionFeeQuote, err = s.quoteConversionFees(ctx, orgRef, req, feeBaseAmount)
		if err != nil {
			return nil, time.Time{}, err
		}
	}
	feeCurrency := ""
	if feeBaseAmount != nil {
		feeCurrency = feeBaseAmount.GetCurrency()
	} else if amount != nil {
		feeCurrency = amount.GetCurrency()
	}
	feeLines := cloneFeeLines(feeQuote.GetLines())
	if conversionFeeQuote != nil {
		feeLines = append(feeLines, cloneFeeLines(conversionFeeQuote.GetLines())...)
	}
	s.assignFeeLedgerAccounts(intent, feeLines)
	feeTotal := extractFeeTotal(feeLines, feeCurrency)

	var networkFee *chainv1.EstimateTransferFeeResponse
	if shouldEstimateNetworkFee(intent) {
		networkFee, err = s.estimateNetworkFee(ctx, intent)
		if err != nil {
			return nil, time.Time{}, err
		}
		s.logger.Debug("Network fee estimated", zap.String("org_ref", orgRef))
	}

	debitAmount, settlementAmount := computeAggregates(
		payAmount,
		settlementAmountBeforeFees,
		feeTotal,
		networkFee,
		fxQuote,
		resolvedFeeTreatmentForQuote(intent),
	)

	quote := &sharedv1.PaymentQuote{
		DebitAmount:              debitAmount,
		DebitSettlementAmount:    payAmount,
		ExpectedSettlementAmount: settlementAmount,
		ExpectedFeeTotal:         feeTotal,
		FeeLines:                 feeLines,
		FeeRules:                 mergeFeeRules(feeQuote, conversionFeeQuote),
		FxQuote:                  fxQuote,
		NetworkFee:               networkFee,
	}

	expiresAt := quoteExpiry(s.clock.Now(), feeQuote, fxQuote)
	if conversionFeeQuote != nil {
		convExpiry := quoteExpiry(s.clock.Now(), conversionFeeQuote, fxQuote)
		if convExpiry.Before(expiresAt) {
			expiresAt = convExpiry
		}
	}

	return quote, expiresAt, nil
}

func (s *Service) quoteFees(ctx context.Context, orgRef string, req *quoteRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
	if !s.deps.fees.available() {
		s.logger.Warn("Fees precompute failed: fee engine unavailable")
		return nil, merrors.Internal("fees_precompute_failed")
	}
	intent := req.GetIntent()
	amount := cloneProtoMoney(baseAmount)
	if amount == nil {
		amount = cloneProtoMoney(intent.GetAmount())
	}
	attrs := ensureFeeAttributes(intent, amount, cloneMetadata(intent.GetAttributes()))
	feeIntent := &feesv1.Intent{
		Trigger:    feeTriggerForIntent(intent),
		BaseAmount: amount,
		BookedAt:   timestamppb.New(s.clock.Now()),
		OriginType: "payments.orchestrator.quote",
		OriginRef:  strings.TrimSpace(req.GetIdempotencyKey()),
		Attributes: attrs,
	}
	timeout := req.GetMeta().GetTrace()
	ctxTimeout, cancel := s.withTimeout(ctx, s.deps.fees.timeout)
	defer cancel()
	resp, err := s.deps.fees.client.PrecomputeFees(ctxTimeout, &feesv1.PrecomputeFeesRequest{
		Meta: &feesv1.RequestMeta{
			OrganizationRef: orgRef,
			Trace:           timeout,
		},
		Intent: feeIntent,
		TtlMs:  defaultFeeQuoteTTLMillis,
	})
	if err != nil {
		s.logger.Warn("Fees precompute failed", zap.Error(err))
		return nil, merrors.Internal("fees_precompute_failed")
	}
	return resp, nil
}

func (s *Service) quoteConversionFees(ctx context.Context, orgRef string, req *quoteRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
	if !s.deps.fees.available() {
		s.logger.Warn("Conversion fee precompute failed: fee engine unavailable")
		return nil, merrors.Internal("fees_precompute_failed")
	}
	intent := req.GetIntent()
	amount := cloneProtoMoney(baseAmount)
	if amount == nil {
		amount = cloneProtoMoney(intent.GetAmount())
	}
	attrs := ensureFeeAttributes(intent, amount, cloneMetadata(intent.GetAttributes()))
	attrs["product"] = "wallet"
	attrs["source_type"] = "managed_wallet"
	attrs["destination_type"] = "ledger"

	feeIntent := &feesv1.Intent{
		Trigger:    feesv1.Trigger_TRIGGER_CAPTURE,
		BaseAmount: amount,
		BookedAt:   timestamppb.New(s.clock.Now()),
		OriginType: "payments.orchestrator.conversion_quote",
		OriginRef:  strings.TrimSpace(req.GetIdempotencyKey()),
		Attributes: attrs,
	}
	timeout := req.GetMeta().GetTrace()
	ctxTimeout, cancel := s.withTimeout(ctx, s.deps.fees.timeout)
	defer cancel()
	resp, err := s.deps.fees.client.PrecomputeFees(ctxTimeout, &feesv1.PrecomputeFeesRequest{
		Meta: &feesv1.RequestMeta{
			OrganizationRef: orgRef,
			Trace:           timeout,
		},
		Intent: feeIntent,
		TtlMs:  defaultFeeQuoteTTLMillis,
	})
	if err != nil {
		s.logger.Warn("Conversion fee precompute failed", zap.Error(err))
		return nil, merrors.Internal("fees_precompute_failed")
	}
	setFeeLineTarget(resp.GetLines(), feeLineTargetWallet)
	if src := intent.GetSource().GetManagedWallet(); src != nil {
		setFeeLineWalletRef(resp.GetLines(), src.GetManagedWalletRef(), "managed_wallet")
	}
	return resp, nil
}

func (s *Service) shouldQuoteConversionFee(intent *sharedv1.PaymentIntent) bool {
	if intent == nil {
		return false
	}
	if !isManagedWalletEndpoint(intent.GetSource()) {
		return false
	}
	if isLedgerEndpoint(intent.GetDestination()) {
		return false
	}
	return true
}

func mergeFeeRules(primary, secondary *feesv1.PrecomputeFeesResponse) []*feesv1.AppliedRule {
	rules := cloneFeeRules(nil)
	if primary != nil {
		rules = append(rules, cloneFeeRules(primary.GetApplied())...)
	}
	if secondary != nil {
		rules = append(rules, cloneFeeRules(secondary.GetApplied())...)
	}
	if len(rules) == 0 {
		return nil
	}
	return rules
}

func ensureFeeAttributes(intent *sharedv1.PaymentIntent, baseAmount *moneyv1.Money, attrs map[string]string) map[string]string {
	if attrs == nil {
		attrs = map[string]string{}
	}
	if intent == nil {
		return attrs
	}
	setFeeAttributeIfMissing(attrs, "product", "wallet")
	if op := feeOperationFromKind(intent.GetKind()); op != "" {
		setFeeAttributeIfMissing(attrs, "operation", op)
	}
	if currency := feeCurrencyFromAmount(baseAmount, intent.GetAmount()); currency != "" {
		setFeeAttributeIfMissing(attrs, "currency", currency)
	}
	if srcType := endpointTypeFromProto(intent.GetSource()); srcType != "" {
		setFeeAttributeIfMissing(attrs, "source_type", srcType)
	}
	if dstType := endpointTypeFromProto(intent.GetDestination()); dstType != "" {
		setFeeAttributeIfMissing(attrs, "destination_type", dstType)
	}
	if asset := assetFromIntent(intent); asset != nil {
		if token := strings.TrimSpace(asset.GetTokenSymbol()); token != "" {
			setFeeAttributeIfMissing(attrs, "asset", token)
		}
		if chain := asset.GetChain(); chain != chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
			if network := strings.TrimSpace(chainpkg.NetworkAlias(chain)); network != "" {
				setFeeAttributeIfMissing(attrs, "network", network)
			}
		}
	}
	return attrs
}

func feeTriggerForIntent(intent *sharedv1.PaymentIntent) feesv1.Trigger {
	if intent == nil {
		return feesv1.Trigger_TRIGGER_UNSPECIFIED
	}
	trigger := triggerFromKind(intent.GetKind(), shouldRequestFX(intent))
	if trigger != feesv1.Trigger_TRIGGER_FX_CONVERSION && isManagedWalletEndpoint(intent.GetSource()) && isLedgerEndpoint(intent.GetDestination()) {
		return feesv1.Trigger_TRIGGER_CAPTURE
	}
	return trigger
}

func isManagedWalletEndpoint(endpoint *sharedv1.PaymentEndpoint) bool {
	return endpoint != nil && endpoint.GetManagedWallet() != nil
}

func isLedgerEndpoint(endpoint *sharedv1.PaymentEndpoint) bool {
	return endpoint != nil && endpoint.GetLedger() != nil
}

func setFeeAttributeIfMissing(attrs map[string]string, key, value string) {
	if attrs == nil {
		return
	}
	if strings.TrimSpace(key) == "" {
		return
	}
	value = strings.TrimSpace(value)
	if value == "" {
		return
	}
	if _, exists := attrs[key]; exists {
		return
	}
	attrs[key] = value
}

func feeOperationFromKind(kind sharedv1.PaymentKind) string {
	switch kind {
	case sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT:
		return "payout"
	case sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER:
		return "internal_transfer"
	case sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION:
		return "fx_conversion"
	default:
		return ""
	}
}

func feeCurrencyFromAmount(baseAmount, intentAmount *moneyv1.Money) string {
	if baseAmount != nil {
		if currency := strings.TrimSpace(baseAmount.GetCurrency()); currency != "" {
			return currency
		}
	}
	if intentAmount != nil {
		return strings.TrimSpace(intentAmount.GetCurrency())
	}
	return ""
}

func endpointTypeFromProto(endpoint *sharedv1.PaymentEndpoint) string {
	if endpoint == nil {
		return ""
	}
	switch {
	case endpoint.GetLedger() != nil:
		return "ledger"
	case endpoint.GetManagedWallet() != nil:
		return "managed_wallet"
	case endpoint.GetExternalChain() != nil:
		return "external_chain"
	case endpoint.GetCard() != nil:
		return "card"
	default:
		return ""
	}
}

func assetFromIntent(intent *sharedv1.PaymentIntent) *chainv1.Asset {
	if intent == nil {
		return nil
	}
	if asset := assetFromEndpoint(intent.GetDestination()); asset != nil {
		return asset
	}
	return assetFromEndpoint(intent.GetSource())
}

func assetFromEndpoint(endpoint *sharedv1.PaymentEndpoint) *chainv1.Asset {
	if endpoint == nil {
		return nil
	}
	if wallet := endpoint.GetManagedWallet(); wallet != nil {
		return wallet.GetAsset()
	}
	if external := endpoint.GetExternalChain(); external != nil {
		return external.GetAsset()
	}
	return nil
}

func (s *Service) estimateNetworkFee(ctx context.Context, intent *sharedv1.PaymentIntent) (*chainv1.EstimateTransferFeeResponse, error) {
	req := &chainv1.EstimateTransferFeeRequest{
		Amount: cloneProtoMoney(intent.GetAmount()),
	}
	if src := intent.GetSource().GetManagedWallet(); src != nil {
		req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
	}
	if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
		req.Destination = &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
		}
	}
	if dst := intent.GetDestination().GetExternalChain(); dst != nil {
		req.Destination = &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
			Memo:        strings.TrimSpace(dst.GetMemo()),
		}
		req.Asset = dst.GetAsset()
	}
	if req.Asset == nil {
		if src := intent.GetSource().GetManagedWallet(); src != nil {
			req.Asset = src.GetAsset()
		}
	}

	network := ""
	if req.Asset != nil {
		network = chainpkg.NetworkName(req.Asset.GetChain())
	}
	instanceID := strings.TrimSpace(intent.GetSource().GetInstanceId())
	if instanceID == "" {
		instanceID = strings.TrimSpace(intent.GetDestination().GetInstanceId())
	}
	client, _, err := s.resolveChainGatewayClient(ctx, network, moneyFromProto(req.Amount), []model.RailOperation{discovery.RailOperationSend}, instanceID, "")
	if err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			s.logger.Debug("Network fee estimation skipped: gateway unavailable", zap.Error(err))
			return nil, nil //nolint:nilnil // nil response means fee estimation skipped when gateway is unavailable
		}
		s.logger.Warn("Chain gateway resolution failed", zap.Error(err))
		return nil, err
	}
	if client == nil {
		return nil, nil //nolint:nilnil // nil response means no chain gateway client is available
	}

	resp, err := client.EstimateTransferFee(ctx, req)
	if err != nil {
		s.logger.Warn("Chain gateway fee estimation failed", zap.Error(err))
		return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
	}
	return resp, nil
}

func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *quoteRequest) (*oraclev1.Quote, error) {
	intent := req.GetIntent()
	fxRequired := shouldRequestFX(intent)

	if !s.deps.oracle.available() {
		if fxRequired {
			return nil, merrors.Internal("fx_oracle_unavailable")
		}
		return nil, nil //nolint:nilnil // nil quote means FX is optional and oracle is unavailable
	}
	meta := req.GetMeta()
	fxIntent := fxIntentForQuote(intent)
	if fxIntent == nil {
		if fxRequired {
			return nil, merrors.InvalidArgument("fx intent missing")
		}
		return nil, nil //nolint:nilnil // nil quote means FX is not required for this intent
	}

	ttl := fxIntent.GetTtlMs()
	if ttl <= 0 {
		ttl = defaultOracleTTLMillis
	}

	params := oracleclient.GetQuoteParams{
		Meta: oracleclient.RequestMeta{
			OrganizationRef: orgRef,
			Trace:           meta.GetTrace(),
		},
		Pair:              fxIntent.GetPair(),
		Side:              fxIntent.GetSide(),
		Firm:              fxIntent.GetFirm(),
		TTL:               time.Duration(ttl) * time.Millisecond,
		PreferredProvider: strings.TrimSpace(fxIntent.GetPreferredProvider()),
	}

	if fxIntent.GetMaxAgeMs() > 0 {
		params.MaxAge = time.Duration(fxIntent.GetMaxAgeMs()) * time.Millisecond
	}

	if amount := intent.GetAmount(); amount != nil {
		pair := fxIntent.GetPair()
		if pair != nil {
			switch {
			case strings.EqualFold(amount.GetCurrency(), pair.GetBase()):
				params.BaseAmount = cloneProtoMoney(amount)
			case strings.EqualFold(amount.GetCurrency(), pair.GetQuote()):
				params.QuoteAmount = cloneProtoMoney(amount)
			default:
				params.BaseAmount = cloneProtoMoney(amount)
			}
		} else {
			params.BaseAmount = cloneProtoMoney(amount)
		}
	}

	quote, err := s.deps.oracle.client.GetQuote(ctx, params)
	if err != nil {
		s.logger.Warn("Fx oracle quote failed", zap.Error(err))
		return nil, merrors.Internal(fmt.Sprintf("orchestrator: fx quote failed, %s", err.Error()))
	}
	if quote == nil {
		if fxRequired {
			return nil, merrors.Internal("orchestrator: fx quote missing")
		}
		return nil, nil //nolint:nilnil // nil quote means FX is optional and oracle returned no quote
	}
	return quoteToProto(quote), nil
}

func feesRequiredForRails(sourceRail, destRail model.Rail) bool {
	if sourceRail == discovery.RailLedger && destRail == discovery.RailLedger {
		return false
	}
	return true
}

func (s *Service) feeLedgerAccountForIntent(intent *sharedv1.PaymentIntent) string {
	if intent == nil || len(s.deps.feeLedgerAccounts) == 0 {
		return ""
	}

	key := s.gatewayKeyFromIntent(intent)
	if key == "" {
		return ""
	}
	return strings.TrimSpace(s.deps.feeLedgerAccounts[key])
}

func (s *Service) assignFeeLedgerAccounts(intent *sharedv1.PaymentIntent, lines []*feesv1.DerivedPostingLine) {
	account := s.feeLedgerAccountForIntent(intent)
	key := s.gatewayKeyFromIntent(intent)

	missing := 0
	for _, line := range lines {
		if line == nil {
			continue
		}
		if strings.TrimSpace(line.GetLedgerAccountRef()) == "" {
			missing++
		}
	}
	if missing == 0 {
		return
	}

	if account == "" {
		s.logger.Debug("No fee ledger account mapping found", zap.String("gateway", key), zap.Int("missing_lines", missing))
		return
	}
	assignLedgerAccounts(lines, account)
	s.logger.Debug("Applied fee ledger account mapping", zap.String("gateway", key), zap.String("ledger_account", account), zap.Int("lines", missing))
}

func (s *Service) gatewayKeyFromIntent(intent *sharedv1.PaymentIntent) string {
	if intent == nil {
		return ""
	}
	key := strings.TrimSpace(intent.GetAttributes()["gateway"])
	if key == "" {
		if dest := intent.GetDestination(); dest != nil && dest.GetCard() != nil {
			key = defaultCardGateway
		}
	}
	return strings.ToLower(key)
}
