package config

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

	"github.com/spf13/viper"
)

// 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
}

// 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 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 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
}

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,
			}
		}
	}
	// CLI legacy: enabled cli adapter with its profiles.
	if node.Adapters.CLI.Enabled {
		for name, profile := range node.Adapters.CLI.Profiles {
			legacyByKey[name] = &legacyAdapterRef{
				Type:    "cli",
				Command: profile.Command,
				Args:    profile.Args,
			}
		}
	}

	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: capacity mismatch (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: capacity mismatch (provider=%d, legacy=%d)",
					nodeIdx, j, p.ID, legacy.Type, p.ID, p.Capacity, legacy.Capacity)
			}
		case "cli":
			if legacy.Type != "cli" {
				continue
			}
			if p.Command != "" && p.Command != legacy.Command {
				return fmt.Errorf("nodes[%d].providers[%d] %q conflicts with adapters.cli profile %q: command mismatch (provider=%q, legacy=%q)",
					nodeIdx, j, p.ID, p.ID, p.Command, legacy.Command)
			}
		}
	}
	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
	Command        string
	Args           []string
}
