package orchestrator

import (
	"context"
	"fmt"
	"github.com/shopspring/decimal"
	chainclient "github.com/tech/sendico/gateway/chain/client"
	"github.com/tech/sendico/pkg/discovery"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	"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"
)

type gatewayCryptoExecutor struct {
	gatewayInvokeResolver GatewayInvokeResolver
	gatewayRegistry       GatewayRegistry
	cardGatewayRoutes     map[string]CardGatewayRoute
}

func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	if req.Payment == nil {
		return nil, merrors.InvalidArgument("crypto send: payment is required")
	}
	action := model.ParseRailOperation(string(req.Step.Action))
	switch action {
	case discovery.RailOperationSend, discovery.RailOperationFee:
	default:
		return nil, merrors.InvalidArgument("crypto send: 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, err := e.resolveDestination(ctx, client, req.Payment, action)
	if err != nil {
		return nil, err
	}
	amount, err := sourceAmount(req.Payment, req.StepExecution, action)
	if err != nil {
		return nil, err
	}

	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:        transferMetadata(req.Step),
	})
	if err != nil {
		return nil, err
	}
	if resp == nil || resp.GetTransfer() == nil {
		return nil, merrors.Internal("crypto send: 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 = transferExecutedMoney(resp.GetTransfer())
	if step.ExecutedMoney == nil {
		step.ExecutedMoney = protoMoneyToPaymentMoney(amount)
	}
	step.ConvertedMoney = nil

	step.State = agg.StepStateCompleted
	step.FailureCode = ""
	step.FailureMsg = ""
	assignDestinationStepComment(&step, req.Payment, req.Step)
	return &sexec.ExecuteOutput{StepExecution: step}, nil
}

func (e *gatewayCryptoExecutor) resolveGateway(ctx context.Context, step xplan.Step) (*model.GatewayInstanceDescriptor, error) {
	if e.gatewayRegistry == nil {
		return nil, merrors.InvalidArgument("crypto send: 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
	cryptoCount := 0
	for i := range items {
		item := items[i]
		if item == nil || model.ParseRail(string(item.Rail)) != discovery.RailCrypto || !item.IsEnabled {
			continue
		}
		cryptoCount++
		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("crypto send: gateway invoke uri is missing")
		}
		return byInstance, nil
	case byGateway != nil:
		if strings.TrimSpace(byGateway.InvokeURI) == "" {
			return nil, merrors.InvalidArgument("crypto send: gateway invoke uri is missing")
		}
		return byGateway, nil
	case stepGateway == "" && stepInstance == "" && cryptoCount == 1:
		if strings.TrimSpace(single.InvokeURI) == "" {
			return nil, merrors.InvalidArgument("crypto send: gateway invoke uri is missing")
		}
		return single, nil
	default:
		return nil, merrors.InvalidArgument("crypto send: gateway instance not found")
	}
}

func sourceManagedWalletRef(payment *agg.Payment) (string, error) {
	if payment == nil {
		return "", merrors.InvalidArgument("crypto send: payment is required")
	}
	if payment.IntentSnapshot.Source.Type != model.EndpointTypeManagedWallet || payment.IntentSnapshot.Source.ManagedWallet == nil {
		return "", merrors.InvalidArgument("crypto send: managed wallet source is required")
	}
	ref := strings.TrimSpace(payment.IntentSnapshot.Source.ManagedWallet.ManagedWalletRef)
	if ref == "" {
		return "", merrors.InvalidArgument("crypto send: source managed wallet ref is required")
	}
	return ref, nil
}

func sourceAmount(payment *agg.Payment, stepExecution agg.StepExecution, action model.RailOperation) (*moneyv1.Money, error) {
	if planned := plannedStepMoney(stepExecution); planned != nil {
		proto := paymentMoneyToProto(planned)
		if proto == nil {
			return nil, merrors.InvalidArgument("crypto send: planned amount is invalid")
		}
		return proto, nil
	}
	if payment == nil {
		return nil, merrors.InvalidArgument("crypto send: payment is required")
	}
	var money *paymenttypes.Money
	switch action {
	case discovery.RailOperationFee:
		resolved, ok, err := feeAmount(payment)
		if err != nil {
			return nil, err
		}
		if !ok {
			return nil, merrors.InvalidArgument("crypto send: fee amount is required")
		}
		money = resolved
	default:
		money = effectiveSourceAmount(payment)
	}
	if money == nil {
		return nil, merrors.InvalidArgument("crypto send: source amount is required")
	}
	amount := strings.TrimSpace(money.Amount)
	currency := strings.TrimSpace(money.Currency)
	if amount == "" || currency == "" {
		return nil, merrors.InvalidArgument("crypto send: source amount is invalid")
	}
	return &moneyv1.Money{
		Amount:   amount,
		Currency: currency,
	}, nil
}

func effectiveSourceAmount(payment *agg.Payment) *paymenttypes.Money {
	if payment == nil {
		return nil
	}
	if payment.QuoteSnapshot != nil && payment.QuoteSnapshot.DebitAmount != nil {
		return payment.QuoteSnapshot.DebitAmount
	}
	return payment.IntentSnapshot.Amount
}

func feeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
	if payment == nil || payment.QuoteSnapshot == nil || len(payment.QuoteSnapshot.FeeLines) == 0 {
		return nil, false, nil
	}

	sourceCurrency := ""
	if source := effectiveSourceAmount(payment); source != nil {
		sourceCurrency = strings.TrimSpace(source.Currency)
	}

	total := decimal.Zero
	currency := ""
	for i, line := range payment.QuoteSnapshot.FeeLines {
		money := line.GetMoney()
		if money == nil {
			continue
		}

		lineCurrency := strings.TrimSpace(money.GetCurrency())
		if lineCurrency == "" {
			return nil, false, merrors.InvalidArgument(fmt.Sprintf("crypto send: fee_lines[%d].money.currency is required", i))
		}
		if sourceCurrency != "" && !strings.EqualFold(sourceCurrency, lineCurrency) {
			continue
		}
		if currency == "" {
			currency = lineCurrency
		} else if !strings.EqualFold(currency, lineCurrency) {
			return nil, false, merrors.InvalidArgument("crypto send: fee currency mismatch")
		}

		amountRaw := strings.TrimSpace(money.GetAmount())
		amount, err := decimal.NewFromString(amountRaw)
		if err != nil {
			return nil, false, merrors.InvalidArgument(fmt.Sprintf("crypto send: fee_lines[%d].money.amount is invalid", i))
		}
		amount = amount.Abs()
		if amount.IsZero() {
			continue
		}
		if line.GetSide() == paymenttypes.EntrySideCredit {
			total = total.Sub(amount)
			continue
		}
		total = total.Add(amount)
	}

	if total.Sign() <= 0 {
		return nil, false, nil
	}
	return &paymenttypes.Money{
		Amount:   total.String(),
		Currency: strings.ToUpper(strings.TrimSpace(currency)),
	}, true, nil
}

func (e *gatewayCryptoExecutor) resolveDestination(
	ctx context.Context,
	client chainclient.Client,
	payment *agg.Payment,
	action model.RailOperation,
) (*chainv1.TransferDestination, error) {
	if payment == nil {
		return nil, merrors.InvalidArgument("crypto send: payment is required")
	}
	destination := payment.IntentSnapshot.Destination
	switch destination.Type {
	case model.EndpointTypeManagedWallet:
		if destination.ManagedWallet == nil || strings.TrimSpace(destination.ManagedWallet.ManagedWalletRef) == "" {
			return nil, merrors.InvalidArgument("crypto send: destination managed wallet ref is required")
		}
		return &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ManagedWalletRef{
				ManagedWalletRef: strings.TrimSpace(destination.ManagedWallet.ManagedWalletRef),
			},
		}, nil
	case model.EndpointTypeExternalChain:
		if destination.ExternalChain == nil || strings.TrimSpace(destination.ExternalChain.Address) == "" {
			return nil, merrors.InvalidArgument("crypto send: destination external address is required")
		}
		return &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ExternalAddress{
				ExternalAddress: strings.TrimSpace(destination.ExternalChain.Address),
			},
			Memo: strings.TrimSpace(destination.ExternalChain.Memo),
		}, nil
	case model.EndpointTypeCard:
		address, err := e.resolveCardFundingAddress(ctx, client, payment, action)
		if err != nil {
			return nil, err
		}
		return &chainv1.TransferDestination{
			Destination: &chainv1.TransferDestination_ExternalAddress{
				ExternalAddress: address,
			},
		}, nil
	default:
		return nil, merrors.InvalidArgument("crypto send: unsupported destination type")
	}
}

func (e *gatewayCryptoExecutor) resolveCardFundingAddress(
	ctx context.Context,
	client chainclient.Client,
	payment *agg.Payment,
	action model.RailOperation,
) (string, error) {
	if payment == nil {
		return "", merrors.InvalidArgument("crypto send: payment is required")
	}
	gatewayKey := destinationCardGatewayKey(payment)
	if gatewayKey == "" {
		return "", merrors.InvalidArgument("crypto send: destination card gateway is required")
	}
	route, ok := lookupCardGatewayRoute(e.cardGatewayRoutes, gatewayKey)
	if !ok {
		return "", merrors.InvalidArgument("crypto send: card gateway route is not configured")
	}
	switch action {
	case discovery.RailOperationFee:
		if feeWalletRef := strings.TrimSpace(route.FeeWalletRef); feeWalletRef != "" {
			address, err := resolveManagedWalletDepositAddress(ctx, client, feeWalletRef)
			if err != nil {
				return "", err
			}
			return address, nil
		}
		if feeAddress := strings.TrimSpace(route.FeeAddress); feeAddress != "" {
			return feeAddress, nil
		}
	}
	address := strings.TrimSpace(route.FundingAddress)
	if address == "" {
		return "", merrors.InvalidArgument("crypto send: card gateway funding address is required")
	}
	return address, nil
}

func resolveManagedWalletDepositAddress(ctx context.Context, client chainclient.Client, walletRef string) (string, error) {
	if client == nil {
		return "", merrors.InvalidArgument("crypto send: gateway client is required to resolve fee wallet")
	}
	ref := strings.TrimSpace(walletRef)
	if ref == "" {
		return "", merrors.InvalidArgument("crypto send: fee wallet ref is required")
	}
	resp, err := client.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: ref})
	if err != nil {
		return "", err
	}
	if resp == nil || resp.GetWallet() == nil {
		return "", merrors.Internal("crypto send: fee wallet response is missing")
	}
	address := strings.TrimSpace(resp.GetWallet().GetDepositAddress())
	if address == "" {
		return "", merrors.InvalidArgument("crypto send: fee wallet deposit address is required")
	}
	return address, nil
}

func destinationCardGatewayKey(payment *agg.Payment) string {
	if payment == nil || payment.QuoteSnapshot == nil || payment.QuoteSnapshot.Route == nil {
		return ""
	}
	hops := payment.QuoteSnapshot.Route.Hops
	fallback := ""
	for i := range hops {
		hop := hops[i]
		if hop == nil || model.ParseRail(hop.Rail) != discovery.RailCardPayout {
			continue
		}
		key := firstNonEmpty(strings.TrimSpace(hop.Gateway), strings.TrimSpace(hop.InstanceID))
		if key == "" {
			continue
		}
		if hop.Role != paymenttypes.QuoteRouteHopRoleDestination {
			fallback = key
			continue
		}
		return key
	}
	return fallback
}

func lookupCardGatewayRoute(routes map[string]CardGatewayRoute, key string) (CardGatewayRoute, bool) {
	if len(routes) == 0 {
		return CardGatewayRoute{}, false
	}
	normalized := strings.TrimSpace(strings.ToLower(key))
	if normalized == "" {
		return CardGatewayRoute{}, false
	}
	route, ok := routes[normalized]
	return route, ok
}

func transferExternalRefs(transfer *chainv1.Transfer, gatewayInstanceID string) ([]agg.ExternalRef, error) {
	if transfer == nil {
		return nil, merrors.InvalidArgument("crypto send: transfer is required")
	}
	refs := make([]agg.ExternalRef, 0, 2)
	if operationRef := strings.TrimSpace(transfer.GetOperationRef()); operationRef != "" {
		refs = append(refs, agg.ExternalRef{
			GatewayInstanceID: strings.TrimSpace(gatewayInstanceID),
			Kind:              erecon.ExternalRefKindOperation,
			Ref:               operationRef,
		})
	}
	if transferRef := strings.TrimSpace(transfer.GetTransferRef()); transferRef != "" {
		refs = append(refs, agg.ExternalRef{
			GatewayInstanceID: strings.TrimSpace(gatewayInstanceID),
			Kind:              erecon.ExternalRefKindTransfer,
			Ref:               transferRef,
		})
	}
	if len(refs) == 0 {
		return nil, merrors.Internal("crypto send: transfer response does not contain references")
	}
	return refs, nil
}

func transferMetadata(step xplan.Step) map[string]string {
	items := map[string]string{
		"step_ref":  strings.TrimSpace(step.StepRef),
		"step_code": strings.TrimSpace(step.StepCode),
		"gateway":   strings.TrimSpace(step.Gateway),
		"rail":      strings.TrimSpace(string(step.Rail)),
		"action":    strings.TrimSpace(string(step.Action)),
	}
	out := map[string]string{}
	for key, value := range items {
		if value == "" {
			continue
		}
		out[key] = value
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func firstNonEmpty(values ...string) string {
	for i := range values {
		if cleaned := strings.TrimSpace(values[i]); cleaned != "" {
			return cleaned
		}
	}
	return ""
}

var _ sexec.CryptoExecutor = (*gatewayCryptoExecutor)(nil)
