package config

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"gopkg.in/yaml.v3"
)

// identityMapPaths is the set of dotted paths whose direct children are
// identity-keyed maps. A child key present in two sources is a hard error;
// such keys name discrete entities (a model, a group, a peer, etc.) and a
// duplicate means the user has split one entity across files by mistake.
var identityMapPaths = map[string]bool{
	"models":                         true,
	"groups":                         true,
	"profiles":                       true,
	"peers":                          true,
	"matrix":                         true,
	"routing.router.settings.groups": true,
	"routing.router.settings.matrix": true,
}

// LoadConfigSources loads and merges configuration from -config (optional)
// and -config-dir (optional). At least one must be provided. The -config file
// is loaded first; *.yml/*.yaml files directly under -config-dir are then
// merged in sorted filename order. The merged document is passed through the
// existing LoadConfigFromReader pipeline unchanged.
func LoadConfigSources(configPath, configDir string) (Config, error) {
	if configPath == "" && configDir == "" {
		return Config{}, fmt.Errorf("at least one of -config or -config-dir must be provided")
	}

	var sourcePaths []string

	if configPath != "" {
		sourcePaths = append(sourcePaths, configPath)
	}

	if configDir != "" {
		dirFiles, err := listYAMLFiles(configDir)
		if err != nil {
			return Config{}, fmt.Errorf("-config-dir %s: %w", configDir, err)
		}

		if configPath != "" {
			absConfig, err := filepath.Abs(configPath)
			if err != nil {
				return Config{}, fmt.Errorf("failed to resolve -config path: %w", err)
			}
			for _, f := range dirFiles {
				absF, err := filepath.Abs(f)
				if err != nil {
					return Config{}, fmt.Errorf("failed to resolve config dir file %s: %w", f, err)
				}
				if absConfig == absF {
					return Config{}, fmt.Errorf("-config path %s is also present in -config-dir %s; remove it from one", configPath, configDir)
				}
			}
		}

		sourcePaths = append(sourcePaths, dirFiles...)
	}

	if len(sourcePaths) == 0 {
		return Config{}, fmt.Errorf("no configuration sources found")
	}

	var merged *yaml.Node
	for _, p := range sourcePaths {
		node, err := parseSource(p)
		if err != nil {
			return Config{}, err
		}
		if node == nil {
			continue // empty file
		}
		if merged == nil {
			merged = node
			continue
		}
		if err := mergeNodes(merged, node, "", p); err != nil {
			return Config{}, err
		}
	}

	if merged == nil {
		// All sources were empty; run the pipeline on empty input so defaults
		// and validation still apply (e.g. startPort, performance defaults).
		return LoadConfigFromReader(strings.NewReader(""))
	}

	out, err := yaml.Marshal(merged)
	if err != nil {
		return Config{}, fmt.Errorf("failed to marshal merged config: %w", err)
	}
	return LoadConfigFromReader(strings.NewReader(string(out)))
}

// listYAMLFiles returns the top-level *.yml and *.yaml files in dir, sorted by
// filename for deterministic merge order. Subdirectories are not traversed.
func listYAMLFiles(dir string) ([]string, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	var files []string
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if !strings.HasSuffix(name, ".yml") && !strings.HasSuffix(name, ".yaml") {
			continue
		}
		files = append(files, filepath.Join(dir, name))
	}
	sort.Strings(files)
	return files, nil
}

// parseSource reads and parses one YAML config file into a root mapping node.
// Returns a nil node (no error) when the file is empty or contains only
// comments.
//
// Env macros (${env.VAR}) are substituted at the string level before YAML
// parsing so that flow-style constructs like [${env.API_KEY}] parse
// correctly — the brace would otherwise be interpreted as a flow mapping.
func parseSource(path string) (*yaml.Node, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read config %s: %w", path, err)
	}
	yamlStr, err := substituteEnvMacros(string(data))
	if err != nil {
		return nil, fmt.Errorf("config %s: %w", path, err)
	}
	var doc yaml.Node
	if err := yaml.Unmarshal([]byte(yamlStr), &doc); err != nil {
		return nil, fmt.Errorf("failed to parse config %s: %w", path, err)
	}
	// yaml.Unmarshal into a yaml.Node yields a DocumentNode whose Content[0]
	// is the actual root. Unwrap it so callers see the real top-level node.
	root := &doc
	if root.Kind == yaml.DocumentNode && len(root.Content) > 0 {
		root = root.Content[0]
	}
	if root.Kind == 0 || root.Content == nil {
		return nil, nil
	}
	if root.Kind != yaml.MappingNode {
		return nil, fmt.Errorf("config %s: top-level YAML must be a mapping", path)
	}
	return root, nil
}

// mergeNodes merges src into dst (both MappingNodes) in place. Keys present in
// only one side are kept; shared keys are merged recursively under the rules
// in mergeValue. srcPath is included in error messages to identify the file
// that introduced the conflict.
func mergeNodes(dst, src *yaml.Node, path, srcPath string) error {
	srcIdx := indexMapping(src)

	// First pass: merge shared keys in place.
	for i := 0; i+1 < len(dst.Content); i += 2 {
		keyNode := dst.Content[i]
		dstVal := dst.Content[i+1]
		key := keyNode.Value

		srcVal, ok := srcIdx[key]
		if !ok {
			continue // dst-only key, keep as-is
		}

		childPath := joinPath(path, key)

		if identityMapPaths[childPath] {
			// Identity-keyed map: each child key names a discrete entity
			// (a model, group, peer, ...). A shared child key is a hard
			// error; src-only children are appended in the second pass.
			if err := mergeIdentityMap(dstVal, srcVal, childPath, key, srcPath); err != nil {
				return err
			}
			continue
		}

		if err := mergeValue(dstVal, srcVal, childPath, srcPath); err != nil {
			return err
		}
	}

	// Second pass: append src-only keys.
	dstIdx := indexMapping(dst)
	for i := 0; i+1 < len(src.Content); i += 2 {
		keyNode := src.Content[i]
		srcVal := src.Content[i+1]
		key := keyNode.Value

		if _, ok := dstIdx[key]; ok {
			continue // already merged above
		}
		keyCopy := *keyNode
		valCopy := *srcVal
		dst.Content = append(dst.Content, &keyCopy, &valCopy)
	}

	return nil
}

// mergeIdentityMap merges two identity-keyed mapping nodes (e.g. `models`,
// `groups`, `peers`). Any child key present in both sides is a duplicate
// entity and produces an error naming the conflicting key and source file.
// src-only keys are appended to dst.
func mergeIdentityMap(dst, src *yaml.Node, path, mapName, srcPath string) error {
	if dst.Kind != yaml.MappingNode || src.Kind != yaml.MappingNode {
		return fmt.Errorf("conflict at %q: expected a mapping, introduced by %s", path, srcPath)
	}
	dstIdx := indexMapping(dst)
	for i := 0; i+1 < len(src.Content); i += 2 {
		keyNode := src.Content[i]
		srcVal := src.Content[i+1]
		key := keyNode.Value
		if _, dup := dstIdx[key]; dup {
			return fmt.Errorf("duplicate %s %q found in %s (already defined in another config source)", mapName, key, srcPath)
		}
		keyCopy := *keyNode
		valCopy := *srcVal
		dst.Content = append(dst.Content, &keyCopy, &valCopy)
	}
	return nil
}

// mergeValue merges srcVal into dstVal (both pointing into the parent's
// Content slice). Mapping+Mapping recurses; Sequence+Sequence concatenates;
// Scalar+Scalar errors on value mismatch; null on either side yields to the
// non-null side.
func mergeValue(dstVal, srcVal *yaml.Node, path, srcPath string) error {
	switch {
	case dstVal.Kind == yaml.MappingNode && srcVal.Kind == yaml.MappingNode:
		return mergeNodes(dstVal, srcVal, path, srcPath)

	case dstVal.Kind == yaml.SequenceNode && srcVal.Kind == yaml.SequenceNode:
		dstVal.Content = append(dstVal.Content, srcVal.Content...)
		return nil

	case dstVal.Kind == yaml.ScalarNode && srcVal.Kind == yaml.ScalarNode:
		if isNullScalar(dstVal) {
			*dstVal = *srcVal
			return nil
		}
		if isNullScalar(srcVal) {
			return nil
		}
		if dstVal.Value == srcVal.Value && dstVal.Tag == srcVal.Tag {
			return nil
		}
		return fmt.Errorf("conflict at %q: %s sets a different value than a previous source", path, srcPath)

	case isNull(dstVal):
		*dstVal = *srcVal
		return nil

	case isNull(srcVal):
		return nil

	default:
		return fmt.Errorf("conflict at %q: incompatible YAML node kinds (kind %d vs %d) introduced by %s", path, dstVal.Kind, srcVal.Kind, srcPath)
	}
}

// isNull reports whether n represents a YAML null (empty or !!null).
func isNull(n *yaml.Node) bool {
	if n == nil || n.Kind == 0 {
		return true
	}
	return isNullScalar(n)
}

func isNullScalar(n *yaml.Node) bool {
	return n.Kind == yaml.ScalarNode && (n.Tag == "!!null" || n.Tag == "") && n.Value == ""
}

// indexMapping builds a key -> value-node index for a mapping node.
func indexMapping(n *yaml.Node) map[string]*yaml.Node {
	idx := make(map[string]*yaml.Node, len(n.Content)/2)
	for i := 0; i+1 < len(n.Content); i += 2 {
		idx[n.Content[i].Value] = n.Content[i+1]
	}
	return idx
}

func joinPath(parent, key string) string {
	if parent == "" {
		return key
	}
	return parent + "." + key
}
