package quote_computation_service

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

	"github.com/tech/sendico/payments/quotation/internal/service/plan"
	"github.com/tech/sendico/payments/quotation/internal/service/quotation/gateway_funding_profile"
	"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	payecon "github.com/tech/sendico/pkg/payments/economics"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func (s *QuoteComputationService) BuildPlan(ctx context.Context, in ComputeInput) (*QuoteComputationPlan, error) {
	if strings.TrimSpace(in.OrganizationRef) == "" {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	if in.OrganizationID == bson.NilObjectID {
		return nil, merrors.InvalidArgument("organization_id is required")
	}
	if len(in.Intents) == 0 {
		return nil, merrors.InvalidArgument("intents are required")
	}
	if !in.PreviewOnly && strings.TrimSpace(in.BaseIdempotencyKey) == "" {
		return nil, merrors.InvalidArgument("idempotency_key is required")
	}

	mode := PlanModeSingle
	if len(in.Intents) > 1 {
		mode = PlanModeBatch
	}

	s.logger.Debug("Building computation plan",
		zap.String("org_ref", in.OrganizationRef),
		zap.String("plan_mode", string(mode)),
		zap.Int("intent_count", len(in.Intents)),
		zap.Bool("preview_only", in.PreviewOnly),
	)

	planModel := &QuoteComputationPlan{
		Mode:               mode,
		OrganizationRef:    strings.TrimSpace(in.OrganizationRef),
		OrganizationID:     in.OrganizationID,
		PreviewOnly:        in.PreviewOnly,
		BaseIdempotencyKey: strings.TrimSpace(in.BaseIdempotencyKey),
		Items:              make([]*QuoteComputationPlanItem, 0, len(in.Intents)),
	}
	managedWalletNetworks := map[string]string{}
	ledgerAccountCurrencies := map[string]string{}

	for i, intent := range in.Intents {
		item, err := s.buildPlanItem(ctx, in, i, intent, managedWalletNetworks, ledgerAccountCurrencies)
		if err != nil {
			s.logger.Warn("Computation plan item build failed",
				zap.String("org_ref", in.OrganizationRef),
				zap.Int("intent_index", i),
				zap.Error(err),
			)

			return nil, wrapIndexedError(err, "intents[%d]", i)
		}

		planModel.Items = append(planModel.Items, item)
	}

	s.logger.Debug("Computation plan built",
		zap.String("org_ref", in.OrganizationRef),
		zap.String("plan_mode", string(planModel.Mode)),
		zap.Int("item_count", len(planModel.Items)),
	)

	return planModel, nil
}

func (s *QuoteComputationService) buildPlanItem(
	ctx context.Context,
	in ComputeInput,
	index int,
	intent *transfer_intent_hydrator.QuoteIntent,
	managedWalletNetworks map[string]string,
	ledgerAccountCurrencies map[string]string,
) (*QuoteComputationPlanItem, error) {
	if intent == nil {
		s.logger.Warn("Plan item build failed: intent is nil", zap.Int("index", index))

		return nil, merrors.InvalidArgument("intent is required")
	}

	modelIntent := modelIntentFromQuoteIntent(intent)
	if modelIntent.Amount == nil {
		s.logger.Warn("Plan item build failed: intent amount is nil", zap.Int("index", index))

		return nil, merrors.InvalidArgument("intent.amount is required")
	}

	if modelIntent.Source.Type == model.EndpointTypeUnspecified {
		s.logger.Warn("Plan item build failed: intent source is unspecified", zap.Int("index", index))

		return nil, merrors.InvalidArgument("intent.source is required")
	}

	if modelIntent.Destination.Type == model.EndpointTypeUnspecified {
		s.logger.Warn("Plan item build failed: intent destination is unspecified", zap.Int("index", index))

		return nil, merrors.InvalidArgument("intent.destination is required")
	}

	s.logger.Debug("Plan item intent validated",
		zap.Int("index", index),
		zap.String("source_type", string(modelIntent.Source.Type)),
		zap.String("dest_type", string(modelIntent.Destination.Type)),
		zap.String("amount_currency", modelIntent.Amount.GetCurrency()),
	)

	itemIdempotencyKey := deriveItemIdempotencyKey(strings.TrimSpace(in.BaseIdempotencyKey), len(in.Intents), index)

	source := clonePaymentEndpoint(modelIntent.Source)
	destination := clonePaymentEndpoint(modelIntent.Destination)
	if err := s.enrichManagedWalletEndpointNetwork(ctx, &source, managedWalletNetworks); err != nil {
		s.logger.Warn("Plan item build failed: source managed wallet network resolution error",
			zap.Int("index", index),
			zap.Error(err),
		)
		return nil, err
	}
	if err := s.enrichManagedWalletEndpointNetwork(ctx, &destination, managedWalletNetworks); err != nil {
		s.logger.Warn("Plan item build failed: destination managed wallet network resolution error",
			zap.Int("index", index),
			zap.Error(err),
		)
		return nil, err
	}
	modelIntent.Source = clonePaymentEndpoint(source)
	modelIntent.Destination = clonePaymentEndpoint(destination)
	currencyInference, err := s.inferEndpointCurrencies(
		ctx,
		strings.TrimSpace(in.OrganizationRef),
		modelIntent,
		ledgerAccountCurrencies,
	)
	if err != nil {
		return nil, merrors.InternalWrap(err, "resolve endpoint currencies")
	}
	fxDecision := ensureDerivedFXIntent(
		&modelIntent,
		currencyInference.SourceCurrency,
		currencyInference.DestinationCurrency,
	)
	if currencyInference.SourceInferred || currencyInference.DestinationInferred {
		s.logger.Info("Resolved endpoint currencies for quote intent",
			zap.Int("index", index),
			zap.String("intent_ref", strings.TrimSpace(modelIntent.Ref)),
			zap.String("inferred_source_currency", fxDecision.InferredSourceCurrency),
			zap.String("inferred_destination_currency", fxDecision.InferredDestinationCurrency),
			zap.String("effective_source_currency", fxDecision.EffectiveSourceCurrency),
			zap.String("effective_destination_currency", fxDecision.EffectiveDestinationCurrency),
		)
	}
	if fxDecision.ExplicitOverrideApplied {
		fxBase, fxQuote, fxSide := "", "", ""
		if modelIntent.FX != nil {
			fxSide = strings.TrimSpace(string(modelIntent.FX.Side))
			if modelIntent.FX.Pair != nil {
				fxBase = strings.TrimSpace(modelIntent.FX.Pair.GetBase())
				fxQuote = strings.TrimSpace(modelIntent.FX.Pair.GetQuote())
			}
		}
		s.logger.Info("Applied explicit FX override to inferred endpoint currencies",
			zap.Int("index", index),
			zap.String("intent_ref", strings.TrimSpace(modelIntent.Ref)),
			zap.String("inferred_source_currency", fxDecision.InferredSourceCurrency),
			zap.String("inferred_destination_currency", fxDecision.InferredDestinationCurrency),
			zap.String("effective_source_currency", fxDecision.EffectiveSourceCurrency),
			zap.String("effective_destination_currency", fxDecision.EffectiveDestinationCurrency),
			zap.String("fx_base_currency", fxBase),
			zap.String("fx_quote_currency", fxQuote),
			zap.String("fx_side", fxSide),
		)
	}
	if fxDecision.RequiresFXInferred {
		s.logger.Info("Inferred FX requirement from endpoint currencies",
			zap.Int("index", index),
			zap.String("intent_ref", strings.TrimSpace(modelIntent.Ref)),
			zap.String("source_currency", fxDecision.EffectiveSourceCurrency),
			zap.String("destination_currency", fxDecision.EffectiveDestinationCurrency),
		)
	}

	sourceRail, sourceNetwork, err := plan.RailFromEndpoint(source, modelIntent.Attributes, true)
	if err != nil {
		s.logger.Warn("Plan item build failed: source rail resolution error",
			zap.Int("index", index),
			zap.Error(err),
		)

		return nil, err
	}

	destRail, destNetwork, err := plan.RailFromEndpoint(destination, modelIntent.Attributes, false)
	if err != nil {
		s.logger.Warn("Plan item build failed: destination rail resolution error",
			zap.Int("index", index),
			zap.Error(err),
		)

		return nil, err
	}

	routeNetwork, err := plan.ResolveRouteNetwork(modelIntent.Attributes, sourceNetwork, destNetwork)
	if err != nil {
		s.logger.Warn("Plan item build failed: route network resolution error",
			zap.Int("index", index),
			zap.String("source_network", sourceNetwork),
			zap.String("dest_network", destNetwork),
			zap.Error(err),
		)

		return nil, err
	}

	s.logger.Debug("Plan item rails resolved",
		zap.Int("index", index),
		zap.String("source_rail", string(sourceRail)),
		zap.String("dest_rail", string(destRail)),
		zap.String("route_network", firstNonEmpty(routeNetwork, destNetwork, sourceNetwork)),
	)

	routeRails, err := s.resolveRouteRails(ctx, sourceRail, destRail, firstNonEmpty(routeNetwork, destNetwork, sourceNetwork))
	if err != nil {
		s.logger.Warn("Plan item build failed: route rails resolution error",
			zap.Int("index", index),
			zap.String("source_rail", string(sourceRail)),
			zap.String("dest_rail", string(destRail)),
			zap.Error(err),
		)

		return nil, err
	}

	s.logger.Debug("Plan item route rails resolved",
		zap.Int("index", index),
		zap.Int("route_rails_count", len(routeRails)),
	)

	steps := buildComputationSteps(index, modelIntent, destination, routeRails)

	s.logger.Debug("Plan item steps built", zap.Int("index", index), zap.Int("step_count", len(steps)))

	if err := s.resolveStepGateways(
		ctx,
		steps,
		firstNonEmpty(routeNetwork, destNetwork, sourceNetwork),
	); err != nil {
		s.logger.Warn("Plan item build failed: step gateway resolution error",
			zap.Int("index", index),
			zap.Error(err),
		)

		return nil, err
	}

	s.logger.Debug("Plan item step gateways resolved", zap.Int("index", index))

	provider := firstNonEmpty(
		destinationGatewayFromSteps(steps),
		gatewayKeyForFunding(modelIntent.Attributes, destination),
	)
	if provider == "" && destRail == discovery.RailLedger {
		provider = "internal"
	}

	funding, err := s.resolveFundingGate(ctx, resolveFundingGateInput{
		OrganizationRef: strings.TrimSpace(in.OrganizationRef),
		Rail:            destRail,
		Network:         firstNonEmpty(routeNetwork, destNetwork, sourceNetwork),
		Amount:          protoMoneyFromModel(modelIntent.Amount),
		Source:          source,
		Destination:     destination,
		Attributes:      modelIntent.Attributes,
		Currency: firstNonEmpty(
			strings.TrimSpace(modelIntent.SettlementCurrency),
			strings.TrimSpace(modelIntent.Amount.GetCurrency()),
		),
		GatewayID:  provider,
		InstanceID: instanceIDForFunding(modelIntent.Attributes),
	})
	if err != nil {
		s.logger.Warn("Plan item build failed: funding gate resolution error",
			zap.Int("index", index),
			zap.String("provider", provider),
			zap.Error(err),
		)

		return nil, err
	}

	s.logger.Debug("Plan item funding gate resolved",
		zap.Int("index", index),
		zap.String("provider", provider),
		zap.Bool("has_funding", funding != nil),
	)

	route := buildRouteSpecification(
		modelIntent,
		firstNonEmpty(routeNetwork, destNetwork, sourceNetwork),
		steps,
	)
	conditions, blockReason := buildExecutionConditions(in.PreviewOnly, steps, funding)

	if route == nil || len(route.GetHops()) == 0 {
		blockReason = quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE

		s.logger.Debug("Plan item route unavailable, item will be blocked",
			zap.Int("index", index),
		)
	}

	quoteInput := BuildQuoteInput{
		OrganizationRef:     strings.TrimSpace(in.OrganizationRef),
		IdempotencyKey:      itemIdempotencyKey,
		Intent:              clonePaymentIntent(modelIntent),
		PreviewOnly:         in.PreviewOnly,
		Route:               cloneRouteSpecification(route),
		ExecutionConditions: cloneExecutionConditions(conditions),
	}
	resolvedSettlementMode := resolvedSettlementModeFromModel(modelIntent.SettlementMode)
	resolvedFeeTreatment := resolvedFeeTreatmentFromHydratedIntent(intent)

	intentRef := strings.TrimSpace(modelIntent.Ref)
	if intentRef == "" {
		intentRef = fmt.Sprintf("intent-%d", index)
	}

	s.logger.Debug("Computation plan item built",
		zap.Int("index", index),
		zap.String("intent_ref", intentRef),
		zap.Int("step_count", len(steps)),
		zap.String("block_reason", blockReason.String()),
		zap.Bool("has_funding", funding != nil),
		zap.Bool("preview_only", quoteInput.PreviewOnly),
	)

	return &QuoteComputationPlanItem{
		Index:                  index,
		IdempotencyKey:         itemIdempotencyKey,
		IntentRef:              intentRef,
		Intent:                 modelIntent,
		QuoteInput:             quoteInput,
		Steps:                  steps,
		Funding:                funding,
		Route:                  route,
		ExecutionConditions:    conditions,
		ResolvedSettlementMode: resolvedSettlementMode,
		ResolvedFeeTreatment:   resolvedFeeTreatment,
		BlockReason:            blockReason,
	}, nil
}

func resolvedFeeTreatmentFromHydratedIntent(intent *transfer_intent_hydrator.QuoteIntent) quotationv2.FeeTreatment {
	if intent == nil {
		return defaultResolvedFeeTreatment()
	}
	return payecon.ResolveFeeTreatmentFromStringOrDefault(string(intent.FeeTreatment))
}

func deriveItemIdempotencyKey(base string, total, index int) string {
	base = strings.TrimSpace(base)
	if base == "" {
		return ""
	}
	if total <= 1 {
		return base
	}
	return fmt.Sprintf("%s:%d", base, index+1)
}

func gatewayKeyForFunding(attrs map[string]string, _ model.PaymentEndpoint) string {
	key := firstNonEmpty(
		lookupAttr(attrs, "gateway", "gateway_id", "gatewayId"),
		lookupAttr(attrs, "destination_gateway", "destinationGateway"),
	)
	return normalizeGatewayKey(key)
}

func instanceIDForFunding(attrs map[string]string) string {
	return strings.TrimSpace(lookupAttr(attrs,
		"instance_id",
		"instanceId",
		"destination_instance_id",
		"destinationInstanceId",
	))
}

type resolveFundingGateInput struct {
	OrganizationRef string
	GatewayID       string
	InstanceID      string
	Rail            model.Rail
	Network         string
	Currency        string
	Amount          *moneyv1.Money
	Source          model.PaymentEndpoint
	Destination     model.PaymentEndpoint
	Attributes      map[string]string
}

func (s *QuoteComputationService) resolveFundingGate(
	ctx context.Context,
	in resolveFundingGateInput,
) (*gateway_funding_profile.QuoteFundingGate, error) {
	if s == nil || s.fundingResolver == nil {
		s.logger.Debug("Funding gate resolution skipped: no funding resolver configured",
			zap.String("gateway_id", in.GatewayID),
			zap.String("rail", string(in.Rail)),
		)

		return nil, nil //nolint:nilnil // nil gate means no resolver configured
	}

	s.logger.Debug("Resolving funding gate",
		zap.String("org_ref", in.OrganizationRef),
		zap.String("gateway_id", in.GatewayID),
		zap.String("instance_id", in.InstanceID),
		zap.String("rail", string(in.Rail)),
		zap.String("network", in.Network),
		zap.String("currency", in.Currency),
	)

	profile, err := s.fundingResolver.ResolveGatewayFundingProfile(ctx, gateway_funding_profile.FundingProfileRequest{
		OrganizationRef: strings.TrimSpace(in.OrganizationRef),
		GatewayID:       normalizeGatewayKey(in.GatewayID),
		InstanceID:      strings.TrimSpace(in.InstanceID),
		Rail:            in.Rail,
		Network:         strings.TrimSpace(in.Network),
		Currency:        strings.ToUpper(strings.TrimSpace(in.Currency)),
		Amount:          in.Amount,
		Source:          &in.Source,
		Destination:     &in.Destination,
		Attributes:      in.Attributes,
	})
	if err != nil {
		s.logger.Warn("Funding gate resolution failed",
			zap.String("org_ref", in.OrganizationRef),
			zap.String("gateway_id", in.GatewayID),
			zap.String("rail", string(in.Rail)),
			zap.Error(err),
		)

		return nil, err
	}

	if profile == nil {
		s.logger.Debug("Funding gate resolution returned no profile",
			zap.String("gateway_id", in.GatewayID),
			zap.String("rail", string(in.Rail)),
		)

		return nil, nil //nolint:nilnil // nil gate means no funding profile for resolved gateway
	}

	gate, err := gateway_funding_profile.BuildFundingGateFromProfile(profile, in.Amount)
	if err != nil {
		s.logger.Warn("Funding gate build from profile failed",
			zap.String("gateway_id", in.GatewayID),
			zap.Error(err),
		)

		return nil, err
	}

	s.logger.Debug("Funding gate resolved",
		zap.String("gateway_id", in.GatewayID),
		zap.String("rail", string(in.Rail)),
		zap.Bool("has_gate", gate != nil),
	)

	return gate, nil
}
