// Package cmd implements nest's subcommands. main wires the real environment
// into an App; tests inject buffers, fake runtimes, and temp paths.
package cmd

import (
	"errors"
	"flag"
	"fmt"
	"io"

	"nest/src/catalog"
	"nest/src/config"
	"nest/src/runtime"
)

// Exit codes (AGENTS.md §4).
const (
	exitOK          = 0
	exitError       = 1
	exitUsage       = 2
	exitNotFound    = 3
	exitRuntimeFail = 4
)

// App carries the shared dependencies of every command.
type App struct {
	Stdout, Stderr io.Writer
	Stdin          io.Reader
	StdinTTY       bool
	Getenv         func(string) string
	Version        string
	CatalogLocs    catalog.Locations
	ConfigSystem   string
	ConfigUser     string
}

const helpText = `nest — Parrot Security tool launcher

Usage: nest <command> [options]

Commands:
  list      List cataloged tools
  search    Search the catalog for a tool
  info      Show full details of a tool
  run       Launch a tool, locally or in a container
  cleanup   Remove nest-managed containers and images
  refresh   Update the tool catalog and local images
  version   Show nest and catalog versions

Run 'nest <command> --help' for command-specific options.
`

// Run dispatches a command line (without argv[0]) and returns the exit code.
// A malformed config file is fatal for every command (§3); help always works.
func (a *App) Run(args []string) int {
	if len(args) == 0 {
		fmt.Fprint(a.Stdout, helpText)
		return exitOK
	}
	switch args[0] {
	case "help", "--help", "-h":
		fmt.Fprint(a.Stdout, helpText)
		return exitOK
	}
	cfg, code := a.loadConfig()
	if code != exitOK {
		return code
	}
	switch args[0] {
	case "list":
		return a.cmdList(args[1:])
	case "search":
		return a.cmdSearch(args[1:])
	case "info":
		return a.cmdInfo(args[1:])
	case "run":
		return a.cmdRun(cfg, args[1:])
	case "cleanup":
		return a.cmdCleanup(cfg, args[1:])
	case "refresh":
		return a.cmdRefresh(cfg, args[1:])
	case "version":
		return a.cmdVersion(args[1:])
	default:
		fmt.Fprintf(a.Stderr, "nest: unknown command %q\n\n%s", args[0], helpText)
		return exitUsage
	}
}

// newFlagSet builds a flag set with nest's conventions: diagnostics to stderr,
// usage text on -h.
func (a *App) newFlagSet(name, usage string) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.SetOutput(a.Stderr)
	fs.Usage = func() { fmt.Fprint(a.Stderr, usage) }
	return fs
}

// parseFlags parses flags, mapping -h to exitOK and parse errors to exitUsage.
func (a *App) parseFlags(fs *flag.FlagSet, args []string) (bool, int) {
	_, ok, code := a.parseInterspersed(fs, args)
	return ok, code
}

// parseInterspersed parses flags that may appear before or after positional
// arguments (stdlib flag stops at the first positional otherwise) and returns
// the positionals in order.
func (a *App) parseInterspersed(fs *flag.FlagSet, args []string) ([]string, bool, int) {
	var positionals []string
	for len(args) > 0 {
		if err := fs.Parse(args); err != nil {
			if errors.Is(err, flag.ErrHelp) {
				return nil, false, exitOK
			}
			return nil, false, exitUsage
		}
		args = fs.Args()
		if len(args) > 0 {
			positionals = append(positionals, args[0])
			args = args[1:]
		}
	}
	return positionals, true, 0
}

// loadConfig loads the layered configuration; a malformed file is fatal (§3).
func (a *App) loadConfig() (*config.Config, int) {
	cfg, err := config.Load(a.ConfigSystem, a.ConfigUser)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return nil, exitError
	}
	return cfg, exitOK
}

// loadCatalog resolves and loads the effective catalog (§2.1). The flag value
// wins over NEST_CATALOG.
func (a *App) loadCatalog(flagValue string) (*catalog.Catalog, string, int) {
	override := flagValue
	if override == "" {
		override = a.Getenv("NEST_CATALOG")
	}
	cat, source, err := catalog.Resolve(override, a.CatalogLocs, a.Stderr)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return nil, "", exitError
	}
	return cat, source, exitOK
}

// resolveRuntime applies §3.1 runtime resolution and probes reachability.
func (a *App) resolveRuntime(cfg *config.Config, flagValue string) (*runtime.Runtime, int) {
	name := cfg.ResolveRuntime(flagValue)
	switch name {
	case "auto", "podman", "docker":
	default:
		fmt.Fprintf(a.Stderr, "nest: unknown runtime %q (want podman or docker)\n", name)
		return nil, exitUsage
	}

	rt, err := findRuntime(name)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v; install podman or docker\n", err)
		return nil, exitRuntimeFail
	}
	if err := rt.Available(); err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return nil, exitRuntimeFail
	}
	return rt, exitOK
}

func findRuntime(name string) (*runtime.Runtime, error) {
	if name == "auto" {
		return runtime.Auto()
	}
	return runtime.Named(name)
}
