package orchestrator

import (
	"context"
	"github.com/tech/sendico/pkg/discovery"
	"net"
	"net/url"
	"strconv"
	"strings"
	"sync"

	"github.com/shopspring/decimal"
	mntxclient "github.com/tech/sendico/gateway/mntx/client"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"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"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
)

type gatewayCardPayoutExecutor struct {
	gatewayRegistry GatewayRegistry

	mu      sync.Mutex
	clients map[string]mntxclient.Client

	dialClient func(ctx context.Context, address string) (mntxclient.Client, error)
}

type cardPayoutCustomer struct {
	id         string
	firstName  string
	middleName string
	lastName   string
	ip         string
	zip        string
	country    string
	state      string
	city       string
	address    string
}

func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
	if req.Payment == nil {
		return nil, merrors.InvalidArgument("card payout send: payment is required")
	}
	if e == nil || e.gatewayRegistry == nil {
		return nil, merrors.InvalidArgument("card payout send: gateway registry is required")
	}
	if model.ParseRailOperation(string(req.Step.Action)) != discovery.RailOperationSend {
		return nil, merrors.InvalidArgument("card payout send: unsupported action")
	}
	gateway, err := e.resolveGateway(ctx, req.Step)
	if err != nil {
		return nil, err
	}
	client, err := e.client(ctx, strings.TrimSpace(gateway.InvokeURI))
	if err != nil {
		return nil, err
	}

	card, err := payoutDestinationCard(req.Payment, req.Step.Metadata)
	if err != nil {
		return nil, err
	}
	amountMinor, currency, err := cardPayoutAmountMinor(req.Payment, req.StepExecution, req.Step.Metadata)
	if err != nil {
		return nil, err
	}

	stepToken := cardPayoutStepToken(req.Step)
	operationRef := cardPayoutOperationRef(req.Payment, stepToken)
	idempotencyKey := cardPayoutIdempotencyKey(req.Payment, stepToken)
	projectID := cardPayoutProjectID(req.Payment)
	customer := cardPayoutCustomerFromPayment(req.Payment, card, req.Step.Metadata)
	cardHolder := cardPayoutCardholder(card, customer)
	metadata := cardPayoutMetadata(req.Payment, req.Step)
	comment := destinationOperationComment(req.Payment, req.Step)
	intentRef := strings.TrimSpace(req.Payment.IntentSnapshot.Ref)

	var responsePayout *mntxv1.CardPayoutState
	if token := strings.TrimSpace(card.Token); token != "" {
		resp, createErr := client.CreateCardTokenPayout(ctx, &mntxv1.CardTokenPayoutRequest{
			ProjectId:          projectID,
			ParentPaymentRef:   strings.TrimSpace(req.Payment.PaymentRef),
			CustomerId:         customer.id,
			CustomerFirstName:  customer.firstName,
			CustomerMiddleName: customer.middleName,
			CustomerLastName:   customer.lastName,
			CustomerIp:         customer.ip,
			CustomerZip:        customer.zip,
			CustomerCountry:    customer.country,
			CustomerState:      customer.state,
			CustomerCity:       customer.city,
			CustomerAddress:    customer.address,
			AmountMinor:        amountMinor,
			Currency:           currency,
			CardToken:          token,
			CardHolder:         cardHolder,
			MaskedPan:          strings.TrimSpace(card.MaskedPan),
			Metadata:           metadata,
			Comment:            comment,
			OperationRef:       operationRef,
			IdempotencyKey:     idempotencyKey,
			IntentRef:          intentRef,
		})
		if createErr != nil {
			return nil, createErr
		}
		if resp == nil {
			return nil, merrors.Internal("card payout send: card token payout response is missing")
		}
		responsePayout = resp.GetPayout()
	} else {
		pan := strings.TrimSpace(card.Pan)
		if pan == "" {
			return nil, merrors.InvalidArgument("card payout send: card pan is required")
		}
		if card.ExpMonth == 0 || card.ExpYear == 0 {
			return nil, merrors.InvalidArgument("card payout send: card expiry is required")
		}
		resp, createErr := client.CreateCardPayout(ctx, &mntxv1.CardPayoutRequest{
			ProjectId:          projectID,
			ParentPaymentRef:   strings.TrimSpace(req.Payment.PaymentRef),
			CustomerId:         customer.id,
			CustomerFirstName:  customer.firstName,
			CustomerMiddleName: customer.middleName,
			CustomerLastName:   customer.lastName,
			CustomerIp:         customer.ip,
			CustomerZip:        customer.zip,
			CustomerCountry:    customer.country,
			CustomerState:      customer.state,
			CustomerCity:       customer.city,
			CustomerAddress:    customer.address,
			AmountMinor:        amountMinor,
			Currency:           currency,
			CardPan:            pan,
			CardExpYear:        card.ExpYear,
			CardExpMonth:       card.ExpMonth,
			CardHolder:         cardHolder,
			Metadata:           metadata,
			Comment:            comment,
			OperationRef:       operationRef,
			IdempotencyKey:     idempotencyKey,
			IntentRef:          intentRef,
		})
		if createErr != nil {
			return nil, createErr
		}
		if resp == nil {
			return nil, merrors.Internal("card payout send: card payout response is missing")
		}
		responsePayout = resp.GetPayout()
	}

	resolvedOperationRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetOperationRef()), operationRef)
	resolvedPayoutRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetPayoutId()), resolvedOperationRef)
	gatewayInstanceID := firstNonEmpty(
		strings.TrimSpace(req.Step.InstanceID),
		strings.TrimSpace(gateway.InstanceID),
		strings.TrimSpace(req.Step.Gateway),
		strings.TrimSpace(gateway.ID),
	)
	externalRefs, refsErr := cardPayoutExternalRefs(resolvedPayoutRef, resolvedOperationRef, gatewayInstanceID)
	if refsErr != nil {
		return nil, refsErr
	}

	step := req.StepExecution
	step.State = agg.StepStateCompleted
	step.ExternalRefs = externalRefs
	step.ExecutedMoney = cardMinorToMoney(responsePayout.GetAmountMinor(), responsePayout.GetCurrency())
	if step.ExecutedMoney == nil {
		step.ExecutedMoney = cardMinorToMoney(amountMinor, currency)
	}
	step.ConvertedMoney = nil
	step.FailureCode = ""
	step.FailureMsg = ""
	assignDestinationStepComment(&step, req.Payment, req.Step)
	return &sexec.ExecuteOutput{StepExecution: step}, nil
}

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

func (e *gatewayCardPayoutExecutor) client(ctx context.Context, invokeURI string) (mntxclient.Client, error) {
	address, err := parseGatewayAddress(invokeURI)
	if err != nil {
		return nil, err
	}

	e.mu.Lock()
	defer e.mu.Unlock()

	if e.clients == nil {
		e.clients = map[string]mntxclient.Client{}
	}
	if c := e.clients[address]; c != nil {
		return c, nil
	}

	dial := e.dialClient
	if dial == nil {
		dial = func(ctx context.Context, address string) (mntxclient.Client, error) {
			return mntxclient.New(ctx, mntxclient.Config{Address: address})
		}
	}

	client, dialErr := dial(ctx, address)
	if dialErr != nil {
		return nil, dialErr
	}
	e.clients[address] = client
	return client, nil
}

func parseGatewayAddress(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", merrors.InvalidArgument("card payout send: gateway invoke uri is required")
	}

	if !strings.Contains(raw, "://") {
		if _, _, splitErr := net.SplitHostPort(raw); splitErr != nil {
			return "", merrors.InvalidArgument("card payout send: gateway invoke uri must include host:port")
		}
		return raw, nil
	}

	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" {
		return "", merrors.InvalidArgument("card payout send: gateway invoke uri must include host:port")
	}

	switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
	case "grpc", "grpcs":
		address := strings.TrimSpace(parsed.Host)
		if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
			return "", merrors.InvalidArgument("card payout send: gateway invoke uri must include host:port")
		}
		return address, nil
	case "dns", "passthrough":
		return raw, nil
	default:
		return "", merrors.InvalidArgument("card payout send: unsupported gateway invoke uri scheme")
	}
}

func payoutDestinationCard(payment *agg.Payment, metadata map[string]string) (*model.CardEndpoint, error) {
	if card, ok := batchmeta.CardFromMetadata(metadata); ok && card != nil {
		return card, nil
	}
	if payment == nil {
		return nil, merrors.InvalidArgument("card payout send: payment is required")
	}
	destination := payment.IntentSnapshot.Destination
	if destination.Type != model.EndpointTypeCard || destination.Card == nil {
		return nil, merrors.InvalidArgument("card payout send: destination card is required")
	}
	return destination.Card, nil
}

func cardPayoutMoney(payment *agg.Payment, stepExecution agg.StepExecution, metadata map[string]string) *paymenttypes.Money {
	if planned := plannedStepMoney(stepExecution); planned != nil {
		return planned
	}
	if override, ok := batchmeta.AmountFromMetadata(metadata); ok && override != nil {
		return override
	}
	if payment != nil && payment.QuoteSnapshot != nil && payment.QuoteSnapshot.ExpectedSettlementAmount != nil {
		return payment.QuoteSnapshot.ExpectedSettlementAmount
	}
	if payment == nil {
		return nil
	}
	return payment.IntentSnapshot.Amount
}

func cardPayoutAmountMinor(payment *agg.Payment, stepExecution agg.StepExecution, metadata map[string]string) (int64, string, error) {
	money := cardPayoutMoney(payment, stepExecution, metadata)
	if money == nil {
		return 0, "", merrors.InvalidArgument("card payout send: payout amount is required")
	}
	amountText := strings.TrimSpace(money.GetAmount())
	currency := strings.ToUpper(strings.TrimSpace(money.GetCurrency()))
	if idx := strings.Index(currency, "-"); idx > 0 {
		currency = currency[:idx]
	}
	if amountText == "" || currency == "" {
		return 0, "", merrors.InvalidArgument("card payout send: payout amount is invalid")
	}

	value, err := decimal.NewFromString(amountText)
	if err != nil || !value.IsPositive() {
		return 0, "", merrors.InvalidArgument("card payout send: payout amount is invalid")
	}
	minor := value.Mul(decimal.NewFromInt(100))
	if !minor.Equal(minor.Truncate(0)) {
		return 0, "", merrors.InvalidArgument("card payout send: payout amount supports at most 2 fractional digits")
	}
	return minor.IntPart(), currency, nil
}

func cardPayoutStepToken(step xplan.Step) string {
	return firstNonEmpty(strings.TrimSpace(step.StepRef), strings.TrimSpace(step.StepCode), "card_payout")
}

func cardPayoutOperationRef(payment *agg.Payment, stepToken string) string {
	base := ""
	if payment != nil {
		base = firstNonEmpty(strings.TrimSpace(payment.PaymentRef), strings.TrimSpace(payment.IdempotencyKey))
	}
	return joinRef(base, stepToken)
}

func cardPayoutIdempotencyKey(payment *agg.Payment, stepToken string) string {
	base := ""
	if payment != nil {
		base = strings.TrimSpace(payment.IdempotencyKey)
		if base == "" {
			base = strings.TrimSpace(payment.PaymentRef)
		}
	}
	if base == "" {
		base = "card_payout"
	}
	return joinRef(base, stepToken)
}

func joinRef(base, suffix string) string {
	base = strings.TrimSpace(base)
	suffix = strings.TrimSpace(suffix)
	switch {
	case base == "":
		return suffix
	case suffix == "":
		return base
	default:
		return base + ":" + suffix
	}
}

func cardPayoutProjectID(payment *agg.Payment) int64 {
	if payment == nil {
		return 0
	}
	raw := cardPayoutAttribute(payment.IntentSnapshot.Attributes, "project_id", "projectId")
	if raw == "" {
		return 0
	}
	value, err := strconv.ParseInt(raw, 10, 64)
	if err != nil || value < 0 {
		return 0
	}
	return value
}

func cardPayoutCustomerFromPayment(payment *agg.Payment, card *model.CardEndpoint, stepMetadata map[string]string) cardPayoutCustomer {
	customer := cardPayoutCustomer{}
	if payment == nil {
		return customer
	}

	cardholder := ""
	cardholderSurname := ""
	cardCountry := ""
	if card != nil {
		cardholder = strings.TrimSpace(card.Cardholder)
		cardholderSurname = strings.TrimSpace(card.CardholderSurname)
		cardCountry = strings.ToUpper(strings.TrimSpace(card.Country))
	}
	attrs := mergeStringMaps(payment.IntentSnapshot.Attributes, stepMetadata)
	intentCustomer := payment.IntentSnapshot.Customer
	if intentCustomer != nil {
		customer.id = strings.TrimSpace(intentCustomer.ID)
		customer.firstName = strings.TrimSpace(intentCustomer.FirstName)
		customer.middleName = strings.TrimSpace(intentCustomer.MiddleName)
		customer.lastName = strings.TrimSpace(intentCustomer.LastName)
		customer.ip = strings.TrimSpace(intentCustomer.IP)
		customer.zip = strings.TrimSpace(intentCustomer.Zip)
		customer.country = strings.ToUpper(strings.TrimSpace(intentCustomer.Country))
		customer.state = strings.TrimSpace(intentCustomer.State)
		customer.city = strings.TrimSpace(intentCustomer.City)
		customer.address = strings.TrimSpace(intentCustomer.Address)
	}
	if override, ok := batchmeta.CustomerFromMetadata(stepMetadata); ok && override != nil {
		if customer.id == "" {
			customer.id = strings.TrimSpace(override.ID)
		}
		if customer.firstName == "" {
			customer.firstName = strings.TrimSpace(override.FirstName)
		}
		if customer.middleName == "" {
			customer.middleName = strings.TrimSpace(override.MiddleName)
		}
		if customer.lastName == "" {
			customer.lastName = strings.TrimSpace(override.LastName)
		}
		if customer.ip == "" {
			customer.ip = strings.TrimSpace(override.IP)
		}
		if customer.zip == "" {
			customer.zip = strings.TrimSpace(override.Zip)
		}
		if customer.country == "" {
			customer.country = strings.ToUpper(strings.TrimSpace(override.Country))
		}
		if customer.state == "" {
			customer.state = strings.TrimSpace(override.State)
		}
		if customer.city == "" {
			customer.city = strings.TrimSpace(override.City)
		}
		if customer.address == "" {
			customer.address = strings.TrimSpace(override.Address)
		}
	}

	customer.id = firstNonEmpty(customer.id,
		cardPayoutAttribute(attrs, "customer_id", "customerId", "initiator_ref", "initiatorRef"),
		strings.TrimSpace(payment.PaymentRef),
		strings.TrimSpace(payment.IdempotencyKey),
		"unknown_customer")
	customer.firstName = firstNonEmpty(customer.firstName, cardholder, "UNKNOWN")
	customer.middleName = firstNonEmpty(customer.middleName, cardPayoutAttribute(attrs, "customer_middle_name", "customerMiddleName"))
	customer.lastName = firstNonEmpty(customer.lastName, cardholderSurname, "UNKNOWN")
	customer.ip = firstNonEmpty(customer.ip,
		cardPayoutAttribute(attrs, "customer_ip", "customerIp", "ip", "ip_address", "ipAddress"),
		"0.0.0.0")
	customer.zip = firstNonEmpty(customer.zip, cardPayoutAttribute(attrs, "customer_zip", "customerZip"))
	customer.country = firstNonEmpty(customer.country, cardCountry, cardPayoutAttribute(attrs, "customer_country", "customerCountry"))
	customer.state = firstNonEmpty(customer.state, cardPayoutAttribute(attrs, "customer_state", "customerState"))
	customer.city = firstNonEmpty(customer.city, cardPayoutAttribute(attrs, "customer_city", "customerCity"))
	customer.address = firstNonEmpty(customer.address, cardPayoutAttribute(attrs, "customer_address", "customerAddress"))

	return customer
}

func cardPayoutCardholder(card *model.CardEndpoint, customer cardPayoutCustomer) string {
	holder := ""
	if card != nil {
		holder = strings.TrimSpace(card.Cardholder)
		surname := strings.TrimSpace(card.CardholderSurname)
		if holder != "" && surname != "" && !strings.Contains(strings.ToLower(holder), strings.ToLower(surname)) {
			holder = holder + " " + surname
		}
	}
	if holder == "" {
		holder = strings.TrimSpace(firstNonEmpty(spaceJoin(customer.firstName, customer.lastName), customer.firstName, customer.lastName))
	}
	if holder == "" {
		holder = "UNKNOWN"
	}
	return holder
}

func spaceJoin(values ...string) string {
	parts := make([]string, 0, len(values))
	for i := range values {
		item := strings.TrimSpace(values[i])
		if item == "" {
			continue
		}
		parts = append(parts, item)
	}
	return strings.Join(parts, " ")
}

func cardPayoutAttribute(attrs map[string]string, keys ...string) string {
	if len(attrs) == 0 {
		return ""
	}
	for i := range keys {
		key := strings.TrimSpace(keys[i])
		if key == "" {
			continue
		}
		if value, ok := attrs[key]; ok {
			if trimmed := strings.TrimSpace(value); trimmed != "" {
				return trimmed
			}
		}
		for attrKey, value := range attrs {
			if !strings.EqualFold(strings.TrimSpace(attrKey), key) {
				continue
			}
			if trimmed := strings.TrimSpace(value); trimmed != "" {
				return trimmed
			}
		}
	}
	return ""
}

func cardPayoutMetadata(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 targetRef := strings.TrimSpace(step.Metadata[batchmeta.MetaPayoutTargetRef]); targetRef != "" {
		out[batchmeta.MetaPayoutTargetRef] = targetRef
	}
	if targetIndex := strings.TrimSpace(step.Metadata[batchmeta.MetaPayoutTargetIndex]); targetIndex != "" {
		out[batchmeta.MetaPayoutTargetIndex] = targetIndex
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func mergeStringMaps(left, right map[string]string) map[string]string {
	if len(left) == 0 && len(right) == 0 {
		return nil
	}
	out := map[string]string{}
	for key, value := range left {
		k := strings.TrimSpace(key)
		if k == "" {
			continue
		}
		out[k] = strings.TrimSpace(value)
	}
	for key, value := range right {
		k := strings.TrimSpace(key)
		if k == "" {
			continue
		}
		out[k] = strings.TrimSpace(value)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func cardPayoutExternalRefs(payoutRef, operationRef, gatewayInstanceID string) ([]agg.ExternalRef, error) {
	gatewayInstanceID = strings.TrimSpace(gatewayInstanceID)
	refs := make([]agg.ExternalRef, 0, 3)
	if operationRef = strings.TrimSpace(operationRef); operationRef != "" {
		refs = append(refs, agg.ExternalRef{
			GatewayInstanceID: gatewayInstanceID,
			Kind:              erecon.ExternalRefKindOperation,
			Ref:               operationRef,
		})
	}
	if payoutRef = strings.TrimSpace(payoutRef); payoutRef != "" {
		refs = append(refs, agg.ExternalRef{
			GatewayInstanceID: gatewayInstanceID,
			Kind:              erecon.ExternalRefKindTransfer,
			Ref:               payoutRef,
		})
		refs = append(refs, agg.ExternalRef{
			GatewayInstanceID: gatewayInstanceID,
			Kind:              erecon.ExternalRefKindCardPayout,
			Ref:               payoutRef,
		})
	}
	if len(refs) == 0 {
		return nil, merrors.Internal("card payout send: payout response does not contain references")
	}
	return refs, nil
}

var _ sexec.CardPayoutExecutor = (*gatewayCardPayoutExecutor)(nil)
