// Package runtime shells out to the podman/docker binaries. nest deliberately
// does not use the Docker/Podman Go SDKs (AGENTS.md §5.3).
package runtime

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"strings"
)

// ManagedLabel marks every container nest creates, so cleanup only ever
// touches nest-managed resources (§4.4, §4.5).
const ManagedLabel = "io.parrot.nest.managed=true"

// ToolLabel records which tool a managed container runs (§4.4).
const ToolLabel = "io.parrot.nest.tool"

// Runtime is a detected container engine CLI.
type Runtime struct {
	bin string
}

// Auto prefers podman — daemonless, rootless by default, and the Parrot OS
// default — then falls back to docker (§5.4).
func Auto() (*Runtime, error) {
	for _, name := range []string{"podman", "docker"} {
		if r, err := Named(name); err == nil {
			return r, nil
		}
	}
	return nil, errors.New("neither podman nor docker found in PATH")
}

// Named returns the requested runtime if its binary exists.
func Named(name string) (*Runtime, error) {
	if _, err := exec.LookPath(name); err != nil {
		return nil, fmt.Errorf("runtime %q not found in PATH", name)
	}
	return &Runtime{bin: name}, nil
}

// Name returns the binary name ("podman" or "docker").
func (r *Runtime) Name() string {
	return r.bin
}

// Available probes whether the engine is actually reachable. On macOS the
// binary can exist while its VM is stopped (§5.4).
func (r *Runtime) Available() error {
	if err := exec.Command(r.bin, "info").Run(); err != nil {
		return fmt.Errorf("runtime %q is installed but not reachable; "+
			"if it runs in a VM, start it first (podman machine start, or start Docker Desktop)", r.bin)
	}
	return nil
}

// ImageExists reports whether an image is present in the local store.
func (r *Runtime) ImageExists(image string) (bool, error) {
	err := exec.Command(r.bin, "image", "inspect", image).Run()
	if err == nil {
		return true, nil
	}
	var exitErr *exec.ExitError
	if errors.As(err, &exitErr) {
		return false, nil
	}
	return false, fmt.Errorf("cannot inspect image %s: %w", image, err)
}

// Pull pulls an image, streaming progress to the given writer, and returns the
// runtime's combined output for status inspection.
func (r *Runtime) Pull(image string, progress io.Writer) (string, error) {
	var buf bytes.Buffer
	cmd := exec.Command(r.bin, "pull", image)
	w := io.MultiWriter(&buf, progress)
	cmd.Stdout = w
	cmd.Stderr = w
	if err := cmd.Run(); err != nil {
		return buf.String(), fmt.Errorf("pulling %s: %w", image, err)
	}
	return buf.String(), nil
}

// Container is a managed container as reported by `<runtime> ps`.
type Container struct {
	ID    string
	Image string
	State string
	Names string
}

// ListManaged returns all containers carrying the managed label (§4.5).
func (r *Runtime) ListManaged() ([]Container, error) {
	out, err := exec.Command(r.bin, "ps", "-a",
		"--filter", "label="+ManagedLabel,
		"--format", "{{.ID}}\t{{.Image}}\t{{.State}}\t{{.Names}}").Output()
	if err != nil {
		return nil, fmt.Errorf("cannot list containers: %w", err)
	}
	var containers []Container
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		fields := strings.Split(line, "\t")
		if len(fields) < 4 || fields[0] == "" {
			continue
		}
		containers = append(containers, Container{
			ID:    fields[0],
			Image: fields[1],
			State: fields[2],
			Names: fields[3],
		})
	}
	return containers, nil
}

// Stop stops a running container.
func (r *Runtime) Stop(id string) error {
	return r.simple("stop", id)
}

// Remove removes a stopped container.
func (r *Runtime) Remove(id string) error {
	return r.simple("rm", id)
}

// RemoveImage removes an image from the local store.
func (r *Runtime) RemoveImage(ref string) error {
	return r.simple("rmi", ref)
}

func (r *Runtime) simple(args ...string) error {
	cmd := exec.Command(r.bin, args...)
	if out, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("%s %s: %w: %s", r.bin, strings.Join(args, " "), err, strings.TrimSpace(string(out)))
	}
	return nil
}
