package orchestrator

import (
	"context"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)

const (
	settlementMetadataQuoteRef    = "quote_ref"
	settlementMetadataOutgoingLeg = "outgoing_leg"
)

type gatewayProviderSettlementExecutor struct {
	gatewayInvokeResolver GatewayInvokeResolver
	gatewayRegistry       GatewayRegistry
}

func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	if req.Payment == nil {
		return nil, merrors.InvalidArgument("settlement fx_convert: payment is required")
	}
	if model.ParseRailOperation(string(req.Step.Action)) != discovery.RailOperationFXConvert {
		return nil, merrors.InvalidArgument("settlement fx_convert: unsupported action")
	}

	gateway, err := e.resolveGateway(ctx, req.Step)
	if err != nil {
		return nil, err
	}
	client, err := e.gatewayInvokeResolver.Resolve(ctx, gateway.InvokeURI)
	if err != nil {
		return nil, err
	}

	sourceWalletRef, err := sourceManagedWalletRef(req.Payment)
	if err != nil {
		return nil, err
	}
	destination := &chainv1.TransferDestination{
		Destination: &chainv1.TransferDestination_ManagedWalletRef{
			ManagedWalletRef: sourceWalletRef,
		},
	}

	amount, err := settlementAmount(req.Payment, req.StepExecution)
	if err != nil {
		return nil, err
	}
	convertedAmount := settlementConvertedMoney(req.Payment, req.StepExecution)
	if convertedAmount == nil {
		return nil, merrors.InvalidArgument("settlement fx_convert: converted amount is required")
	}

	stepRef := strings.TrimSpace(req.Step.StepRef)
	operationRef := strings.TrimSpace(req.Payment.PaymentRef) + ":" + stepRef
	idempotencyKey := strings.TrimSpace(req.Payment.IdempotencyKey)
	if idempotencyKey == "" {
		idempotencyKey = operationRef
	}
	idempotencyKey += ":" + stepRef

	resp, err := client.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
		IdempotencyKey:  idempotencyKey,
		OrganizationRef: req.Payment.OrganizationRef.Hex(),
		SourceWalletRef: sourceWalletRef,
		Destination:     destination,
		Amount:          amount,
		OperationRef:    operationRef,
		IntentRef:       strings.TrimSpace(req.Payment.IntentSnapshot.Ref),
		PaymentRef:      strings.TrimSpace(req.Payment.PaymentRef),
		Metadata:        settlementTransferMetadata(req.Payment, req.Step),
	})
	if err != nil {
		return nil, err
	}
	if resp == nil || resp.GetTransfer() == nil {
		return nil, merrors.Internal("settlement fx_convert: transfer response is missing")
	}

	step := req.StepExecution
	refs, refsErr := transferExternalRefs(resp.GetTransfer(), firstNonEmpty(
		strings.TrimSpace(req.Step.InstanceID),
		strings.TrimSpace(gateway.InstanceID),
		strings.TrimSpace(req.Step.Gateway),
		strings.TrimSpace(gateway.ID),
	))
	if refsErr != nil {
		return nil, refsErr
	}
	step.ExternalRefs = refs
	step.ExecutedMoney = protoMoneyToPaymentMoney(amount)
	step.ConvertedMoney = convertedAmount
	step.State = agg.StepStateCompleted
	step.FailureCode = ""
	step.FailureMsg = ""
	assignDestinationStepComment(&step, req.Payment, req.Step)
	return &sexec.ExecuteOutput{StepExecution: step}, nil
}

func settlementConvertedMoney(payment *agg.Payment, stepExecution agg.StepExecution) *paymenttypes.Money {
	if planned := plannedStepConvertedMoney(stepExecution); planned != nil {
		return planned
	}
	if payment == nil || payment.QuoteSnapshot == nil {
		return nil
	}
	return svcshared.CloneMoneyTrimNonEmpty(payment.QuoteSnapshot.ExpectedSettlementAmount)
}

func (e *gatewayProviderSettlementExecutor) resolveGateway(ctx context.Context, step xplan.Step) (*model.GatewayInstanceDescriptor, error) {
	if e.gatewayRegistry == nil {
		return nil, merrors.InvalidArgument("settlement fx_convert: gateway registry is required")
	}
	items, err := e.gatewayRegistry.List(ctx)
	if err != nil {
		return nil, err
	}

	stepGateway := strings.TrimSpace(step.Gateway)
	stepInstance := strings.TrimSpace(step.InstanceID)

	var byInstance *model.GatewayInstanceDescriptor
	var byGateway *model.GatewayInstanceDescriptor
	var single *model.GatewayInstanceDescriptor
	settlementCount := 0
	for i := range items {
		item := items[i]
		if item == nil || model.ParseRail(string(item.Rail)) != discovery.RailProviderSettlement || !item.IsEnabled {
			continue
		}
		settlementCount++
		single = item
		if stepInstance != "" && (strings.EqualFold(strings.TrimSpace(item.InstanceID), stepInstance) || strings.EqualFold(strings.TrimSpace(item.ID), stepInstance)) {
			byInstance = item
			break
		}
		if stepGateway != "" && (strings.EqualFold(strings.TrimSpace(item.ID), stepGateway) || strings.EqualFold(strings.TrimSpace(item.InstanceID), stepGateway)) {
			byGateway = item
		}
	}
	switch {
	case byInstance != nil:
		if strings.TrimSpace(byInstance.InvokeURI) == "" {
			return nil, merrors.InvalidArgument("settlement fx_convert: gateway invoke uri is missing")
		}
		return byInstance, nil
	case byGateway != nil:
		if strings.TrimSpace(byGateway.InvokeURI) == "" {
			return nil, merrors.InvalidArgument("settlement fx_convert: gateway invoke uri is missing")
		}
		return byGateway, nil
	case stepGateway == "" && stepInstance == "" && settlementCount == 1:
		if strings.TrimSpace(single.InvokeURI) == "" {
			return nil, merrors.InvalidArgument("settlement fx_convert: gateway invoke uri is missing")
		}
		return single, nil
	default:
		return nil, merrors.InvalidArgument("settlement fx_convert: gateway instance not found")
	}
}

func settlementAmount(payment *agg.Payment, stepExecution agg.StepExecution) (*moneyv1.Money, error) {
	if planned := plannedStepMoney(stepExecution); planned != nil {
		amount := paymentMoneyToProto(planned)
		if amount == nil {
			return nil, merrors.InvalidArgument("settlement fx_convert: planned amount is invalid")
		}
		return amount, nil
	}
	if payment == nil {
		return nil, merrors.InvalidArgument("settlement fx_convert: payment is required")
	}

	money := sourceAmountForSettlement(payment)
	if money == nil {
		return nil, merrors.InvalidArgument("settlement fx_convert: debit amount is required")
	}
	amount := strings.TrimSpace(money.Amount)
	currency := strings.TrimSpace(money.Currency)
	if amount == "" || currency == "" {
		return nil, merrors.InvalidArgument("settlement fx_convert: debit amount is invalid")
	}
	return &moneyv1.Money{
		Amount:   amount,
		Currency: currency,
	}, nil
}

func sourceAmountForSettlement(payment *agg.Payment) *moneyv1.Money {
	if payment != nil && payment.QuoteSnapshot != nil && payment.QuoteSnapshot.DebitAmount != nil {
		return &moneyv1.Money{
			Amount:   strings.TrimSpace(payment.QuoteSnapshot.DebitAmount.Amount),
			Currency: strings.TrimSpace(payment.QuoteSnapshot.DebitAmount.Currency),
		}
	}
	if payment != nil && payment.IntentSnapshot.Amount != nil {
		return &moneyv1.Money{
			Amount:   strings.TrimSpace(payment.IntentSnapshot.Amount.Amount),
			Currency: strings.TrimSpace(payment.IntentSnapshot.Amount.Currency),
		}
	}
	return nil
}

func settlementTransferMetadata(payment *agg.Payment, step xplan.Step) map[string]string {
	out := transferMetadata(step)
	if out == nil {
		out = map[string]string{}
	}
	if payment != nil {
		if quoteRef := firstNonEmpty(
			strings.TrimSpace(payment.QuotationRef),
			strings.TrimSpace(quoteRefFromSnapshot(payment.QuoteSnapshot)),
		); quoteRef != "" {
			out[settlementMetadataQuoteRef] = quoteRef
		}
	}
	if outgoingLeg := strings.TrimSpace(string(step.Rail)); outgoingLeg != "" {
		out[settlementMetadataOutgoingLeg] = outgoingLeg
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func quoteRefFromSnapshot(snapshot *model.PaymentQuoteSnapshot) string {
	if snapshot == nil {
		return ""
	}
	return strings.TrimSpace(snapshot.QuoteRef)
}

var _ sexec.ProviderSettlementExecutor = (*gatewayProviderSettlementExecutor)(nil)
