package psvc

import (
	"strconv"
	"strings"

	"github.com/tech/sendico/pkg/discovery"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/opagg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.uber.org/zap"
)

// BatchOptimizeInput is optimizer payload for one ExecuteBatchPayment call.
type BatchOptimizeInput struct {
	Items []qsnap.ResolvedItem
}

// BatchOptimizeOutput is optimizer result payload.
type BatchOptimizeOutput struct {
	Groups []opagg.Group
}

// BatchOptimizer encapsulates batch item compaction policy and aggregation.
type BatchOptimizer interface {
	Optimize(in BatchOptimizeInput) (*BatchOptimizeOutput, error)
}

// BatchOptimizationMergeKeyResolver allows external packages to resolve custom merge keys.
type BatchOptimizationMergeKeyResolver interface {
	ResolveMergeKey(item qsnap.ResolvedItem, selection BatchOptimizationSelection) (string, bool)
}

// PolicyBatchOptimizerDependencies configures policy-based optimizer implementation.
type PolicyBatchOptimizerDependencies struct {
	Logger           mlogger.Logger
	Aggregator       opagg.Aggregator
	Policy           BatchOptimizationPolicy
	MergeKeyResolver BatchOptimizationMergeKeyResolver
}

type policyBatchOptimizer struct {
	logger           mlogger.Logger
	aggregator       opagg.Aggregator
	policy           BatchOptimizationPolicy
	mergeKeyResolver BatchOptimizationMergeKeyResolver
}

// NewPolicyBatchOptimizer creates default rule-based optimizer implementation.
func NewPolicyBatchOptimizer(deps PolicyBatchOptimizerDependencies) BatchOptimizer {
	logger := deps.Logger
	if logger == nil {
		logger = zap.NewNop()
	}
	aggregator := deps.Aggregator
	if aggregator == nil {
		aggregator = opagg.New(opagg.Dependencies{Logger: logger})
	}
	return &policyBatchOptimizer{
		logger:           logger.Named("batch_optimizer"),
		aggregator:       aggregator,
		policy:           normalizeBatchOptimizationPolicy(deps.Policy),
		mergeKeyResolver: deps.MergeKeyResolver,
	}
}

func (o *policyBatchOptimizer) Optimize(in BatchOptimizeInput) (*BatchOptimizeOutput, error) {
	if len(in.Items) == 0 {
		return nil, merrors.InvalidArgument("items are required")
	}

	aggItems := make([]opagg.Item, 0, len(in.Items))
	for i := range in.Items {
		item := in.Items[i]
		selection := o.policy.SelectMany(batchOptimizationContextsFromItem(item))
		intentSnapshot := applyBatchOptimizationSelection(item.IntentSnapshot, selection)
		mergeKey := batchOptimizationMergeKey(item, selection, o.mergeKeyResolver)
		aggItems = append(aggItems, opagg.Item{
			IntentRef:      item.IntentRef,
			IntentSnapshot: intentSnapshot,
			QuoteSnapshot:  item.QuoteSnapshot,
			MergeMode:      mergeModeFromBatchOptimization(selection.Mode),
			MergeKey:       mergeKey,
			PolicyTag:      batchOptimizationSelectionTag(selection),
		})
	}

	aggOutput, err := o.aggregator.Aggregate(opagg.Input{Items: aggItems})
	if err != nil {
		return nil, err
	}
	if aggOutput == nil || len(aggOutput.Groups) == 0 {
		return nil, merrors.InvalidArgument("aggregation produced no groups")
	}
	return &BatchOptimizeOutput{Groups: aggOutput.Groups}, nil
}

func batchOptimizationContextsFromItem(item qsnap.ResolvedItem) []BatchOptimizationContext {
	out := make([]BatchOptimizationContext, 0, 6)
	add := func(base BatchOptimizationContext) {
		if base.Rail == discovery.RailUnspecified {
			return
		}
		money := selectSettlementMoney(item)
		if money != nil {
			base.Amount = strings.TrimSpace(money.Amount)
			base.Currency = strings.TrimSpace(money.Currency)
		}
		out = append(out, base)
	}

	add(batchOptimizationContextFromItem(item))
	add(mergeBatchOptimizationContext(
		endpointOptimizationContext(item.IntentSnapshot.Source),
		sourceHopContext(item.QuoteSnapshot),
	))
	route := item.QuoteSnapshot
	if route != nil && route.Route != nil {
		if parsed := model.ParseRail(route.Route.Rail); parsed != discovery.RailUnspecified {
			add(BatchOptimizationContext{
				Rail:     parsed,
				Provider: strings.TrimSpace(route.Route.Provider),
				Network:  strings.TrimSpace(route.Route.Network),
			})
		}
		for i := range route.Route.Hops {
			hop := route.Route.Hops[i]
			if hop == nil {
				continue
			}
			parsed := model.ParseRail(hop.Rail)
			if parsed == discovery.RailUnspecified {
				continue
			}
			add(BatchOptimizationContext{
				Rail:     parsed,
				Provider: strings.TrimSpace(hop.Gateway),
				Network:  strings.TrimSpace(hop.Network),
			})
		}
	}
	if len(out) == 0 {
		add(batchOptimizationContextFromItem(item))
	}
	return out
}

func batchOptimizationMergeKey(
	item qsnap.ResolvedItem,
	sel BatchOptimizationSelection,
	resolver BatchOptimizationMergeKeyResolver,
) string {
	if mergeModeFromBatchOptimization(sel.Mode) != opagg.MergeModeByRecipient {
		return ""
	}

	groupBy := normalizeBatchOptimizationGrouping(sel.GroupBy)
	if groupBy == BatchOptimizationGroupingUnspecified {
		groupBy = BatchOptimizationGroupingOperationDestination
	}
	switch groupBy {
	case BatchOptimizationGroupingOperationSource:
		return mergeEndpointKey(item.IntentSnapshot.Source)
	case BatchOptimizationGroupingRailTarget:
		if resolver != nil {
			if key, ok := resolver.ResolveMergeKey(item, sel); ok {
				return strings.TrimSpace(key)
			}
		}
		return railTargetMergeKey(item, sel.MatchRail)
	default:
		return mergeEndpointKey(item.IntentSnapshot.Destination)
	}
}

func railTargetMergeKey(item qsnap.ResolvedItem, rail model.Rail) string {
	matchRail := model.ParseRail(string(rail))
	if matchRail == discovery.RailUnspecified {
		ctx := batchOptimizationContextFromItem(item)
		matchRail = ctx.Rail
	}

	switch matchRail {
	case discovery.RailCardPayout:
		return mergeEndpointKey(item.IntentSnapshot.Destination)
	case discovery.RailCrypto:
		if endpointRail(item.IntentSnapshot.Destination) == discovery.RailCrypto {
			return mergeEndpointKey(item.IntentSnapshot.Destination)
		}
		if endpointRail(item.IntentSnapshot.Source) == discovery.RailCrypto {
			return mergeEndpointKey(item.IntentSnapshot.Source)
		}
		return routeRailTargetKey(item.QuoteSnapshot, matchRail)
	default:
		return routeRailTargetKey(item.QuoteSnapshot, matchRail)
	}
}

func routeRailTargetKey(snapshot *model.PaymentQuoteSnapshot, rail model.Rail) string {
	if snapshot == nil || snapshot.Route == nil {
		return ""
	}
	route := snapshot.Route
	target := ""
	fallback := ""
	for i := range route.Hops {
		hop := route.Hops[i]
		if hop == nil || model.ParseRail(hop.Rail) != rail {
			continue
		}
		key := strings.Join([]string{
			"rail=" + strings.ToUpper(strings.TrimSpace(hop.Rail)),
			"gateway=" + strings.TrimSpace(hop.Gateway),
			"instance=" + strings.TrimSpace(hop.InstanceID),
			"network=" + strings.TrimSpace(hop.Network),
		}, "|")
		if fallback == "" {
			fallback = key
		}
		if hop.Role == paymenttypes.QuoteRouteHopRoleDestination {
			target = key
			break
		}
	}
	if target != "" {
		return target
	}
	if fallback != "" {
		return fallback
	}
	return ""
}

func mergeEndpointKey(ep model.PaymentEndpoint) string {
	switch normalizeEndpointType(ep) {
	case model.EndpointTypeLedger:
		if ep.Ledger == nil {
			return ""
		}
		return strings.Join([]string{
			"type=ledger",
			"account=" + strings.TrimSpace(ep.Ledger.LedgerAccountRef),
			"contra=" + strings.TrimSpace(ep.Ledger.ContraLedgerAccountRef),
		}, "|")
	case model.EndpointTypeManagedWallet:
		if ep.ManagedWallet == nil {
			return ""
		}
		return strings.Join([]string{
			"type=managed_wallet",
			"wallet=" + strings.TrimSpace(ep.ManagedWallet.ManagedWalletRef),
			"asset=" + mergeAssetKey(ep.ManagedWallet.Asset),
		}, "|")
	case model.EndpointTypeExternalChain:
		if ep.ExternalChain == nil {
			return ""
		}
		return strings.Join([]string{
			"type=external_chain",
			"address=" + strings.TrimSpace(ep.ExternalChain.Address),
			"memo=" + strings.TrimSpace(ep.ExternalChain.Memo),
			"asset=" + mergeAssetKey(ep.ExternalChain.Asset),
		}, "|")
	case model.EndpointTypeCard:
		if ep.Card == nil {
			return ""
		}
		return strings.Join([]string{
			"type=card",
			"token=" + strings.TrimSpace(ep.Card.Token),
			"pan=" + strings.TrimSpace(ep.Card.Pan),
			"masked=" + strings.TrimSpace(ep.Card.MaskedPan),
			"country=" + strings.TrimSpace(ep.Card.Country),
			"exp=" + strconv.FormatUint(uint64(ep.Card.ExpMonth), 10) + "-" + strconv.FormatUint(uint64(ep.Card.ExpYear), 10),
		}, "|")
	default:
		return ""
	}
}

func mergeAssetKey(asset *paymenttypes.Asset) string {
	if asset == nil {
		return ""
	}
	return strings.Join([]string{
		strings.ToUpper(strings.TrimSpace(asset.Chain)),
		strings.ToUpper(strings.TrimSpace(asset.TokenSymbol)),
		strings.TrimSpace(asset.ContractAddress),
	}, ":")
}

func batchOptimizationContextFromItem(item qsnap.ResolvedItem) BatchOptimizationContext {
	// Match policy against operation target (intent destination), not route leg semantics.
	target := mergeBatchOptimizationContext(
		endpointOptimizationContext(item.IntentSnapshot.Destination),
		destinationHopContext(item.QuoteSnapshot),
	)
	money := selectSettlementMoney(item)
	ctx := BatchOptimizationContext{
		Rail:     target.Rail,
		Provider: target.Provider,
		Network:  target.Network,
	}
	if money != nil {
		ctx.Amount = strings.TrimSpace(money.Amount)
		ctx.Currency = strings.TrimSpace(money.Currency)
	}
	return ctx
}

func mergeBatchOptimizationContext(primary, fallback BatchOptimizationContext) BatchOptimizationContext {
	out := primary
	if out.Rail == "" || out.Rail == discovery.RailUnspecified {
		out.Rail = fallback.Rail
	}
	if strings.TrimSpace(out.Provider) == "" {
		out.Provider = fallback.Provider
	}
	if strings.TrimSpace(out.Network) == "" {
		out.Network = fallback.Network
	}
	return out
}

func endpointOptimizationContext(endpoint model.PaymentEndpoint) BatchOptimizationContext {
	out := BatchOptimizationContext{
		Rail: endpointRail(endpoint),
	}
	switch normalizeEndpointType(endpoint) {
	case model.EndpointTypeManagedWallet:
		if endpoint.ManagedWallet != nil && endpoint.ManagedWallet.Asset != nil {
			out.Network = strings.TrimSpace(endpoint.ManagedWallet.Asset.Chain)
		}
	case model.EndpointTypeExternalChain:
		if endpoint.ExternalChain != nil {
			if endpoint.ExternalChain.Asset != nil {
				out.Network = strings.TrimSpace(endpoint.ExternalChain.Asset.Chain)
			}
		}
	}
	return out
}

func endpointRail(endpoint model.PaymentEndpoint) model.Rail {
	switch normalizeEndpointType(endpoint) {
	case model.EndpointTypeManagedWallet, model.EndpointTypeExternalChain:
		return discovery.RailCrypto
	case model.EndpointTypeCard:
		return discovery.RailCardPayout
	case model.EndpointTypeLedger:
		return discovery.RailLedger
	default:
		return discovery.RailUnspecified
	}
}

func normalizeEndpointType(endpoint model.PaymentEndpoint) model.PaymentEndpointType {
	if endpoint.Type != model.EndpointTypeUnspecified {
		return endpoint.Type
	}
	switch {
	case endpoint.Ledger != nil:
		return model.EndpointTypeLedger
	case endpoint.ManagedWallet != nil:
		return model.EndpointTypeManagedWallet
	case endpoint.ExternalChain != nil:
		return model.EndpointTypeExternalChain
	case endpoint.Card != nil:
		return model.EndpointTypeCard
	default:
		return model.EndpointTypeUnspecified
	}
}

func selectSettlementMoney(item qsnap.ResolvedItem) *paymenttypes.Money {
	if item.QuoteSnapshot != nil && item.QuoteSnapshot.ExpectedSettlementAmount != nil {
		return item.QuoteSnapshot.ExpectedSettlementAmount
	}
	return item.IntentSnapshot.Amount
}

func destinationHopContext(snapshot *model.PaymentQuoteSnapshot) BatchOptimizationContext {
	if snapshot == nil || snapshot.Route == nil {
		return BatchOptimizationContext{}
	}
	route := snapshot.Route
	ctx := BatchOptimizationContext{
		Rail:     model.ParseRail(route.Rail),
		Provider: strings.TrimSpace(route.Provider),
		Network:  strings.TrimSpace(route.Network),
	}

	var destinationHop *paymenttypes.QuoteRouteHop
	for i := range route.Hops {
		hop := route.Hops[i]
		if hop == nil {
			continue
		}
		destinationHop = hop
		if hop.Role == paymenttypes.QuoteRouteHopRoleDestination {
			break
		}
	}
	if destinationHop != nil {
		if parsed := model.ParseRail(destinationHop.Rail); parsed != discovery.RailUnspecified {
			ctx.Rail = parsed
		}
		if ctx.Provider == "" {
			ctx.Provider = strings.TrimSpace(destinationHop.Gateway)
		}
		if ctx.Network == "" {
			ctx.Network = strings.TrimSpace(destinationHop.Network)
		}
	}
	return ctx
}

func sourceHopContext(snapshot *model.PaymentQuoteSnapshot) BatchOptimizationContext {
	if snapshot == nil || snapshot.Route == nil {
		return BatchOptimizationContext{}
	}
	route := snapshot.Route
	ctx := BatchOptimizationContext{
		Rail:     model.ParseRail(route.Rail),
		Provider: strings.TrimSpace(route.Provider),
		Network:  strings.TrimSpace(route.Network),
	}
	var sourceHop *paymenttypes.QuoteRouteHop
	for i := range route.Hops {
		hop := route.Hops[i]
		if hop == nil {
			continue
		}
		if sourceHop == nil {
			sourceHop = hop
		}
		if hop.Role == paymenttypes.QuoteRouteHopRoleSource {
			sourceHop = hop
			break
		}
	}
	if sourceHop != nil {
		if parsed := model.ParseRail(sourceHop.Rail); parsed != discovery.RailUnspecified {
			ctx.Rail = parsed
		}
		if ctx.Provider == "" {
			ctx.Provider = strings.TrimSpace(sourceHop.Gateway)
		}
		if ctx.Network == "" {
			ctx.Network = strings.TrimSpace(sourceHop.Network)
		}
	}
	return ctx
}

func mergeModeFromBatchOptimization(mode BatchOptimizationMode) opagg.MergeMode {
	switch normalizeBatchOptimizationMode(mode) {
	case BatchOptimizationModeMergeByDestination:
		return opagg.MergeModeByRecipient
	default:
		return opagg.MergeModeNever
	}
}

func batchOptimizationSelectionTag(sel BatchOptimizationSelection) string {
	mode := normalizeBatchOptimizationMode(sel.Mode)
	matched := strconv.FormatBool(sel.Matched)
	ruleID := strings.TrimSpace(sel.RuleID)
	if ruleID == "" {
		ruleID = "default"
	}
	return string(mode) + "|" + matched + "|" + ruleID
}

func applyBatchOptimizationSelection(intent model.PaymentIntent, sel BatchOptimizationSelection) model.PaymentIntent {
	if intent.Attributes == nil {
		intent.Attributes = map[string]string{}
	}
	mode := normalizeBatchOptimizationMode(sel.Mode)
	if mode == BatchOptimizationModeUnspecified {
		mode = BatchOptimizationModeNoOptimization
	}
	intent.Attributes[attrBatchOptimizerMode] = string(mode)
	intent.Attributes[attrBatchOptimizerRuleMatch] = strconv.FormatBool(sel.Matched)
	groupBy := normalizeBatchOptimizationGrouping(sel.GroupBy)
	if groupBy == BatchOptimizationGroupingUnspecified {
		delete(intent.Attributes, attrBatchOptimizerGrouping)
	} else {
		intent.Attributes[attrBatchOptimizerGrouping] = string(groupBy)
	}
	ruleID := strings.TrimSpace(sel.RuleID)
	if ruleID == "" {
		delete(intent.Attributes, attrBatchOptimizerRuleID)
	} else {
		intent.Attributes[attrBatchOptimizerRuleID] = ruleID
	}
	return intent
}
