package config

import (
	"fmt"
	"net/url"
	"os"
	"slices"
	"strings"
)

const (
	DefaultAutoModelName    = "MoM"
	LegacyAutoModelAlias    = "auto"
	DefaultVSRAutoModelName = "vllm-sr/auto"
)

// GetModelReasoningFamily returns the reasoning family configuration for a given model name
func (rc *RouterConfig) GetModelReasoningFamily(modelName string) *ReasoningFamilyConfig {
	familyName, ok := rc.GetModelReasoningFamilyName(modelName)
	if !ok {
		return nil
	}

	familyConfig, exists := rc.ReasoningFamilies[familyName]
	if !exists {
		return nil
	}

	return &familyConfig
}

func (rc *RouterConfig) GetModelReasoningFamilyName(modelName string) (string, bool) {
	if rc == nil || rc.ModelConfig == nil || rc.ReasoningFamilies == nil {
		return "", false
	}

	// Look up the model in model_config, including provider_model_id/external IDs
	// because request-body mutation may already have rewritten the model field.
	modelParams, exists := rc.resolveModelConfig(modelName)
	if !exists || modelParams.ReasoningFamily == "" {
		_, baseParams, fallback := rc.resolveLoRABaseModel(modelName)
		if !fallback || baseParams.ReasoningFamily == "" {
			return "", false
		}
		modelParams = baseParams
	}
	_, exists = rc.ReasoningFamilies[modelParams.ReasoningFamily]
	return modelParams.ReasoningFamily, exists
}

func (rc *RouterConfig) ModelNameMatches(candidateName string, modelName string) bool {
	if candidateName == modelName {
		return true
	}
	if rc == nil || rc.ModelConfig == nil {
		return false
	}

	candidateKey, candidateExists := rc.resolveModelConfigKey(candidateName)
	modelKey, modelExists := rc.resolveModelConfigKey(modelName)
	return candidateExists && modelExists && candidateKey == modelKey
}

func (c *RouterConfig) resolveModelConfigKey(modelName string) (string, bool) {
	if _, ok := c.ModelConfig[modelName]; ok {
		return modelName, true
	}
	for configuredName, params := range c.ModelConfig {
		for _, extID := range params.ExternalModelIDs {
			if extID == modelName {
				return configuredName, true
			}
		}
	}
	return "", false
}

// GetEffectiveAutoModelName returns the effective auto model name for automatic model selection
// Returns the configured AutoModelName if set, otherwise defaults to "MoM"
// This is the primary model name that triggers automatic routing
func (c *RouterConfig) GetEffectiveAutoModelName() string {
	if c.AutoModelName != "" {
		return c.AutoModelName
	}
	return DefaultAutoModelName
}

func DefaultAutoModelNames() []string {
	return []string{DefaultVSRAutoModelName, LegacyAutoModelAlias, DefaultAutoModelName}
}

// EffectiveAutoModelNames returns all request model names that trigger
// automatic routing. auto_model_names is an explicit allow-list; when omitted,
// vLLM-SR keeps the new namespaced alias plus legacy auto/MoM compatibility.
func (c *RouterConfig) EffectiveAutoModelNames() []string {
	if c == nil {
		return DefaultAutoModelNames()
	}
	if names := normalizeAutoModelNames(c.AutoModelNames); len(names) > 0 {
		return names
	}
	return normalizeAutoModelNames([]string{
		DefaultVSRAutoModelName,
		LegacyAutoModelAlias,
		c.GetEffectiveAutoModelName(),
	})
}

func normalizeAutoModelNames(names []string) []string {
	seen := make(map[string]bool, len(names))
	result := make([]string, 0, len(names))
	for _, name := range names {
		trimmed := strings.TrimSpace(name)
		if trimmed == "" || seen[trimmed] {
			continue
		}
		seen[trimmed] = true
		result = append(result, trimmed)
	}
	return result
}

// IsAutoModelName checks if the given model name should trigger automatic model selection.
func (c *RouterConfig) IsAutoModelName(modelName string) bool {
	normalized := strings.TrimSpace(modelName)
	if normalized == "" {
		return false
	}
	return slices.Contains(c.EffectiveAutoModelNames(), normalized)
}

// GetCategoryDescriptions returns all category descriptions for similarity matching
func (c *RouterConfig) GetCategoryDescriptions() []string {
	var descriptions []string
	for _, category := range c.Categories {
		if category.Description != "" {
			descriptions = append(descriptions, category.Description)
		} else {
			// Use category name if no description is available
			descriptions = append(descriptions, category.Name)
		}
	}
	return descriptions
}

// GetModelForDecisionIndex returns the best LLM model name for the decision at the given index
func (c *RouterConfig) GetModelForDecisionIndex(index int) string {
	if index < 0 || index >= len(c.Decisions) {
		return c.DefaultModel
	}

	decision := c.Decisions[index]
	if len(decision.ModelRefs) > 0 {
		return decision.ModelRefs[0].Model
	}

	// Fall back to default model if decision has no models
	return c.DefaultModel
}

// resolveModelConfig looks up a model by name, falling back to a reverse
// lookup through ExternalModelIDs (provider_model_id) when the name is not
// a direct key in ModelConfig. This handles the case where the Envoy AI
// Gateway rewrites the model field to the provider model ID.
func (c *RouterConfig) resolveModelConfig(modelName string) (ModelParams, bool) {
	if params, ok := c.ModelConfig[modelName]; ok {
		return params, true
	}
	for _, params := range c.ModelConfig {
		for _, extID := range params.ExternalModelIDs {
			if extID == modelName {
				return params, true
			}
		}
	}
	return ModelParams{}, false
}

// GetModelPricing returns pricing per 1M tokens and its currency for the given model.
// The currency indicates the unit of the returned rates (e.g., "USD").
// Accepts both short names ("claude-haiku-4-5") and provider model IDs
// ("eu.anthropic.claude-haiku-4-5-20251001-v1:0").
func (c *RouterConfig) GetModelPricing(modelName string) (promptPer1M float64, completionPer1M float64, currency string, ok bool) {
	if modelConfig, okc := c.resolveModelConfig(modelName); okc {
		p := modelConfig.Pricing
		// Treat an explicit zero-price entry as configured pricing when a currency is
		// present so self-hosted/free models still produce cost=0 and savings data.
		if p.PromptPer1M != 0 || p.CompletionPer1M != 0 || p.Currency != "" {
			cur := p.Currency
			if cur == "" {
				cur = "USD"
			}
			return p.PromptPer1M, p.CompletionPer1M, cur, true
		}
	}
	return 0, 0, "", false
}

// GetMostExpensivePricedModel returns the configured model with the highest combined
// prompt+completion rate among models that define pricing.
func (c *RouterConfig) GetMostExpensivePricedModel() (modelName string, promptPer1M float64, completionPer1M float64, currency string, ok bool) {
	if c == nil || c.ModelConfig == nil {
		return "", 0, 0, "", false
	}

	bestScore := 0.0
	for candidate := range c.ModelConfig {
		promptRate, completionRate, candidateCurrency, found := c.GetModelPricing(candidate)
		if !found {
			continue
		}

		score := promptRate + completionRate
		if !ok || score > bestScore {
			modelName = candidate
			promptPer1M = promptRate
			completionPer1M = completionRate
			currency = candidateCurrency
			bestScore = score
			ok = true
		}
	}

	return modelName, promptPer1M, completionPer1M, currency, ok
}

// GetModelAPIFormat returns the API format for the given model.
// Returns APIFormatAnthropic if configured, otherwise APIFormatOpenAI (default).
func (c *RouterConfig) GetModelAPIFormat(modelName string) string {
	if c == nil || c.ModelConfig == nil {
		return APIFormatOpenAI
	}
	if modelConfig, ok := c.ModelConfig[modelName]; ok && modelConfig.APIFormat != "" {
		return modelConfig.APIFormat
	}
	if _, baseConfig, ok := c.resolveLoRABaseModel(modelName); ok && baseConfig.APIFormat != "" {
		return baseConfig.APIFormat
	}
	return APIFormatOpenAI
}

// GetModelAccessKey returns the access key for the given model.
func (c *RouterConfig) GetModelAccessKey(modelName string) string {
	if c == nil || c.ModelConfig == nil {
		return ""
	}
	if modelConfig, ok := c.ModelConfig[modelName]; ok {
		rawKey := modelConfig.AccessKey
		if rawKey != "" {
			expandedKey := os.ExpandEnv(rawKey)
			return expandedKey
		}
	}
	if _, baseConfig, ok := c.resolveLoRABaseModel(modelName); ok && baseConfig.AccessKey != "" {
		return os.ExpandEnv(baseConfig.AccessKey)
	}
	return ""
}

// GetDecisionPIIPolicy returns the PII policy for a given decision by looking at
// the PIIRule signals referenced in the decision's rules tree.
// If the decision doesn't reference any PII signals, returns a default policy that allows all PII.
func (d *Decision) GetDecisionPIIPolicy(piiRules []PIIRule) PIIPolicy {
	// Collect PII signal names referenced in the decision's rules
	piiSignalNames := collectSignalNames(&d.Rules, "pii")
	if len(piiSignalNames) == 0 {
		// No PII signals → allow all PII
		return PIIPolicy{
			AllowByDefault: true,
			PIITypes:       []string{},
		}
	}

	// Build a lookup for PIIRules by name
	rulesByName := make(map[string]*PIIRule, len(piiRules))
	for i := range piiRules {
		rulesByName[piiRules[i].Name] = &piiRules[i]
	}

	// Aggregate PIITypesAllowed from all referenced PIIRules
	var allAllowed []string
	for _, name := range piiSignalNames {
		if rule, ok := rulesByName[name]; ok {
			allAllowed = append(allAllowed, rule.PIITypesAllowed...)
		}
	}

	return PIIPolicy{
		AllowByDefault: false,
		PIITypes:       allAllowed,
	}
}

// IsDecisionAllowedForPIIType checks if a decision is allowed to process a specific PII type
func (d *Decision) IsDecisionAllowedForPIIType(piiType string, piiRules []PIIRule) bool {
	policy := d.GetDecisionPIIPolicy(piiRules)

	// If allow_by_default is true, all PII types are allowed unless explicitly denied
	if policy.AllowByDefault {
		return true
	}

	// If allow_by_default is false, only explicitly allowed PII types are permitted
	return slices.Contains(policy.PIITypes, piiType)
}

// IsDecisionAllowedForPIITypes checks if a decision is allowed to process any of the given PII types
func (d *Decision) IsDecisionAllowedForPIITypes(piiTypes []string, piiRules []PIIRule) bool {
	for _, piiType := range piiTypes {
		if !d.IsDecisionAllowedForPIIType(piiType, piiRules) {
			return false
		}
	}
	return true
}

// IsPIIClassifierEnabled checks if PII classification is enabled
func (c *RouterConfig) IsPIIClassifierEnabled() bool {
	return c.PIIModel.ModelID != "" && c.PIIMappingPath != ""
}

// IsCategoryClassifierEnabled checks if category classification is enabled
func (c *RouterConfig) IsCategoryClassifierEnabled() bool {
	return c.CategoryModel.ModelID != "" && c.CategoryMappingPath != ""
}

// IsMCPCategoryClassifierEnabled checks if MCP-based category classification is enabled
func (c *RouterConfig) IsMCPCategoryClassifierEnabled() bool {
	return c.Enabled && c.ToolName != ""
}

// GetPromptGuardConfig returns the prompt guard configuration
func (c *RouterConfig) GetPromptGuardConfig() PromptGuardConfig {
	return c.PromptGuard
}

// IsPromptGuardEnabled checks if prompt guard jailbreak detection is enabled
func (c *RouterConfig) IsPromptGuardEnabled() bool {
	if !c.PromptGuard.Enabled || c.PromptGuard.JailbreakMappingPath == "" {
		return false
	}

	// Check configuration based on whether using vLLM or Candle
	if c.PromptGuard.UseVLLM {
		// For vLLM: need external model with role="guardrail"
		externalCfg := c.FindExternalModelByRole(ModelRoleGuardrail)
		return externalCfg != nil &&
			externalCfg.ModelEndpoint.Address != "" &&
			externalCfg.ModelName != ""
	}

	// For Candle: need model ID
	return c.PromptGuard.ModelID != ""
}

// GetEndpointsForModel returns all endpoints that can serve the specified model
// Returns endpoints based on the model's preferred_endpoints configuration in model_config
func (c *RouterConfig) GetEndpointsForModel(modelName string) []VLLMEndpoint {
	if c == nil || c.ModelConfig == nil {
		return nil
	}

	if modelConfig, ok := c.ModelConfig[modelName]; ok && len(modelConfig.PreferredEndpoints) > 0 {
		return c.collectPreferredEndpoints(modelConfig.PreferredEndpoints)
	}
	if _, baseConfig, ok := c.resolveLoRABaseModel(modelName); ok && len(baseConfig.PreferredEndpoints) > 0 {
		return c.collectPreferredEndpoints(baseConfig.PreferredEndpoints)
	}
	return nil
}

// GetEndpointByName returns the endpoint with the specified name
func (c *RouterConfig) GetEndpointByName(name string) (*VLLMEndpoint, bool) {
	for _, endpoint := range c.VLLMEndpoints {
		if endpoint.Name == name {
			return &endpoint, true
		}
	}
	return nil, false
}

// GetAllModels returns a list of all models configured in model_config
func (c *RouterConfig) GetAllModels() []string {
	var models []string

	for modelName := range c.ModelConfig {
		models = append(models, modelName)
	}

	return models
}

// GetModelReasoningForDecision returns whether a specific model supports reasoning in a given decision
func (c *RouterConfig) GetModelReasoningForDecision(decisionName string, modelName string) bool {
	for _, decision := range c.Decisions {
		if decision.Name == decisionName {
			for _, modelRef := range decision.ModelRefs {
				if modelRef.Model == modelName {
					return modelRef.UseReasoning != nil && *modelRef.UseReasoning
				}
			}
		}
	}
	return false // Default to false if decision or model not found
}

// GetBestModelForDecision returns the best model for a given decision (first model in ModelRefs)
func (c *RouterConfig) GetBestModelForDecision(decisionName string) (string, bool) {
	for _, decision := range c.Decisions {
		if decision.Name == decisionName {
			if len(decision.ModelRefs) > 0 {
				useReasoning := decision.ModelRefs[0].UseReasoning != nil && *decision.ModelRefs[0].UseReasoning
				return decision.ModelRefs[0].Model, useReasoning
			}
		}
	}
	return "", false // Return empty string and false if decision not found or has no models
}

// ValidateEndpoints validates that all configured models have at least one endpoint
func (c *RouterConfig) ValidateEndpoints() error {
	// Get all models from decisions
	allCategoryModels := make(map[string]bool)
	for _, decision := range c.Decisions {
		for _, modelRef := range decision.ModelRefs {
			allCategoryModels[modelRef.Model] = true
		}
	}

	// Add default model
	if c.DefaultModel != "" {
		allCategoryModels[c.DefaultModel] = true
	}

	// Check that each model has at least one endpoint
	for model := range allCategoryModels {
		endpoints := c.GetEndpointsForModel(model)
		if len(endpoints) == 0 {
			return fmt.Errorf("model '%s' has no available endpoints", model)
		}
	}

	return nil
}

// IsSystemPromptEnabled returns whether system prompt injection is enabled for a decision
func (d *Decision) IsSystemPromptEnabled() bool {
	config := d.GetSystemPromptConfig()
	if config == nil {
		return false
	}
	// If Enabled is explicitly set, use that value
	if config.Enabled != nil {
		return *config.Enabled
	}
	// Default to true if SystemPrompt is not empty
	return config.SystemPrompt != ""
}

// GetSystemPromptMode returns the system prompt injection mode, defaulting to "replace"
func (d *Decision) GetSystemPromptMode() string {
	config := d.GetSystemPromptConfig()
	if config == nil || config.Mode == "" {
		return "insert" // Default mode
	}
	return config.Mode
}

// GetCategoryByName returns a category by name
func (c *RouterConfig) GetCategoryByName(name string) *Category {
	for i := range c.Categories {
		if c.Categories[i].Name == name {
			return &c.Categories[i]
		}
	}
	return nil
}

// GetDecisionByName returns a decision by name
func (c *RouterConfig) GetDecisionByName(name string) *Decision {
	for i := range c.Decisions {
		if c.Decisions[i].Name == name {
			return &c.Decisions[i]
		}
	}
	return nil
}

// IsCacheEnabledForDecision returns whether semantic caching is enabled for a specific decision
// Returns true only if the decision has an explicit semantic-cache plugin configured with enabled: true
// This ensures per-decision scoping - decisions without semantic-cache plugin won't execute caching
func (c *RouterConfig) IsCacheEnabledForDecision(decisionName string) bool {
	decision := c.GetDecisionByName(decisionName)
	if decision != nil {
		config := decision.GetSemanticCacheConfig()
		if config != nil {
			return config.Enabled
		}
	}
	// No explicit semantic-cache plugin configured for this decision
	// Return false to respect per-decision plugin scoping
	return false
}

// GetCacheSimilarityThresholdForDecision returns the effective cache similarity threshold for a decision
func (c *RouterConfig) GetCacheSimilarityThresholdForDecision(decisionName string) float32 {
	decision := c.GetDecisionByName(decisionName)
	if decision != nil {
		config := decision.GetSemanticCacheConfig()
		if config != nil && config.SimilarityThreshold != nil {
			return *config.SimilarityThreshold
		}
	}
	// Fall back to global cache threshold or bert threshold
	return c.GetCacheSimilarityThreshold()
}

// GetCacheTTLSecondsForDecision returns the effective TTL for a decision
// Returns 0 if caching should be skipped for this decision
// Returns -1 to use the global default TTL when not specified at decision level
func (c *RouterConfig) GetCacheTTLSecondsForDecision(decisionName string) int {
	decision := c.GetDecisionByName(decisionName)
	if decision != nil {
		config := decision.GetSemanticCacheConfig()
		if config != nil && config.TTLSeconds != nil {
			return *config.TTLSeconds
		}
	}
	// Return -1 to indicate "use global default"
	return -1
}

// ResolveExternalModelID resolves the external model ID for a given model name and endpoint.
// When a model alias (e.g., "qwen14b-rack1") is configured with external_model_ids,
// this returns the real model name that the backend expects (e.g., "Qwen/Qwen2.5-14B-Instruct").
// The endpoint type (e.g., "vllm", "ollama") is looked up from the resolved backend metadata.
// Returns the original modelName if no mapping is found.
func (c *RouterConfig) ResolveExternalModelID(modelName string, endpointName string) string {
	if c == nil || c.ModelConfig == nil {
		return modelName
	}

	modelConfig, ok := c.ModelConfig[modelName]
	if !ok || len(modelConfig.ExternalModelIDs) == 0 {
		return modelName
	}

	// Get the endpoint type from the endpoint name
	endpointType := ""
	if endpoint, found := c.GetEndpointByName(endpointName); found && endpoint.Type != "" {
		endpointType = endpoint.Type
	} else {
		// Default endpoint type is "vllm"
		endpointType = "vllm"
	}

	// Look up the external model ID for this endpoint type
	if externalID, ok := modelConfig.ExternalModelIDs[endpointType]; ok && externalID != "" {
		return externalID
	}

	return modelName
}

// ResolvePrimaryBackendForModel resolves backend metadata for a model and returns
// the backend address plus the backend config name. This helper is for
// provider-profile/body-shaping metadata; it does not choose an Envoy upstream
// endpoint. Envoy owns endpoint load balancing inside the selected cluster.
func (c *RouterConfig) ResolvePrimaryBackendForModel(modelName string) (string, string, bool, error) {
	endpoints := c.GetEndpointsForModel(modelName)
	if len(endpoints) == 0 {
		return "", "", false, nil
	}

	bestEndpoint := endpoints[0]
	for _, endpoint := range endpoints[1:] {
		if endpoint.Weight > bestEndpoint.Weight {
			bestEndpoint = endpoint
		}
	}

	addr, err := bestEndpoint.ResolveAddress(c.ProviderProfiles)
	if err != nil {
		return "", "", false, fmt.Errorf("endpoint %q for model %q: %w", bestEndpoint.Name, modelName, err)
	}
	return addr, bestEndpoint.Name, true, nil
}

// ---------------------------------------------------------------------------
// Provider profile helpers
// ---------------------------------------------------------------------------

// providerTypeInfo holds the per-type defaults for a cloud provider.
// Every supported type MUST have an entry — no default/fallback branch.
type providerTypeInfo struct {
	AuthHeader string // HTTP header name for the API key
	AuthPrefix string // value prefix ("Bearer", "" etc.)
	ChatPath   string // path suffix appended after base_url path
}

// providerTypeRegistry is the single source of truth for type defaults.
// To add a new provider, add one entry here and a matching LLMProvider
// constant in pkg/authz/provider.go — nothing else needs a switch/default.
var providerTypeRegistry = map[string]providerTypeInfo{
	"openai":       {AuthHeader: "Authorization", AuthPrefix: "Bearer", ChatPath: "/chat/completions"},
	"anthropic":    {AuthHeader: "x-api-key", AuthPrefix: "", ChatPath: "/v1/messages"},
	"azure-openai": {AuthHeader: "api-key", AuthPrefix: "", ChatPath: "/chat/completions"},
	"bedrock":      {AuthHeader: "Authorization", AuthPrefix: "Bearer", ChatPath: "/chat/completions"},
	"gemini":       {AuthHeader: "Authorization", AuthPrefix: "Bearer", ChatPath: "/chat/completions"},
	"vertex-ai":    {AuthHeader: "Authorization", AuthPrefix: "Bearer", ChatPath: "/chat/completions"},
	"minimax":      {AuthHeader: "Authorization", AuthPrefix: "Bearer", ChatPath: "/v1/chat/completions"},
}

// ValidProviderTypes returns the set of recognised type strings (for error messages).
func ValidProviderTypes() []string {
	types := make([]string, 0, len(providerTypeRegistry))
	for t := range providerTypeRegistry {
		types = append(types, t)
	}
	return types
}

// GetProviderProfileForEndpoint resolves the ProviderProfile for a named endpoint.
//
// Returns (nil, nil) when the endpoint exists but has no provider_profile set
// (legacy address:port endpoint — this is not an error).
//
// Returns a non-nil error when:
//   - endpointName does not match any VLLMEndpoint
//   - the endpoint references a provider_profile name that does not exist in the map
func (c *RouterConfig) GetProviderProfileForEndpoint(endpointName string) (*ProviderProfile, error) {
	if endpointName == "" {
		return nil, nil // no backend metadata resolved (e.g., model has no preferred_endpoints)
	}
	ep, found := c.GetEndpointByName(endpointName)
	if !found {
		return nil, fmt.Errorf("endpoint %q not found in vllm_endpoints", endpointName)
	}
	if ep.ProviderProfileName == "" {
		return nil, nil // legacy endpoint, no profile — not an error
	}
	if c.ProviderProfiles == nil {
		return nil, fmt.Errorf("endpoint %q references provider_profile %q but no provider_profiles map is defined",
			endpointName, ep.ProviderProfileName)
	}
	profile, ok := c.ProviderProfiles[ep.ProviderProfileName]
	if !ok {
		return nil, fmt.Errorf("endpoint %q references provider_profile %q which does not exist in provider_profiles (have: %v)",
			endpointName, ep.ProviderProfileName, mapKeys(c.ProviderProfiles))
	}
	return &profile, nil
}

// mapKeys returns the keys of a map for diagnostic messages.
func mapKeys(m map[string]ProviderProfile) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

// ResolveAddress returns the host:port string for this endpoint.
//
// Two distinct modes — no silent fallback between them:
//   - provider_profile set → host:port is extracted from the profile's base_url.
//     Returns error if profile is missing, has no base_url, or base_url is unparsable.
//   - provider_profile NOT set → uses address:port fields directly.
func (ep *VLLMEndpoint) ResolveAddress(profiles map[string]ProviderProfile) (string, error) {
	if ep.ProviderProfileName == "" {
		// Legacy endpoint: address:port is the intended mode.
		return fmt.Sprintf("%s:%d", ep.Address, ep.Port), nil
	}

	// Profile-based endpoint: MUST resolve from base_url.
	if profiles == nil {
		return "", fmt.Errorf("endpoint %q has provider_profile %q but no provider_profiles map is defined",
			ep.Name, ep.ProviderProfileName)
	}
	profile, ok := profiles[ep.ProviderProfileName]
	if !ok {
		return "", fmt.Errorf("endpoint %q references provider_profile %q which does not exist",
			ep.Name, ep.ProviderProfileName)
	}
	if profile.BaseURL == "" {
		return "", fmt.Errorf("endpoint %q: provider_profile %q has no base_url",
			ep.Name, ep.ProviderProfileName)
	}

	u, err := url.Parse(profile.BaseURL)
	if err != nil {
		return "", fmt.Errorf("endpoint %q: cannot parse base_url %q: %w",
			ep.Name, profile.BaseURL, err)
	}
	if u.Host == "" {
		return "", fmt.Errorf("endpoint %q: base_url %q has no host",
			ep.Name, profile.BaseURL)
	}

	host := u.Host
	if !strings.Contains(host, ":") {
		switch u.Scheme {
		case "https":
			host += ":443"
		case "http":
			host += ":80"
		default:
			return "", fmt.Errorf("endpoint %q: base_url %q has unsupported scheme %q (expected http or https)",
				ep.Name, profile.BaseURL, u.Scheme)
		}
	}
	return host, nil
}

// ProviderType returns the provider type string, which matches authz.LLMProvider values.
// Returns an error if the type is empty or not in providerTypeRegistry.
func (p *ProviderProfile) ProviderType() (string, error) {
	if p == nil {
		return "", fmt.Errorf("provider profile is nil")
	}
	if p.Type == "" {
		return "", fmt.Errorf("provider profile has empty type")
	}
	if _, ok := providerTypeRegistry[p.Type]; !ok {
		return "", fmt.Errorf("unknown provider profile type %q (valid types: %v)", p.Type, ValidProviderTypes())
	}
	return p.Type, nil
}

// ResolveAuthHeader returns the (headerName, prefix) for the upstream auth header.
// Explicit AuthHeader/AuthPrefix fields override the type defaults.
// Returns error if the profile's type is not recognised.
func (p *ProviderProfile) ResolveAuthHeader() (string, string, error) {
	info, ok := providerTypeRegistry[p.Type]
	if !ok {
		return "", "", fmt.Errorf("unknown provider type %q — cannot determine auth header", p.Type)
	}
	headerName := info.AuthHeader
	prefix := info.AuthPrefix
	if p.AuthHeader != "" {
		headerName = p.AuthHeader
	}
	if p.AuthPrefix != "" {
		prefix = p.AuthPrefix
	}
	return headerName, prefix, nil
}

// ResolveChatPath returns the HTTP path for upstream requests.
//
// Resolution order (no silent fallback):
//  1. Explicit ChatPath field on the profile (used as-is, plus ?api-version for azure-openai).
//  2. base_url path + type-default suffix from providerTypeRegistry.
//  3. Type-default suffix alone if base_url has no path component.
//
// Returns error if the type is not recognised or base_url is unparsable.
func (p *ProviderProfile) ResolveChatPath() (string, error) {
	if p == nil {
		return "", fmt.Errorf("provider profile is nil")
	}

	info, ok := providerTypeRegistry[p.Type]
	if !ok {
		return "", fmt.Errorf("unknown provider type %q — cannot determine chat path", p.Type)
	}

	// Explicit override
	if p.ChatPath != "" {
		path := p.ChatPath
		if p.Type == "azure-openai" && p.APIVersion != "" {
			path += "?api-version=" + p.APIVersion
		}
		return path, nil
	}

	suffix := info.ChatPath
	if p.Type == "azure-openai" && p.APIVersion != "" {
		suffix += "?api-version=" + p.APIVersion
	}

	// Prepend base_url path component if present
	if p.BaseURL != "" {
		u, err := url.Parse(p.BaseURL)
		if err != nil {
			return "", fmt.Errorf("cannot parse base_url %q: %w", p.BaseURL, err)
		}
		if u.Path != "" && u.Path != "/" {
			return strings.TrimRight(u.Path, "/") + suffix, nil
		}
	}

	return suffix, nil
}
