package apiimp

import (
	"context"
	"fmt"
	"net"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/tech/sendico/pkg/discovery"
	msg "github.com/tech/sendico/pkg/messaging"
	"github.com/tech/sendico/pkg/mservice"
	eapi "github.com/tech/sendico/server/interface/api"
	"go.uber.org/zap"
)

const (
	discoveryBootstrapTimeout    = 3 * time.Second
	discoveryBootstrapSender     = "server_bootstrap"
	defaultClientDialTimeoutSecs = 5
	defaultClientCallTimeoutSecs = 5
)

var (
	ledgerDiscoveryServiceNames = []string{
		"LEDGER",
		mservice.Ledger,
	}
	paymentOrchestratorDiscoveryServiceNames = []string{
		"PAYMENTS_ORCHESTRATOR",
		mservice.PaymentOrchestrator,
	}
	paymentQuotationDiscoveryServiceNames = []string{
		"PAYMENTS_QUOTATION",
		"PAYMENTS_QUOTE",
		"PAYMENT_QUOTATION",
		"payment_quotation",
	}
	paymentMethodsDiscoveryServiceNames = []string{
		"PAYMENTS_METHODS",
		"PAYMENT_METHODS",
		mservice.PaymentMethods,
	}
)

type discoveryEndpoint struct {
	address  string
	insecure bool
	raw      string
}

type serviceSelection struct {
	service  discovery.ServiceSummary
	endpoint discoveryEndpoint
	opMatch  bool
	nameRank int
}

type gatewaySelection struct {
	gateway      discovery.GatewaySummary
	endpoint     discoveryEndpoint
	networkMatch bool
	opMatch      bool
}

// resolveServiceAddressesFromDiscovery looks up downstream service addresses once
// during startup and applies them to the runtime config.
func (a *APIImp) resolveServiceAddressesFromDiscovery() {
	if a == nil || a.config == nil || a.config.Mw == nil {
		return
	}

	msgCfg := a.config.Mw.Messaging
	if msgCfg.Driver == "" {
		return
	}

	logger := a.logger.Named("discovery_bootstrap")
	broker, err := msg.CreateMessagingBroker(logger.Named("bus"), &msgCfg)
	if err != nil {
		logger.Warn("Failed to create discovery bootstrap broker", zap.Error(err))
		return
	}

	client, err := discovery.NewClient(logger, broker, nil, discoveryBootstrapSender)
	if err != nil {
		logger.Warn("Failed to create discovery bootstrap client", zap.Error(err))
		return
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), discoveryBootstrapTimeout)
	defer cancel()

	lookup, err := client.Lookup(ctx)
	if err != nil {
		logger.Warn("Failed to fetch discovery registry during startup", zap.Error(err))
		return
	}

	a.resolveChainGatewayAddress(lookup.Gateways)
	orchestratorFound, orchestratorEndpoint := a.resolvePaymentOrchestratorAddress(lookup.Services)
	a.resolveLedgerAddress(lookup.Services)
	a.resolvePaymentQuotationAddress(lookup.Services, orchestratorFound, orchestratorEndpoint)
	a.resolvePaymentMethodsAddress(lookup.Services)
}

func (a *APIImp) resolveChainGatewayAddress(gateways []discovery.GatewaySummary) {
	cfg := a.config.ChainGateway
	if cfg == nil {
		return
	}

	endpoint, selected, ok := selectGatewayEndpoint(
		gateways,
		cfg.DefaultAsset.Chain,
		[]string{discovery.OperationBalanceRead},
	)
	if !ok {
		return
	}

	cfg.Address = endpoint.address
	cfg.Insecure = endpoint.insecure
	ensureTimeoutsChainGateway(cfg)

	a.logger.Info("Resolved chain gateway address from discovery",
		zap.String("rail", selected.Rail),
		zap.String("gateway_id", selected.ID),
		zap.String("network", selected.Network),
		zap.String("invoke_uri", endpoint.raw),
		zap.String("address", endpoint.address),
		zap.Bool("insecure", endpoint.insecure))
}

func (a *APIImp) resolveLedgerAddress(services []discovery.ServiceSummary) {
	endpoint, selected, ok := selectServiceEndpoint(
		services,
		ledgerDiscoveryServiceNames,
		discovery.LedgerServiceOperations(),
	)
	if !ok {
		return
	}

	cfg := ensureLedgerConfig(a.config)
	cfg.Address = endpoint.address
	cfg.Insecure = endpoint.insecure
	ensureTimeoutsLedger(cfg)

	a.logger.Info("Resolved ledger address from discovery",
		zap.String("service", selected.Service),
		zap.String("service_id", selected.ID),
		zap.String("instance_id", selected.InstanceID),
		zap.String("invoke_uri", endpoint.raw),
		zap.String("address", endpoint.address),
		zap.Bool("insecure", endpoint.insecure))
}

func (a *APIImp) resolvePaymentOrchestratorAddress(services []discovery.ServiceSummary) (bool, discoveryEndpoint) {
	endpoint, selected, ok := selectServiceEndpoint(
		services,
		paymentOrchestratorDiscoveryServiceNames,
		[]string{discovery.OperationPaymentInitiate},
	)
	if !ok {
		return false, discoveryEndpoint{}
	}

	cfg := ensurePaymentOrchestratorConfig(a.config)
	cfg.Address = endpoint.address
	cfg.Insecure = endpoint.insecure
	ensureTimeoutsPayment(cfg)

	a.logger.Info("Resolved payment orchestrator address from discovery",
		zap.String("service", selected.Service),
		zap.String("service_id", selected.ID),
		zap.String("instance_id", selected.InstanceID),
		zap.String("invoke_uri", endpoint.raw),
		zap.String("address", endpoint.address),
		zap.Bool("insecure", endpoint.insecure))

	return true, endpoint
}

func (a *APIImp) resolvePaymentQuotationAddress(services []discovery.ServiceSummary, orchestratorFound bool, orchestratorEndpoint discoveryEndpoint) {
	endpoint, selected, ok := selectServiceEndpoint(
		services,
		paymentQuotationDiscoveryServiceNames,
		[]string{discovery.OperationPaymentQuote},
	)
	if !ok {
		cfg := a.config.PaymentQuotation
		if cfg != nil && strings.TrimSpace(cfg.Address) != "" {
			return
		}
		if !orchestratorFound {
			return
		}
		// Fall back to orchestrator endpoint when quotation service is not announced.
		endpoint = orchestratorEndpoint
		selected = discovery.ServiceSummary{Service: "PAYMENTS_ORCHESTRATOR"}
	}

	cfg := ensurePaymentQuotationConfig(a.config)
	cfg.Address = endpoint.address
	cfg.Insecure = endpoint.insecure
	ensureTimeoutsPayment(cfg)

	a.logger.Info("Resolved payment quotation address from discovery",
		zap.String("service", selected.Service),
		zap.String("service_id", selected.ID),
		zap.String("instance_id", selected.InstanceID),
		zap.String("invoke_uri", endpoint.raw),
		zap.String("address", endpoint.address),
		zap.Bool("insecure", endpoint.insecure))
}

func (a *APIImp) resolvePaymentMethodsAddress(services []discovery.ServiceSummary) {
	endpoint, selected, ok := selectServiceEndpoint(
		services,
		paymentMethodsDiscoveryServiceNames,
		[]string{discovery.OperationPaymentMethodsRead},
	)
	if !ok {
		return
	}

	cfg := ensurePaymentMethodsConfig(a.config)
	cfg.Address = endpoint.address
	cfg.Insecure = endpoint.insecure
	ensureTimeoutsPayment(cfg)

	a.logger.Info("Resolved payment methods address from discovery",
		zap.String("service", selected.Service),
		zap.String("service_id", selected.ID),
		zap.String("instance_id", selected.InstanceID),
		zap.String("invoke_uri", endpoint.raw),
		zap.String("address", endpoint.address),
		zap.Bool("insecure", endpoint.insecure))
}

func selectServiceEndpoint(services []discovery.ServiceSummary, serviceNames []string, requiredOps []string) (discoveryEndpoint, discovery.ServiceSummary, bool) {
	selections := make([]serviceSelection, 0)
	for _, svc := range services {
		if !svc.Healthy {
			continue
		}
		if strings.TrimSpace(svc.InvokeURI) == "" {
			continue
		}
		nameRank, ok := serviceRank(svc.Service, serviceNames)
		if !ok {
			continue
		}
		endpoint, err := parseDiscoveryInvokeURI(svc.InvokeURI)
		if err != nil {
			continue
		}
		selections = append(selections, serviceSelection{
			service:  svc,
			endpoint: endpoint,
			opMatch:  discovery.HasAnyOperation(svc.Ops, requiredOps),
			nameRank: nameRank,
		})
	}
	if len(selections) == 0 {
		return discoveryEndpoint{}, discovery.ServiceSummary{}, false
	}

	sort.Slice(selections, func(i, j int) bool {
		if selections[i].opMatch != selections[j].opMatch {
			return selections[i].opMatch
		}
		if selections[i].nameRank != selections[j].nameRank {
			return selections[i].nameRank < selections[j].nameRank
		}
		if selections[i].service.ID != selections[j].service.ID {
			return selections[i].service.ID < selections[j].service.ID
		}
		return selections[i].service.InstanceID < selections[j].service.InstanceID
	})

	selected := selections[0]
	return selected.endpoint, selected.service, true
}

func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork string, requiredOps []string) (discoveryEndpoint, discovery.GatewaySummary, bool) {
	preferredNetwork = strings.TrimSpace(preferredNetwork)
	selections := make([]gatewaySelection, 0)

	for _, gateway := range gateways {
		if !gateway.Healthy {
			continue
		}
		if !strings.EqualFold(strings.TrimSpace(gateway.Rail), discovery.RailCrypto) {
			continue
		}
		if strings.TrimSpace(gateway.InvokeURI) == "" {
			continue
		}
		endpoint, err := parseDiscoveryInvokeURI(gateway.InvokeURI)
		if err != nil {
			continue
		}
		selections = append(selections, gatewaySelection{
			gateway:      gateway,
			endpoint:     endpoint,
			networkMatch: preferredNetwork != "" && strings.EqualFold(strings.TrimSpace(gateway.Network), preferredNetwork),
			opMatch:      discovery.HasAnyOperation(gateway.Ops, requiredOps),
		})
	}
	if len(selections) == 0 {
		return discoveryEndpoint{}, discovery.GatewaySummary{}, false
	}

	sort.Slice(selections, func(i, j int) bool {
		if selections[i].networkMatch != selections[j].networkMatch {
			return selections[i].networkMatch
		}
		if selections[i].opMatch != selections[j].opMatch {
			return selections[i].opMatch
		}
		if selections[i].gateway.RoutingPriority != selections[j].gateway.RoutingPriority {
			return selections[i].gateway.RoutingPriority > selections[j].gateway.RoutingPriority
		}
		if selections[i].gateway.ID != selections[j].gateway.ID {
			return selections[i].gateway.ID < selections[j].gateway.ID
		}
		return selections[i].gateway.InstanceID < selections[j].gateway.InstanceID
	})

	selected := selections[0]
	return selected.endpoint, selected.gateway, true
}

func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty")
	}

	// Without a scheme we expect a plain host:port target.
	if !strings.Contains(raw, "://") {
		if _, _, err := net.SplitHostPort(raw); err != nil {
			return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err)
		}
		return discoveryEndpoint{
			address:  raw,
			insecure: true,
			raw:      raw,
		}, nil
	}

	parsed, err := url.Parse(raw)
	if err != nil {
		return discoveryEndpoint{}, err
	}

	switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
	case "grpc":
		address := strings.TrimSpace(parsed.Host)
		if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
			return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr)
		}
		return discoveryEndpoint{
			address:  address,
			insecure: true,
			raw:      raw,
		}, nil
	case "grpcs":
		address := strings.TrimSpace(parsed.Host)
		if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
			return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr)
		}
		return discoveryEndpoint{
			address:  address,
			insecure: false,
			raw:      raw,
		}, nil
	case "dns", "passthrough":
		// gRPC resolver targets such as dns:///service:port.
		return discoveryEndpoint{
			address:  raw,
			insecure: true,
			raw:      raw,
		}, nil
	default:
		return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme)
	}
}

func serviceRank(service string, names []string) (int, bool) {
	service = strings.TrimSpace(service)
	if service == "" {
		return 0, false
	}
	for i, name := range names {
		if strings.EqualFold(service, strings.TrimSpace(name)) {
			return i, true
		}
	}
	return 0, false
}

func ensureLedgerConfig(cfg *eapi.Config) *eapi.LedgerConfig {
	if cfg == nil {
		return nil
	}
	if cfg.Ledger == nil {
		cfg.Ledger = &eapi.LedgerConfig{}
	}
	return cfg.Ledger
}

func ensurePaymentOrchestratorConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
	if cfg == nil {
		return nil
	}
	if cfg.PaymentOrchestrator == nil {
		cfg.PaymentOrchestrator = &eapi.PaymentOrchestratorConfig{}
	}
	return cfg.PaymentOrchestrator
}

func ensurePaymentQuotationConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
	if cfg == nil {
		return nil
	}
	if cfg.PaymentQuotation == nil {
		cfg.PaymentQuotation = &eapi.PaymentOrchestratorConfig{}
	}
	return cfg.PaymentQuotation
}

func ensurePaymentMethodsConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig {
	if cfg == nil {
		return nil
	}
	if cfg.PaymentMethods == nil {
		cfg.PaymentMethods = &eapi.PaymentOrchestratorConfig{}
	}
	return cfg.PaymentMethods
}

func ensureTimeoutsLedger(cfg *eapi.LedgerConfig) {
	if cfg == nil {
		return
	}
	if cfg.DialTimeoutSeconds <= 0 {
		cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
	}
	if cfg.CallTimeoutSeconds <= 0 {
		cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
	}
}

func ensureTimeoutsChainGateway(cfg *eapi.ChainGatewayConfig) {
	if cfg == nil {
		return
	}
	if cfg.DialTimeoutSeconds <= 0 {
		cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
	}
	if cfg.CallTimeoutSeconds <= 0 {
		cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
	}
}

func ensureTimeoutsPayment(cfg *eapi.PaymentOrchestratorConfig) {
	if cfg == nil {
		return
	}
	if cfg.DialTimeoutSeconds <= 0 {
		cfg.DialTimeoutSeconds = defaultClientDialTimeoutSecs
	}
	if cfg.CallTimeoutSeconds <= 0 {
		cfg.CallTimeoutSeconds = defaultClientCallTimeoutSecs
	}
}
