package quotation

import (
	"context"
	"strings"
	"time"

	oracleclient "github.com/tech/sendico/fx/oracle/client"
	chainclient "github.com/tech/sendico/gateway/chain/client"
	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/storage/model"
	clockpkg "github.com/tech/sendico/pkg/clock"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/payments/rail"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
)

// Option configures service dependencies.
type Option func(*Service)

// GatewayInvokeResolver resolves gateway invoke URIs into chain gateway clients.
type GatewayInvokeResolver interface {
	Resolve(ctx context.Context, invokeURI string) (chainclient.Client, error)
}

// ChainGatewayResolver resolves chain gateway clients by network.
type ChainGatewayResolver interface {
	Resolve(ctx context.Context, network string) (chainclient.Client, error)
}

// GatewayRegistry exposes gateway instances for capability-based selection.
type GatewayRegistry interface {
	List(ctx context.Context) ([]*model.GatewayInstanceDescriptor, error)
}

// CardGatewayRoute maps a gateway to its funding and fee destinations.
type CardGatewayRoute struct {
	FundingAddress string
	FeeAddress     string
	FeeWalletRef   string
}

type feesDependency struct {
	client  feesv1.FeeEngineClient
	timeout time.Duration
}

func (f feesDependency) available() bool {
	if f.client == nil {
		return false
	}
	if checker, ok := f.client.(interface{ Available() bool }); ok {
		return checker.Available()
	}
	return true
}

type gatewayDependency struct {
	resolver ChainGatewayResolver
}

type oracleDependency struct {
	client oracleclient.Client
}

func (o oracleDependency) available() bool {
	if o.client == nil {
		return false
	}
	if checker, ok := o.client.(interface{ Available() bool }); ok {
		return checker.Available()
	}
	return true
}

// WithFeeEngine wires the fee engine client.
func WithFeeEngine(client feesv1.FeeEngineClient, timeout time.Duration) Option {
	return func(s *Service) {
		s.deps.fees = feesDependency{client: client, timeout: timeout}
	}
}

// WithOracleClient wires the FX oracle client.
func WithOracleClient(client oracleclient.Client) Option {
	return func(s *Service) {
		s.deps.oracle = oracleDependency{client: client}
	}
}

// WithChainGatewayResolver wires a resolver for chain gateway clients.
func WithChainGatewayResolver(resolver ChainGatewayResolver) Option {
	return func(s *Service) {
		if resolver != nil {
			s.deps.gateway = gatewayDependency{resolver: resolver}
		}
	}
}

// WithGatewayInvokeResolver wires a resolver for invoke URIs.
func WithGatewayInvokeResolver(resolver GatewayInvokeResolver) Option {
	return func(s *Service) {
		if resolver != nil {
			s.deps.gatewayInvokeResolver = resolver
		}
	}
}

// WithGatewayRegistry wires gateway descriptors used by quote computation/gateway selection.
func WithGatewayRegistry(registry GatewayRegistry) Option {
	return func(s *Service) {
		if registry != nil {
			s.deps.gatewayRegistry = registry
		}
	}
}

// WithCardGatewayRoutes configures funding/fee wallet routing per gateway.
func WithCardGatewayRoutes(routes map[string]CardGatewayRoute) Option {
	return func(s *Service) {
		if len(routes) == 0 {
			return
		}
		s.deps.cardRoutes = make(map[string]CardGatewayRoute, len(routes))
		for key, route := range routes {
			normalized := strings.ToLower(strings.TrimSpace(key))
			if normalized == "" {
				continue
			}
			s.deps.cardRoutes[normalized] = route
		}
	}
}

// WithFeeLedgerAccounts maps gateway IDs to fee ledger accounts.
func WithFeeLedgerAccounts(accounts map[string]string) Option {
	return func(s *Service) {
		if len(accounts) == 0 {
			return
		}
		s.deps.feeLedgerAccounts = make(map[string]string, len(accounts))
		for key, account := range accounts {
			normalized := strings.ToLower(strings.TrimSpace(key))
			value := strings.TrimSpace(account)
			if normalized == "" || value == "" {
				continue
			}
			s.deps.feeLedgerAccounts[normalized] = value
		}
	}
}

// WithClock overrides the default clock.
func WithClock(clock clockpkg.Clock) Option {
	return func(s *Service) {
		if clock != nil {
			s.clock = clock
		}
	}
}

// WithLedgerClient wires the ledger client used for account-currency inference.
func WithLedgerClient(client ledgerclient.Client) Option {
	return func(s *Service) {
		if s != nil && client != nil {
			s.deps.ledger = client
		}
	}
}

// WithProviderSettlementGatewayClient is retained for backward compatibility and is currently a no-op.
func WithProviderSettlementGatewayClient(_ chainclient.Client) Option {
	return func(*Service) {}
}

// WithProviderSettlementGatewayResolver is retained for backward compatibility and is currently a no-op.
func WithProviderSettlementGatewayResolver(_ ChainGatewayResolver) Option {
	return func(*Service) {}
}

// WithRailGateways is retained for backward compatibility and is currently a no-op.
func WithRailGateways(_ map[string]rail.RailGateway) Option {
	return func(*Service) {}
}

type discoveryGatewayRegistry struct {
	registry *discovery.Registry
}

// NewDiscoveryGatewayRegistry adapts discovery entries into gateway descriptors.
func NewDiscoveryGatewayRegistry(_ mlogger.Logger, registry *discovery.Registry) GatewayRegistry {
	if registry == nil {
		return nil
	}
	return &discoveryGatewayRegistry{registry: registry}
}

func (r *discoveryGatewayRegistry) List(_ context.Context) ([]*model.GatewayInstanceDescriptor, error) {
	if r == nil || r.registry == nil {
		return nil, nil
	}

	entries := r.registry.List(time.Now(), true)
	items := make([]*model.GatewayInstanceDescriptor, 0, len(entries))
	for _, entry := range entries {
		railID := railFromDiscovery(entry.Rail)
		if railID == discovery.RailUnspecified {
			continue
		}
		operations := operationsFromDiscovery(entry.Operations)
		items = append(items, &model.GatewayInstanceDescriptor{
			ID:           strings.TrimSpace(entry.ID),
			InstanceID:   strings.TrimSpace(entry.InstanceID),
			Rail:         railID,
			Network:      strings.ToUpper(strings.TrimSpace(entry.Network)),
			InvokeURI:    strings.TrimSpace(entry.InvokeURI),
			Currencies:   currenciesFromDiscovery(entry.Currencies),
			Operations:   operations,
			Capabilities: model.RailCapabilitiesFromOperations(operations),
			Limits:       limitsFromDiscovery(entry.Limits),
			IsEnabled:    entry.Healthy,
		})
	}
	return items, nil
}

func railFromDiscovery(value string) model.Rail {
	switch discovery.NormalizeRail(value) {
	case discovery.RailCrypto:
		return discovery.RailCrypto
	case discovery.RailProviderSettlement:
		return discovery.RailProviderSettlement
	case discovery.RailLedger:
		return discovery.RailLedger
	case discovery.RailCardPayout:
		return discovery.RailCardPayout
	case discovery.RailFiatOnRamp:
		return discovery.RailFiatOnRamp
	default:
		return discovery.RailUnspecified
	}
}

func operationsFromDiscovery(values []string) []model.RailOperation {
	return model.NormalizeRailOperationStrings(discovery.NormalizeRailOperations(values))
}

func currenciesFromDiscovery(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	result := make([]string, 0, len(values))
	seen := map[string]bool{}
	for _, value := range values {
		currency := strings.ToUpper(strings.TrimSpace(value))
		if currency == "" || seen[currency] {
			continue
		}
		seen[currency] = true
		result = append(result, currency)
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

func limitsFromDiscovery(src *discovery.Limits) model.Limits {
	limits := model.Limits{}
	if src == nil {
		return limits
	}

	limits.MinAmount = strings.TrimSpace(src.MinAmount)
	limits.MaxAmount = strings.TrimSpace(src.MaxAmount)

	if len(src.VolumeLimit) > 0 {
		limits.VolumeLimit = map[string]string{}
		for bucket, value := range src.VolumeLimit {
			key := strings.TrimSpace(bucket)
			amount := strings.TrimSpace(value)
			if key == "" || amount == "" {
				continue
			}
			limits.VolumeLimit[key] = amount
		}
	}

	if len(src.VelocityLimit) > 0 {
		limits.VelocityLimit = map[string]int{}
		for bucket, value := range src.VelocityLimit {
			key := strings.TrimSpace(bucket)
			if key == "" || value <= 0 {
				continue
			}
			limits.VelocityLimit[key] = value
		}
	}

	return limits
}
