package route

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"sync"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

// ServiceDiscovery resolves service names in a namespace by regex pattern.
// Implementations are expected to cache results for performance.
type ServiceDiscovery interface {
	// FindService returns the first service name matching the pattern within the namespace.
	// The pattern is a regular expression (e.g., "^tidb-.*-pd$").
	FindService(ctx context.Context, namespace, pattern string) (string, error)
}

// cachedResult stores a service discovery result (success or error).
type cachedResult struct {
	service string
	err     error
}

// k8sDiscovery implements ServiceDiscovery using the Kubernetes API.
//
// Caching behavior:
//   - Only successful results are cached permanently per (namespace, pattern) pair
//   - Errors (including "not found") are NOT cached, allowing retry on next request
//   - This enables dynamic service discovery for services created after startup
type k8sDiscovery struct {
	client kubernetes.Interface
	cache  sync.Map // key: namespace|pattern, value: cachedResult
}

// NewServiceDiscovery creates a discovery backed by in-cluster Kubernetes client.
func NewServiceDiscovery() (ServiceDiscovery, error) {
	restConfig, err := rest.InClusterConfig()
	if err != nil {
		return nil, fmt.Errorf("get in-cluster config: %w", err)
	}

	client, err := kubernetes.NewForConfig(restConfig)
	if err != nil {
		return nil, fmt.Errorf("create kubernetes client: %w", err)
	}

	return &k8sDiscovery{
		client: client,
	}, nil
}

// NewServiceDiscoveryWithClient creates a discovery using provided Kubernetes client (for tests).
func NewServiceDiscoveryWithClient(client kubernetes.Interface) ServiceDiscovery {
	return &k8sDiscovery{
		client: client,
	}
}

func (d *k8sDiscovery) cacheKey(namespace, pattern string) string {
	return namespace + "|" + pattern
}

// FindService returns the first service name matching the pattern within the namespace.
//
// The pattern is compiled as a regular expression. Example patterns:
//   - "^tidb-.*-pd$" matches "tidb-123-pd", "tidb-456-pd"
//   - "tibr-proxy-.*" matches "tibr-proxy-123"
//
// Only successful results are cached. Errors (API failures, "not found") are not cached,
// allowing automatic retry on subsequent requests.
func (d *k8sDiscovery) FindService(ctx context.Context, namespace, pattern string) (string, error) {
	key := d.cacheKey(namespace, pattern)
	if val, ok := d.cache.Load(key); ok {
		c := val.(cachedResult)
		return c.service, c.err
	}

	re, err := regexp.Compile(pattern)
	if err != nil {
		return "", fmt.Errorf("compile pattern %q: %w", pattern, err)
	}

	svcs, err := d.client.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{})
	if err != nil {
		return "", fmt.Errorf("list services in namespace %s: %w", namespace, err)
	}

	for _, svc := range svcs.Items {
		if re.MatchString(svc.Name) {
			c := cachedResult{svc.Name, nil}
			d.cache.Store(key, c)
			return svc.Name, nil
		}
	}

	return "", fmt.Errorf("no service matches pattern %s in namespace %s", pattern, namespace)
}

// MockDiscovery is a test double for ServiceDiscovery.
// Exported for use in other packages' tests.
type MockDiscovery struct {
	Results map[string]string // key: "namespace|pattern", value: service name
	Errs    map[string]error  // key: "namespace|pattern", value: error
	Calls   int               // number of FindService calls
}

// FindService implements ServiceDiscovery.
func (m *MockDiscovery) FindService(_ context.Context, namespace, pattern string) (string, error) {
	m.Calls++
	key := namespace + "|" + pattern
	if m.Errs != nil {
		if err, ok := m.Errs[key]; ok {
			return "", err
		}
	}
	if m.Results != nil {
		if svc, ok := m.Results[key]; ok {
			return svc, nil
		}
	}
	return "", errors.New("no service found")
}
