package quote_computation_service

import (
	"context"
	"github.com/tech/sendico/pkg/discovery"
	"sort"
	"strings"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/payments/quotation/internal/service/plan"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	"go.uber.org/zap"
)

func (s *QuoteComputationService) resolveStepGateways(
	ctx context.Context,
	steps []*QuoteComputationStep,
	routeNetwork string,
) error {
	if s == nil || s.gatewayRegistry == nil {
		s.logger.Debug("Step gateway resolution skipped: no gateway registry configured")

		return nil
	}

	s.logger.Debug("Loading gateway registry",
		zap.Int("step_count", len(steps)),
		zap.String("route_network", routeNetwork),
	)

	gateways, err := s.gatewayRegistry.List(ctx)
	if err != nil {
		s.logger.Warn("Step gateway resolution failed: gateway registry list error",
			zap.Error(err),
		)

		return err
	}

	if len(gateways) == 0 {
		s.logger.Warn("Step gateway resolution failed: gateway registry has no entries")

		return merrors.InvalidArgument("gateway registry has no entries")
	}

	sorted := make([]*model.GatewayInstanceDescriptor, 0, len(gateways))
	for _, gw := range gateways {
		if gw != nil {
			sorted = append(sorted, gw)
		}
	}
	sort.Slice(sorted, func(i, j int) bool {
		return model.LessGatewayDescriptor(sorted[i], sorted[j])
	})

	s.logger.Debug("Gateway registry loaded", zap.Int("gateway_count", len(sorted)))

	for idx, step := range steps {
		if step == nil {
			continue
		}

		if step.Rail == discovery.RailLedger {
			step.GatewayID = "internal"
			step.GatewayInvokeURI = ""

			s.logger.Debug("Step gateway assigned: ledger rail uses internal gateway",
				zap.String("step_id", strings.TrimSpace(step.StepID)),
				zap.Int("step_index", idx),
			)

			continue
		}

		selected, selectErr := s.selectGatewayForStep(sorted, step, routeNetwork)
		if selectErr != nil {
			s.logger.Warn("Step gateway resolution failed: no eligible gateway for step",
				zap.String("step_id", strings.TrimSpace(step.StepID)),
				zap.Int("step_index", idx),
				zap.String("rail", string(step.Rail)),
				zap.String("route_network", routeNetwork),
				zap.Error(selectErr),
			)

			return selectErr
		}

		step.GatewayID = strings.TrimSpace(selected.ID)
		step.InstanceID = strings.TrimSpace(selected.InstanceID)
		step.GatewayInvokeURI = strings.TrimSpace(selected.InvokeURI)

		s.logger.Debug("Gateway selected for step",
			zap.String("step_id", strings.TrimSpace(step.StepID)),
			zap.String("rail", string(step.Rail)),
			zap.String("gateway_id", step.GatewayID),
			zap.String("instance_id", step.InstanceID),
		)
	}

	return nil
}

func (s *QuoteComputationService) selectGatewayForStep(
	gateways []*model.GatewayInstanceDescriptor,
	step *QuoteComputationStep,
	routeNetwork string,
) (*model.GatewayInstanceDescriptor, error) {
	if step == nil {
		return nil, merrors.InvalidArgument("step is required")
	}
	if len(gateways) == 0 {
		return nil, merrors.InvalidArgument("gateway list is empty")
	}

	currency := ""
	amount := decimal.Zero
	if step.Amount != nil {
		currency = strings.ToUpper(strings.TrimSpace(step.Amount.GetCurrency()))
		if parsed, err := parseDecimalAmount(step.Amount); err == nil {
			amount = parsed
		}
	}
	action := step.Operation
	direction := plan.SendDirectionForRail(step.Rail)
	network := networkForGatewaySelection(step.Rail, routeNetwork)

	s.logger.Debug("Selecting gateway for step",
		zap.String("step_id", strings.TrimSpace(step.StepID)),
		zap.String("rail", string(step.Rail)),
		zap.String("network", network),
		zap.String("currency", currency),
		zap.String("action", string(action)),
		zap.String("preferred_gateway", step.GatewayID),
	)

	eligible := make([]*model.GatewayInstanceDescriptor, 0, len(gateways))
	for i, gw := range gateways {
		if gw == nil {
			s.logger.Warn("Nil gateway found", zap.Int("gateway_index", i))
			continue
		}
		eligErr := plan.IsGatewayEligible(gw, step.Rail, network, currency, action, direction, amount)
		s.logger.Debug("Gateway eligibility check",
			zap.String("step_id", strings.TrimSpace(step.StepID)),
			zap.String("gateway_id", strings.TrimSpace(gw.ID)),
			zap.String("instance_id", strings.TrimSpace(gw.InstanceID)),
			zap.Bool("eligible", eligErr == nil),
			zap.Error(eligErr),
		)
		if eligErr != nil {
			continue
		}
		eligible = append(eligible, gw)
	}

	s.logger.Debug("Gateway eligibility evaluated",
		zap.String("step_id", strings.TrimSpace(step.StepID)),
		zap.Int("eligible_count", len(eligible)),
		zap.Int("total_count", len(gateways)),
	)

	selected, _ := model.SelectGatewayByPreference(eligible, step.GatewayID, step.InstanceID, step.GatewayInvokeURI)
	if selected == nil && len(eligible) > 0 {
		selected = eligible[0]
	}
	if selected != nil {
		s.logger.Debug("Gateway selected",
			zap.String("step_id", strings.TrimSpace(step.StepID)),
			zap.String("gateway_id", strings.TrimSpace(selected.ID)),
			zap.String("instance_id", strings.TrimSpace(selected.InstanceID)),
		)

		return selected, nil
	}

	s.logger.Warn("No eligible gateway found for step",
		zap.String("step_id", strings.TrimSpace(step.StepID)),
		zap.String("rail", string(step.Rail)),
		zap.String("network", network),
		zap.String("currency", currency),
	)

	return nil, merrors.NoData(model.NoEligibleGatewayMessage(network, currency, action, toGatewayDirection(direction)))
}

func parseDecimalAmount(m *moneyv1.Money) (decimal.Decimal, error) {
	if m == nil {
		return decimal.Zero, nil
	}
	value := strings.TrimSpace(m.GetAmount())
	if value == "" {
		return decimal.Zero, nil
	}
	parsed, err := decimal.NewFromString(value)
	if err != nil {
		return decimal.Zero, err
	}
	return parsed, nil
}

func networkForGatewaySelection(rail model.Rail, routeNetwork string) string {
	switch rail {
	case discovery.RailCrypto, discovery.RailProviderSettlement, discovery.RailFiatOnRamp:
		return strings.ToUpper(strings.TrimSpace(routeNetwork))
	default:
		return ""
	}
}

func toGatewayDirection(dir plan.SendDirection) model.GatewayDirection {
	switch dir {
	case plan.SendDirectionOut:
		return model.GatewayDirectionOut
	case plan.SendDirectionIn:
		return model.GatewayDirectionIn
	default:
		return model.GatewayDirectionAny
	}
}

func destinationGatewayFromSteps(steps []*QuoteComputationStep) string {
	for i := len(steps) - 1; i >= 0; i-- {
		step := steps[i]
		if step == nil {
			continue
		}
		if gateway := normalizeGatewayKey(step.GatewayID); gateway != "" {
			return gateway
		}
	}
	return ""
}
