package config

import (
	"fmt"
	"strings"

	"github.com/spf13/viper"
)

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
	}
	if err := validateReconnect(cfg.Reconnect); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// validateReconnect enforces the reconnect policy contract (SDD S17). setDefaults
// supplies the omitted defaults (interval_sec=10, max_attempts=10) before this
// runs, so an omitted max_attempts is already 10 here while an explicit 0 stays
// 0 (unlimited). Negative max_attempts or interval_sec are rejected, and an
// unlimited policy requires a positive interval so it cannot hot-loop.
func validateReconnect(rc ReconnectConf) error {
	if rc.MaxAttempts < 0 {
		return fmt.Errorf("reconnect.max_attempts must not be negative")
	}
	if rc.IntervalSec < 0 {
		return fmt.Errorf("reconnect.interval_sec must not be negative")
	}
	if rc.MaxAttempts == 0 && rc.IntervalSec <= 0 {
		return fmt.Errorf("reconnect.interval_sec must be positive when reconnect.max_attempts=0 (unlimited)")
	}
	return 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
	}
	if err := validateOpenAIPrincipalTokens(cfg.OpenAI.PrincipalTokens); err != nil {
		return nil, err
	}
	if err := normalizeOpenAIProviderAuth(v, &cfg.OpenAI.ProviderAuth); err != nil {
		return nil, err
	}
	if cfg.LongContextThresholdTokens <= 0 {
		return nil, fmt.Errorf("long_context_threshold_tokens must be positive")
	}

	// Resolve the canonical Edge root provider_pool queue policy.
	// SDD S06: root provider_pool is the canonical owner of the effective
	// queue policy. Legacy per-provider max_queue/queue_timeout_ms are ignored
	// once the canonical owner is resolved.
	if err := resolveProviderPoolPolicy(v, &cfg); 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{})
	providerByID := make(map[string]NodeProviderConf)
	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{}{}
			providerByID[p.ID] = p
		}
	}

	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)
		}
		if err := validateProviderLongContextBudget(m, providerByID); err != nil {
			return nil, fmt.Errorf("models[%d]: %w", i, err)
		}
	}

	return &cfg, nil
}

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")
	v.SetDefault("long_context_threshold_tokens", 100000)
}

// resolveProviderPoolPolicy resolves the effective queue policy from the
// canonical Edge root or, when absent, promotes the matching legacy per-
// provider pair. SDD S06.
//
// Canonical path: if either provider_pool.max_queue or provider_pool.queue_timeout_ms
// is present in the raw source config, treat the canonical owner as set.
// Missing/max=0 values fall back to defaults, while explicit timeout 0 stays
// as no-timeout.
//
// Legacy path: when no canonical key is present, collect every node provider
// that contributes a usable legacy pair (max_queue>0 or queue_timeout_ms>0),
// require all of them to agree, and promote the single shared pair. Conflicts
// trigger a deterministic error naming the offending providers.
func resolveProviderPoolPolicy(v *viper.Viper, cfg *EdgeConfig) error {
	canonicalHasKey := v.InConfig("provider_pool.max_queue") || v.InConfig("provider_pool.queue_timeout_ms")

	if canonicalHasKey {
		// Canonical present: validate boundaries before applying default semantics.
		// SDD S06: negative values are rejected; max_queue=0 normalizes to
		// default; explicit queue_timeout_ms=0 stays as no-timeout.
		if cfg.ProviderPool.MaxQueue < 0 {
			return fmt.Errorf("provider_pool.max_queue must be non-negative")
		}
		if cfg.ProviderPool.QueueTimeoutMS < 0 {
			return fmt.Errorf("provider_pool.queue_timeout_ms must be non-negative")
		}
		if cfg.ProviderPool.MaxQueue == 0 {
			cfg.ProviderPool.MaxQueue = DefaultProviderPoolMaxQueue
		}
		if v.InConfig("provider_pool.queue_timeout_ms") {
			// Key present: preserve decoded value (0 = no-timeout, N>0 = explicit).
		} else {
			cfg.ProviderPool.QueueTimeoutMS = DefaultProviderPoolQueueTimeoutMS
		}
		return nil
	}

	// Canonical absent: collect legacy effective pairs from all node providers.
	type legacyPair struct {
		id   string
		maxQ int
		tMS  int
	}
	var candidates []legacyPair
	seen := make(map[string]struct{})

	for i := range cfg.Nodes {
		for _, p := range cfg.Nodes[i].Providers {
			// Skip if this provider already seen (cross-node dedupe).
			if _, dup := seen[p.ID]; dup {
				continue
			}
			seen[p.ID] = struct{}{}

			maxQ := p.MaxQueue
			tMS := p.QueueTimeoutMS

			// A legacy pair must have at least one usable field (>0).
			// Both unset/zero alone does not participate (same as runtime default).
			if maxQ <= 0 && tMS <= 0 {
				continue
			}

			// Normalize participating legacy pair to effective value before
			// comparison/promotion. max_queue==0 in a participating pair means the
			// default queue depth, so replace it with DefaultProviderPoolMaxQueue.
			// max_queue>0 with timeout==0 is a
			// max-only candidate; its no-timeout semantics are preserved as-is.
			if maxQ == 0 {
				maxQ = DefaultProviderPoolMaxQueue
			}

			candidates = append(candidates, legacyPair{id: p.ID, maxQ: maxQ, tMS: tMS})
		}
	}

	if len(candidates) == 0 {
		cfg.ProviderPool.MaxQueue = DefaultProviderPoolMaxQueue
		cfg.ProviderPool.QueueTimeoutMS = DefaultProviderPoolQueueTimeoutMS
		return nil
	}

	// All candidates must agree on the same pair.
	ref := candidates[0]
	for _, c := range candidates[1:] {
		if c.maxQ != ref.maxQ || c.tMS != ref.tMS {
			return fmt.Errorf("conflicting provider queue policy: provider %q has max_queue=%d queue_timeout_ms=%d; provider %q has max_queue=%d queue_timeout_ms=%d; canonical provider_pool is required to disambiguate", ref.id, ref.maxQ, ref.tMS, c.id, c.maxQ, c.tMS)
		}
	}

	// Promote the single agreed legacy pair as-is (explicit 0 stays no-timeout).
	cfg.ProviderPool.MaxQueue = ref.maxQ
	cfg.ProviderPool.QueueTimeoutMS = ref.tMS
	return nil
}
