package config

import (
	"encoding/hex"
	"fmt"
	"net/url"
	"strings"

	"github.com/spf13/viper"
)

const (
	MinCredentialLeaseTTLSeconds = 5
	MaxCredentialLeaseTTLSeconds = 300
	MinCredentialCacheSize       = 1
	MaxCredentialCacheSize       = 4096
)

func validateNodeCredentialPlane(cfg *NodeConfig) error {
	if cfg == nil || !cfg.CredentialPlane.Enabled {
		return nil
	}
	if !cfg.Transport.TLS.Enabled {
		return fmt.Errorf("credential_plane requires transport.tls")
	}
	if err := cfg.Transport.TLS.ValidateClient("edge"); err != nil {
		return fmt.Errorf("transport.tls: %w", err)
	}
	if strings.TrimSpace(cfg.CredentialPlane.RecipientKeyID) == "" || strings.TrimSpace(cfg.CredentialPlane.RecipientPrivateKey) == "" {
		return fmt.Errorf("credential_plane recipient_key_id and recipient_private_key are required")
	}
	if strings.TrimSpace(cfg.CredentialPlane.IssuerKeyID) == "" || strings.TrimSpace(cfg.CredentialPlane.IssuerPublicKey) == "" {
		return fmt.Errorf("credential_plane issuer_key_id and issuer_public_key are required")
	}
	if cfg.CredentialPlane.ReplayCacheSize < MinCredentialCacheSize || cfg.CredentialPlane.ReplayCacheSize > MaxCredentialCacheSize {
		return fmt.Errorf("credential_plane.replay_cache_size must be between %d and %d", MinCredentialCacheSize, MaxCredentialCacheSize)
	}
	return nil
}

func validateEdgeCredentialPlane(cfg *EdgeConfig) error {
	if cfg == nil || !cfg.CredentialPlane.Enabled {
		return nil
	}
	if cfg.CredentialPlane.LeaseTTLSeconds < MinCredentialLeaseTTLSeconds || cfg.CredentialPlane.LeaseTTLSeconds > MaxCredentialLeaseTTLSeconds {
		return fmt.Errorf("credential_plane.lease_ttl_seconds must be between %d and %d", MinCredentialLeaseTTLSeconds, MaxCredentialLeaseTTLSeconds)
	}
	if cfg.CredentialPlane.LeaseCacheSize < MinCredentialCacheSize || cfg.CredentialPlane.LeaseCacheSize > MaxCredentialCacheSize {
		return fmt.Errorf("credential_plane.lease_cache_size must be between %d and %d", MinCredentialCacheSize, MaxCredentialCacheSize)
	}
	if !cfg.TLS.Enabled {
		return fmt.Errorf("credential_plane requires edge-node tls")
	}
	if err := cfg.TLS.ValidateServer("node"); err != nil {
		return fmt.Errorf("tls: %w", err)
	}
	if !cfg.ControlPlane.Enabled || strings.TrimSpace(cfg.ControlPlane.WireAddr) == "" || !cfg.ControlPlane.TLS.Enabled {
		return fmt.Errorf("credential_plane requires an enabled TLS control_plane connector")
	}
	if err := cfg.ControlPlane.TLS.ValidateClient("control-plane"); err != nil {
		return fmt.Errorf("control_plane.tls: %w", err)
	}
	if cfg.OpenAI.Enabled {
		if !cfg.OpenAI.TLS.Enabled {
			return fmt.Errorf("credential_plane requires openai.tls when OpenAI ingress is enabled")
		}
		if err := cfg.OpenAI.TLS.ValidateHTTPServer(); err != nil {
			return fmt.Errorf("openai.tls: %w", err)
		}
	}
	if len(cfg.OpenAI.PrincipalTokens) != 0 || strings.TrimSpace(cfg.OpenAI.BearerToken) != "" || cfg.OpenAI.ProviderAuth.Enabled {
		return fmt.Errorf("credential_plane cannot be combined with legacy principal or provider auth sources")
	}
	if source := managedRawProviderCredentialSource(cfg); source != "" {
		return fmt.Errorf("credential_plane cannot be combined with raw provider credential source %s", source)
	}
	return nil
}

// managedRawProviderCredentialSource returns a safe config path for the first
// static provider credential source that would compete with a managed lease.
// It never includes a configured header value.
func managedRawProviderCredentialSource(cfg *EdgeConfig) string {
	for nodeIndex := range cfg.Nodes {
		node := &cfg.Nodes[nodeIndex]
		for providerIndex := range node.Providers {
			provider := &node.Providers[providerIndex]
			profile := provider.RuntimeProfile
			effectiveAuthHeader := ""
			if profile != nil {
				effectiveAuthHeader = strings.TrimSpace(profile.Auth.Header)
			}
			for header := range provider.Headers {
				if isCredentialHeaderName(header) || (effectiveAuthHeader != "" && strings.EqualFold(strings.TrimSpace(header), effectiveAuthHeader)) {
					return fmt.Sprintf("nodes[%d].providers[%d].headers[%q]", nodeIndex, providerIndex, header)
				}
			}
			if urlContainsUserInfo(provider.Endpoint) {
				return fmt.Sprintf("nodes[%d].providers[%d].endpoint", nodeIndex, providerIndex)
			}
			if urlContainsUserInfo(provider.BaseURL) {
				return fmt.Sprintf("nodes[%d].providers[%d].base_url", nodeIndex, providerIndex)
			}
			if profile != nil {
				if urlContainsUserInfo(profile.BaseURL) {
					return fmt.Sprintf("nodes[%d].providers[%d].profile[%q].base_url", nodeIndex, providerIndex, profile.ID)
				}
				for operation, rawURL := range profile.Operations {
					if urlContainsUserInfo(rawURL) {
						return fmt.Sprintf("nodes[%d].providers[%d].profile[%q].operations[%q]", nodeIndex, providerIndex, profile.ID, operation)
					}
				}
			}
		}
		effectiveProfileAuthHeaders := collectEffectiveProfileAuthHeaders(node.Providers)
		for header := range node.Adapters.OpenAICompat.Headers {
			if isCredentialHeaderName(header) || matchesEffectiveProfileAuthHeader(header, effectiveProfileAuthHeaders) {
				return fmt.Sprintf("nodes[%d].adapters.openai_compat.headers[%q]", nodeIndex, header)
			}
		}
		for instanceIndex := range node.Adapters.OpenAICompatInstances {
			instance := &node.Adapters.OpenAICompatInstances[instanceIndex]
			for header := range instance.Headers {
				if isCredentialHeaderName(header) || matchesEffectiveProfileAuthHeader(header, effectiveProfileAuthHeaders) {
					return fmt.Sprintf("nodes[%d].adapters.openai_compat_instances[%d].headers[%q]", nodeIndex, instanceIndex, header)
				}
			}
			if urlContainsUserInfo(instance.Endpoint) {
				return fmt.Sprintf("nodes[%d].adapters.openai_compat_instances[%d].endpoint", nodeIndex, instanceIndex)
			}
		}
	}
	return ""
}

func collectEffectiveProfileAuthHeaders(providers []NodeProviderConf) map[string]struct{} {
	headers := make(map[string]struct{})
	for i := range providers {
		profile := providers[i].RuntimeProfile
		if profile != nil {
			h := strings.TrimSpace(profile.Auth.Header)
			if h != "" {
				headers[strings.ToLower(h)] = struct{}{}
			}
		}
	}
	return headers
}

func matchesEffectiveProfileAuthHeader(header string, effective map[string]struct{}) bool {
	if len(effective) == 0 {
		return false
	}
	_, ok := effective[strings.ToLower(strings.TrimSpace(header))]
	return ok
}

func isCredentialHeaderName(name string) bool {
	normalized := strings.ToLower(strings.TrimSpace(name))
	switch normalized {
	case "authorization", "proxy-authorization", "x-api-key", "api-key", "x-auth", "x-auth-token":
		return true
	default:
		return strings.Contains(normalized, "credential") || strings.Contains(normalized, "secret")
	}
}

func urlContainsUserInfo(raw string) bool {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	return err == nil && parsed.User != nil
}

// validateOpenAIRoutes rejects duplicate and empty model ids in the route catalog.
func validateOpenAIRoutes(routes []OpenAIRouteEntry) error {
	seen := make(map[string]struct{}, len(routes))
	for i, r := range routes {
		model := strings.TrimSpace(r.Model)
		if model == "" {
			return fmt.Errorf("openai.model_routes[%d]: model must not be empty", i)
		}
		if _, dup := seen[model]; dup {
			return fmt.Errorf("openai.model_routes: duplicate model %q", model)
		}
		seen[model] = struct{}{}
		if r.MaxQueue < 0 {
			return fmt.Errorf("openai.model_routes[%d].max_queue must be non-negative", i)
		}
		if r.QueueTimeoutMS < 0 {
			return fmt.Errorf("openai.model_routes[%d].queue_timeout_ms must be non-negative", i)
		}
	}
	return nil
}

// validateOpenAIAttributionBindings requires every enabled direct OpenAI route
// to have a stable provider identity. A route-level provider_id overrides the
// top-level fallback, but the fallback itself remains independently
// dispatchable and therefore also requires a binding. Provider catalog
// cross-reference validation is deliberately not applied to legacy direct
// adapter routes.
func validateOpenAIAttributionBindings(openAI EdgeOpenAIConf) error {
	if !openAI.Enabled {
		return nil
	}
	fallbackProviderID := strings.TrimSpace(openAI.ProviderID)
	if fallbackProviderID == "" {
		return fmt.Errorf("openai.provider_id must not be empty when OpenAI is enabled")
	}
	for i, route := range openAI.ModelRoutes {
		providerID := strings.TrimSpace(route.ProviderID)
		if providerID == "" {
			providerID = fallbackProviderID
		}
		if providerID == "" {
			return fmt.Errorf("openai.model_routes[%d].provider_id must not be empty without openai.provider_id fallback", i)
		}
	}
	return nil
}

// validateOpenAIPrincipalTokens validates the raw-token-free bearer token
// operation mapping. Each entry must resolve to a unique, 64-char lowercase
// hex SHA-256 hash and a non-empty principal_ref; token_ref must be unique.
func validateOpenAIPrincipalTokens(tokens []OpenAIPrincipalTokenConf) error {
	seenTokenRef := make(map[string]struct{}, len(tokens))
	seenHash := make(map[string]struct{}, len(tokens))
	for i, t := range tokens {
		ref := strings.TrimSpace(t.TokenRef)
		if ref == "" {
			return fmt.Errorf("openai.principal_tokens[%d]: token_ref must not be empty", i)
		}
		if _, dup := seenTokenRef[ref]; dup {
			return fmt.Errorf("openai.principal_tokens: duplicate token_ref %q", ref)
		}
		seenTokenRef[ref] = struct{}{}

		hash := strings.ToLower(strings.TrimSpace(t.TokenHashSHA256))
		if len(hash) != 64 {
			return fmt.Errorf("openai.principal_tokens[%q]: token_hash_sha256 must be a 64-character hex SHA-256 hash", ref)
		}
		if _, err := hex.DecodeString(hash); err != nil {
			return fmt.Errorf("openai.principal_tokens[%q]: token_hash_sha256 must be hex-encoded: %w", ref, err)
		}
		if _, dup := seenHash[hash]; dup {
			return fmt.Errorf("openai.principal_tokens: duplicate token_hash_sha256 for token_ref %q", ref)
		}
		seenHash[hash] = struct{}{}

		if strings.TrimSpace(t.PrincipalRef) == "" {
			return fmt.Errorf("openai.principal_tokens[%q]: principal_ref must not be empty", ref)
		}
	}
	return nil
}

func isInboundCallerAuthHeader(header string) bool {
	switch strings.ToLower(strings.TrimSpace(header)) {
	case "authorization", "x-api-key":
		return true
	default:
		return false
	}
}

func normalizeOpenAIProviderAuth(v *viper.Viper, auth *EdgeOpenAIProviderAuthConf) error {
	if !auth.Enabled {
		return nil
	}
	if v.InConfig("openai.provider_auth.from_header") {
		auth.FromHeader = strings.TrimSpace(auth.FromHeader)
		if auth.FromHeader == "" {
			return fmt.Errorf("openai.provider_auth.from_header must not be empty when provider_auth is enabled")
		}
	} else {
		auth.FromHeader = "X-IOP-Provider-Authorization"
	}
	if isInboundCallerAuthHeader(auth.FromHeader) {
		return fmt.Errorf("openai.provider_auth.from_header must not reuse inbound caller authentication header %q", auth.FromHeader)
	}
	if v.InConfig("openai.provider_auth.target_header") {
		auth.TargetHeader = strings.TrimSpace(auth.TargetHeader)
		if auth.TargetHeader == "" {
			return fmt.Errorf("openai.provider_auth.target_header must not be empty when provider_auth is enabled")
		}
	} else {
		auth.TargetHeader = "Authorization"
	}
	auth.Scheme = strings.TrimSpace(auth.Scheme)
	if auth.Scheme == "" {
		auth.Scheme = "Bearer"
	}
	if !v.InConfig("openai.provider_auth.required") {
		auth.Required = true
	}
	return nil
}

// validateProviderLongContextBudget enforces the long-context admission budget:
// every provider referenced by a model group with a positive context window must
// have total_context_tokens >= context_window_tokens * long_context_capacity.
// Providers with long_context_capacity == 0 declare no long-slot budget and are
// therefore unconstrained by this check.
func validateProviderLongContextBudget(m ModelCatalogEntry, providerByID map[string]NodeProviderConf) error {
	if m.ContextWindowTokens <= 0 {
		return nil
	}
	for pid := range m.Providers {
		p := strings.TrimSpace(pid)
		prov, ok := providerByID[p]
		if !ok {
			continue
		}
		if prov.LongContextCapacity <= 0 {
			continue
		}
		required := m.ContextWindowTokens * prov.LongContextCapacity
		if prov.TotalContextTokens < required {
			return fmt.Errorf("providers[%q].total_context_tokens (%d) must be >= context_window_tokens (%d) * long_context_capacity (%d) = %d",
				p, prov.TotalContextTokens, m.ContextWindowTokens, prov.LongContextCapacity, required)
		}
	}
	return nil
}

// validateModelTokenCounter keeps token counting local and Chat-only. The
// deterministic mode takes no tuning parameter; estimate mode uses an explicit
// positive tokens-per-1k-characters rate bounded to one token per character.
func validateModelTokenCounter(m ModelCatalogEntry, providerByID map[string]NodeProviderConf) error {
	if m.TokenCounter == nil {
		return nil
	}
	counter := *m.TokenCounter
	switch counter.Mode {
	case TokenCounterDeterministic:
		if counter.Per1kInput != 0 {
			return fmt.Errorf("models[%q].token_counter.per_1k_input must be 0 in deterministic mode", m.ID)
		}
	case TokenCounterEstimate:
		if counter.Per1kInput < 1 || counter.Per1kInput > 1000 {
			return fmt.Errorf("models[%q].token_counter.per_1k_input must be between 1 and 1000 in estimate mode", m.ID)
		}
	default:
		return fmt.Errorf("models[%q].token_counter.mode must be %q or %q, got %q", m.ID, TokenCounterDeterministic, TokenCounterEstimate, counter.Mode)
	}
	for providerID := range m.Providers {
		provider, ok := providerByID[strings.TrimSpace(providerID)]
		if !ok || provider.RuntimeProfile == nil {
			continue
		}
		driver := provider.RuntimeProfile.Driver
		if driver != ProtocolDriverOpenAIChat {
			return fmt.Errorf("models[%q].token_counter is Chat-only; provider %q selects profile %q with driver %q", m.ID, providerID, provider.RuntimeProfile.ID, driver)
		}
	}
	return nil
}

func checkUniqueNames(field string, name func(int) string, n int) error {
	seen := make(map[string]struct{}, n)
	for i := 0; i < n; i++ {
		k := name(i)
		if k == "" {
			return fmt.Errorf("%s[%d]: name must not be empty", field, i)
		}
		if _, dup := seen[k]; dup {
			return fmt.Errorf("%s: duplicate name %q", field, k)
		}
		seen[k] = struct{}{}
	}
	return nil
}

func validateProviderQueueConfig(field string, capacity, maxQueue, queueTimeoutMS, requestTimeoutMS int) error {
	if capacity < 0 {
		return fmt.Errorf("%s.capacity must be non-negative", field)
	}
	if maxQueue < 0 {
		return fmt.Errorf("%s.max_queue must be non-negative", field)
	}
	if queueTimeoutMS < 0 {
		return fmt.Errorf("%s.queue_timeout_ms must be non-negative", field)
	}
	if requestTimeoutMS < 0 {
		return fmt.Errorf("%s.request_timeout_ms must be non-negative", field)
	}
	return nil
}

// CheckProviderLegacyConflict validates that when a provider-first resource
// declares an ID matching an enabled legacy adapter instance name, the
// provider's execution fields match the legacy adapter's fields. A mismatch
// produces a validation error to prevent silent precedence between the two
// config paths.
func CheckProviderLegacyConflict(nodeIdx int, nodeName string, node *NodeDefinition) error {
	// Build a map of enabled adapter instance names to their configs per type.
	legacyByKey := make(map[string]*legacyAdapterRef)

	for _, inst := range node.Adapters.OllamaInstances {
		if inst.Enabled {
			legacyByKey[inst.Name] = &legacyAdapterRef{
				Type:        "ollama",
				BaseURL:     inst.BaseURL,
				ContextSize: inst.ContextSize,
				Capacity:    inst.Capacity,
			}
		}
	}
	for _, inst := range node.Adapters.VllmInstances {
		if inst.Enabled {
			legacyByKey[inst.Name] = &legacyAdapterRef{
				Type:     "vllm",
				Endpoint: inst.Endpoint,
				Capacity: inst.Capacity,
			}
		}
	}
	for _, inst := range node.Adapters.OpenAICompatInstances {
		if inst.Enabled {
			legacyByKey[inst.Name] = &legacyAdapterRef{
				Type:           "openai_compat",
				Provider:       inst.Provider,
				Endpoint:       inst.Endpoint,
				Headers:        inst.Headers,
				Capacity:       inst.Capacity,
				MaxQueue:       inst.MaxQueue,
				QueueTimeoutMS: inst.QueueTimeoutMS,
			}
		}
	}
	for j, p := range node.Providers {
		legacy, exists := legacyByKey[p.ID]
		if !exists {
			continue
		}

		normType := NormalizeProviderType(p.Type)
		switch normType {
		case "ollama":
			if legacy.Type != "ollama" {
				continue
			}
			if p.BaseURL != "" && p.BaseURL != legacy.BaseURL {
				return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: base_url mismatch (provider=%q, legacy=%q)",
					nodeIdx, j, p.ID, p.ID, p.BaseURL, legacy.BaseURL)
			}
			if p.ContextSize != 0 && p.ContextSize != legacy.ContextSize {
				return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: context_size mismatch (provider=%d, legacy=%d)",
					nodeIdx, j, p.ID, p.ID, p.ContextSize, legacy.ContextSize)
			}
			if p.Capacity > legacy.Capacity {
				return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.ollama instance %q: provider capacity exceeds adapter capacity (provider=%d, legacy=%d)",
					nodeIdx, j, p.ID, p.ID, p.Capacity, legacy.Capacity)
			}
		case "vllm", "openai_compat":
			if legacy.Type != "vllm" && legacy.Type != "openai_compat" {
				continue
			}
			if p.Endpoint != "" && p.Endpoint != legacy.Endpoint {
				return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.%s instance %q: endpoint mismatch (provider=%q, legacy=%q)",
					nodeIdx, j, p.ID, legacy.Type, p.ID, p.Endpoint, legacy.Endpoint)
			}
			if p.Capacity > legacy.Capacity {
				return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.%s instance %q: provider capacity exceeds adapter capacity (provider=%d, legacy=%d)",
					nodeIdx, j, p.ID, legacy.Type, p.ID, p.Capacity, legacy.Capacity)
			}
		}
	}
	return nil
}

type legacyAdapterRef struct {
	Type           string
	Endpoint       string
	BaseURL        string
	Provider       string
	Headers        map[string]string
	ContextSize    int
	Capacity       int
	MaxQueue       int
	QueueTimeoutMS int
}
