package config

import (
	"fmt"
	"strings"

	"github.com/spf13/viper"
)

const AgentKindGenericNode = "generic-node"

// NormalizeAgentKind returns the canonical agent kind for a node definition.
// An empty value defaults to generic-node; any other unsupported value is
// rejected so misconfigured kinds fail at config load time.
func NormalizeAgentKind(kind string) (string, error) {
	switch kind {
	case "":
		return AgentKindGenericNode, nil
	case AgentKindGenericNode:
		return kind, nil
	default:
		return "", fmt.Errorf("invalid agent_kind %q (allowed: %q)", kind, AgentKindGenericNode)
	}
}

// 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", "lemonade", "sglang":
		return "openai_compat"
	case "ollama":
		return "ollama"
	case "cli":
		return "cli"
	default:
		return t
	}
}

type NodeConfig struct {
	Transport TransportConf `mapstructure:"transport" yaml:"transport"`
	Reconnect ReconnectConf `mapstructure:"reconnect" yaml:"reconnect"`
	Logging   LoggingConf   `mapstructure:"logging"   yaml:"logging"`
	Metrics   MetricsConf   `mapstructure:"metrics"   yaml:"metrics"`
}

// ReconnectConf controls Edge reconnect behaviour after a disconnect.
type ReconnectConf struct {
	IntervalSec int `mapstructure:"interval_sec" yaml:"interval_sec"`
	MaxAttempts int `mapstructure:"max_attempts" yaml:"max_attempts"`
}

// EdgeRefreshConf configures the Edge-local refresh admin HTTP server.
// When Enabled is false (default) the admin server is not started and
// config refresh is not available without a restart.
type EdgeRefreshConf struct {
	Enabled bool   `mapstructure:"enabled" yaml:"enabled"`
	Listen  string `mapstructure:"listen"  yaml:"listen"`
}

type EdgeConfig struct {
	Edge         EdgeInfo             `mapstructure:"edge"          yaml:"edge"`
	Server       EdgeServerConf       `mapstructure:"server"        yaml:"server"`
	Bootstrap    EdgeBootstrapConf    `mapstructure:"bootstrap"     yaml:"bootstrap"`
	OpenAI       EdgeOpenAIConf       `mapstructure:"openai"        yaml:"openai"`
	A2A          EdgeA2AConf          `mapstructure:"a2a"           yaml:"a2a"`
	TLS          TLSConf              `mapstructure:"tls"           yaml:"tls"`
	Logging      LoggingConf          `mapstructure:"logging"       yaml:"logging"`
	Metrics      MetricsConf          `mapstructure:"metrics"       yaml:"metrics"`
	Console      EdgeConsoleConf      `mapstructure:"console"       yaml:"console"`
	ControlPlane EdgeControlPlaneConf `mapstructure:"control_plane" yaml:"control_plane"`
	Refresh      EdgeRefreshConf      `mapstructure:"refresh"       yaml:"refresh,omitempty"`
	Nodes        []NodeDefinition     `mapstructure:"nodes"         yaml:"nodes"`
	// Models is the top-level model catalog.  Each entry defines a canonical
	// routing key (ID) and its provider-pool mapping.  When set it takes
	// precedence over the legacy openai.model_routes for runtime dispatch.
	Models []ModelCatalogEntry `mapstructure:"models" yaml:"models,omitempty"`
}

// EdgeControlPlaneConf holds the outbound connector settings for the
// Control Plane-Edge wire. When Enabled is false or WireAddr is empty the
// connector is a no-op and existing Edge behaviour is unaffected.
type EdgeControlPlaneConf struct {
	Enabled              bool   `mapstructure:"enabled"               yaml:"enabled"`
	WireAddr             string `mapstructure:"wire_addr"             yaml:"wire_addr"`
	ReconnectIntervalSec int    `mapstructure:"reconnect_interval_sec" yaml:"reconnect_interval_sec"`
}

// EdgeInfo carries this edge instance's stable identity for loading and logging.
// It is not used for Control Plane registration.
type EdgeInfo struct {
	ID   string `mapstructure:"id"   yaml:"id"`
	Name string `mapstructure:"name" yaml:"name"`
}

// NodeDefinition is the edge-side record for a pre-registered node.
type NodeDefinition struct {
	ID        string             `mapstructure:"id"         yaml:"id"` // stable node identity; if empty, a UUID v4 is auto-assigned (dev fallback only)
	Alias     string             `mapstructure:"alias"      yaml:"alias"`
	Token     string             `mapstructure:"token"      yaml:"token"`
	AgentKind string             `mapstructure:"agent_kind" yaml:"agent_kind"` // generic-node (default)
	Adapters  AdaptersConf       `mapstructure:"adapters"   yaml:"adapters"`
	Providers []NodeProviderConf `mapstructure:"providers" yaml:"providers,omitempty"`
	Runtime   RuntimeConf        `mapstructure:"runtime"    yaml:"runtime"`
}

// Category represents the provider category.
type Category string

const (
	CategoryAPI            Category = "api"
	CategoryCLI            Category = "cli"
	CategoryLocalInference Category = "local_inference"
)

// validateCategory returns nil when category is valid.
func validateCategory(c Category) error {
	switch c {
	case CategoryAPI, CategoryCLI, CategoryLocalInference:
		return nil
	default:
		return fmt.Errorf("invalid provider category %q (allowed: %q, %q, %q)", c, CategoryAPI, CategoryCLI, CategoryLocalInference)
	}
}

// NodeProviderConf defines a resource/provider candidate that belongs to a
// node. Provider-first resources can declare type-specific execution fields
// directly, while Adapter remains available for legacy/compat overrides.
type NodeProviderConf struct {
	// ID uniquely identifies this provider within the node and is referenced
	// by models[].providers keys.
	ID string `mapstructure:"id" yaml:"id"`
	// Type is the runtime type (e.g. "ollama", "vllm", "lemonade", "sglang", "openai_api", "cli").
	Type string `mapstructure:"type" yaml:"type"`
	// Category classifies the resource; MVP values: api, cli, local_inference.
	Category Category `mapstructure:"category" yaml:"category"`
	// Adapter is the optional concrete enabled adapter instance key used by
	// legacy/compat provider resources. A legacy type-name route is valid only
	// when exactly one enabled adapter instance of that type exists on the same
	// node.
	Adapter string `mapstructure:"adapter" yaml:"adapter,omitempty"`
	// Models lists the served model names this provider can actually serve.
	Models []string `mapstructure:"models" yaml:"models,omitempty"`
	// Health is the observed provider health state.
	Health string `mapstructure:"health" yaml:"health,omitempty"`
	// Capacity is the provider/resource maximum concurrent execution slots.
	// Provider-pool dispatch treats 0 as not dispatchable rather than falling
	// back to node runtime concurrency metadata.
	Capacity int `mapstructure:"capacity" yaml:"capacity,omitempty"`
	// MaxQueue is the maximum queue depth.
	MaxQueue int `mapstructure:"max_queue" yaml:"max_queue,omitempty"`
	// QueueTimeoutMS is the queue timeout in milliseconds.
	QueueTimeoutMS int `mapstructure:"queue_timeout_ms" yaml:"queue_timeout_ms,omitempty"`
	// Priority is the provider dispatch priority. Lower values are preferred
	// when in_flight counts are equal. Must be non-negative; zero is the
	// default priority.
	Priority int `mapstructure:"priority" yaml:"priority,omitempty"`
	// LifecycleCapabilities lists coarse lifecycle capabilities.
	LifecycleCapabilities []string `mapstructure:"lifecycle_capabilities" yaml:"lifecycle_capabilities,omitempty"`
	// Enabled controls whether this provider participates in the dispatch pool.
	// Nil (omitted) means enabled; false disables the provider without removing it.
	Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`

	// Provider-First fields (G06)
	Provider         string            `mapstructure:"provider" yaml:"provider,omitempty"`
	Endpoint         string            `mapstructure:"endpoint" yaml:"endpoint,omitempty"`
	BaseURL          string            `mapstructure:"base_url" yaml:"base_url,omitempty"`
	Headers          map[string]string `mapstructure:"headers" yaml:"headers,omitempty"`
	Command          string            `mapstructure:"command" yaml:"command,omitempty"`
	Args             []string          `mapstructure:"args" yaml:"args,omitempty"`
	Env              []string          `mapstructure:"env" yaml:"env,omitempty"`
	Mode             string            `mapstructure:"mode" yaml:"mode,omitempty"`
	ResumeArgs       []string          `mapstructure:"resume_args" yaml:"resume_args,omitempty"`
	OutputFormat     string            `mapstructure:"output_format" yaml:"output_format,omitempty"`
	ContextSize      int               `mapstructure:"context_size" yaml:"context_size,omitempty"`
	RequestTimeoutMS int               `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms,omitempty"`
}

// Validate checks internal consistency of the provider candidate config.
func (p NodeProviderConf) Validate() error {
	id := strings.TrimSpace(p.ID)
	if id == "" {
		return fmt.Errorf("nodes[].providers[].id must not be empty")
	}
	if strings.TrimSpace(p.Type) == "" {
		return fmt.Errorf("nodes[].providers[%q]: type must not be empty", id)
	}
	if err := validateCategory(p.Category); err != nil {
		return fmt.Errorf("nodes[].providers[%q]: %w", id, err)
	}
	for i, m := range p.Models {
		if strings.TrimSpace(m) == "" {
			return fmt.Errorf("nodes[].providers[%q].models[%d]: served model name must not be empty", id, i)
		}
	}
	if p.Capacity < 0 {
		return fmt.Errorf("nodes[].providers[%q].capacity must be non-negative", id)
	}
	if p.MaxQueue < 0 {
		return fmt.Errorf("nodes[].providers[%q].max_queue must be non-negative", id)
	}
	if p.QueueTimeoutMS < 0 {
		return fmt.Errorf("nodes[].providers[%q].queue_timeout_ms must be non-negative", id)
	}
	if p.RequestTimeoutMS < 0 {
		return fmt.Errorf("nodes[].providers[%q].request_timeout_ms must be non-negative", id)
	}
	if p.ContextSize < 0 {
		return fmt.Errorf("nodes[].providers[%q].context_size must be non-negative", id)
	}
	if p.Priority < 0 {
		return fmt.Errorf("nodes[].providers[%q].priority must be non-negative", id)
	}

	return nil
}

// ProviderEnabled reports whether a provider is enabled.
// Returns true when Enabled is nil (omitted) or *Enabled is true.
// Returns false only when Enabled is explicitly set to false.
func ProviderEnabled(p NodeProviderConf) bool {
	return p.Enabled == nil || *p.Enabled
}

type NodeInfo struct {
	ID   string `mapstructure:"id"   yaml:"id"`
	Name string `mapstructure:"name" yaml:"name"`
}

type EdgeServerConf struct {
	Listen        string `mapstructure:"listen"         yaml:"listen"`
	AdvertiseHost string `mapstructure:"advertise_host" yaml:"advertise_host"`
}

type EdgeBootstrapConf struct {
	ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
	Listen          string `mapstructure:"listen"            yaml:"listen"`
	ArtifactDir     string `mapstructure:"artifact_dir"      yaml:"artifact_dir"`
}

// OpenAIRouteEntry maps an external model id to an internal adapter/target routing.
// Fields not set here fall back to the top-level EdgeOpenAIConf defaults.
type OpenAIRouteEntry struct {
	Model             string `mapstructure:"model"              yaml:"model"`
	NodeRef           string `mapstructure:"node"               yaml:"node,omitempty"`
	Adapter           string `mapstructure:"adapter"            yaml:"adapter,omitempty"`
	Target            string `mapstructure:"target"             yaml:"target"`
	SessionID         string `mapstructure:"session_id"         yaml:"session_id,omitempty"`
	TimeoutSec        int    `mapstructure:"timeout_sec"        yaml:"timeout_sec,omitempty"`
	MaxQueue          int    `mapstructure:"max_queue"          yaml:"max_queue,omitempty"`
	QueueTimeoutMS    int    `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms,omitempty"`
	WorkspaceRequired bool   `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"`
}

// ModelCatalogEntry is a top-level Edge config entry that defines a canonical
// routing key (`ID`) and its provider-pool mapping.  Each provider id key
// maps to the concrete served model name that the provider actually exposes.
type ModelCatalogEntry struct {
	// ID is the canonical routing key (e.g. "qwen3.6:35b") that matches the
	// external OpenAI-compatible model field.
	ID string `mapstructure:"id" yaml:"id"`
	// DisplayName is a human-readable name shown in operation dashboards.
	DisplayName string `mapstructure:"display_name" yaml:"display_name,omitempty"`
	// Providers maps provider id to the concrete served model name that the
	// provider actually exposes.  Keys must match nodes[].providers[].id.
	Providers map[string]string `mapstructure:"providers" yaml:"providers"`
}

// buildProviderServedModelsIndex aggregates all nodes[].providers[].models
// keyed by provider id. Returns the index for Validate.
func buildProviderServedModelsIndex(nodes []NodeDefinition) map[string]map[string]struct{} {
	serveModels := make(map[string]map[string]struct{})
	for i := range nodes {
		for _, p := range nodes[i].Providers {
			if _, ok := serveModels[p.ID]; !ok {
				serveModels[p.ID] = make(map[string]struct{})
			}
			for _, m := range p.Models {
				serveModels[p.ID][m] = struct{}{}
			}
		}
	}
	return serveModels
}

// Validate checks the structure and internal references of the model catalog entry.
// When serveModels is non-nil, it also verifies that each models[].providers[pid]=model
// refers to a served model that exists in nodes[].providers[id].models.
func (e ModelCatalogEntry) Validate(resolvedProviderIDs map[string]struct{}, serveModels map[string]map[string]struct{}) error {
	id := strings.TrimSpace(e.ID)
	if id == "" {
		return fmt.Errorf("models[].id must not be empty")
	}
	if len(e.Providers) == 0 {
		return fmt.Errorf("models[%q].providers must not be empty", id)
	}
	for pid, model := range e.Providers {
		p := strings.TrimSpace(pid)
		if p == "" {
			return fmt.Errorf("models[%q].providers: provider id must not be empty", id)
		}
		m := strings.TrimSpace(model)
		if m == "" {
			return fmt.Errorf("models[%q].providers[%q]: served model name must not be empty", id, p)
		}
		if _, ok := resolvedProviderIDs[p]; !ok {
			return fmt.Errorf("models[%q].providers[%q]: provider id %q does not resolve to any nodes[].providers[].id", id, p, p)
		}
		// Serve model membership validation (REVIEW_API-1):
		// The served model must be listed in the provider's own models[].
		if serveModels != nil {
			servedSet, exists := serveModels[p]
			if !exists {
				// No providers list yet (should not happen in modern configs).
				return fmt.Errorf("models[%q].providers[%q]: provider %q has no models[] defined", id, p, p)
			}
			if _, ok := servedSet[m]; !ok {
				return fmt.Errorf("models[%q].providers[%q]: served model %q is not listed in nodes[].providers[%q].models", id, p, m, p)
			}
		}
	}
	return nil
}

type EdgeOpenAIConf struct {
	Enabled            bool               `mapstructure:"enabled"              yaml:"enabled"`
	Listen             string             `mapstructure:"listen"               yaml:"listen"`
	BearerToken        string             `mapstructure:"bearer_token"         yaml:"bearer_token"`
	NodeRef            string             `mapstructure:"node"                 yaml:"node"`
	Adapter            string             `mapstructure:"adapter"              yaml:"adapter"`
	Target             string             `mapstructure:"target"               yaml:"target"`
	Models             []string           `mapstructure:"models"               yaml:"models"`
	ModelRoutes        []OpenAIRouteEntry `mapstructure:"model_routes"         yaml:"model_routes,omitempty"`
	SessionID          string             `mapstructure:"session_id"           yaml:"session_id"`
	TimeoutSec         int                `mapstructure:"timeout_sec"          yaml:"timeout_sec"`
	StrictOutput       bool               `mapstructure:"strict_output"        yaml:"strict_output"`
	StrictStreamBuffer bool               `mapstructure:"strict_stream_buffer" yaml:"strict_stream_buffer"`
}

type EdgeA2AConf struct {
	Enabled     bool   `mapstructure:"enabled"      yaml:"enabled"`
	Listen      string `mapstructure:"listen"       yaml:"listen"`
	Path        string `mapstructure:"path"         yaml:"path"`
	NodeRef     string `mapstructure:"node"         yaml:"node"`
	Adapter     string `mapstructure:"adapter"      yaml:"adapter"`
	Target      string `mapstructure:"target"       yaml:"target"`
	SessionID   string `mapstructure:"session_id"   yaml:"session_id"`
	TimeoutSec  int    `mapstructure:"timeout_sec"  yaml:"timeout_sec"`
	BearerToken string `mapstructure:"bearer_token" yaml:"bearer_token"`
}

type EdgeConsoleConf struct {
	Adapter    string `mapstructure:"adapter"     yaml:"adapter"`
	Target     string `mapstructure:"target"      yaml:"target"`
	Agent      string `mapstructure:"agent"       yaml:"agent"` // legacy alias for target
	Model      string `mapstructure:"model"       yaml:"model"` // legacy alias for target
	SessionID  string `mapstructure:"session_id"  yaml:"session_id"`
	Background bool   `mapstructure:"background"  yaml:"background"`
	TimeoutSec int    `mapstructure:"timeout_sec" yaml:"timeout_sec"`
}

func (c EdgeConsoleConf) ResolveTarget() string {
	if c.Target != "" {
		return c.Target
	}
	if c.Agent != "" {
		return c.Agent
	}
	return c.Model
}

// ResolveAgent is kept for callers still using the legacy console.agent name.
func (c EdgeConsoleConf) ResolveAgent() string {
	return c.ResolveTarget()
}

type TransportConf struct {
	EdgeAddr string `mapstructure:"edge_addr" yaml:"edge_addr"`
	Token    string `mapstructure:"token"     yaml:"token"`
}

type TLSConf struct {
	Enabled bool   `mapstructure:"enabled" yaml:"enabled"`
	Cert    string `mapstructure:"cert"    yaml:"cert"`
	Key     string `mapstructure:"key"     yaml:"key"`
	CA      string `mapstructure:"ca"      yaml:"ca"`
}

type RuntimeConf struct {
	// Concurrency is legacy compatibility metadata. Runtime admission must use
	// provider/resource capacity rather than treating this as a node-wide gate.
	Concurrency int `mapstructure:"concurrency" yaml:"concurrency"`
}

type SQLiteConf struct {
	DSN string `mapstructure:"dsn" yaml:"dsn"`
}

type LoggingConf struct {
	Level  string `mapstructure:"level"  yaml:"level"`
	Pretty bool   `mapstructure:"pretty" yaml:"pretty"`
	Path   string `mapstructure:"path"   yaml:"path"`
}

type MetricsConf struct {
	Port int `mapstructure:"port" yaml:"port"`
}

type AdaptersConf struct {
	// Legacy single-instance fields. When non-empty they are normalised into
	// OllamaInstances / VllmInstances / OpenAICompatInstances during config load
	// so that all downstream code only needs to inspect the slice fields.
	Ollama       OllamaConf       `mapstructure:"ollama"        yaml:"ollama"`
	Vllm         VllmConf         `mapstructure:"vllm"          yaml:"vllm"`
	OpenAICompat OpenAICompatConf `mapstructure:"openai_compat" yaml:"openai_compat"`
	CLI          CLIConf          `mapstructure:"cli"           yaml:"cli"`

	// Multi-instance collections. Each entry carries a unique Name that acts as
	// the stable adapter instance identity within the node. Names must be unique
	// within each type collection; duplicate names are rejected at load time.
	OllamaInstances       []OllamaInstanceConf       `mapstructure:"ollama_instances"        yaml:"ollama_instances,omitempty"`
	VllmInstances         []VllmInstanceConf         `mapstructure:"vllm_instances"          yaml:"vllm_instances,omitempty"`
	OpenAICompatInstances []OpenAICompatInstanceConf `mapstructure:"openai_compat_instances" yaml:"openai_compat_instances,omitempty"`
}

// OllamaInstanceConf is one named Ollama engine instance within a node.
type OllamaInstanceConf struct {
	Name             string `mapstructure:"name"               yaml:"name"`
	Enabled          bool   `mapstructure:"enabled"            yaml:"enabled"`
	BaseURL          string `mapstructure:"base_url"           yaml:"base_url"`
	ContextSize      int    `mapstructure:"context_size"       yaml:"context_size"`
	Capacity         int    `mapstructure:"capacity"           yaml:"capacity"`
	MaxQueue         int    `mapstructure:"max_queue"          yaml:"max_queue"`
	QueueTimeoutMS   int    `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms"`
	RequestTimeoutMS int    `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
}

// VllmInstanceConf is one named vLLM engine instance within a node.
type VllmInstanceConf struct {
	Name             string `mapstructure:"name"               yaml:"name"`
	Enabled          bool   `mapstructure:"enabled"            yaml:"enabled"`
	Endpoint         string `mapstructure:"endpoint"           yaml:"endpoint"`
	Capacity         int    `mapstructure:"capacity"           yaml:"capacity"`
	MaxQueue         int    `mapstructure:"max_queue"          yaml:"max_queue"`
	QueueTimeoutMS   int    `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms"`
	RequestTimeoutMS int    `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
}

type OpenAICompatConf struct {
	Enabled          bool              `mapstructure:"enabled"            yaml:"enabled"`
	Provider         string            `mapstructure:"provider"           yaml:"provider"`
	Endpoint         string            `mapstructure:"endpoint"           yaml:"endpoint"`
	Headers          map[string]string `mapstructure:"headers"            yaml:"headers"`
	Capacity         int               `mapstructure:"capacity"           yaml:"capacity"`
	MaxQueue         int               `mapstructure:"max_queue"          yaml:"max_queue"`
	QueueTimeoutMS   int               `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms"`
	RequestTimeoutMS int               `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
}

type OpenAICompatInstanceConf struct {
	Name             string            `mapstructure:"name"               yaml:"name"`
	Enabled          bool              `mapstructure:"enabled"            yaml:"enabled"`
	Provider         string            `mapstructure:"provider"           yaml:"provider"`
	Endpoint         string            `mapstructure:"endpoint"           yaml:"endpoint"`
	Headers          map[string]string `mapstructure:"headers"            yaml:"headers"`
	Capacity         int               `mapstructure:"capacity"           yaml:"capacity"`
	MaxQueue         int               `mapstructure:"max_queue"          yaml:"max_queue"`
	QueueTimeoutMS   int               `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms"`
	RequestTimeoutMS int               `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
}

type OllamaConf struct {
	Enabled          bool   `mapstructure:"enabled"            yaml:"enabled"`
	BaseURL          string `mapstructure:"base_url"           yaml:"base_url"`
	ContextSize      int    `mapstructure:"context_size"       yaml:"context_size"`
	Capacity         int    `mapstructure:"capacity"           yaml:"capacity"`
	MaxQueue         int    `mapstructure:"max_queue"          yaml:"max_queue"`
	QueueTimeoutMS   int    `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms"`
	RequestTimeoutMS int    `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
}

type VllmConf struct {
	Enabled          bool   `mapstructure:"enabled"            yaml:"enabled"`
	Endpoint         string `mapstructure:"endpoint"           yaml:"endpoint"`
	Capacity         int    `mapstructure:"capacity"           yaml:"capacity"`
	MaxQueue         int    `mapstructure:"max_queue"          yaml:"max_queue"`
	QueueTimeoutMS   int    `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms"`
	RequestTimeoutMS int    `mapstructure:"request_timeout_ms" yaml:"request_timeout_ms"`
}

type CLIConf struct {
	Enabled  bool                      `mapstructure:"enabled"  yaml:"enabled"`
	Profiles map[string]CLIProfileConf `mapstructure:"profiles" yaml:"profiles"`
}

type CLIProfileConf struct {
	Command               string               `mapstructure:"command"                  yaml:"command"`
	Args                  []string             `mapstructure:"args"                     yaml:"args"`
	Env                   []string             `mapstructure:"env"                      yaml:"env"`
	Persistent            bool                 `mapstructure:"persistent"               yaml:"persistent"`
	Terminal              bool                 `mapstructure:"terminal"                 yaml:"terminal"`
	ResponseIdleTimeoutMS int                  `mapstructure:"response_idle_timeout_ms" yaml:"response_idle_timeout_ms"`
	StartupIdleTimeoutMS  int                  `mapstructure:"startup_idle_timeout_ms"  yaml:"startup_idle_timeout_ms"`
	OutputFormat          string               `mapstructure:"output_format"            yaml:"output_format"`
	CompletionMarker      CompletionMarkerConf `mapstructure:"completion_marker"        yaml:"completion_marker"`
	Mode                  string               `mapstructure:"mode"                     yaml:"mode"`
	ResumeArgs            []string             `mapstructure:"resume_args"              yaml:"resume_args"`
}

type CompletionMarkerConf struct {
	Line  string `mapstructure:"line"  yaml:"line"`
	Regex string `mapstructure:"regex" yaml:"regex"`
}

func (m CompletionMarkerConf) Empty() bool {
	return m.Line == "" && m.Regex == ""
}

func Load(cfgFile string) (*NodeConfig, error) {
	v := viper.New()
	v.SetConfigFile(cfgFile)
	setDefaults(v)
	if err := v.ReadInConfig(); err != nil {
		return nil, err
	}
	var cfg NodeConfig
	if err := v.Unmarshal(&cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

func LoadEdge(cfgFile string) (*EdgeConfig, error) {
	v := viper.New()
	v.SetConfigFile(cfgFile)
	setEdgeDefaults(v)
	if err := v.ReadInConfig(); err != nil {
		return nil, err
	}
	var cfg EdgeConfig
	if err := v.Unmarshal(&cfg); err != nil {
		return nil, err
	}
	if !v.InConfig("console.target") {
		if v.InConfig("console.agent") {
			cfg.Console.Target = cfg.Console.Agent
		} else if v.InConfig("console.model") {
			cfg.Console.Target = cfg.Console.Model
		}
	}
	if err := validateOpenAIRoutes(cfg.OpenAI.ModelRoutes); err != nil {
		return nil, err
	}

	// Collect all provider IDs from nodes[].providers[] for cross-referencing
	// and validate uniqueness within each node.
	providerIDs := make(map[string]struct{})
	for i := range cfg.Nodes {
		kind, err := NormalizeAgentKind(cfg.Nodes[i].AgentKind)
		if err != nil {
			return nil, fmt.Errorf("nodes[%d] alias=%q: %w", i, cfg.Nodes[i].Alias, err)
		}
		cfg.Nodes[i].AgentKind = kind

		if err := normalizeAdapters(&cfg.Nodes[i].Adapters); err != nil {
			name := cfg.Nodes[i].ID
			if name == "" {
				name = cfg.Nodes[i].Alias
			}
			return nil, fmt.Errorf("nodes[%d] %q adapters: %w", i, name, err)
		}

		// Validate unique provider IDs within this node and across all nodes.
		seenProviderIDs := make(map[string]struct{}, len(cfg.Nodes[i].Providers))
		for j, p := range cfg.Nodes[i].Providers {
			if err := p.Validate(); err != nil {
				return nil, fmt.Errorf("nodes[%d].providers[%d]: %w", i, j, err)
			}
			if _, dup := seenProviderIDs[p.ID]; dup {
				return nil, fmt.Errorf("nodes[%d].providers: duplicate provider id %q within node", i, p.ID)
			}
			seenProviderIDs[p.ID] = struct{}{}
			if _, globalDup := providerIDs[p.ID]; globalDup {
				return nil, fmt.Errorf("nodes[%d].providers[%d]: duplicate provider id %q across nodes (global uniqueness violation)", i, j, p.ID)
			}
			providerIDs[p.ID] = struct{}{}
		}
	}

	for i := range cfg.Nodes {
		if err := CheckProviderLegacyConflict(i, cfg.Nodes[i].Alias, &cfg.Nodes[i]); err != nil {
			return nil, err
		}
	}

	// Build the provider->servedModels index so Validate can check membership.
	serveModels := buildProviderServedModelsIndex(cfg.Nodes)

	// Validate models[].providers reference valid provider IDs.
	seenModelIDs := make(map[string]struct{}, len(cfg.Models))
	for i, m := range cfg.Models {
		id := strings.TrimSpace(m.ID)
		if id == "" {
			return nil, fmt.Errorf("models[%d]: id must not be empty", i)
		}
		if _, dup := seenModelIDs[id]; dup {
			return nil, fmt.Errorf("models: duplicate model id %q", id)
		}
		seenModelIDs[id] = struct{}{}
		if err := m.Validate(providerIDs, serveModels); err != nil {
			return nil, fmt.Errorf("models[%d]: %w", i, err)
		}
	}

	return &cfg, nil
}

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

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
}

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

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
}

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

func setDefaults(v *viper.Viper) {
	v.SetDefault("transport.edge_addr", "localhost:9090")
	v.SetDefault("reconnect.interval_sec", 10)
	v.SetDefault("reconnect.max_attempts", 10)
	v.SetDefault("logging.level", "info")
	v.SetDefault("metrics.port", 9091)
}

func setEdgeDefaults(v *viper.Viper) {
	v.SetDefault("server.listen", "0.0.0.0:9090")
	v.SetDefault("bootstrap.listen", "0.0.0.0:18080")
	v.SetDefault("bootstrap.artifact_dir", "artifacts")
	v.SetDefault("openai.enabled", false)
	v.SetDefault("openai.listen", "0.0.0.0:18081")
	v.SetDefault("openai.bearer_token", "")
	v.SetDefault("openai.adapter", "ollama")
	v.SetDefault("openai.session_id", "openai")
	v.SetDefault("openai.timeout_sec", 120)
	v.SetDefault("openai.strict_output", true)
	v.SetDefault("openai.strict_stream_buffer", false)
	v.SetDefault("a2a.enabled", false)
	v.SetDefault("a2a.listen", "0.0.0.0:8081")
	v.SetDefault("a2a.path", "/a2a")
	v.SetDefault("a2a.adapter", "cli")
	v.SetDefault("a2a.session_id", "a2a")
	v.SetDefault("a2a.timeout_sec", 120)
	v.SetDefault("logging.level", "info")
	v.SetDefault("metrics.port", 19092)
	v.SetDefault("tls.enabled", false)
	v.SetDefault("console.adapter", "cli")
	v.SetDefault("console.target", "claude")
	v.SetDefault("console.session_id", "default")
	v.SetDefault("console.background", false)
	v.SetDefault("console.timeout_sec", 120)
	v.SetDefault("control_plane.enabled", false)
	v.SetDefault("control_plane.wire_addr", "")
	v.SetDefault("control_plane.reconnect_interval_sec", 5)
	v.SetDefault("refresh.enabled", false)
	v.SetDefault("refresh.listen", "127.0.0.1:19093")
}
