package config

import (
	"fmt"
	"net/url"
	"sort"
	"strings"
)

// resolveProtocolProfiles resolves the top-level protocol_profiles catalog
// (built-in + custom overlays) and writes an immutable ConcreteProtocolProfile
// snapshot onto each provider that has a profile selector or a legacy type
// mapping. It is called once during Edge config normalization.
func resolveProtocolProfiles(cfg *EdgeConfig) error {
	catalog, err := buildProfileCatalog(cfg)
	if err != nil {
		return err
	}
	profileIDs := make([]string, 0, len(catalog))
	for id := range catalog {
		profileIDs = append(profileIDs, id)
	}
	sort.Strings(profileIDs)
	for _, id := range profileIDs {
		if _, err := ResolveProtocolProfile(id, "", catalog); err != nil {
			return fmt.Errorf("protocol_profiles[%q]: %w", id, err)
		}
	}
	for i := range cfg.Nodes {
		for j := range cfg.Nodes[i].Providers {
			p := &cfg.Nodes[i].Providers[j]
			// Skip providers that have no explicit profile selector and no
			// legacy type mapping. Only OpenAI-compatible aliases and explicit
			// profile selectors produce a concrete profile snapshot; other
			// provider types (for example ollama) are handled by their own
			// adapter paths.
			if strings.TrimSpace(p.Profile) == "" && LegacyProviderTypeToProfile(p.Type) == "" {
				continue
			}
			resolved, err := ResolveProtocolProfile(p.Profile, p.Type, catalog)
			if err != nil {
				return fmt.Errorf("nodes[%d].providers[%d] %q: %w", i, j, p.ID, err)
			}
			resolved = resolved.Clone()
			if adapterRef := strings.TrimSpace(p.Adapter); adapterRef != "" {
				backingEndpoint, err := resolveProtocolBackingEndpoint(cfg.Nodes[i].Adapters, adapterRef)
				if err != nil {
					nodeID := cfg.Nodes[i].ID
					if nodeID == "" {
						nodeID = cfg.Nodes[i].Alias
					}
					return fmt.Errorf("node %q provider %q adapter %q: %w", nodeID, p.ID, adapterRef, err)
				}
				normalized, err := overlayProtocolEndpoint(resolved.BaseURL, backingEndpoint)
				if err != nil {
					return fmt.Errorf("nodes[%d].providers[%d] %q: backing adapter endpoint: %w", i, j, p.ID, err)
				}
				resolved.BaseURL = normalized
			}
			if endpoint := effectiveProtocolEndpoint(*p); endpoint != "" {
				normalized, err := overlayProtocolEndpoint(resolved.BaseURL, endpoint)
				if err != nil {
					return fmt.Errorf("nodes[%d].providers[%d] %q: protocol endpoint: %w", i, j, p.ID, err)
				}
				resolved.BaseURL = normalized
			}
			snapshot := resolved.Clone()
			p.RuntimeProfile = &snapshot
		}
	}
	return nil
}

// resolveProtocolBackingEndpoint resolves the legacy HTTP adapter instance
// that supplies transport settings for an adapter-backed protocol profile.
// Exact instance names win; a type-name route is valid only with one enabled
// instance, matching the Node router's ambiguity contract.
func resolveProtocolBackingEndpoint(adapters AdaptersConf, ref string) (string, error) {
	ref = strings.TrimSpace(ref)
	type exactMatch struct {
		enabled  bool
		endpoint string
		typeName string
	}
	var exact []exactMatch
	for _, inst := range adapters.VllmInstances {
		if inst.Name == ref {
			exact = append(exact, exactMatch{inst.Enabled, inst.Endpoint, "vllm"})
		}
	}
	for _, inst := range adapters.OpenAICompatInstances {
		if inst.Name == ref {
			exact = append(exact, exactMatch{inst.Enabled, inst.Endpoint, "openai_compat"})
		}
	}
	if len(exact) > 1 {
		return "", fmt.Errorf("backing adapter reference is ambiguous across supported HTTP adapter types")
	}
	if len(exact) == 1 {
		if !exact[0].enabled {
			return "", fmt.Errorf("backing %s adapter instance is disabled", exact[0].typeName)
		}
		return exact[0].endpoint, nil
	}

	var enabled []string
	disabled := 0
	switch ref {
	case "vllm":
		for _, inst := range adapters.VllmInstances {
			if inst.Enabled {
				enabled = append(enabled, inst.Endpoint)
			} else {
				disabled++
			}
		}
	case "openai_compat":
		for _, inst := range adapters.OpenAICompatInstances {
			if inst.Enabled {
				enabled = append(enabled, inst.Endpoint)
			} else {
				disabled++
			}
		}
	default:
		return "", fmt.Errorf("backing adapter reference does not match a vllm or openai_compat instance")
	}
	switch len(enabled) {
	case 1:
		return enabled[0], nil
	case 0:
		if disabled > 0 {
			return "", fmt.Errorf("backing adapter type has no enabled instance")
		}
		return "", fmt.Errorf("backing adapter reference is missing")
	default:
		return "", fmt.Errorf("backing adapter type is ambiguous (%d enabled instances)", len(enabled))
	}
}

// buildProfileCatalog combines the immutable built-in profiles with the
// top-level custom overlays from EdgeConfig. Custom overlays may extend
// built-ins via the Base field.
func buildProfileCatalog(cfg *EdgeConfig) (map[string]ProtocolProfileConf, error) {
	catalog := BuiltInProtocolProfileCatalog()
	for id, p := range cfg.ProtocolProfiles {
		id = strings.TrimSpace(id)
		if id == "" {
			return nil, fmt.Errorf("protocol_profiles: profile id must not be empty")
		}
		if _, reserved := catalog[id]; reserved {
			return nil, fmt.Errorf("protocol_profiles[%q]: built-in profile id is reserved", id)
		}
		catalog[id] = deepCopyProfileConf(p)
	}
	return catalog, nil
}

// effectiveProtocolEndpoint applies provider-first precedence: endpoint is the
// HTTP adapter endpoint, while base_url is accepted as a compatibility alias.
func effectiveProtocolEndpoint(p NodeProviderConf) string {
	if endpoint := strings.TrimSpace(p.Endpoint); endpoint != "" {
		return endpoint
	}
	return strings.TrimSpace(p.BaseURL)
}

func normalizeProtocolEndpoint(raw string) (string, error) {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
		return "", fmt.Errorf("%q must be an absolute http(s) URL", raw)
	}
	parsed.Path = strings.TrimRight(parsed.Path, "/")
	return parsed.String(), nil
}

// overlayProtocolEndpoint replaces the upstream origin and honors an explicit
// provider path. A root-only endpoint retains the profile's documented base
// path (for example /v1), preserving legacy root endpoint compatibility.
func overlayProtocolEndpoint(profileBaseURL, rawEndpoint string) (string, error) {
	endpoint, err := normalizeProtocolEndpoint(rawEndpoint)
	if err != nil {
		return "", err
	}
	parsedEndpoint, _ := url.Parse(endpoint)
	if parsedEndpoint.Path == "" || parsedEndpoint.Path == "/" {
		parsedProfile, parseErr := url.Parse(profileBaseURL)
		if parseErr != nil {
			return "", fmt.Errorf("profile base_url %q is invalid: %w", profileBaseURL, parseErr)
		}
		parsedEndpoint.Path = strings.TrimRight(parsedProfile.Path, "/")
	}
	return parsedEndpoint.String(), nil
}

// NormalizeProviderType returns the canonical driver name for provider type.
func NormalizeProviderType(t string) string {
	switch strings.ToLower(strings.TrimSpace(t)) {
	case "openai_compat", "openai_api", "vllm", "vllm-mlx", "lemonade", "sglang", "seulgivibe_claude", "seulgivibe_openai":
		return "openai_compat"
	case "ollama":
		return "ollama"
	default:
		return t
	}
}

// NormalizeAdapters promotes legacy single-instance Ollama/Vllm fields into the
// typed instance slices and validates that all instance names are unique.
func NormalizeAdapters(a *AdaptersConf) error {
	return normalizeAdapters(a)
}

func normalizeAdapters(a *AdaptersConf) error {
	if a.Ollama.Enabled {
		existing := findOllamaInstance(a.OllamaInstances, "ollama")
		if existing == nil {
			a.OllamaInstances = append([]OllamaInstanceConf{{
				Name:             "ollama",
				Enabled:          a.Ollama.Enabled,
				BaseURL:          a.Ollama.BaseURL,
				ContextSize:      a.Ollama.ContextSize,
				Capacity:         a.Ollama.Capacity,
				MaxQueue:         a.Ollama.MaxQueue,
				QueueTimeoutMS:   a.Ollama.QueueTimeoutMS,
				RequestTimeoutMS: a.Ollama.RequestTimeoutMS,
			}}, a.OllamaInstances...)
		} else if !sameOllamaInstance(*existing, a.Ollama) {
			return fmt.Errorf("ollama: legacy field conflicts with explicit instance %q: enabled/base_url/context_size/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "ollama")
		}
	}
	if a.Vllm.Enabled {
		existing := findVllmInstance(a.VllmInstances, "vllm")
		if existing == nil {
			a.VllmInstances = append([]VllmInstanceConf{{
				Name:             "vllm",
				Enabled:          a.Vllm.Enabled,
				Endpoint:         a.Vllm.Endpoint,
				Capacity:         a.Vllm.Capacity,
				MaxQueue:         a.Vllm.MaxQueue,
				QueueTimeoutMS:   a.Vllm.QueueTimeoutMS,
				RequestTimeoutMS: a.Vllm.RequestTimeoutMS,
			}}, a.VllmInstances...)
		} else if !sameVllmInstance(*existing, a.Vllm) {
			return fmt.Errorf("vllm: legacy field conflicts with explicit instance %q: enabled/endpoint/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "vllm")
		}
	}
	if a.OpenAICompat.Enabled {
		existing := findOpenAICompatInstance(a.OpenAICompatInstances, "openai_compat")
		if existing == nil {
			a.OpenAICompatInstances = append([]OpenAICompatInstanceConf{{
				Name:             "openai_compat",
				Enabled:          a.OpenAICompat.Enabled,
				Provider:         a.OpenAICompat.Provider,
				Endpoint:         a.OpenAICompat.Endpoint,
				Headers:          a.OpenAICompat.Headers,
				Capacity:         a.OpenAICompat.Capacity,
				MaxQueue:         a.OpenAICompat.MaxQueue,
				QueueTimeoutMS:   a.OpenAICompat.QueueTimeoutMS,
				RequestTimeoutMS: a.OpenAICompat.RequestTimeoutMS,
			}}, a.OpenAICompatInstances...)
		} else if !sameOpenAICompatInstance(*existing, a.OpenAICompat) {
			return fmt.Errorf("openai_compat: legacy field conflicts with explicit instance %q: enabled/provider/endpoint/headers/capacity/max_queue/queue_timeout_ms/request_timeout_ms mismatch", "openai_compat")
		}
	}

	if err := checkUniqueNames("ollama_instances", func(i int) string { return a.OllamaInstances[i].Name }, len(a.OllamaInstances)); err != nil {
		return err
	}
	if err := checkUniqueNames("vllm_instances", func(i int) string { return a.VllmInstances[i].Name }, len(a.VllmInstances)); err != nil {
		return err
	}
	if err := checkUniqueNames("openai_compat_instances", func(i int) string { return a.OpenAICompatInstances[i].Name }, len(a.OpenAICompatInstances)); err != nil {
		return err
	}
	for i, inst := range a.OllamaInstances {
		field := fmt.Sprintf("ollama_instances[%d]", i)
		if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil {
			return err
		}
	}
	for i, inst := range a.VllmInstances {
		field := fmt.Sprintf("vllm_instances[%d]", i)
		if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil {
			return err
		}
	}
	for i, inst := range a.OpenAICompatInstances {
		field := fmt.Sprintf("openai_compat_instances[%d]", i)
		if err := validateProviderQueueConfig(field, inst.Capacity, inst.MaxQueue, inst.QueueTimeoutMS, inst.RequestTimeoutMS); err != nil {
			return err
		}
	}
	return nil
}

func sameOllamaInstance(inst OllamaInstanceConf, legacy OllamaConf) bool {
	return inst.Enabled == legacy.Enabled &&
		inst.BaseURL == legacy.BaseURL &&
		inst.ContextSize == legacy.ContextSize &&
		inst.Capacity == legacy.Capacity &&
		inst.MaxQueue == legacy.MaxQueue &&
		inst.QueueTimeoutMS == legacy.QueueTimeoutMS &&
		inst.RequestTimeoutMS == legacy.RequestTimeoutMS
}

func sameVllmInstance(inst VllmInstanceConf, legacy VllmConf) bool {
	return inst.Enabled == legacy.Enabled &&
		inst.Endpoint == legacy.Endpoint &&
		inst.Capacity == legacy.Capacity &&
		inst.MaxQueue == legacy.MaxQueue &&
		inst.QueueTimeoutMS == legacy.QueueTimeoutMS &&
		inst.RequestTimeoutMS == legacy.RequestTimeoutMS
}

func findOllamaInstance(instances []OllamaInstanceConf, name string) *OllamaInstanceConf {
	for i := range instances {
		if instances[i].Name == name {
			return &instances[i]
		}
	}
	return nil
}

func findVllmInstance(instances []VllmInstanceConf, name string) *VllmInstanceConf {
	for i := range instances {
		if instances[i].Name == name {
			return &instances[i]
		}
	}
	return nil
}

func sameOpenAICompatInstance(inst OpenAICompatInstanceConf, legacy OpenAICompatConf) bool {
	if inst.Enabled != legacy.Enabled ||
		inst.Provider != legacy.Provider ||
		inst.Endpoint != legacy.Endpoint ||
		inst.Capacity != legacy.Capacity ||
		inst.MaxQueue != legacy.MaxQueue ||
		inst.QueueTimeoutMS != legacy.QueueTimeoutMS ||
		inst.RequestTimeoutMS != legacy.RequestTimeoutMS {
		return false
	}
	if len(inst.Headers) != len(legacy.Headers) {
		return false
	}
	for k, v := range inst.Headers {
		if lv, ok := legacy.Headers[k]; !ok || lv != v {
			return false
		}
	}
	return true
}

func findOpenAICompatInstance(instances []OpenAICompatInstanceConf, name string) *OpenAICompatInstanceConf {
	for i := range instances {
		if instances[i].Name == name {
			return &instances[i]
		}
	}
	return nil
}
