package orchestrator

import (
	"context"
	"errors"
	"strings"
	"time"

	oracleclient "github.com/tech/sendico/fx/oracle/client"
	"github.com/tech/sendico/payments/orchestrator/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"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.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
		}
	}
	feeCurrency := ""
	if feeBaseAmount != nil {
		feeCurrency = feeBaseAmount.GetCurrency()
	} else if amount != nil {
		feeCurrency = amount.GetCurrency()
	}
	feeLines := cloneFeeLines(feeQuote.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, intent.GetSettlementMode())

	quote := &orchestratorv1.PaymentQuote{
		DebitAmount:              debitAmount,
		ExpectedSettlementAmount: settlementAmount,
		ExpectedFeeTotal:         feeTotal,
		FeeLines:                 feeLines,
		FeeRules:                 cloneFeeRules(feeQuote.GetApplied()),
		FxQuote:                  fxQuote,
		NetworkFee:               networkFee,
	}

	expiresAt := quoteExpiry(s.clock.Now(), feeQuote, fxQuote)

	return quote, expiresAt, nil
}

func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
	if !s.deps.fees.available() {
		return &feesv1.PrecomputeFeesResponse{}, nil
	}
	intent := req.GetIntent()
	amount := cloneProtoMoney(baseAmount)
	if amount == nil {
		amount = cloneProtoMoney(intent.GetAmount())
	}
	feeIntent := &feesv1.Intent{
		Trigger:    triggerFromKind(intent.GetKind(), intent.GetRequiresFx()),
		BaseAmount: amount,
		BookedAt:   timestamppb.New(s.clock.Now()),
		OriginType: "payments.orchestrator.quote",
		OriginRef:  strings.TrimSpace(req.GetIdempotencyKey()),
		Attributes: cloneMetadata(intent.GetAttributes()),
	}
	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) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.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{model.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
		}
		s.logger.Warn("chain gateway resolution failed", zap.Error(err))
		return nil, err
	}
	if client == nil {
		return nil, nil
	}

	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 *orchestratorv1.QuotePaymentRequest) (*oraclev1.Quote, error) {
	if !s.deps.oracle.available() {
		return nil, nil
	}
	intent := req.GetIntent()
	meta := req.GetMeta()
	fxIntent := fxIntentForQuote(intent)
	if fxIntent == nil {
		if intent.GetRequiresFx() {
			return nil, merrors.InvalidArgument("fx intent missing")
		}
		return nil, nil
	}

	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("fx_quote_failed")
	}
	return quoteToProto(quote), nil
}

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

func (s *Service) feeLedgerAccountForIntent(intent *orchestratorv1.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 *orchestratorv1.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 *orchestratorv1.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)
}
