package orchestrator

import (
	"context"
	"strings"
	"time"

	oracleclient "github.com/tech/sendico/fx/oracle/client"
	"github.com/tech/sendico/payments/orchestrator/storage"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"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, error) {
	intent := req.GetIntent()
	amount := intent.GetAmount()
	baseAmount := cloneMoney(amount)
	feeQuote, err := s.quoteFees(ctx, orgRef, req)
	if err != nil {
		return nil, err
	}
	feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())

	var networkFee *gatewayv1.EstimateTransferFeeResponse
	if shouldEstimateNetworkFee(intent) {
		networkFee, err = s.estimateNetworkFee(ctx, intent)
		if err != nil {
			return nil, err
		}
	}

	var fxQuote *oraclev1.Quote
	if shouldRequestFX(intent) {
		fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
		if err != nil {
			return nil, err
		}
	}

	debitAmount, settlementAmount := computeAggregates(baseAmount, feeTotal, networkFee)

	return &orchestratorv1.PaymentQuote{
		DebitAmount:              debitAmount,
		ExpectedSettlementAmount: settlementAmount,
		ExpectedFeeTotal:         feeTotal,
		FeeLines:                 cloneFeeLines(feeQuote.GetLines()),
		FeeRules:                 cloneFeeRules(feeQuote.GetApplied()),
		FxQuote:                  fxQuote,
		NetworkFee:               networkFee,
		FeeQuoteToken:            feeQuote.GetFeeQuoteToken(),
	}, nil
}

func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*feesv1.PrecomputeFeesResponse, error) {
	if !s.fees.available() {
		return &feesv1.PrecomputeFeesResponse{}, nil
	}
	intent := req.GetIntent()
	feeIntent := &feesv1.Intent{
		Trigger:    triggerFromKind(intent.GetKind(), intent.GetRequiresFx()),
		BaseAmount: cloneMoney(intent.GetAmount()),
		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.fees.timeout)
	defer cancel()
	resp, err := s.fees.client.PrecomputeFees(ctxTimeout, &feesv1.PrecomputeFeesRequest{
		Meta: &feesv1.RequestMeta{
			OrganizationRef: orgRef,
			Trace:           timeout,
		},
		Intent: feeIntent,
		TtlMs:  defaultFeeQuoteTTLMillis,
	})
	if err != nil {
		s.logger.Error("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) (*gatewayv1.EstimateTransferFeeResponse, error) {
	if !s.gateway.available() {
		return nil, nil
	}

	req := &gatewayv1.EstimateTransferFeeRequest{
		Amount: cloneMoney(intent.GetAmount()),
	}
	if src := intent.GetSource().GetManagedWallet(); src != nil {
		req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
	}
	if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
		req.Destination = &gatewayv1.TransferDestination{
			Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
		}
	}
	if dst := intent.GetDestination().GetExternalChain(); dst != nil {
		req.Destination = &gatewayv1.TransferDestination{
			Destination: &gatewayv1.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()
		}
	}

	resp, err := s.gateway.client.EstimateTransferFee(ctx, req)
	if err != nil {
		s.logger.Error("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.oracle.available() {
		return nil, nil
	}
	intent := req.GetIntent()
	meta := req.GetMeta()
	fxIntent := intent.GetFx()
	if fxIntent == nil {
		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 {
		params.BaseAmount = cloneMoney(amount)
	}

	quote, err := s.oracle.client.GetQuote(ctx, params)
	if err != nil {
		s.logger.Error("fx oracle quote failed", zap.Error(err))
		return nil, merrors.Internal("fx_quote_failed")
	}
	return quoteToProto(quote), nil
}

func (s *Service) executePayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, quote *orchestratorv1.PaymentQuote) error {
	if store == nil {
		return errStorageUnavailable
	}

	charges := ledgerChargesFromFeeLines(quote.GetFeeLines())
	ledgerNeeded := requiresLedger(payment)
	chainNeeded := requiresChain(payment)

	exec := payment.Execution
	if exec == nil {
		exec = &model.ExecutionRefs{}
	}

	if ledgerNeeded {
		if !s.ledger.available() {
			return s.failPayment(ctx, store, payment, model.PaymentFailureCodeLedger, "ledger_client_unavailable", merrors.Internal("ledger_client_unavailable"))
		}
		if err := s.performLedgerOperation(ctx, payment, quote, charges); err != nil {
			return s.failPayment(ctx, store, payment, model.PaymentFailureCodeLedger, strings.TrimSpace(err.Error()), err)
		}
		payment.State = model.PaymentStateFundsReserved
		if err := s.persistPayment(ctx, store, payment); err != nil {
			return err
		}
	}

	if chainNeeded {
		if !s.gateway.available() {
			return s.failPayment(ctx, store, payment, model.PaymentFailureCodeChain, "chain_client_unavailable", merrors.Internal("chain_client_unavailable"))
		}
		resp, err := s.submitChainTransfer(ctx, payment, quote)
		if err != nil {
			return s.failPayment(ctx, store, payment, model.PaymentFailureCodeChain, strings.TrimSpace(err.Error()), err)
		}
		exec = payment.Execution
		if exec == nil {
			exec = &model.ExecutionRefs{}
		}
		if resp != nil && resp.GetTransfer() != nil {
			exec.ChainTransferRef = strings.TrimSpace(resp.GetTransfer().GetTransferRef())
		}
		payment.Execution = exec
		payment.State = model.PaymentStateSubmitted
		if err := s.persistPayment(ctx, store, payment); err != nil {
			return err
		}
		return nil
	}

	payment.State = model.PaymentStateSettled
	return s.persistPayment(ctx, store, payment)
}

func (s *Service) performLedgerOperation(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine) error {
	intent := payment.Intent
	if payment.OrganizationRef == primitive.NilObjectID {
		return merrors.InvalidArgument("ledger: organization_ref is required")
	}

	amount := cloneMoney(intent.Amount)
	if amount == nil {
		return merrors.InvalidArgument("ledger: amount is required")
	}

	description := paymentDescription(payment)
	metadata := cloneMetadata(payment.Metadata)
	exec := payment.Execution
	if exec == nil {
		exec = &model.ExecutionRefs{}
	}

	switch intent.Kind {
	case model.PaymentKindFXConversion:
		if err := s.applyFX(ctx, payment, quote, charges, description, metadata, exec); err != nil {
			return err
		}
	case model.PaymentKindInternalTransfer, model.PaymentKindPayout, model.PaymentKindUnspecified:
		from, to, err := resolveLedgerAccounts(intent)
		if err != nil {
			return err
		}
		req := &ledgerv1.TransferRequest{
			IdempotencyKey:       payment.IdempotencyKey,
			OrganizationRef:      payment.OrganizationRef.Hex(),
			FromLedgerAccountRef: from,
			ToLedgerAccountRef:   to,
			Money:                amount,
			Description:          description,
			Charges:              charges,
			Metadata:             metadata,
		}
		resp, err := s.ledger.client.TransferInternal(ctx, req)
		if err != nil {
			return err
		}
		exec.DebitEntryRef = strings.TrimSpace(resp.GetJournalEntryRef())
		payment.Execution = exec
	default:
		return merrors.InvalidArgument("ledger: unsupported payment kind")
	}

	return nil
}

func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine, description string, metadata map[string]string, exec *model.ExecutionRefs) error {
	intent := payment.Intent
	source := intent.Source.Ledger
	destination := intent.Destination.Ledger
	if source == nil || destination == nil {
		return merrors.InvalidArgument("ledger: fx conversion requires ledger source and destination")
	}
	fq := quote.GetFxQuote()
	if fq == nil {
		return merrors.InvalidArgument("ledger: fx quote missing")
	}
	fromMoney := cloneMoney(fq.GetBaseAmount())
	if fromMoney == nil {
		fromMoney = cloneMoney(intent.Amount)
	}
	toMoney := cloneMoney(fq.GetQuoteAmount())
	if toMoney == nil {
		toMoney = cloneMoney(quote.GetExpectedSettlementAmount())
	}
	rate := ""
	if fq.GetPrice() != nil {
		rate = fq.GetPrice().GetValue()
	}
	req := &ledgerv1.FXRequest{
		IdempotencyKey:       payment.IdempotencyKey,
		OrganizationRef:      payment.OrganizationRef.Hex(),
		FromLedgerAccountRef: strings.TrimSpace(source.LedgerAccountRef),
		ToLedgerAccountRef:   strings.TrimSpace(destination.LedgerAccountRef),
		FromMoney:            fromMoney,
		ToMoney:              toMoney,
		Rate:                 rate,
		Description:          description,
		Charges:              charges,
		Metadata:             metadata,
	}
	resp, err := s.ledger.client.ApplyFXWithCharges(ctx, req)
	if err != nil {
		return err
	}
	exec.FXEntryRef = strings.TrimSpace(resp.GetJournalEntryRef())
	payment.Execution = exec
	return nil
}

func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*gatewayv1.SubmitTransferResponse, error) {
	intent := payment.Intent
	source := intent.Source.ManagedWallet
	destination := intent.Destination
	if source == nil || strings.TrimSpace(source.ManagedWalletRef) == "" {
		return nil, merrors.InvalidArgument("chain: source managed wallet is required")
	}
	dest, err := toGatewayDestination(destination)
	if err != nil {
		return nil, err
	}
	amount := cloneMoney(intent.Amount)
	if amount == nil {
		return nil, merrors.InvalidArgument("chain: amount is required")
	}
	fees := feeBreakdownFromQuote(quote)
	req := &gatewayv1.SubmitTransferRequest{
		IdempotencyKey:  payment.IdempotencyKey,
		OrganizationRef: payment.OrganizationRef.Hex(),
		SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
		Destination:     dest,
		Amount:          amount,
		Fees:            fees,
		Metadata:        cloneMetadata(payment.Metadata),
		ClientReference: payment.PaymentRef,
	}
	return s.gateway.client.SubmitTransfer(ctx, req)
}

func (s *Service) persistPayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error {
	if store == nil {
		return errStorageUnavailable
	}
	return store.Update(ctx, payment)
}

func (s *Service) failPayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, code model.PaymentFailureCode, reason string, err error) error {
	payment.State = model.PaymentStateFailed
	payment.FailureCode = code
	payment.FailureReason = strings.TrimSpace(reason)
	if store != nil {
		if updateErr := store.Update(ctx, payment); updateErr != nil {
			s.logger.Error("failed to persist payment failure", zap.Error(updateErr), zap.String("payment_ref", payment.PaymentRef))
		}
	}
	if err != nil {
		return err
	}
	return merrors.Internal(reason)
}

func resolveLedgerAccounts(intent model.PaymentIntent) (string, string, error) {
	source := intent.Source.Ledger
	destination := intent.Destination.Ledger
	if source == nil || strings.TrimSpace(source.LedgerAccountRef) == "" {
		return "", "", merrors.InvalidArgument("ledger: source account is required")
	}
	to := ""
	if destination != nil && strings.TrimSpace(destination.LedgerAccountRef) != "" {
		to = strings.TrimSpace(destination.LedgerAccountRef)
	} else if strings.TrimSpace(source.ContraLedgerAccountRef) != "" {
		to = strings.TrimSpace(source.ContraLedgerAccountRef)
	}
	if to == "" {
		return "", "", merrors.InvalidArgument("ledger: destination account is required")
	}
	return strings.TrimSpace(source.LedgerAccountRef), to, nil
}

func paymentDescription(payment *model.Payment) string {
	if payment == nil {
		return ""
	}
	if val := strings.TrimSpace(payment.Intent.Attributes["description"]); val != "" {
		return val
	}
	if payment.Metadata != nil {
		if val := strings.TrimSpace(payment.Metadata["description"]); val != "" {
			return val
		}
	}
	return payment.PaymentRef
}

func requiresLedger(payment *model.Payment) bool {
	if payment == nil {
		return false
	}
	if payment.Intent.Kind == model.PaymentKindFXConversion {
		return true
	}
	return hasLedgerEndpoint(payment.Intent.Source) || hasLedgerEndpoint(payment.Intent.Destination)
}

func requiresChain(payment *model.Payment) bool {
	if payment == nil {
		return false
	}
	if !hasManagedWallet(payment.Intent.Source) {
		return false
	}
	switch payment.Intent.Destination.Type {
	case model.EndpointTypeManagedWallet, model.EndpointTypeExternalChain:
		return true
	default:
		return false
	}
}

func hasLedgerEndpoint(endpoint model.PaymentEndpoint) bool {
	return endpoint.Type == model.EndpointTypeLedger && endpoint.Ledger != nil && strings.TrimSpace(endpoint.Ledger.LedgerAccountRef) != ""
}

func hasManagedWallet(endpoint model.PaymentEndpoint) bool {
	return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != ""
}

func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDestination, error) {
	switch endpoint.Type {
	case model.EndpointTypeManagedWallet:
		if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" {
			return nil, merrors.InvalidArgument("chain: destination managed wallet is required")
		}
		return &gatewayv1.TransferDestination{
			Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)},
		}, nil
	case model.EndpointTypeExternalChain:
		if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" {
			return nil, merrors.InvalidArgument("chain: external address is required")
		}
		return &gatewayv1.TransferDestination{
			Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)},
			Memo:        strings.TrimSpace(endpoint.ExternalChain.Memo),
		}, nil
	default:
		return nil, merrors.InvalidArgument("chain: unsupported destination type")
	}
}

func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *model.Payment) {
	if payment.Execution == nil {
		payment.Execution = &model.ExecutionRefs{}
	}
	if event == nil || event.GetTransfer() == nil {
		return
	}
	transfer := event.GetTransfer()
	payment.Execution.ChainTransferRef = strings.TrimSpace(transfer.GetTransferRef())
	reason := strings.TrimSpace(event.GetReason())
	if reason == "" {
		reason = strings.TrimSpace(transfer.GetFailureReason())
	}
	switch transfer.GetStatus() {
	case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
		payment.State = model.PaymentStateSettled
		payment.FailureCode = model.PaymentFailureCodeUnspecified
		payment.FailureReason = ""
	case gatewayv1.TransferStatus_TRANSFER_FAILED:
		payment.State = model.PaymentStateFailed
		payment.FailureCode = model.PaymentFailureCodeChain
		payment.FailureReason = reason
	case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
		payment.State = model.PaymentStateCancelled
		payment.FailureCode = model.PaymentFailureCodePolicy
		payment.FailureReason = reason
	case gatewayv1.TransferStatus_TRANSFER_SIGNING,
		gatewayv1.TransferStatus_TRANSFER_PENDING,
		gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
		payment.State = model.PaymentStateSubmitted
	default:
		// retain previous state
	}
}
