package transfer_intent_hydrator

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tech/sendico/pkg/merrors"
	payecon "github.com/tech/sendico/pkg/payments/economics"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
	methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/grpc"
)

type HydrateOneInput struct {
	OrganizationRef string
	InitiatorRef    string
	Intent          *quotationv2.QuoteIntent
}

type HydrateManyInput struct {
	OrganizationRef string
	InitiatorRef    string
	Intents         []*quotationv2.QuoteIntent
}

type PaymentMethodsClient interface {
	GetPaymentMethodPrivate(ctx context.Context, in *methodsv1.GetPaymentMethodPrivateRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodPrivateResponse, error)
}

type Option func(*TransferIntentHydrator)

type TransferIntentHydrator struct {
	methodsClient PaymentMethodsClient
	newRef        func() string
}

func New(methodsClient PaymentMethodsClient, opts ...Option) *TransferIntentHydrator {
	h := &TransferIntentHydrator{
		methodsClient: methodsClient,
		newRef: func() string {
			return bson.NewObjectID().Hex()
		},
	}
	for _, opt := range opts {
		if opt != nil {
			opt(h)
		}
	}
	if h.newRef == nil {
		h.newRef = func() string { return bson.NewObjectID().Hex() }
	}
	return h
}

func WithRefFactory(newRef func() string) Option {
	return func(h *TransferIntentHydrator) {
		if newRef != nil {
			h.newRef = newRef
		}
	}
}

func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneInput) (*QuoteIntent, error) {
	if strings.TrimSpace(in.OrganizationRef) == "" {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	if strings.TrimSpace(in.InitiatorRef) == "" {
		return nil, merrors.InvalidArgument("initiator_ref is required")
	}
	if in.Intent == nil {
		return nil, merrors.InvalidArgument("intent is required")
	}
	if in.Intent.GetAmount() == nil {
		return nil, merrors.InvalidArgument("intent.amount is required")
	}

	amount := &paymenttypes.Money{
		Amount:   strings.TrimSpace(in.Intent.GetAmount().GetAmount()),
		Currency: strings.TrimSpace(in.Intent.GetAmount().GetCurrency()),
	}
	if amount.Amount == "" {
		return nil, merrors.InvalidArgument("intent.amount.amount is required")
	}
	if amount.Currency == "" {
		return nil, merrors.InvalidArgument("intent.amount.currency is required")
	}
	settlementMode, feeTreatment, err := resolveEconomics(
		in.Intent.GetSettlementMode(),
		in.Intent.GetFeeTreatment(),
	)
	if err != nil {
		return nil, err
	}

	source, err := h.hydrateEndpoint(
		ctx,
		in.OrganizationRef,
		in.Intent.GetSource(),
		"intent.source",
		methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_SOURCE,
	)
	if err != nil {
		return nil, err
	}
	destination, err := h.hydrateEndpoint(
		ctx,
		in.OrganizationRef,
		in.Intent.GetDestination(),
		"intent.destination",
		methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_DESTINATION,
	)
	if err != nil {
		return nil, err
	}

	settlementCurrency := strings.ToUpper(strings.TrimSpace(in.Intent.GetSettlementCurrency()))
	if settlementCurrency == "" {
		settlementCurrency = strings.ToUpper(strings.TrimSpace(amount.Currency))
	}
	fxIntent := fxIntentFromProto(in.Intent.GetFx())
	if settlementCurrency == "" {
		settlementCurrency = settlementCurrencyFromFX(fxIntent)
	}
	requiresFX := fxIntent != nil && fxIntent.Pair != nil
	if !requiresFX {
		requiresFX = !strings.EqualFold(amount.Currency, settlementCurrency)
	}

	intent := &QuoteIntent{
		Ref:                h.newRef(),
		Kind:               inferKind(destination),
		Source:             source,
		Destination:        destination,
		Amount:             amount,
		Comment:            strings.TrimSpace(in.Intent.GetComment()),
		SettlementMode:     settlementMode,
		FeeTreatment:       feeTreatment,
		FX:                 fxIntent,
		SettlementCurrency: settlementCurrency,
		RequiresFX:         requiresFX,
		Attributes: map[string]string{
			"initiator_ref": strings.TrimSpace(in.InitiatorRef),
		},
	}
	return intent, nil
}

func (h *TransferIntentHydrator) HydrateMany(ctx context.Context, in HydrateManyInput) ([]*QuoteIntent, error) {
	if len(in.Intents) == 0 {
		return nil, merrors.InvalidArgument("intents are required")
	}
	out := make([]*QuoteIntent, 0, len(in.Intents))
	for i, intent := range in.Intents {
		item, err := h.HydrateOne(ctx, HydrateOneInput{
			OrganizationRef: in.OrganizationRef,
			InitiatorRef:    in.InitiatorRef,
			Intent:          intent,
		})
		if err != nil {
			return nil, wrapIndexedIntentError(i, err)
		}
		out = append(out, item)
	}
	return out, nil
}

func wrapIndexedIntentError(index int, err error) error {
	msg := fmt.Sprintf("intents[%d]", index)
	if errors.Is(err, merrors.ErrInvalidArg) {
		return merrors.InvalidArgumentWrap(err, msg)
	}
	return merrors.InternalWrap(err, msg)
}

func resolveEconomics(
	mode paymentv1.SettlementMode,
	feeTreatment quotationv2.FeeTreatment,
) (QuoteSettlementMode, QuoteFeeTreatment, error) {
	if !payecon.IsValidSettlementMode(mode) {
		return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, merrors.InvalidArgument("intent.settlement_mode is invalid")
	}
	if !payecon.IsValidFeeTreatment(feeTreatment) {
		return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, merrors.InvalidArgument("intent.fee_treatment is invalid")
	}
	resolvedModeProto, resolvedFeeProto, err := payecon.ResolveSettlementAndFee(mode, feeTreatment)
	if err != nil {
		return QuoteSettlementModeUnspecified, QuoteFeeTreatmentUnspecified, err
	}
	return settlementModeFromProto(resolvedModeProto), feeTreatmentFromProto(resolvedFeeProto), nil
}

func settlementModeFromProto(mode paymentv1.SettlementMode) QuoteSettlementMode {
	switch mode {
	case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE:
		return QuoteSettlementModeFixSource
	case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED:
		return QuoteSettlementModeFixReceived
	default:
		return QuoteSettlementModeUnspecified
	}
}

func feeTreatmentFromProto(value quotationv2.FeeTreatment) QuoteFeeTreatment {
	switch value {
	case quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE:
		return QuoteFeeTreatmentAddToSource
	case quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION:
		return QuoteFeeTreatmentDeductFromDestination
	default:
		return QuoteFeeTreatmentUnspecified
	}
}

func fxSideFromProto(side fxv1.Side) paymenttypes.FXSide {
	switch side {
	case fxv1.Side_BUY_BASE_SELL_QUOTE:
		return paymenttypes.FXSideBuyBaseSellQuote
	case fxv1.Side_SELL_BASE_BUY_QUOTE:
		return paymenttypes.FXSideSellBaseBuyQuote
	default:
		return paymenttypes.FXSideUnspecified
	}
}

func fxIntentFromProto(src *sharedv1.FXIntent) *QuoteFXIntent {
	if src == nil || src.GetPair() == nil {
		return nil
	}
	base := strings.ToUpper(strings.TrimSpace(src.GetPair().GetBase()))
	quote := strings.ToUpper(strings.TrimSpace(src.GetPair().GetQuote()))
	if base == "" || quote == "" {
		return nil
	}
	side := fxSideFromProto(src.GetSide())
	if side == paymenttypes.FXSideUnspecified {
		side = paymenttypes.FXSideSellBaseBuyQuote
	}
	return &QuoteFXIntent{
		Pair: &paymenttypes.CurrencyPair{
			Base:  base,
			Quote: quote,
		},
		Side:              side,
		Firm:              src.GetFirm(),
		TTLms:             src.GetTtlMs(),
		PreferredProvider: strings.TrimSpace(src.GetPreferredProvider()),
		MaxAgeMs:          src.GetMaxAgeMs(),
	}
}

func settlementCurrencyFromFX(fx *QuoteFXIntent) string {
	if fx == nil || fx.Pair == nil {
		return ""
	}
	base := strings.ToUpper(strings.TrimSpace(fx.Pair.GetBase()))
	quote := strings.ToUpper(strings.TrimSpace(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 firstNonEmpty(values ...string) string {
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed != "" {
			return trimmed
		}
	}
	return ""
}
