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

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

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"`
	Nodes        []NodeDefinition     `mapstructure:"nodes"         yaml:"nodes"`
}

// 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"`
	Runtime   RuntimeConf  `mapstructure:"runtime"    yaml:"runtime"`
}

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"`
	WorkspaceRequired bool   `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"`
}

type EdgeOpenAIConf struct {
	Enabled            bool               `mapstructure:"enabled"              yaml:"enabled"`
	Listen             string             `mapstructure:"listen"               yaml:"listen"`
	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   int    `mapstructure:"concurrency"    yaml:"concurrency"`
	WorkspaceRoot string `mapstructure:"workspace_root" yaml:"workspace_root"`
}

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 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"`
	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"`
}

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

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

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"`
}

type VllmConf struct {
	Enabled  bool   `mapstructure:"enabled"  yaml:"enabled"`
	Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
}

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
	}
	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)
		}
	}
	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,
			}}, a.OllamaInstances...)
		} else if !existing.Enabled || existing.BaseURL != a.Ollama.BaseURL || existing.ContextSize != a.Ollama.ContextSize {
			return fmt.Errorf("ollama: legacy field conflicts with explicit instance %q: enabled/base_url/context_size 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,
			}}, a.VllmInstances...)
		} else if !existing.Enabled || existing.Endpoint != a.Vllm.Endpoint {
			return fmt.Errorf("vllm: legacy field conflicts with explicit instance %q: enabled/endpoint mismatch", "vllm")
		}
	}

	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
	}
	return nil
}

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
}

// 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{}{}
	}
	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 setDefaults(v *viper.Viper) {
	v.SetDefault("transport.edge_addr", "localhost:9090")
	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.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)
}
