package route

import (
	"context"
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/tidbcloud/service-proxy/pkg/config"
)

// dnsLabelRegex validates Kubernetes service names (DNS-1123 subdomain).
// Must be lowercase alphanumeric, may contain hyphens, must start/end with alphanumeric.
var dnsLabelRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)

// Route represents a matched route with resolved backend URL.
type Route struct {
	Name       string
	Config     *config.RouteConfig
	BackendURL *url.URL // Base URL: scheme://host:port (no path)
	Vars       *PathVars
}

// routeEntry wraps the static config together with runtime-usable helpers
// (e.g., compiled matcher) so we avoid mutating RouteConfig and keep hot-path
// lookups fast and cache-friendly.
type routeEntry struct {
	cfg                *config.RouteConfig
	serviceNamePattern *regexp.Regexp
	matcher            Matcher  // Matcher for prefix/path matching
	pathParams         []string // Parameter names for serviceNameTemplate resolution
}

// router manages route matching and resolution.
type router struct {
	clusterRoutes []*routeEntry
	serviceRoutes []*routeEntry
	discovery     ServiceDiscovery
}

// Router defines the interface for route matching.
type Router interface {
	Match(ctx context.Context, path string) (*Route, error)
}

// NewRouter creates a new router.
func NewRouter(cfg *config.Config) (Router, error) {
	discovery, err := NewServiceDiscovery()
	if err != nil {
		return nil, fmt.Errorf("failed to create service discovery: %w", err)
	}
	return NewRouterWithDiscovery(cfg, discovery)
}

// NewRouterWithDiscovery creates a new router with a custom discovery (for testing).
func NewRouterWithDiscovery(cfg *config.Config, discovery ServiceDiscovery) (Router, error) {
	r := &router{
		discovery: discovery,
	}
	if err := r.loadConfig(cfg); err != nil {
		return nil, err
	}
	return r, nil
}

// loadConfig loads route configurations.
func (r *router) loadConfig(cfg *config.Config) error {
	r.clusterRoutes = make([]*routeEntry, 0, len(cfg.ClusterRoutes))
	r.serviceRoutes = make([]*routeEntry, 0, len(cfg.ServiceRoutes))

	for i := range cfg.ClusterRoutes {
		routeCfg := &cfg.ClusterRoutes[i]
		prefixPattern, params, err := parsePrefixPattern(routeCfg.Match.Prefix)
		if err != nil {
			return fmt.Errorf("failed to parse prefix of route %s: %w", routeCfg.Name, err)
		}

		re, err := compileServicePattern(routeCfg.Backend.ServiceNamePattern)
		if err != nil {
			return fmt.Errorf("failed to compile service pattern of route %s: %w", routeCfg.Name, err)
		}
		if re == nil && routeCfg.Backend.ServiceNameTemplate == "" {
			return fmt.Errorf("route %s: backend.serviceNamePattern is required when serviceNameTemplate is not set", routeCfg.Name)
		}

		// Create appropriate matcher based on prefix pattern
		var matcher Matcher
		if prefixPattern != nil {
			// Has path parameters - use RegexMatcher
			matcher = NewRegexMatcher(prefixPattern, params)
		} else if routeCfg.Match.Prefix != "" {
			// Static prefix - use PrefixMatcher
			matcher = NewPrefixMatcher(routeCfg.Match.Prefix)
		}

		entry := &routeEntry{
			cfg:                routeCfg,
			serviceNamePattern: re,
			matcher:            matcher,
			pathParams:         params,
		}
		r.clusterRoutes = append(r.clusterRoutes, entry)
	}

	for i := range cfg.ServiceRoutes {
		routeCfg := &cfg.ServiceRoutes[i]
		r.serviceRoutes = append(r.serviceRoutes, &routeEntry{cfg: routeCfg})
	}
	return nil
}

// Match finds a matching route for the given path.
// The matching process follows these steps:
//  1. Parse the path to extract PathVars and determine PathType
//  2. Find a matching route entry using the Matcher interface
//  3. Apply match result (PathParams, Remaining) to vars
//  4. Resolve the actual service name for cluster routes
//  5. Build the backend URL
func (r *router) Match(ctx context.Context, path string) (*Route, error) {
	// Step 1: Parse path
	vars, pathType, err := parsePath(path)
	if err != nil {
		log.Error("path pattern match failed", zap.String("path", path), zap.Error(err))
		return nil, err
	}

	// Step 2: Find matching route
	entry, matchResult, err := r.getRouteEntry(pathType, vars)
	if err != nil {
		log.Error("failed to get route entry", zap.Error(err))
		return nil, err
	}

	// Step 3: Apply match result to vars
	if matchResult != nil {
		vars.PathParams = matchResult.PathParams
		vars.Remaining = matchResult.Remaining
	}

	// Step 4: Resolve service name
	if err := r.resolveServiceName(ctx, pathType, vars, entry); err != nil {
		log.Error("failed to resolve service name", zap.Error(err))
		return nil, err
	}

	// Step 5: Build backend URL
	backendURL, err := r.buildBackendURL(entry.cfg, vars)
	if err != nil {
		log.Error("failed to build backend url", zap.Error(err))
		return nil, err
	}

	log.Info("route matched successfully",
		zap.String("route_name", entry.cfg.Name),
		zap.String("backend_url", backendURL.String()),
		zap.String("suffix_path", vars.Remaining),
	)

	return &Route{
		Name:       entry.cfg.Name,
		Config:     entry.cfg,
		BackendURL: backendURL,
		Vars:       vars,
	}, nil
}

// getRouteEntry retrieves the appropriate route entry based on path type.
// For cluster routes, a matching entry must exist in the configuration.
// For service routes, it falls back to a default entry if no explicit config is found.
// Returns the matched entry, match result (for applying PathParams/Remaining), and error.
func (r *router) getRouteEntry(pathType PathType, vars *PathVars) (*routeEntry, *MatchResult, error) {
	switch pathType {
	case PathTypeCluster:
		entry, result := findMatchingRoute(r.clusterRoutes, vars)
		if entry == nil {
			log.Warn("no route config found for cluster route",
				zap.String("service_key", vars.ServiceKey),
				zap.String("cluster_id", vars.ClusterID),
			)
			return nil, nil, fmt.Errorf("no route configured for cluster service: %s", vars.ServiceKey)
		}
		return entry, result, nil
	case PathTypeService:
		entry, result := findMatchingRoute(r.serviceRoutes, vars)
		if entry != nil {
			return entry, result, nil
		}
		log.Debug("no route config found, using defaults for service route",
			zap.String("service_key", vars.ServiceKey),
			zap.String("namespace", vars.Namespace),
			zap.String("port", vars.Port),
		)
		return &routeEntry{
			cfg: &config.RouteConfig{
				Name:    fmt.Sprintf("service:%s:%s:%s", vars.ServiceKey, vars.Namespace, vars.Port),
				Match:   config.MatchConfig{},
				Backend: config.BackendConfig{},
				TLS: config.TLSConfig{
					Enabled: false,
				},
			},
		}, nil, nil // No match result for default route
	default:
		return nil, nil, fmt.Errorf("unsupported path type: %v", pathType)
	}
}

// findMatchingRoute searches for a matching route entry using the Matcher interface.
// Returns the matched entry and match result, or nil if no match found.
//
// Route matching priority:
//   - Routes are evaluated in configuration order (first match wins)
//   - Each entry's Matcher is invoked; first successful match is returned
//   - If an entry has no Matcher but has a static prefix, prefix matching is used
//
// This function has no side effects on vars - the caller is responsible for
// applying the MatchResult to vars.
func findMatchingRoute(entries []*routeEntry, vars *PathVars) (*routeEntry, *MatchResult) {
	if vars == nil {
		return nil, nil
	}

	// Build the path to match against
	pathToMatch := "/" + vars.ServiceKey
	if vars.Remaining != "" {
		pathToMatch += vars.Remaining
	}

	// Try each entry's matcher
	for _, entry := range entries {
		if entry.matcher != nil {
			result := entry.matcher.Match(pathToMatch)
			if result.Matched {
				return entry, result
			}
		} else if entry.cfg.Match.Prefix != "" {
			// Fallback for entries without explicit matcher (static prefix)
			if strings.HasPrefix(pathToMatch, entry.cfg.Match.Prefix) {
				return entry, &MatchResult{
					Matched:   true,
					Remaining: strings.TrimPrefix(pathToMatch, entry.cfg.Match.Prefix),
				}
			}
		}
	}
	return nil, nil
}

// resolveServiceName determines the actual backend service name for cluster routes.
// For service routes (PathTypeService), the service name is already known from the path.
// For cluster routes, it either:
//   - Uses serviceNameTemplate to construct the name from path parameters, or
//   - Uses serviceNamePattern with service discovery to find the matching service
func (r *router) resolveServiceName(ctx context.Context, pathType PathType, vars *PathVars, entry *routeEntry) error {
	// Service routes already have the service name resolved from the path
	if pathType == PathTypeService {
		return nil
	}

	// Strategy 1: Template-based resolution (preferred when configured)
	if entry.cfg.Backend.ServiceNameTemplate != "" {
		serviceName, err := resolveServiceNameFromTemplate(entry.cfg.Backend.ServiceNameTemplate, entry.pathParams, vars)
		if err != nil {
			return err
		}
		vars.ResolvedService = serviceName
		return nil
	}

	// Strategy 2: Pattern-based discovery (requires Kubernetes service discovery)
	if entry.cfg.Backend.ServiceNamePattern == "" {
		return fmt.Errorf("serviceNamePattern not configured for cluster route %s", entry.cfg.Name)
	}
	if r.discovery == nil {
		return fmt.Errorf("service discovery not configured")
	}

	serviceName, err := r.discovery.FindService(ctx, vars.Namespace, entry.cfg.Backend.ServiceNamePattern)
	if err != nil {
		return err
	}

	vars.ResolvedService = serviceName
	return nil
}

// resolveServiceNameFromTemplate constructs a service name by substituting path
// parameters into the template. For example, with template "tibr-proxy-{tibr_id}"
// and PathParams {"tibr_id": "12345"}, it returns "tibr-proxy-12345".
// The resulting service name is validated against Kubernetes DNS naming rules.
func resolveServiceNameFromTemplate(tmpl string, params []string, vars *PathVars) (string, error) {
	name := tmpl
	for _, param := range params {
		value, ok := vars.PathParams[param]
		if !ok || value == "" {
			return "", fmt.Errorf("missing path parameter %q for serviceNameTemplate", param)
		}
		name = strings.ReplaceAll(name, "{"+param+"}", value)
	}
	// Validate the resulting service name is a valid DNS label
	if err := validateDNSLabel(name); err != nil {
		return "", fmt.Errorf("service name from template %q: %w", tmpl, err)
	}
	return name, nil
}

// validateDNSLabel checks if the name is a valid Kubernetes service name (DNS-1123 subdomain).
func validateDNSLabel(name string) error {
	if len(name) == 0 {
		return fmt.Errorf("service name cannot be empty")
	}
	if len(name) > 63 {
		return fmt.Errorf("service name %q exceeds 63 characters", name)
	}
	if !dnsLabelRegex.MatchString(name) {
		return fmt.Errorf("service name %q is not a valid DNS label (must be lowercase alphanumeric with hyphens, starting and ending with alphanumeric)", name)
	}
	return nil
}

// buildBackendURL constructs the base URL for the backend service.
// It determines the scheme (http/https) and port from config or path variables,
// then builds a Kubernetes service DNS URL (scheme://service.namespace.svc:port).
func (r *router) buildBackendURL(cfg *config.RouteConfig, vars *PathVars) (baseURL *url.URL, err error) {
	// Scheme resolution priority: config > TLS detection > default (http)
	scheme := cfg.Backend.Scheme
	if scheme == "" {
		if cfg.TLS.Enabled {
			scheme = "https"
		} else {
			scheme = "http"
		}
	}

	// Port resolution priority: config > path variable
	port := cfg.Backend.Port
	if port == 0 && vars.Port != "" {
		parsed, parseErr := strconv.Atoi(vars.Port)
		if parseErr != nil {
			return nil, BadRequestError{msg: fmt.Sprintf("invalid port %q", vars.Port)}
		}
		port = parsed
	}
	if port <= 0 || port > 65535 {
		return nil, BadRequestError{msg: fmt.Sprintf("port out of range: %d", port)}
	}

	baseURL, err = buildServiceURL(scheme, vars.ResolvedService, vars.Namespace, port)
	if err != nil {
		return nil, err
	}
	return baseURL, nil
}

// buildServiceURL constructs scheme://service.namespace.svc:port.
func buildServiceURL(scheme, serviceName, namespace string, port int) (*url.URL, error) {
	if serviceName == "" {
		return nil, fmt.Errorf("service name is empty")
	}
	if namespace == "" {
		return nil, fmt.Errorf("namespace is empty")
	}
	if port <= 0 || port > 65535 {
		return nil, fmt.Errorf("port out of range: %d", port)
	}
	if scheme == "" {
		scheme = "http"
	}
	raw := fmt.Sprintf("%s://%s.%s.svc:%d", scheme, serviceName, namespace, port)
	return url.Parse(raw)
}
