// Package cli: ns.go implements the `orca ns` subcommand family
// (REQ-082, D-176). Subcommands:
//
//	orca ns list               — list all namespaces under ORCA_HOME
//	orca ns create <name>      — create a namespace dir + ns.md
//	orca ns delete <name>       — remove an empty namespace dir
//	orca ns inspect <name>      — print effective chain + merged env
//	orca ns validate <name>     — cycle + missing-parent + schema checks
//	orca ns inherit <name>      — set the parent namespace (R-002)
//	orca ns set-constraint <name> <key>=<value> — set a constraint
//
// All subcommands honor $ORCA_HOME via internal/paths. The inheritance
// resolver (internal/ns) is a pure function shared by inspect + validate.
package cli

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

	"github.com/spf13/cobra"

	"git.cloudinit.dev/coreci/orca/internal/ns"
	"git.cloudinit.dev/coreci/orca/internal/paths"
)

var nsCmd = &cobra.Command{
	Use:   "ns",
	Short: "Manage orca namespaces",
	Long: `Manage orca namespaces under ORCA_HOME (R-002).

Each namespace is a directory with ns.md, .env, .env.secrets, db/,
jobs/, alloc/. The implicit root namespace _defaults always exists
(D-159); every namespace inherits from _defaults (D-185) and cannot
opt out (D-187).`,
}

var (
	nsCreateParent         string
	nsCreateInheritsEnv    bool
	nsCreateInheritsSecret bool
)

var nsListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all namespaces under ORCA_HOME",
	Long:  `List all namespaces under ORCA_HOME (directories containing ns.md, plus the implicit _defaults).`,
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		// Cache (R-008): read path only. ns list has no --watch flag.
		var cachedRows []nsRow
		if cacheGetList(cacheNamespaceClass, cacheListKey, &cachedRows) {
			return renderNSRows(cmd, cachedRows)
		}

		root := paths.Root()
		entries, err := os.ReadDir(root)
		if err != nil {
			return fmt.Errorf("read ORCA_HOME %s: %w", root, err)
		}
		var rows []nsRow
		for _, ent := range entries {
			if !ent.IsDir() {
				continue
			}
			if ent.Name() == "cluster" {
				continue
			}
			nsMd := filepath.Join(root, ent.Name(), "ns.md")
			if _, err := os.Stat(nsMd); err != nil {
				continue
			}
			rows = append(rows, nsRow{
				Name:    ent.Name(),
				Path:    filepath.Join(root, ent.Name()),
				Default: ent.Name() == paths.DefaultNamespace(),
			})
		}
		sort.Slice(rows, func(i, j int) bool {
			if rows[i].Name == paths.DefaultNamespace() {
				return true
			}
			if rows[j].Name == paths.DefaultNamespace() {
				return false
			}
			return rows[i].Name < rows[j].Name
		})
		cachePutList(cacheNamespaceClass, cacheListKey, rows, cacheNamespaceTTL)
		return renderNSRows(cmd, rows)
	},
}

// nsRow is one row of `orca ns list` output (shared by the cached and
// uncached read paths so the JSON tag set stays in one place).
type nsRow struct {
	Name    string `json:"name"`
	Path    string `json:"path"`
	Default bool   `json:"default"`
}

// renderNSRows prints the namespace list in either JSON or table form.
func renderNSRows(cmd *cobra.Command, rows []nsRow) error {
	if jsonOutput {
		return printJSON(rows)
	}
	if len(rows) == 0 {
		fmt.Fprintln(cmd.OutOrStdout(), "No namespaces found. Run 'orca init' first.")
		return nil
	}
	fmt.Fprintf(cmd.OutOrStdout(), "%-20s  %-10s  %s\n", "NAME", "DEFAULT", "PATH")
	for _, r := range rows {
		def := ""
		if r.Default {
			def = "*"
		}
		fmt.Fprintf(cmd.OutOrStdout(), "%-20s  %-10s  %s\n", r.Name, def, r.Path)
	}
	return nil
}

var nsCreateCmd = &cobra.Command{
	Use:   "create <name>",
	Short: "Create a namespace directory + ns.md",
	Long: `Create a namespace under ORCA_HOME. Builds the dir structure
(db/, jobs/, alloc/) and writes ns.md frontmatter. --parent may be
repeated to declare inheritance; _defaults is always appended last.`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		name := args[0]
		if err := ns.ValidateName(name); err != nil {
			return err
		}
		if name == paths.DefaultNamespace() {
			return fmt.Errorf("cannot create the implicit root namespace %q with `ns create` (it is auto-managed)", name)
		}
		if nsCreateParent == "" {
			nsCreateParent = paths.DefaultNamespace()
		}
		nsDir := paths.NamespaceDir(name)
		if _, err := os.Stat(nsDir); err == nil {
			if _, statErr := os.Stat(paths.NSMd(name)); statErr == nil {
				return fmt.Errorf("namespace %q already exists at %s", name, nsDir)
			}
		}
		for _, sub := range []string{"db", "jobs", "alloc"} {
			if err := os.MkdirAll(filepath.Join(nsDir, sub), 0o755); err != nil {
				return fmt.Errorf("create %s/%s: %w", nsDir, sub, err)
			}
		}
		parents := []string{nsCreateParent}
		if nsCreateParent == paths.DefaultNamespace() {
			// Explicit _defaults listing is allowed (de-duped silently).
		}
		body := renderNSMd(name, parents, nsCreateInheritsEnv, nsCreateInheritsSecret)
		if err := os.WriteFile(paths.NSMd(name), []byte(body), 0o644); err != nil {
			return fmt.Errorf("write ns.md: %w", err)
		}
		if jsonOutput {
			return printJSON(map[string]any{
				"name":             name,
				"path":             nsDir,
				"parents":          parents,
				"ns_md":            paths.NSMd(name),
				"inherits_env":     nsCreateInheritsEnv,
				"inherits_secrets": nsCreateInheritsSecret,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ Namespace created: %s (%s)\n", name, nsDir)
		return nil
	},
}

var nsDeleteCmd = &cobra.Command{
	Use:   "delete <name>",
	Short: "Remove an empty namespace directory",
	Long: `Remove a namespace directory. Refuses if jobs/ or alloc/
contain any files (non-empty namespace). The implicit root _defaults
cannot be deleted.`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		name := args[0]
		if err := ns.ValidateName(name); err != nil {
			return err
		}
		if name == paths.DefaultNamespace() {
			return fmt.Errorf("cannot delete the implicit root namespace %q", name)
		}
		nsDir := paths.NamespaceDir(name)
		if _, err := os.Stat(nsDir); err != nil {
			return fmt.Errorf("namespace %q not found: %w", name, err)
		}
		for _, sub := range []string{"jobs", "alloc"} {
			dir := filepath.Join(nsDir, sub)
			if err := dirNonEmpty(dir); err != nil {
				return fmt.Errorf("refusing to delete %q: %s is non-empty (%w); clear it first", name, sub, err)
			}
		}
		if err := os.RemoveAll(nsDir); err != nil {
			return fmt.Errorf("delete %s: %w", nsDir, err)
		}
		if jsonOutput {
			return printJSON(map[string]string{"name": name, "deleted": nsDir})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ Namespace deleted: %s (%s)\n", name, nsDir)
		return nil
	},
}

var nsInspectCmd = &cobra.Command{
	Use:   "inspect <name>",
	Short: "Print the effective chain, merged env, and constraints",
	Long:  `Resolve a namespace's inheritance chain and print the merged env and unioned constraints (uses the resolver).`,
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		name := args[0]
		if err := ns.ValidateName(name); err != nil {
			return err
		}
		root := paths.Root()
		cfgs, err := ns.ParseNSMdDir(root)
		if err != nil {
			return fmt.Errorf("load namespaces: %w", err)
		}
		if _, ok := cfgs[name]; !ok {
			return fmt.Errorf("namespace %q not found under %s", name, root)
		}
		resolved, err := ns.Resolve(cfgs)
		if err != nil {
			return fmt.Errorf("resolve: %w", err)
		}
		r := resolved[name]
		if r == nil {
			return fmt.Errorf("namespace %q resolved to nil", name)
		}
		if jsonOutput {
			return printJSON(map[string]any{
				"name":        r.Name,
				"chain":       r.Chain,
				"env":         r.Env,
				"constraints": r.Constraints,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "Namespace: %s\n", r.Name)
		fmt.Fprintf(cmd.OutOrStdout(), "Chain:     %s\n", strings.Join(r.Chain, " -> "))
		fmt.Fprintln(cmd.OutOrStdout(), "Env:")
		keys := sortedKeys(r.Env)
		for _, k := range keys {
			fmt.Fprintf(cmd.OutOrStdout(), "  %s = %s\n", k, r.Env[k])
		}
		fmt.Fprintln(cmd.OutOrStdout(), "Constraints:")
		if len(r.Constraints) == 0 {
			fmt.Fprintln(cmd.OutOrStdout(), "  (none)")
		} else {
			for _, c := range r.Constraints {
				fmt.Fprintf(cmd.OutOrStdout(), "  - %s\n", c)
			}
		}
		return nil
	},
}

var nsValidateCmd = &cobra.Command{
	Use:   "validate <name>",
	Short: "Run cycle + missing-parent + schema checks on a namespace",
	Long: `Validate a namespace's inheritance chain and ns.md frontmatter.
Exits 0 if valid, 1 on error. Runs over ALL namespaces under ORCA_HOME
(parsing + resolving validates cycles and missing parents across the
set).`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		name := args[0]
		if err := ns.ValidateName(name); err != nil {
			return err
		}
		root := paths.Root()
		cfgs, err := ns.ParseNSMdDir(root)
		if err != nil {
			return fmt.Errorf("load namespaces: %w", err)
		}
		if _, ok := cfgs[name]; !ok {
			return fmt.Errorf("namespace %q not found under %s", name, root)
		}
		resolved, err := ns.Resolve(cfgs)
		if err != nil {
			return fmt.Errorf("validate: %w", err)
		}
		r := resolved[name]
		if r == nil {
			return fmt.Errorf("namespace %q resolved to nil", name)
		}
		if jsonOutput {
			return printJSON(map[string]any{
				"name":  r.Name,
				"valid": true,
				"chain": r.Chain,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ %s valid\n  chain: %s\n", name, strings.Join(r.Chain, " -> "))
		return nil
	},
}

var (
	nsInheritParent string
)

var nsInheritCmd = &cobra.Command{
	Use:   "inherit <name>",
	Short: "Set the parent namespace for inheritance (R-002)",
	Long: `Set the parent namespace for a namespace. Updates ns.md
frontmatter (parents) and validates the new chain has no cycles
(child cannot inherit from itself transitively). The implicit root
_defaults is always appended last (D-185).`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		name := args[0]
		if err := ns.ValidateName(name); err != nil {
			return err
		}
		if name == paths.DefaultNamespace() {
			return fmt.Errorf("cannot set parent on the implicit root namespace %q", name)
		}
		if nsInheritParent == "" {
			return fmt.Errorf("--parent is required")
		}
		if err := ns.ValidateName(nsInheritParent); err != nil {
			return fmt.Errorf("--parent: %w", err)
		}
		if nsInheritParent == name {
			return fmt.Errorf("namespace %q cannot inherit from itself", name)
		}
		nsMd := paths.NSMd(name)
		cfg, nsBody, err := ns.ParseNSMdWithBody(nsMd)
		if err != nil {
			return fmt.Errorf("parse %s: %w", nsMd, err)
		}
		cfg.Parents = []string{nsInheritParent}

		root := paths.Root()
		cfgs, err := ns.ParseNSMdDir(root)
		if err != nil {
			return fmt.Errorf("load namespaces: %w", err)
		}
		cfgs[name] = cfg
		if _, err := ns.Resolve(cfgs); err != nil {
			return fmt.Errorf("cycle check: %w", err)
		}

		body := renderNSMdFull(cfg, nsBody)
		if err := os.WriteFile(nsMd, []byte(body), 0o644); err != nil {
			return fmt.Errorf("write %s: %w", nsMd, err)
		}
		if jsonOutput {
			return printJSON(map[string]any{
				"name":    name,
				"parents": cfg.Parents,
				"ns_md":   nsMd,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "\u2713 Namespace %s now inherits from %s\n", name, nsInheritParent)
		return nil
	},
}

var nsSetConstraintCmd = &cobra.Command{
	Use:   "set-constraint <name> <key>=<value>",
	Short: "Set a constraint on a namespace (stored in ns.md frontmatter)",
	Long: `Set a constraint on a namespace. Constraints are key=value
strings (e.g. max-allocs=10) stored in ns.md frontmatter and unioned
across the inheritance chain by the resolver.`,
	Args: cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		name := args[0]
		if err := ns.ValidateName(name); err != nil {
			return err
		}
		kv := args[1]
		if name == paths.DefaultNamespace() {
			return fmt.Errorf("cannot set a constraint on the implicit root namespace %q with set-constraint; edit ns.md directly", name)
		}
		idx := strings.Index(kv, "=")
		if idx <= 0 || idx == len(kv)-1 {
			return fmt.Errorf("constraint must be <key>=<value>, got %q", kv)
		}
		constraint := kv
		nsMd := paths.NSMd(name)
		cfg, nsBody, err := ns.ParseNSMdWithBody(nsMd)
		if err != nil {
			return fmt.Errorf("parse %s: %w", nsMd, err)
		}
		for _, c := range cfg.Constraints {
			if c == constraint {
				return fmt.Errorf("constraint %q already set on namespace %q", constraint, name)
			}
		}
		cfg.Constraints = append(cfg.Constraints, constraint)

		body := renderNSMdFull(cfg, nsBody)
		if err := os.WriteFile(nsMd, []byte(body), 0o644); err != nil {
			return fmt.Errorf("write %s: %w", nsMd, err)
		}
		if jsonOutput {
			return printJSON(map[string]any{
				"name":        name,
				"constraints": cfg.Constraints,
				"ns_md":       nsMd,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "\u2713 Constraint set on %s: %s\n", name, constraint)
		return nil
	},
}

// renderNSMdFull renders a complete ns.md from a parsed *ns.NSConfig
// plus an optional body (the markdown after the frontmatter). Used by
// the ns inherit / set-constraint editors to rewrite frontmatter while
// preserving the body.
func renderNSMdFull(cfg *ns.NSConfig, body string) string {
	var b strings.Builder
	b.WriteString("---\n")
	b.WriteString("kind: Namespace\n")
	b.WriteString("name: ")
	b.WriteString(cfg.Name)
	b.WriteString("\n")
	if len(cfg.Parents) > 0 {
		quoted := make([]string, len(cfg.Parents))
		for i, p := range cfg.Parents {
			quoted[i] = fmt.Sprintf("%q", p)
		}
		b.WriteString("parents: [")
		b.WriteString(strings.Join(quoted, ", "))
		b.WriteString("]\n")
	}
	fmt.Fprintf(&b, "inherits_env: %t\n", cfg.InheritsEnv)
	fmt.Fprintf(&b, "inherits_secrets: %t\n", cfg.InheritsSecrets)
	if len(cfg.Constraints) > 0 {
		quoted := make([]string, len(cfg.Constraints))
		for i, c := range cfg.Constraints {
			quoted[i] = fmt.Sprintf("%q", c)
		}
		b.WriteString("constraints: [")
		b.WriteString(strings.Join(quoted, ", "))
		b.WriteString("]\n")
	}
	b.WriteString("---\n")
	b.WriteString(body)
	return b.String()
}

// renderNSMd writes a minimal ns.md frontmatter for `orca ns create`.
func renderNSMd(name string, parents []string, inheritsEnv, inheritsSecrets bool) string {
	var b strings.Builder
	b.WriteString("---\n")
	b.WriteString("kind: Namespace\n")
	b.WriteString("name: ")
	b.WriteString(name)
	b.WriteString("\n")
	if len(parents) > 0 {
		quoted := make([]string, len(parents))
		for i, p := range parents {
			quoted[i] = fmt.Sprintf("%q", p)
		}
		b.WriteString("parents: [")
		b.WriteString(strings.Join(quoted, ", "))
		b.WriteString("]\n")
	}
	fmt.Fprintf(&b, "inherits_env: %t\n", inheritsEnv)
	fmt.Fprintf(&b, "inherits_secrets: %t\n", inheritsSecrets)
	b.WriteString("---\n")
	return b.String()
}

// dirNonEmpty returns an error wrapping the offending entry if dir
// contains any entries.
func dirNonEmpty(dir string) error {
	entries, err := os.ReadDir(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	for _, e := range entries {
		return fmt.Errorf("contains %s", e.Name())
	}
	return nil
}

func sortedKeys(m map[string]string) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

func init() {
	nsCreateCmd.Flags().StringVar(&nsCreateParent, "parent", "", "parent namespace (default _defaults; the implicit root is always appended last)")
	nsCreateCmd.Flags().BoolVar(&nsCreateInheritsEnv, "inherits-env", true, "inherit env from parents (default true)")
	nsCreateCmd.Flags().BoolVar(&nsCreateInheritsSecret, "inherits-secrets", true, "inherit secrets from parents (default true)")

	nsInheritCmd.Flags().StringVar(&nsInheritParent, "parent", "", "parent namespace to inherit from (required)")

	nsCmd.AddCommand(nsListCmd)
	nsCmd.AddCommand(nsCreateCmd)
	nsCmd.AddCommand(nsDeleteCmd)
	nsCmd.AddCommand(nsInspectCmd)
	nsCmd.AddCommand(nsValidateCmd)
	nsCmd.AddCommand(nsInheritCmd)
	nsCmd.AddCommand(nsSetConstraintCmd)
	rootCmd.AddCommand(nsCmd)
}
