// Package ns implements the namespace inheritance resolver (REQ-082)
// and the ns.md frontmatter parser used by `orca ns` CLI subcommands.
//
// The resolver is a PURE function (no I/O): it takes a map of parsed
// namespace configs keyed by name and returns a map of resolved
// namespaces with merged env and unioned constraints. The inheritance
// model is:
//
//   - Each namespace declares zero or more parents in `ns.md`
//     frontmatter (`parents: ["ns1", "ns2"]`).
//   - The implicit root namespace `_defaults` (R-002 D-159) always
//     exists and has no parents; it is ALWAYS appended as the last
//     element of the chain (D-185).
//   - Opting out of `_defaults` is impossible (D-187): even with
//     `parents: []`, `_defaults` still appears at the end of the chain.
//   - Merge semantics: child overrides parent for scalars (env keys);
//     arrays union (child constraints add to parent constraints, with
//     duplicates removed, order: most-specific first).
//   - The chain order is most-specific first, `_defaults` last.
//   - `_defaults` may be listed explicitly in `parents`; the explicit
//     listing is de-duped silently (still appears once, at the end).
//   - Misordering (`parents: ["_defaults", "x"]`) is rejected: an
//     explicit `_defaults` entry must be the only entry (or omitted).
//   - Cycle detection uses DFS with a visited set; a cycle returns an
//     error with the cycle path.
//   - Missing parents return "parent X not found".
package ns

import (
	"fmt"
	"sort"
)

const defaultsName = "_defaults"

// NSConfig is a parsed namespace declaration from ns.md frontmatter.
// The resolver consumes this; the parser populates it.
type NSConfig struct {
	Name            string
	Parents         []string
	Env             map[string]string
	Constraints     []string
	InheritsEnv     bool
	InheritsSecrets bool
}

// ResolvedNS is the output of the resolver: the namespace with its
// fully-merged env and unioned constraints, plus the ordered
// inheritance chain (most-specific first, `_defaults` last).
type ResolvedNS struct {
	Name        string
	Chain       []string
	Env         map[string]string
	Constraints []string
}

// Resolve walks the parent chain for each namespace, merges env (child
// wins scalars), unions constraints (child adds to parent, de-duped),
// and detects cycles. It is PURE (no I/O). The empty-configs case
// returns an empty map and no error.
//
// The `_defaults` namespace is ALWAYS the last element of every chain
// (D-185); opting out is impossible (D-187). An explicit `_defaults`
// entry in `parents` is de-duped silently. Misordering (e.g.
// `parents: ["_defaults", "x"]`) is rejected.
func Resolve(configs map[string]*NSConfig) (map[string]*ResolvedNS, error) {
	if len(configs) == 0 {
		return map[string]*ResolvedNS{}, nil
	}

	// Validate each config's parents reference exists and the
	// _defaults entry (if explicit) is the only entry.
	for name, cfg := range configs {
		if cfg == nil {
			return nil, fmt.Errorf("namespace %q has nil config", name)
		}
		for _, p := range cfg.Parents {
			if p == defaultsName {
				// Explicit _defaults must be the only parent.
				if len(cfg.Parents) != 1 {
					return nil, fmt.Errorf("namespace %q: %s must be the only parent if listed explicitly (misordering rejected)", name, defaultsName)
				}
				continue
			}
			if _, ok := configs[p]; !ok {
				return nil, fmt.Errorf("namespace %q: parent %q not found", name, p)
			}
		}
	}

	// `_defaults` must be present in the configs map (the parser
	// enforces this for ParseNSMdDir; Resolve trusts its input but
	// still requires _defaults to exist for chain assembly).
	if _, ok := configs[defaultsName]; !ok {
		return nil, fmt.Errorf("namespace %q not found (implicit root must be present)", defaultsName)
	}

	resolved := make(map[string]*ResolvedNS, len(configs))
	// Resolve in deterministic order for stable error reporting.
	names := make([]string, 0, len(configs))
	for n := range configs {
		names = append(names, n)
	}
	sort.Strings(names)

	for _, name := range names {
		r, err := resolveOne(configs, name)
		if err != nil {
			return nil, err
		}
		resolved[name] = r
	}
	return resolved, nil
}

// resolveOne resolves a single namespace. The chain is built by walking
// parents depth-first in POST-order (least-specific first), then
// reversing so the returned chain is most-specific first with
// `_defaults` last (D-185). Cycle detection uses a visiting set.
func resolveOne(configs map[string]*NSConfig, name string) (*ResolvedNS, error) {
	post, err := buildChain(configs, name)
	if err != nil {
		return nil, err
	}
	// post is least-specific first; reverse to most-specific first.
	reverseStrings(post)
	chain := post

	// Env: child (most-specific) wins. Walk least-specific to
	// most-specific (end -> beginning) so later writes override.
	env := make(map[string]string)
	for i := len(chain) - 1; i >= 0; i-- {
		c := configs[chain[i]]
		if c == nil {
			continue
		}
		for k, v := range c.Env {
			env[k] = v
		}
	}

	// Constraints: union, child (most-specific) first. Walk the chain
	// front-to-back (most-specific first) and append unseen items.
	constraintsSeen := make(map[string]bool)
	var constraints []string
	for _, ns := range chain {
		c := configs[ns]
		if c == nil {
			continue
		}
		for _, con := range c.Constraints {
			if !constraintsSeen[con] {
				constraintsSeen[con] = true
				constraints = append(constraints, con)
			}
		}
	}

	return &ResolvedNS{
		Name:        name,
		Chain:       chain,
		Env:         env,
		Constraints: constraints,
	}, nil
}

// buildChain walks parents depth-first and returns the chain in
// POST-order (least-specific first, `_defaults` first). The caller
// reverses to get most-specific first. Cycle detection uses the
// visiting set: a node currently being walked indicates a back-edge.
func buildChain(configs map[string]*NSConfig, name string) ([]string, error) {
	var post []string
	seen := make(map[string]bool) // final chain membership (de-dup)
	visiting := make(map[string]bool)
	if err := dfsChain(configs, name, &post, seen, visiting); err != nil {
		return nil, err
	}
	// `_defaults` is the implicit root: it must be the FIRST element
	// in post-order (so it ends up LAST after reversal). If it was not
	// reached via parents (no explicit listing and no chain leads to
	// it), prepend it.
	if !seen[defaultsName] {
		post = append([]string{defaultsName}, post...)
		seen[defaultsName] = true
	}
	return post, nil
}

// dfsChain appends each node AFTER its parents (post-order), producing
// least-specific first. Cycle detection uses the visiting set.
func dfsChain(configs map[string]*NSConfig, name string, post *[]string, seen, visiting map[string]bool) error {
	if visiting[name] {
		return fmt.Errorf("cycle detected: %s", cyclePath(visiting, configs, name))
	}
	if seen[name] {
		return nil
	}
	visiting[name] = true
	cfg := configs[name]
	if cfg != nil {
		for _, p := range cfg.Parents {
			if err := dfsChain(configs, p, post, seen, visiting); err != nil {
				return err
			}
		}
	}
	delete(visiting, name)
	seen[name] = true
	*post = append(*post, name)
	return nil
}

// cyclePath reconstructs a readable cycle path from the visiting set.
// Since visiting is a set (not ordered), we reconstruct by re-walking
// parents from the offending node until we revisit it.
func cyclePath(visiting map[string]bool, configs map[string]*NSConfig, start string) string {
	// Walk parents from start, collecting names until we hit start
	// again or run out.
	var path []string
	cur := start
	for i := 0; i < len(visiting)+1; i++ {
		path = append(path, cur)
		cfg := configs[cur]
		if cfg == nil || len(cfg.Parents) == 0 {
			break
		}
		next := cfg.Parents[0]
		if next == start {
			path = append(path, next)
			break
		}
		cur = next
	}
	return joinArrows(path)
}

func joinArrows(parts []string) string {
	out := ""
	for i, p := range parts {
		if i > 0 {
			out += " -> "
		}
		out += p
	}
	return out
}

func reverseStrings(s []string) {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
}
