package serverimp

import (
	"context"
	"fmt"
	"net"
	"net/url"
	"sort"
	"strings"
	"sync"
	"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/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
)

const discoveryLogThrottle = 30 * time.Second

var (
	feesRequiredOps   = []string{discovery.OperationFeeCalc}
	ledgerRequiredOps = discovery.LedgerServiceOperations()
	oracleRequiredOps = []string{discovery.OperationFXQuote}
)

type discoveryEndpoint struct {
	address  string
	insecure bool
	raw      string
}

func (e discoveryEndpoint) key() string {
	return fmt.Sprintf("%s|%t", e.address, e.insecure)
}

type discoveryClientResolver struct {
	logger   mlogger.Logger
	registry *discovery.Registry

	mu sync.Mutex

	feesConn     *grpc.ClientConn
	feesEndpoint discoveryEndpoint

	ledgerClient   ledgerclient.Client
	ledgerEndpoint discoveryEndpoint

	oracleClient   oracleclient.Client
	oracleEndpoint discoveryEndpoint

	chainClients map[string]chainclient.Client

	lastSelection map[string]string
	lastMissing   map[string]time.Time
}

func newDiscoveryClientResolver(logger mlogger.Logger, registry *discovery.Registry) *discoveryClientResolver {
	if logger != nil {
		logger = logger.Named("discovery_clients")
	}
	return &discoveryClientResolver{
		logger:        logger,
		registry:      registry,
		chainClients:  map[string]chainclient.Client{},
		lastSelection: map[string]string{},
		lastMissing:   map[string]time.Time{},
	}
}

func (r *discoveryClientResolver) Close() {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.feesConn != nil {
		_ = r.feesConn.Close()
		r.feesConn = nil
	}
	if r.ledgerClient != nil {
		_ = r.ledgerClient.Close()
		r.ledgerClient = nil
	}
	if r.oracleClient != nil {
		_ = r.oracleClient.Close()
		r.oracleClient = nil
	}
	for key, client := range r.chainClients {
		if client != nil {
			_ = client.Close()
		}
		delete(r.chainClients, key)
	}
}

func (r *discoveryClientResolver) FeesAvailable() bool {
	_, ok := r.findEntry(mservice.BillingFees, "", "", feesRequiredOps)
	return ok
}

func (r *discoveryClientResolver) LedgerAvailable() bool {
	_, ok := r.findEntry(mservice.Ledger, "", "", ledgerRequiredOps)
	return ok
}

func (r *discoveryClientResolver) OracleAvailable() bool {
	_, ok := r.findEntry(mservice.FXOracle, "", "", oracleRequiredOps)
	return ok
}

func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEngineClient, error) {
	entry, ok := r.findEntry(mservice.BillingFees, "", "", feesRequiredOps)
	if !ok {
		return nil, merrors.NoData("discovery: fees service unavailable")
	}
	endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
	if err != nil {
		r.logMissing("fees", "invalid fees invoke uri", entry.InvokeURI, err)
		return nil, err
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	if r.feesConn == nil || r.feesEndpoint.key() != endpoint.key() || r.feesEndpoint.address != endpoint.address {
		if r.feesConn != nil {
			_ = r.feesConn.Close()
			r.feesConn = nil
		}
		conn, dialErr := dialGrpc(ctx, endpoint)
		if dialErr != nil {
			r.logMissing("fees", "Failed to dial fees service", endpoint.raw, dialErr)
			return nil, dialErr
		}
		r.feesConn = conn
		r.feesEndpoint = endpoint
	}

	return feesv1.NewFeeEngineClient(r.feesConn), nil
}

func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclient.Client, error) {
	entry, ok := r.findEntry(mservice.Ledger, "", "", ledgerRequiredOps)
	if !ok {
		return nil, merrors.NoData("discovery: ledger service unavailable")
	}
	endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
	if err != nil {
		r.logMissing("ledger", "Invalid ledger invoke uri", entry.InvokeURI, err)
		return nil, err
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	if r.ledgerClient == nil || r.ledgerEndpoint.key() != endpoint.key() || r.ledgerEndpoint.address != endpoint.address {
		if r.ledgerClient != nil {
			_ = r.ledgerClient.Close()
			r.ledgerClient = nil
		}
		client, dialErr := ledgerclient.New(ctx, ledgerclient.Config{
			Address:  endpoint.address,
			Insecure: endpoint.insecure,
		})
		if dialErr != nil {
			r.logMissing("ledger", "Failed to dial ledger service", endpoint.raw, dialErr)
			return nil, dialErr
		}
		r.ledgerClient = client
		r.ledgerEndpoint = endpoint
	}

	return r.ledgerClient, nil
}

func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclient.Client, error) {
	entry, ok := r.findEntry(mservice.FXOracle, "", "", oracleRequiredOps)
	if !ok {
		return nil, merrors.NoData("discovery: oracle service unavailable")
	}
	endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
	if err != nil {
		r.logMissing("oracle", "Invalid oracle invoke uri", entry.InvokeURI, err)
		return nil, err
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	if r.oracleClient == nil || r.oracleEndpoint.key() != endpoint.key() || r.oracleEndpoint.address != endpoint.address {
		if r.oracleClient != nil {
			_ = r.oracleClient.Close()
			r.oracleClient = nil
		}
		client, dialErr := oracleclient.New(ctx, oracleclient.Config{
			Address:  endpoint.address,
			Insecure: endpoint.insecure,
		})
		if dialErr != nil {
			r.logMissing("oracle", "Failed to dial oracle service", endpoint.raw, dialErr)
			return nil, dialErr
		}
		r.oracleClient = client
		r.oracleEndpoint = endpoint
	}

	return r.oracleClient, nil
}

func (r *discoveryClientResolver) ChainClient(ctx context.Context, invokeURI string) (chainclient.Client, error) {
	endpoint, err := parseDiscoveryEndpoint(invokeURI)
	if err != nil {
		r.logMissing("chain", "Invalid chain gateway invoke uri", invokeURI, err)
		return nil, err
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	if client, ok := r.chainClients[endpoint.key()]; ok && client != nil {
		return client, nil
	}

	client, dialErr := chainclient.New(ctx, chainclient.Config{
		Address:  endpoint.address,
		Insecure: endpoint.insecure,
	})
	if dialErr != nil {
		r.logMissing("chain", "Failed to dial chain gateway", endpoint.raw, dialErr)
		return nil, dialErr
	}
	r.chainClients[endpoint.key()] = client
	return client, nil
}

func (r *discoveryClientResolver) PaymentGatewayClient(ctx context.Context, invokeURI string) (chainclient.Client, error) {
	endpoint, err := parseDiscoveryEndpoint(invokeURI)
	if err != nil {
		r.logMissing("payment_gateway", "Invalid payment gateway invoke uri", invokeURI, err)
		return nil, err
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	if client, ok := r.chainClients[endpoint.key()]; ok && client != nil {
		return client, nil
	}

	client, dialErr := chainclient.New(ctx, chainclient.Config{
		Address:  endpoint.address,
		Insecure: endpoint.insecure,
	})
	if dialErr != nil {
		r.logMissing("payment_gateway", "Failed to dial payment gateway", endpoint.raw, dialErr)
		return nil, dialErr
	}
	r.chainClients[endpoint.key()] = client
	return client, nil
}

func (r *discoveryClientResolver) findEntry(service mservice.Type, rail string, network string, requiredOps []string) (*discovery.RegistryEntry, bool) {
	key := discoverySelectionKey(service, rail, network)

	if r == nil || r.registry == nil {
		r.logMissing(key, "Discovery registry unavailable", "", nil)
		return nil, false
	}

	type discoveryMatch struct {
		entry   discovery.RegistryEntry
		opMatch bool
	}

	entries := r.registry.List(time.Now(), true)
	matches := make([]discoveryMatch, 0)
	for _, entry := range entries {
		if service != "" && !matchesService(entry.Service, service) {
			continue
		}
		if rail != "" && !strings.EqualFold(strings.TrimSpace(entry.Rail), rail) {
			continue
		}
		if network != "" && !strings.EqualFold(strings.TrimSpace(entry.Network), network) {
			continue
		}
		matches = append(matches, discoveryMatch{
			entry:   entry,
			opMatch: discovery.HasAnyOperation(entry.Operations, requiredOps),
		})
	}

	if len(matches) == 0 {
		r.logMissing(key, "Discovery entry missing", "", nil)
		return nil, false
	}

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

	entry := matches[0].entry
	entryKey := discoveryEntryKey(entry)
	r.logSelection(key, entryKey, entry)
	return &entry, true
}

func discoverySelectionKey(service mservice.Type, rail, network string) string {
	serviceName := strings.TrimSpace(service)
	railName := strings.ToUpper(strings.TrimSpace(rail))
	networkName := strings.ToUpper(strings.TrimSpace(network))

	switch {
	case serviceName != "" && railName != "" && networkName != "":
		return fmt.Sprintf("%s|%s|%s", serviceName, railName, networkName)
	case serviceName != "" && railName != "":
		return fmt.Sprintf("%s|%s", serviceName, railName)
	case serviceName != "":
		return serviceName
	case railName != "" && networkName != "":
		return fmt.Sprintf("rail:%s|%s", railName, networkName)
	case railName != "":
		return "rail:" + railName
	default:
		return "discovery"
	}
}

func (r *discoveryClientResolver) logSelection(key, entryKey string, entry discovery.RegistryEntry) {
	r.mu.Lock()
	last := r.lastSelection[key]
	if last == entryKey {
		r.mu.Unlock()
		return
	}
	r.lastSelection[key] = entryKey
	r.mu.Unlock()
	r.logger.Info("Discovery endpoint selected",
		zap.String("service_key", key),
		zap.String("service", entry.Service),
		zap.String("rail", entry.Rail),
		zap.String("network", entry.Network),
		zap.String("entry_id", entry.ID),
		zap.String("instance_id", entry.InstanceID),
		zap.String("invoke_uri", entry.InvokeURI))
}

func (r *discoveryClientResolver) logMissing(key, message, invokeURI string, err error) {
	now := time.Now()
	r.mu.Lock()
	last := r.lastMissing[key]
	if !last.IsZero() && now.Sub(last) < discoveryLogThrottle {
		r.mu.Unlock()
		return
	}
	r.lastMissing[key] = now
	r.mu.Unlock()
	fields := []zap.Field{zap.String("service_key", key)}
	if invokeURI != "" {
		fields = append(fields, zap.String("invoke_uri", strings.TrimSpace(invokeURI)))
	}
	if err != nil {
		fields = append(fields, zap.Error(err))
	}
	r.logger.Warn(message, fields...)
}

func discoveryEntryKey(entry discovery.RegistryEntry) string {
	return fmt.Sprintf("%s|%s|%s|%s|%s|%s",
		strings.TrimSpace(entry.Service),
		strings.TrimSpace(entry.ID),
		strings.TrimSpace(entry.InstanceID),
		strings.TrimSpace(entry.Rail),
		strings.TrimSpace(entry.Network),
		strings.TrimSpace(entry.InvokeURI))
}

func matchesService(service string, candidate mservice.Type) bool {
	service = strings.TrimSpace(service)
	if service == "" || strings.TrimSpace(candidate) == "" {
		return false
	}
	return strings.EqualFold(service, strings.TrimSpace(candidate))
}

func parseDiscoveryEndpoint(raw string) (discoveryEndpoint, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri is required")
	}

	// No scheme: treat as a gRPC target (default to insecure grpc).
	if !strings.Contains(raw, "://") {
		if _, _, splitErr := net.SplitHostPort(raw); splitErr != nil {
			return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
		}
		return discoveryEndpoint{address: raw, insecure: true, raw: raw}, nil
	}

	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" {
		if err != nil {
			return discoveryEndpoint{}, err
		}
		return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
	}

	scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
	switch scheme {
	case "grpc":
		address := strings.TrimSpace(parsed.Host)
		if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
			return discoveryEndpoint{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
		}
		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{}, merrors.InvalidArgument("discovery: invoke uri must include host:port")
		}
		return discoveryEndpoint{address: address, insecure: false, raw: raw}, nil
	case "dns", "passthrough":
		return discoveryEndpoint{address: raw, insecure: true, raw: raw}, nil
	default:
		return discoveryEndpoint{}, merrors.InvalidArgument("discovery: unsupported invoke uri scheme")
	}
}

func dialGrpc(_ context.Context, endpoint discoveryEndpoint) (*grpc.ClientConn, error) {
	dialOpts := []grpc.DialOption{}
	if endpoint.insecure {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	} else {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(nil)))
	}
	return grpc.NewClient(endpoint.address, dialOpts...)
}
