package globalservice

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

	"github.com/pingcap/log"
	"github.com/tidbcloud/observability/pkg/globalservice/structs"
	"go.uber.org/zap"
)

// LogicalRegionRouter defines the interface for logical region routing
type LogicalRegionRouter interface {
	// GetLogicalRegionRouteInfo returns the route info for the logical region
	GetLogicalRegionRouteInfo(ctx context.Context, routingCtx structs.RoutingContext) (*structs.RegionalRouter, error)

	// GetLogicalRegionName determines the logical region name for the given routing context
	GetLogicalRegionName(ctx context.Context, routingCtx structs.RoutingContext) (string, error)

	// IsLogicalRegionEnabled checks if logical region routing is enabled
	IsLogicalRegionEnabled() bool

	// BuildPatternCache builds pattern caches for all regions
	BuildPatternCache(routes structs.RouteTable)
}

// compiledPattern represents a pre-compiled regex pattern with its specificity
type compiledPattern struct {
	pattern     string
	regex       *regexp.Regexp
	destination string
	specificity int // higher number = more specific
}

// logicalRegionRouter implements LogicalRegionRouter interface
type logicalRegionRouter struct {
	metaGetter MetaCacheGetter
	// patternCache holds wildcard/regex patterns, sorted by specificity (most specific first).
	patternCache map[string][]*compiledPattern
	// exactMatchCache holds non-wildcard patterns keyed by the normalized 4-segment
	// route key, used for O(1) exact lookups before falling back to regex matching.
	exactMatchCache map[string]map[string]string
	cacheMutex      sync.RWMutex
}

// NewLogicalRegionRouter creates a new logical region router instance
func NewLogicalRegionRouter(metaGetter MetaCacheGetter) LogicalRegionRouter {
	return &logicalRegionRouter{
		metaGetter:      metaGetter,
		patternCache:    make(map[string][]*compiledPattern),
		exactMatchCache: make(map[string]map[string]string),
	}
}

// routeKeySeparator is the delimiter used in route keys and patterns.
const routeKeySeparator = ":"

// normalizeRoutePattern upgrades a legacy 3-segment pattern to the 4-segment
// format by inserting a wildcard projectID segment:
//
//	{bizType}:{tenantID}:{applicationID} -> {bizType}:{tenantID}:.*:{applicationID}
//
// Patterns that are already 4 segments (or fewer/more than 3) are returned
// unchanged.
func normalizeRoutePattern(pattern string) string {
	parts := strings.Split(pattern, routeKeySeparator)
	if len(parts) == 3 {
		// Insert wildcard projectID segment between tenant (index 1) and application (index 2).
		return parts[0] + routeKeySeparator + parts[1] + routeKeySeparator + ".*" + routeKeySeparator + parts[2]
	}
	return pattern
}

// hasRegexMeta reports whether the pattern contains regex metacharacters that
// require regex matching rather than plain string comparison.
func hasRegexMeta(pattern string) bool {
	return strings.ContainsAny(pattern, "*[](){}+^$|\\")
}

// calculatePatternSpecificity returns specificity score for pattern sorting
func calculatePatternSpecificity(pattern string) int {
	parts := strings.Split(pattern, ":")

	specificity := 0
	for _, part := range parts {
		if part != ".*" {
			specificity += 10 // non-wildcard parts are more specific
		}
		specificity += 1 // each part adds base specificity
	}
	return specificity
}

// BuildPatternCache builds pattern caches for all regions proactively
func (r *logicalRegionRouter) BuildPatternCache(routes structs.RouteTable) {
	r.cacheMutex.Lock()
	defer r.cacheMutex.Unlock()

	// Clear existing caches
	r.patternCache = make(map[string][]*compiledPattern)
	r.exactMatchCache = make(map[string]map[string]string)

	// Build caches for all regions
	for k, regionRoutes := range routes {
		var patterns []*compiledPattern
		exactMap := make(map[string]string)

		for pattern, destination := range regionRoutes {
			// Upgrade legacy 3-segment patterns to the 4-segment format so they
			// remain compatible with the new BuildBizRouteKey output.
			normalizedPattern := normalizeRoutePattern(pattern)

			if !hasRegexMeta(normalizedPattern) {
				// Exact match candidate (no wildcards or regex metacharacters).
				exactMap[normalizedPattern] = destination
				continue
			}

			// Convert wildcard pattern to regex
			regex, err := regexp.Compile("^" + normalizedPattern + "$")
			if err != nil {
				log.Error("Invalid regex pattern, skipping",
					zap.String("pattern", pattern),
					zap.String("normalized_pattern", normalizedPattern),
					zap.String("routeKey", k),
					zap.Error(err))
				continue
			}

			patterns = append(patterns, &compiledPattern{
				pattern:     normalizedPattern,
				regex:       regex,
				destination: destination,
				specificity: calculatePatternSpecificity(normalizedPattern),
			})
		}

		// Sort by specificity (most specific first)
		sort.Slice(patterns, func(i, j int) bool {
			return patterns[i].specificity > patterns[j].specificity
		})

		r.patternCache[k] = patterns
		r.exactMatchCache[k] = exactMap

		log.Info("Built pattern cache for region",
			zap.String("routeKey", k),
			zap.Int("pattern_count", len(patterns)),
			zap.Int("exact_count", len(exactMap)))
	}

	log.Info("Built pattern caches for all regions",
		zap.Int("region_count", len(routes)))
}

// IsLogicalRegionEnabled checks if logical region routing is enabled
func (r *logicalRegionRouter) IsLogicalRegionEnabled() bool {
	if r == nil || r.metaGetter == nil {
		return false
	}

	meta := r.metaGetter.GetGlobalMetaCache()
	if meta == nil {
		return false
	}

	return len(meta.Routes) > 0
}

// GetLogicalRegionName determines the logical region name for routing
func (r *logicalRegionRouter) GetLogicalRegionName(ctx context.Context, routingCtx structs.RoutingContext) (string, error) {
	// When the caller already knows the logical region name (e.g. Kong plugin
	// forwarding X-Logical-Region directly), skip the route-table lookup.
	if routingCtx.LogicalRegion != "" {
		return routingCtx.LogicalRegion, nil
	}

	meta := r.metaGetter.GetGlobalMetaCache()

	// Get route table for the region
	_, exists := meta.Routes[routingCtx.BuildRegionRouteKey()]
	if !exists {
		log.Debug("No logical region routes found for region",
			zap.String("region", routingCtx.Region))
		return routingCtx.Region, fmt.Errorf("no logical region routes found for region %s", routingCtx.Region)
	}

	// Try to find matching route patterns
	logicalRegion, found := r.findMatchingRoute(routingCtx)
	if found {
		log.Debug("Found matching route pattern",
			zap.String("logical_region", logicalRegion),
			zap.Any("routing_context", routingCtx))
		return logicalRegion, nil
	}

	// No explicit routing found
	log.Debug("No explicit route found",
		zap.Any("routing_context", routingCtx))
	return routingCtx.Region, fmt.Errorf("no logical region routes found for region %s", routingCtx.Region)
}

// findMatchingRoute searches for matching route patterns using pre-built caches.
// It first tries an O(1) exact match against the normalized exact cache, then
// falls back to specificity-ordered regex matching against the pattern cache.
func (r *logicalRegionRouter) findMatchingRoute(routingCtx structs.RoutingContext) (string, bool) {
	key := routingCtx.BuildBizRouteKey()
	routeKey := routingCtx.BuildRegionRouteKey()

	r.cacheMutex.RLock()
	defer r.cacheMutex.RUnlock()

	// Try exact matches first (fastest path)
	if exactPatterns, ok := r.exactMatchCache[routeKey]; ok {
		if logicalRegion, exists := exactPatterns[key]; exists {
			log.Debug("Found exact route match",
				zap.String("pattern", key),
				zap.String("logical_region", logicalRegion))
			return logicalRegion, true
		}
	}

	// Fall back to regex matching against pre-built pattern cache
	for _, compiledPattern := range r.patternCache[routeKey] {
		if compiledPattern.regex.MatchString(key) {
			log.Debug("Found regex route match",
				zap.String("pattern", compiledPattern.pattern),
				zap.String("matched_key", key),
				zap.String("logical_region", compiledPattern.destination),
				zap.Int("specificity", compiledPattern.specificity))
			return compiledPattern.destination, true
		}
	}

	return "", false
}

func (r *logicalRegionRouter) GetLogicalRegionRouteInfo(ctx context.Context, routingCtx structs.RoutingContext) (*structs.RegionalRouter, error) {
	// Get logical region name
	logicalRegion, err := r.GetLogicalRegionName(ctx, routingCtx)
	if err != nil {
		return nil, fmt.Errorf("failed to get logical region name: %w", err)
	}

	meta := r.metaGetter.GetGlobalMetaCache()
	if meta == nil {
		return nil, errors.New("global meta cache is nil")
	}

	// When vendor is known, restrict search to that vendor's regions.
	// When vendor is empty (e.g. caller supplied X-Logical-Region directly
	// without a vendor/region triple), scan all vendors.
	for vendor, vendorRegions := range meta.Vendors {
		if routingCtx.Vendor != "" && vendor != routingCtx.Vendor {
			continue
		}
		for _, regionInfo := range vendorRegions {
			if routingCtx.Region != "" && regionInfo.Region != routingCtx.Region {
				continue
			}
			for _, router := range regionInfo.Routers {
				if router.LogicalRegionName == logicalRegion {
					log.Debug("Found logical region route address",
						zap.String("logical_region", logicalRegion),
						zap.String("vendor", string(vendor)),
						zap.String("region", regionInfo.Region),
						zap.String("address", router.DnsAddress))
					return &router, nil
				}
			}
		}
	}

	return nil, fmt.Errorf("no route address found for logical region %s", logicalRegion)
}
