package config

import (
	"errors"
	"fmt"
	"log/slog"
	"net"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/thushan/olla/internal/core/domain"
)

// Config holds all configuration for the application
type Config struct {

	// ModelAliases maps a virtual model name (alias) to a list of actual model names
	// that backends may know. When a request arrives with an alias model name, olla
	// resolves it to all configured actual model names, finds endpoints that serve any
	// of them, and rewrites the model name in the request to match the selected backend.
	// Example:
	//   model_aliases:
	//     gpt-oss-120b:
	//       - gpt-oss:120b
	//       - gpt-oss-120b-MLX
	//       - gguf_gpt_oss_120b.gguf
	ModelAliases  map[string][]string `yaml:"model_aliases,omitempty"`
	Logging       LoggingConfig       `yaml:"logging"`
	Filename      string              `yaml:"-"`
	Translators   TranslatorsConfig   `yaml:"translators"`
	ModelRegistry ModelRegistryConfig `yaml:"model_registry"`
	Discovery     DiscoveryConfig     `yaml:"discovery"`
	Proxy         ProxyConfig         `yaml:"proxy"`
	Server        ServerConfig        `yaml:"server"`
	Engineering   EngineeringConfig   `yaml:"engineering"`
}

// CorsConfig controls browser cross-origin access to the Olla API.
// This matters when a browser-based client (OpenWebUI, a custom dashboard, etc.)
// calls Olla from a different origin. Non-browser clients (curl, SDKs, other
// services) ignore CORS headers entirely, so enabling this has no effect on them.
//
// Security note: combining AllowCredentials=true with AllowedOrigins=["*"] is
// forbidden by the CORS spec — the browser will reject the response. Validate()
// enforces this at startup rather than letting it silently break at runtime.
//
// ExposedHeaders is intentionally left empty here; the middleware phase will
// automatically expose the full X-Olla-* response header set.
type CorsConfig struct {
	AllowedOrigins   []string `yaml:"allowed_origins"`
	AllowedMethods   []string `yaml:"allowed_methods"`
	AllowedHeaders   []string `yaml:"allowed_headers"`
	ExposedHeaders   []string `yaml:"exposed_headers"`
	MaxAge           int      `yaml:"max_age"`
	Enabled          bool     `yaml:"enabled"`
	AllowCredentials bool     `yaml:"allow_credentials"`
}

// ServerConfig holds HTTP server configuration
type ServerConfig struct {
	Host              string              `yaml:"host"`
	Cors              CorsConfig          `yaml:"cors"`
	RateLimits        ServerRateLimits    `yaml:"rate_limits"`
	RequestLimits     ServerRequestLimits `yaml:"request_limits"`
	Port              int                 `yaml:"port"`
	ReadTimeout       time.Duration       `yaml:"read_timeout"`
	ReadHeaderTimeout time.Duration       `yaml:"read_header_timeout"`
	WriteTimeout      time.Duration       `yaml:"write_timeout"`
	IdleTimeout       time.Duration       `yaml:"idle_timeout"`
	ShutdownTimeout   time.Duration       `yaml:"shutdown_timeout"`
	RequestLogging    bool                `yaml:"request_logging"`
}

// Validate checks CORS configuration for spec-violating combinations that would
// silently break browser clients at runtime rather than failing loudly at startup.
func (c *CorsConfig) Validate() error {
	if !c.Enabled {
		return nil
	}
	// rs/cors treats an empty AllowedOrigins list as allow-all, which is the
	// opposite of what an operator expects when they write allowed_origins: [].
	// Require an explicit ["*"] to opt into allow-all so the intent is unambiguous.
	if len(c.AllowedOrigins) == 0 {
		return errors.New("cors: allowed_origins must not be empty when cors is enabled; set [\"*\"] to allow all origins explicitly")
	}
	if c.MaxAge < 0 {
		return fmt.Errorf("cors.max_age must be non-negative, got %d", c.MaxAge)
	}
	// The CORS spec forbids Access-Control-Allow-Origin: * when credentials are in play.
	// Browsers will refuse the response, so this is a startup error rather than a warning.
	if c.AllowCredentials {
		for _, origin := range c.AllowedOrigins {
			if origin == "*" {
				return errors.New("cors: allow_credentials=true is incompatible with allowed_origins=[\"*\"]; list explicit origins instead")
			}
		}
	}
	return nil
}

// GetAddress returns the server address in host:port format
func (s *ServerConfig) GetAddress() string {
	return fmt.Sprintf("%s:%d", s.Host, s.Port)
}

// ServerRequestLimits defines request size and validation limits
type ServerRequestLimits struct {
	MaxBodySize   int64 `yaml:"max_body_size"`
	MaxHeaderSize int64 `yaml:"max_header_size"`
}

// ServerRateLimits defines rate limiting configuration
type ServerRateLimits struct {
	TrustedProxyCIDRs       []string      `yaml:"trusted_proxy_cidrs"`
	TrustedProxyCIDRsParsed []*net.IPNet  // to avoid parsing every time :D
	GlobalRequestsPerMinute int           `yaml:"global_requests_per_minute"`
	PerIPRequestsPerMinute  int           `yaml:"per_ip_requests_per_minute"`
	BurstSize               int           `yaml:"burst_size"`
	HealthRequestsPerMinute int           `yaml:"health_requests_per_minute"`
	CleanupInterval         time.Duration `yaml:"cleanup_interval"`
	TrustProxyHeaders       bool          `yaml:"trust_proxy_headers"`
}

// StickySessionConfig controls KV-cache affinity routing.
// When enabled, the balancer wrapper remembers which backend handled a conversation
// and routes subsequent turns to the same backend, maximising KV-cache reuse.
type StickySessionConfig struct {
	// KeySources lists the ordered cascade of strategies used to identify a conversation.
	// The first match wins: session_header, prefix_hash, auth_header, ip.
	KeySources     []string `yaml:"key_sources"`
	MaxSessions    uint64   `yaml:"max_sessions"`
	IdleTTLSeconds int      `yaml:"idle_ttl_seconds"`
	// PrefixHashBytes is how many leading bytes of the messages field to hash.
	// Larger values increase hash uniqueness at the cost of slightly more CPU per request.
	PrefixHashBytes int  `yaml:"prefix_hash_bytes"`
	Enabled         bool `yaml:"enabled"`
}

// ProxyConfig holds proxy-specific configuration
type ProxyConfig struct {
	ProfileFilter         *domain.FilterConfig `yaml:"profile_filter,omitempty"`
	Engine                string               `yaml:"engine"`
	LoadBalancer          string               `yaml:"load_balancer"`
	Profile               string               `yaml:"profile"`
	StickySessions        StickySessionConfig  `yaml:"sticky_sessions"`
	ConnectionTimeout     time.Duration        `yaml:"connection_timeout"`
	ConnectionKeepAlive   time.Duration        `yaml:"connection_keep_alive"`
	ResponseTimeout       time.Duration        `yaml:"response_timeout"`
	ReadTimeout           time.Duration        `yaml:"read_timeout"`
	ResponseHeaderTimeout time.Duration        `yaml:"response_header_timeout"`
	TLSHandshakeTimeout   time.Duration        `yaml:"tls_handshake_timeout"`
	RetryBackoff          time.Duration        `yaml:"retry_backoff"` // Deprecated: Use model_registry.routing_strategy instead. TODO: Removal: v0.1.0
	StreamBufferSize      int                  `yaml:"stream_buffer_size"`
	MaxRetries            int                  `yaml:"max_retries"` // Deprecated: Use model_registry.routing_strategy instead. TODO: Removal: v0.1.0
}

// DiscoveryConfig holds service discovery configuration
type DiscoveryConfig struct {
	Type            string                `yaml:"type"` // Only "static" is implemented
	Static          StaticDiscoveryConfig `yaml:"static"`
	RefreshInterval time.Duration         `yaml:"refresh_interval"`
	ModelDiscovery  ModelDiscoveryConfig  `yaml:"model_discovery"`
}

// ModelDiscoveryConfig holds model discvery specific settings
type ModelDiscoveryConfig struct {
	Enabled           bool          `yaml:"enabled"`
	Interval          time.Duration `yaml:"interval"`
	Timeout           time.Duration `yaml:"timeout"`
	ConcurrentWorkers int           `yaml:"concurrent_workers"`
	RetryAttempts     int           `yaml:"retry_attempts"`
	RetryBackoff      time.Duration `yaml:"retry_backoff"`
}

// StaticDiscoveryConfig holds static endpoint configuration
type StaticDiscoveryConfig struct {
	Endpoints []EndpointConfig `yaml:"endpoints"`
}

// AuthConfig holds per-endpoint outbound authentication configuration.
// Inline credential and _file variants are mutually exclusive; validation
// (fatal startup error on conflict) is enforced in P3.
type AuthConfig struct {
	// header overrides the default header name for api_key auth (default X-Api-Key).
	Header       string `yaml:"header,omitempty"`
	Token        string `yaml:"token,omitempty"`
	TokenFile    string `yaml:"token_file,omitempty"`
	Key          string `yaml:"key,omitempty"`
	KeyFile      string `yaml:"key_file,omitempty"`
	Username     string `yaml:"username,omitempty"`
	UsernameFile string `yaml:"username_file,omitempty"`
	Password     string `yaml:"password,omitempty"`
	PasswordFile string `yaml:"password_file,omitempty"`
	Type         string `yaml:"type,omitempty"`
}

// EndpointConfig holds configuration for an AI inference endpoint
type EndpointConfig struct {
	ModelFilter *domain.FilterConfig `yaml:"model_filter,omitempty"`
	Auth        *AuthConfig          `yaml:"auth,omitempty"`
	Headers     map[string]string    `yaml:"headers,omitempty"`
	// Priority uses a pointer so nil means "omitted in config" rather than explicitly zero.
	// This lets applyEndpointDefaults distinguish "user set 0" from "user said nothing",
	// since 0 is a valid, lower-than-default priority value.
	Priority       *int          `yaml:"priority"`
	URL            string        `yaml:"url"`
	Name           string        `yaml:"name"`
	Type           string        `yaml:"type"`
	HealthCheckURL string        `yaml:"health_check_url"`
	ModelURL       string        `yaml:"model_url"`
	CheckInterval  time.Duration `yaml:"check_interval"`
	CheckTimeout   time.Duration `yaml:"check_timeout"`
	PreservePath   bool          `yaml:"preserve_path"`
}

// LoggingConfig holds logging configuration
type LoggingConfig struct {
	Level  string `yaml:"level"`
	Format string `yaml:"format"`
	Output string `yaml:"output"`
}

// EngineeringConfig holds development/debugging configuration
type EngineeringConfig struct {
	ShowNerdStats bool `yaml:"show_nerdstats"`
}

// ModelRegistryConfig holds model registry configuration
type ModelRegistryConfig struct {
	RoutingStrategy ModelRoutingStrategy `yaml:"routing_strategy"`
	Type            string               `yaml:"type"`
	Unification     UnificationConfig    `yaml:"unification"`
	EnableUnifier   bool                 `yaml:"enable_unifier"`
}

// ModelRoutingStrategy configures how models are routed when not all endpoints have them
type ModelRoutingStrategy struct {
	Type    string                      `yaml:"type"` // strict, optimistic, discovery
	Options ModelRoutingStrategyOptions `yaml:"options"`
}

// ModelRoutingStrategyOptions holds routing strategy configuration
type ModelRoutingStrategyOptions struct {
	FallbackBehavior       string        `yaml:"fallback_behavior"` // compatible_only, none, all
	DiscoveryTimeout       time.Duration `yaml:"discovery_timeout"`
	DiscoveryRefreshOnMiss bool          `yaml:"discovery_refresh_on_miss"`
}

// UnificationConfig holds model unification configuration
type UnificationConfig struct {
	CustomRules     []UnificationRuleConfig `yaml:"custom_rules"`
	CacheTTL        time.Duration           `yaml:"cache_ttl"`
	Enabled         bool                    `yaml:"enabled"`
	StaleThreshold  time.Duration           `yaml:"stale_threshold"`
	CleanupInterval time.Duration           `yaml:"cleanup_interval"`
}

// UnificationRuleConfig defines custom unification rules
type UnificationRuleConfig struct {
	FamilyOverrides map[string]string `yaml:"family_overrides"`
	NamePatterns    map[string]string `yaml:"name_patterns"`
	Platform        string            `yaml:"platform"`
}

// TranslatorsConfig holds translator-specific configuration
type TranslatorsConfig struct {
	Anthropic AnthropicTranslatorConfig `yaml:"anthropic"`
}

// Validate validates all translator configurations
// Provides defence-in-depth by ensuring all translator configs are valid
func (c *TranslatorsConfig) Validate() error {
	if err := c.Anthropic.Validate(); err != nil {
		return fmt.Errorf("anthropic translator config invalid: %w", err)
	}
	return nil
}

// MaxAnthropicMessageSize is the maximum allowed message size for Anthropic API requests (100 MiB)
const MaxAnthropicMessageSize = 100 << 20

// AnthropicTranslatorConfig holds configuration for the Anthropic translator
type AnthropicTranslatorConfig struct {
	Inspector      InspectorConfig `yaml:"inspector"`
	MaxMessageSize int64           `yaml:"max_message_size"`
	Enabled        bool            `yaml:"enabled"`

	// PassthroughEnabled controls whether requests can be forwarded directly
	// to backends that natively support the Anthropic Messages API, bypassing
	// the Anthropic-to-OpenAI translation pipeline (olla v0.0.23+).
	PassthroughEnabled bool `yaml:"passthrough_enabled"`
}

// InspectorConfig holds configuration for request/response inspection
type InspectorConfig struct {
	OutputDir     string `yaml:"output_dir"`
	SessionHeader string `yaml:"session_header"`
	Enabled       bool   `yaml:"enabled"`
}

// validHTTPHeaderPattern matches valid HTTP header names per RFC 7230
// Header names must be tokens (alphanumeric and !#$%&'*+-.^_`|~)
var validHTTPHeaderPattern = regexp.MustCompile(`^[A-Za-z0-9!#$%&'*+\-.^_` + "`" + `|~]+$`)

// validateOutputPath checks if a path is dangerous system path
// Prevents writing to critical system directories
func validateOutputPath(cleanPath string) error {
	// Convert to absolute path for checking
	absPath, err := filepath.Abs(cleanPath)
	if err != nil {
		return fmt.Errorf("invalid output_dir path: %w", err)
	}

	// Normalize for case-insensitive comparison on Windows
	absPathLower := strings.ToLower(absPath)
	cleanPathLower := strings.ToLower(cleanPath)

	// Unix-specific dangerous paths (only check on Unix-like systems)
	unixDangerousPaths := []string{
		"/etc",
		"/var",
		"/usr",
		"/bin",
		"/sbin",
		"/boot",
		"/sys",
		"/proc",
		"/dev",
		"/root",
	}

	// Windows-specific dangerous paths
	windowsDangerousPaths := []string{
		"c:\\windows",
		"c:\\program files",
		"c:\\program files (x86)",
	}

	// Universal dangerous paths (root directories)
	universalDangerousPaths := []string{
		"/",
		"\\",
		"c:\\",
	}

	// Check universal dangerous paths
	for _, dangerous := range universalDangerousPaths {
		dangerousLower := strings.ToLower(dangerous)
		if cleanPathLower == dangerousLower || absPathLower == dangerousLower {
			return fmt.Errorf("output_dir cannot be set to dangerous system path: %s", cleanPath)
		}
	}

	// Check Unix paths only if the path starts with / (Unix-style)
	if strings.HasPrefix(cleanPath, "/") {
		for _, dangerous := range unixDangerousPaths {
			dangerousWithSep := dangerous + "/"
			if cleanPath == dangerous || strings.HasPrefix(cleanPath, dangerousWithSep) {
				return fmt.Errorf("output_dir cannot be set to dangerous system path: %s", cleanPath)
			}
			// Also check absolute path
			if absPath == dangerous || strings.HasPrefix(absPath, dangerousWithSep) {
				return fmt.Errorf("output_dir resolves to dangerous system path: %s", absPath)
			}
		}
	}

	// Check Windows paths
	// Normalise both clean and absolute paths to Windows-style separators for matching
	cleanWin := strings.ReplaceAll(cleanPathLower, "/", "\\")
	absWin := strings.ReplaceAll(absPathLower, "/", "\\")
	for _, dangerous := range windowsDangerousPaths {
		dangerousLower := strings.ToLower(dangerous)
		dangerousWithSep := dangerousLower + "\\"
		if cleanWin == dangerousLower || strings.HasPrefix(cleanWin, dangerousWithSep) ||
			absWin == dangerousLower || strings.HasPrefix(absWin, dangerousWithSep) {
			return fmt.Errorf("output_dir cannot be set to dangerous system path: %s", cleanPath)
		}
	}

	return nil
}

// Validate validates the inspector configuration
// Prevents dangerous output paths and ensures session header is valid
func (c *InspectorConfig) Validate() error {
	if !c.Enabled {
		return nil // Skip validation if disabled
	}

	// Validate output directory is not a dangerous path
	if c.OutputDir == "" {
		// Set sensible default
		c.OutputDir = "./inspector-logs"
	} else {
		// Clean the path to normalize it
		cleanPath := filepath.Clean(c.OutputDir)

		// Check for dangerous paths based on platform
		if err := validateOutputPath(cleanPath); err != nil {
			return err
		}

		c.OutputDir = cleanPath
	}

	// Validate session header is a valid HTTP header name
	if c.SessionHeader == "" {
		// Set default
		c.SessionHeader = "X-Session-ID"
	} else {
		// Remove any whitespace
		c.SessionHeader = strings.TrimSpace(c.SessionHeader)

		// Validate against HTTP header name rules (RFC 7230)
		if !validHTTPHeaderPattern.MatchString(c.SessionHeader) {
			return fmt.Errorf("session_header contains invalid characters (must be valid HTTP header name): %s", c.SessionHeader)
		}

		// Additional checks for common mistakes
		if strings.Contains(c.SessionHeader, " ") {
			return fmt.Errorf("session_header cannot contain spaces: %s", c.SessionHeader)
		}
		if strings.Contains(c.SessionHeader, ":") {
			return fmt.Errorf("session_header cannot contain colons: %s", c.SessionHeader)
		}
	}

	return nil
}

// Validate validates the Anthropic translator configuration
// Ensures message size is within safe bounds to prevent DoS and API errors
func (c *AnthropicTranslatorConfig) Validate() error {
	if c.MaxMessageSize < 0 {
		return fmt.Errorf("max_message_size must be non-negative, got %d", c.MaxMessageSize)
	}
	if c.MaxMessageSize > MaxAnthropicMessageSize {
		return fmt.Errorf("max_message_size exceeds 100 MiB safety limit, got %d", c.MaxMessageSize)
	}

	// Validate inspector configuration
	if err := c.Inspector.Validate(); err != nil {
		return fmt.Errorf("inspector config invalid: %w", err)
	}

	return nil
}

// ValidateModelAliases validates the model alias configuration.
// Returns an error for invalid configurations that would cause runtime failures.
// Logs warnings for non-fatal issues like duplicate model names.
func (c *Config) ValidateModelAliases() error {
	if len(c.ModelAliases) == 0 {
		return nil // no aliases configured, nothing to validate
	}

	for aliasName, actualModels := range c.ModelAliases {
		// Empty alias name
		if aliasName == "" {
			return errors.New("model alias name cannot be empty")
		}

		// Whitespace in alias name
		if strings.TrimSpace(aliasName) != aliasName {
			return fmt.Errorf("model alias %q contains leading/trailing whitespace", aliasName)
		}

		// Empty model list
		if len(actualModels) == 0 {
			return fmt.Errorf("model alias %q has no actual models configured", aliasName)
		}

		// Check each model name in the list
		seen := make(map[string]bool)
		for i, modelName := range actualModels {
			if modelName == "" {
				return fmt.Errorf("model alias %q has empty model name at position %d", aliasName, i)
			}

			// Duplicate model names (warning, not a fatal error)
			if seen[modelName] {
				slog.Warn("Duplicate model name in alias configuration",
					"alias", aliasName,
					"model", modelName)
			}
			seen[modelName] = true
		}
	}

	return nil
}
