package cmd

import (
	"fmt"
	"os"
	"strings"
	"syscall"

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

const runUsage = `Usage: nest run <tool> [--local | --container] [--runtime podman|docker]
                [--image ref] [--catalog path] [-- args for the tool...]

Launch a tool. The command executed is the tool's first catalog executable.
Container runs are anonymous and --rm, mount the persistence directory at
/persistence, and carry the io.parrot.nest.managed label (AGENTS.md §4.4).
Flags may appear before or after the tool name; everything after -- is passed
verbatim to the tool.
`

// runArgs is the result of run's manual flag parsing: nest flags are accepted
// before or after the tool name (§4.4 examples put --container after it),
// everything after -- is verbatim tool input.
type runArgs struct {
	local, container   bool
	runtimeName, image string
	catalogPath        string
	help               bool
	tool               string
	toolArgs           []string
}

func parseRunArgs(args []string) (runArgs, error) {
	var ra runArgs
	for i := 0; i < len(args); i++ {
		tok := args[i]
		if tok == "--" {
			ra.toolArgs = append(ra.toolArgs, args[i+1:]...)
			break
		}
		if ra.tool != "" && !strings.HasPrefix(tok, "-") {
			ra.toolArgs = append(ra.toolArgs, tok)
			continue
		}
		name, value, hasValue := splitFlag(tok)
		next := func() (string, error) {
			if hasValue {
				return value, nil
			}
			i++
			if i >= len(args) {
				return "", fmt.Errorf("flag %s needs a value", name)
			}
			return args[i], nil
		}
		var err error
		switch name {
		case "-h", "--help":
			ra.help = true
		case "--local":
			ra.local = true
		case "--container":
			ra.container = true
		case "--runtime":
			if ra.runtimeName, err = next(); err != nil {
				return ra, err
			}
		case "--image":
			if ra.image, err = next(); err != nil {
				return ra, err
			}
		case "--catalog":
			if ra.catalogPath, err = next(); err != nil {
				return ra, err
			}
		default:
			if strings.HasPrefix(tok, "-") {
				return ra, fmt.Errorf("unknown flag %q (use -- to pass flags to the tool)", tok)
			}
			ra.tool = tok
		}
	}
	return ra, nil
}

func splitFlag(tok string) (name, value string, hasValue bool) {
	if strings.HasPrefix(tok, "--") {
		if eq := strings.Index(tok, "="); eq >= 0 {
			return tok[:eq], tok[eq+1:], true
		}
	}
	return tok, "", false
}

func (a *App) cmdRun(cfg *config.Config, args []string) int {
	ra, err := parseRunArgs(args)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return exitUsage
	}
	if ra.help {
		fmt.Fprint(a.Stdout, runUsage)
		return exitOK
	}
	if ra.tool == "" {
		fmt.Fprintf(a.Stderr, "nest: run needs a tool name\n%s", runUsage)
		return exitUsage
	}

	cat, _, code := a.loadCatalog(ra.catalogPath)
	if code != exitOK {
		return code
	}
	tool, ok := cat.Find(ra.tool)
	if !ok {
		fmt.Fprintf(a.Stderr, "nest: tool %q not found in catalog; try: nest search %s\n", ra.tool, ra.tool)
		return exitNotFound
	}

	mode, err := cfg.ResolveMode(ra.local, ra.container)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return exitUsage
	}
	// §4.4: inside a container only local runs are possible.
	if detect.InContainer(a.Getenv) {
		if ra.container {
			fmt.Fprintf(a.Stderr, "nest: cannot launch containers from inside a container\n")
			return exitRuntimeFail
		}
		mode = "local"
	}

	if mode == "local" {
		return a.runLocal(tool, ra.toolArgs)
	}
	return a.runContainer(cfg, tool, &ra)
}

func (a *App) runLocal(tool *catalog.Tool, args []string) int {
	exe := tool.Executables[0]
	if !detect.Executable(exe) {
		if tool.HasImage() {
			fmt.Fprintf(a.Stderr, "nest: tool %q is not installed; try: nest run %s --container\n", tool.Name, tool.Name)
		} else {
			fmt.Fprintf(a.Stderr, "nest: tool %q is not installed; try: sudo apt install %s\n", tool.Name, tool.Package)
		}
		return exitNotFound
	}
	if err := syscall.Exec(exe, append([]string{exe}, args...), os.Environ()); err != nil {
		fmt.Fprintf(a.Stderr, "nest: cannot execute %s: %v\n", exe, err)
		return exitError
	}
	return exitOK // unreachable: syscall.Exec replaces the process
}

func (a *App) runContainer(cfg *config.Config, tool *catalog.Tool, ra *runArgs) int {
	image := ra.image
	if image == "" {
		// §2.3: without a catalog image and without --image, no runtime is
		// even needed — fail before resolving one.
		if !tool.HasImage() {
			fmt.Fprintf(a.Stderr, "nest: tool %q has no container image; try: sudo apt install %s\n", tool.Name, tool.Package)
			return exitNotFound
		}
		image = tool.Images[0]
	}

	rt, code := a.resolveRuntime(cfg, ra.runtimeName)
	if code != exitOK {
		return code
	}
	exists, err := rt.ImageExists(image)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return exitRuntimeFail
	}
	if !exists {
		fmt.Fprintf(a.Stderr, "nest: pulling image %s ...\n", image)
		if _, err := rt.Pull(image, a.Stderr); err != nil {
			fmt.Fprintf(a.Stderr, "nest: %v\n", err)
			return exitRuntimeFail
		}
	}

	// §4.4: nest creates the persistence dir itself, or the runtime would
	// create it root-owned.
	dir := cfg.ResolvePersistenceDir(a.Getenv)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		fmt.Fprintf(a.Stderr, "nest: cannot create persistence directory %s: %v\n", dir, err)
		return exitError
	}

	args := nestruntime.BuildRunArgs(image, tool.Executables[0], tool.Name, dir, tool.Container, a.StdinTTY, ra.toolArgs)
	exitCode, err := rt.RunAttached(args, a.Stdin, a.Stdout, a.Stderr)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return exitRuntimeFail
	}
	return exitCode
}
