package edgevalidate

import (
	"fmt"
	"strings"

	edgenode "iop/apps/edge/internal/node"
	"iop/packages/go/config"
)

// ValidateEdgeConfig validates Edge config fields that config.LoadEdge does
// not check: adapter endpoint presence, per-node adapter-enabled requirements,
// and node store consistency (token uniqueness). It is the shared validation
// path used by both `config check` and the refresh candidate loader so that
// the same ruleset applies regardless of the entry point.
func ValidateEdgeConfig(cfg *config.EdgeConfig) error {
	_, err := edgenode.LoadFromConfig(cfg.Nodes)
	if err != nil {
		return err
	}

	for i, n := range cfg.Nodes {
		name := n.ID
		if name == "" {
			name = n.Alias
		}
		if name == "" {
			name = fmt.Sprintf("index %d", i)
		}

		hasOllama := anyOllamaEnabled(n.Adapters)
		hasVllm := anyVllmEnabled(n.Adapters)
		hasOpenAICompat := anyOpenAICompatEnabled(n.Adapters)
		hasProviders := len(n.Providers) > 0
		if !hasOllama && !hasVllm && !hasOpenAICompat && !hasProviders && !n.Adapters.Mock.Enabled {
			return fmt.Errorf("node %q: at least one adapter must be enabled", name)
		}
		if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" {
			return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name)
		}
		for j, inst := range n.Adapters.OllamaInstances {
			if inst.Enabled && inst.BaseURL == "" {
				return fmt.Errorf("node %q: ollama_instances[%d] %q: base_url must not be empty", name, j, inst.Name)
			}
		}
		if n.Adapters.Vllm.Enabled && n.Adapters.Vllm.Endpoint == "" {
			return fmt.Errorf("node %q: vllm adapter endpoint must not be empty", name)
		}
		for j, inst := range n.Adapters.VllmInstances {
			if inst.Enabled && inst.Endpoint == "" {
				return fmt.Errorf("node %q: vllm_instances[%d] %q: endpoint must not be empty", name, j, inst.Name)
			}
		}
		if n.Adapters.OpenAICompat.Enabled && n.Adapters.OpenAICompat.Endpoint == "" {
			return fmt.Errorf("node %q: openai_compat adapter endpoint must not be empty", name)
		}
		for j, inst := range n.Adapters.OpenAICompatInstances {
			if inst.Enabled && inst.Endpoint == "" {
				return fmt.Errorf("node %q: openai_compat_instances[%d] %q: endpoint must not be empty", name, j, inst.Name)
			}
		}
		if n.Runtime.Concurrency < 0 {
			return fmt.Errorf("node %q: runtime concurrency must be non-negative", name)
		}
		if err := validateProviderAdapterReferences(name, n); err != nil {
			return err
		}
		// COMPAT: provider-first / legacy adapter field conflicts
		if err := config.CheckProviderLegacyConflict(i, name, &n); err != nil {
			return err
		}
	}

	if err := validateMockSurfaceReferences(cfg); err != nil {
		return err
	}
	return nil
}

func validateProviderAdapterReferences(nodeName string, n config.NodeDefinition) error {
	idx := buildAdapterIndex(n.Adapters)
	for i, p := range n.Providers {
		// Disabled providers are validated for structural fields (type, category,
		// models, numeric bounds) but adapter reference checks are skipped since
		// the provider won't be dispatched and its adapter may be offline.
		if !config.ProviderEnabled(p) {
			if err := p.Validate(); err != nil {
				return fmt.Errorf("node %q: providers[%d]: %w", nodeName, i, err)
			}
			normType := config.NormalizeProviderType(p.Type)
			if normType != "openai_compat" && normType != "ollama" {
				return fmt.Errorf("node %q: providers[%d] %q: unknown provider type %q", nodeName, i, p.ID, p.Type)
			}
			continue
		}
		trimmedType := strings.TrimSpace(p.Type)
		if trimmedType == "" {
			return fmt.Errorf("node %q: providers[%d] %q: type must not be empty", nodeName, i, p.ID)
		}
		normType := config.NormalizeProviderType(p.Type)
		if normType != "openai_compat" && normType != "ollama" {
			return fmt.Errorf("node %q: providers[%d] %q: unknown provider type %q", nodeName, i, p.ID, p.Type)
		}

		adapter := strings.TrimSpace(p.Adapter)
		if adapter == "" {
			if p.Endpoint == "" && p.BaseURL == "" {
				if len(p.Models) == 0 {
					return fmt.Errorf("node %q: providers[%d] %q: adapter must not be empty", nodeName, i, p.ID)
				}
				count := 0
				switch normType {
				case "openai_compat":
					count = idx.typeCount["openai_compat"] + idx.typeCount["vllm"]
				case "ollama":
					count = idx.typeCount["ollama"]
				}

				if count == 1 {
					continue
				} else if count > 1 {
					return fmt.Errorf("node %q: providers[%d] %q: adapter %q is ambiguous; use an enabled adapter instance key", nodeName, i, p.ID, p.Type)
				}
				return fmt.Errorf("node %q: providers[%d] %q: adapter must not be empty", nodeName, i, p.ID)
			}

			// provider-first execution fields present
			switch normType {
			case "openai_compat":
				if p.Endpoint == "" {
					return fmt.Errorf("node %q: providers[%d] %q: openai_compat provider endpoint must not be empty", nodeName, i, p.ID)
				}
				if p.BaseURL != "" {
					return fmt.Errorf("node %q: providers[%d] %q: openai_compat provider must not set base_url", nodeName, i, p.ID)
				}
			case "ollama":
				if p.BaseURL == "" {
					return fmt.Errorf("node %q: providers[%d] %q: ollama provider base_url must not be empty", nodeName, i, p.ID)
				}
				if p.Endpoint != "" {
					return fmt.Errorf("node %q: providers[%d] %q: ollama provider must not set endpoint", nodeName, i, p.ID)
				}
			}
			continue
		}
		if idx.exact[adapter] {
			continue
		}
		if count, knownType := idx.typeCount[adapter]; knownType {
			switch count {
			case 0:
				return fmt.Errorf("node %q: providers[%d] %q: adapter %q does not resolve to an enabled adapter instance", nodeName, i, p.ID, adapter)
			case 1:
				continue
			default:
				return fmt.Errorf("node %q: providers[%d] %q: adapter %q is ambiguous; use an enabled adapter instance key", nodeName, i, p.ID, adapter)
			}
		}
		return fmt.Errorf("node %q: providers[%d] %q: adapter %q does not resolve to an enabled adapter instance", nodeName, i, p.ID, adapter)
	}
	return nil
}

type adapterIndex struct {
	exact     map[string]bool
	typeKeys  map[string]map[string]struct{}
	typeCount map[string]int
}

func buildAdapterIndex(a config.AdaptersConf) adapterIndex {
	idx := adapterIndex{
		exact:     make(map[string]bool),
		typeKeys:  make(map[string]map[string]struct{}),
		typeCount: map[string]int{"ollama": 0, "vllm": 0, "openai_compat": 0},
	}
	if a.Ollama.Enabled {
		idx.add("ollama", "ollama")
	}
	for _, inst := range a.OllamaInstances {
		if inst.Enabled {
			idx.add("ollama", defaultAdapterKey(inst.Name, "ollama"))
		}
	}
	if a.Vllm.Enabled {
		idx.add("vllm", "vllm")
	}
	for _, inst := range a.VllmInstances {
		if inst.Enabled {
			idx.add("vllm", defaultAdapterKey(inst.Name, "vllm"))
		}
	}
	if a.OpenAICompat.Enabled {
		idx.add("openai_compat", "openai_compat")
	}
	for _, inst := range a.OpenAICompatInstances {
		if inst.Enabled {
			idx.add("openai_compat", defaultAdapterKey(inst.Name, "openai_compat"))
		}
	}
	if a.Mock.Enabled {
		idx.add("mock", "mock")
	}
	for typeName, keys := range idx.typeKeys {
		idx.typeCount[typeName] = len(keys)
	}
	return idx
}

func (idx adapterIndex) add(typeName, key string) {
	key = strings.TrimSpace(key)
	if key == "" {
		return
	}
	idx.exact[key] = true
	if idx.typeKeys[typeName] == nil {
		idx.typeKeys[typeName] = make(map[string]struct{})
	}
	idx.typeKeys[typeName][key] = struct{}{}
}

func defaultAdapterKey(name, typeName string) string {
	if strings.TrimSpace(name) == "" {
		return typeName
	}
	return name
}

func anyOllamaEnabled(a config.AdaptersConf) bool {
	if a.Ollama.Enabled {
		return true
	}
	for _, inst := range a.OllamaInstances {
		if inst.Enabled {
			return true
		}
	}
	return false
}

func anyVllmEnabled(a config.AdaptersConf) bool {
	if a.Vllm.Enabled {
		return true
	}
	for _, inst := range a.VllmInstances {
		if inst.Enabled {
			return true
		}
	}
	return false
}

func anyOpenAICompatEnabled(a config.AdaptersConf) bool {
	if a.OpenAICompat.Enabled {
		return true
	}
	for _, inst := range a.OpenAICompatInstances {
		if inst.Enabled {
			return true
		}
	}
	return false
}

// validateMockSurfaceReferences ensures that mock adapter references in
// edge-wide surfaces (console, openai, a2a) are backed by explicit mock
// activation on the relevant node(s).
func validateMockSurfaceReferences(cfg *config.EdgeConfig) error {
	// Build maps: node id/alias -> has mock enabled
	nodeIDtoMock := make(map[string]bool)
	nodeAliasToMock := make(map[string]bool)
	for _, n := range cfg.Nodes {
		nodeIDtoMock[n.ID] = n.Adapters.Mock.Enabled
		if n.Alias != "" {
			nodeAliasToMock[n.Alias] = n.Adapters.Mock.Enabled
		}
	}

	// resolveMockNode checks whether a node reference resolves to a node
	// with mock enabled. Returns (hasMock, found).
	resolveMockNode := func(ref string) (bool, bool) {
		ref = strings.TrimSpace(ref)
		if ref == "" {
			return false, false
		}
		if v, ok := nodeIDtoMock[ref]; ok {
			return v, true
		}
		if v, ok := nodeAliasToMock[ref]; ok {
			return v, true
		}
		return false, false
	}

	// hasAnyMock reports whether at least one node has explicit mock enabled.
	hasAnyMock := false
	for _, v := range nodeIDtoMock {
		if v {
			hasAnyMock = true
			break
		}
	}

	// console.adapter: mock - console has no node ref; any node with mock suffices.
	if cfg.Console.Adapter == "mock" && !hasAnyMock {
		return fmt.Errorf("console.adapter references mock but no node has explicit mock enabled")
	}

	// openai.adapter and openai.model_routes[].adapter.
	if cfg.OpenAI.Enabled {
		if cfg.OpenAI.Adapter == "mock" {
			if cfg.OpenAI.NodeRef != "" {
				hasMock, found := resolveMockNode(cfg.OpenAI.NodeRef)
				if !found {
					return fmt.Errorf("openai adapter node reference %q does not resolve to any node", cfg.OpenAI.NodeRef)
				}
				if !hasMock {
					return fmt.Errorf("openai adapter references mock but node %q does not have explicit mock enabled", cfg.OpenAI.NodeRef)
				}
			} else if !hasAnyMock {
				return fmt.Errorf("openai.adapter references mock but no node has explicit mock enabled")
			}
		}

		for i, route := range cfg.OpenAI.ModelRoutes {
			if route.Adapter == "mock" {
				if route.NodeRef != "" {
					hasMock, found := resolveMockNode(route.NodeRef)
					if !found {
						return fmt.Errorf("openai.model_routes[%d] adapter references mock but node reference %q does not resolve to any node", i, route.NodeRef)
					}
					if !hasMock {
						return fmt.Errorf("openai.model_routes[%d] adapter references mock but node %q does not have explicit mock enabled", i, route.NodeRef)
					}
				} else if !hasAnyMock {
					return fmt.Errorf("openai.model_routes[%d] adapter references mock but no node has explicit mock enabled", i)
				}
			}
		}
	}

	// a2a.adapter: mock.
	if cfg.A2A.Enabled {
		if cfg.A2A.Adapter == "mock" {
			if cfg.A2A.NodeRef != "" {
				hasMock, found := resolveMockNode(cfg.A2A.NodeRef)
				if !found {
					return fmt.Errorf("a2a adapter node reference %q does not resolve to any node", cfg.A2A.NodeRef)
				}
				if !hasMock {
					return fmt.Errorf("a2a adapter references mock but node %q does not have explicit mock enabled", cfg.A2A.NodeRef)
				}
			} else if !hasAnyMock {
				return fmt.Errorf("a2a.adapter references mock but no node has explicit mock enabled")
			}
		}
	}

	return nil
}
