// Package pkgcmd wires pigo's package-management subcommands (#162, #163, #164)
// into the CLI: `pigo install|list|uninstall|update ...`. These are positional
// subcommands, distinct from the flag-driven agent modes, so main() peels them
// off before pflag parsing (the agent flags don't apply to package management).
//
// Each subcommand is a thin shell over internal/pkgmgr, which owns all the real
// work (fetch, classify, distribute, lockfile). This file only parses argv,
// resolves the lockfile path, calls pkgmgr, and prints human-readable results.
package pkgcmd

import (
	"fmt"
	"io"

	"github.com/smallnest/pigo/internal/pkgmgr"
)

// Subcommands are the argv[1] values routed to Run.
var Subcommands = map[string]bool{
	"install":   true,
	"list":      true,
	"uninstall": true,
	"update":    true,
}

// Run executes a package-management subcommand (cmd) with its arguments (args),
// writing output to out and errors to errOut. It returns a process exit code.
// install is #162; list/uninstall are #163; update is #164.
func Run(cmd string, args []string, out, errOut io.Writer) int {
	lockPath := pkgmgr.DefaultLockfilePath()
	switch cmd {
	case "install":
		return runInstall(args, lockPath, out, errOut)
	case "list":
		return runList(lockPath, out, errOut)
	case "uninstall":
		return runUninstall(args, lockPath, out, errOut)
	case "update":
		return runUpdate(args, lockPath, out, errOut)
	default:
		fmt.Fprintf(errOut, "pigo: %q is not yet implemented\n", cmd)
		return 2
	}
}

// runList handles `pigo list`: it prints one line per installed package
// (name, version, types, source), or a friendly notice when none are installed.
func runList(lockPath string, out, errOut io.Writer) int {
	pkgs, err := pkgmgr.ListInstalled(lockPath)
	if err != nil {
		fmt.Fprintf(errOut, "pigo: %v\n", err)
		return 1
	}
	if len(pkgs) == 0 {
		fmt.Fprintln(out, "no packages installed")
		return 0
	}
	for _, p := range pkgs {
		fmt.Fprintf(out, "%s\t%s\t%s\t%s\n", p.Name, p.Version, joinPkgTypes(p.Types), p.Source)
	}
	return 0
}

// runUninstall handles `pigo uninstall <name> [more...]`. It removes each named
// package's files and lockfile entry, continuing past a failure so one bad name
// does not abort the rest; the exit code is non-zero if any uninstall failed.
func runUninstall(names []string, lockPath string, out, errOut io.Writer) int {
	if len(names) == 0 {
		fmt.Fprintln(errOut, "pigo: uninstall requires a package name, e.g. pigo uninstall pi-mcp-adapter")
		return 2
	}
	failed := false
	for _, name := range names {
		if err := pkgmgr.Uninstall(name, lockPath, out); err != nil {
			fmt.Fprintf(errOut, "pigo: uninstall %s failed: %v\n", name, err)
			failed = true
			continue
		}
		fmt.Fprintf(out, "Uninstalled %s\n", name)
	}
	if failed {
		return 1
	}
	return 0
}

// runInstall handles `pigo install npm:<name>[@version] [more...]`. It requires
// npm on PATH (checked once up front for a clear early failure) and installs
// each reference in turn, continuing past a failure so one bad package does not
// abort the rest; the exit code is non-zero if any install failed.
func runInstall(refs []string, lockPath string, out, errOut io.Writer) int {
	if len(refs) == 0 {
		fmt.Fprintln(errOut, "pigo: install requires a package reference, e.g. pigo install npm:pi-mcp-adapter")
		return 2
	}
	if err := pkgmgr.EnsureNPM(); err != nil {
		fmt.Fprintf(errOut, "pigo: %v\n", err)
		return 1
	}

	failed := false
	for _, ref := range refs {
		res, err := pkgmgr.Install(ref, lockPath, out)
		if err != nil {
			fmt.Fprintf(errOut, "pigo: install %s failed: %v\n", ref, err)
			failed = true
			continue
		}
		fmt.Fprintf(out, "Installed %s@%s (%s) — %d file(s)\n",
			res.Name, res.Version, joinPkgTypes(res.Types), len(res.Files))
	}
	if failed {
		return 1
	}
	return 0
}

// runUpdate handles `pigo update <name> [more...]`: it updates each named
// package, continuing past a failure so one bad package does not abort the rest.
// The exit code is non-zero if any update failed. A no-name `pigo update` is
// binary self-update, routed away by main() before pkgcmd is reached (US-003),
// so here an empty name list is a usage error rather than an update-all.
func runUpdate(names []string, lockPath string, out, errOut io.Writer) int {
	if len(names) == 0 {
		fmt.Fprintln(errOut, "pigo: update requires a package name, e.g. pigo update pi-mcp-adapter")
		return 2
	}
	failed := false
	for _, name := range names {
		if _, err := pkgmgr.Update(name, lockPath, out); err != nil {
			fmt.Fprintf(errOut, "pigo: update %s failed: %v\n", name, err)
			failed = true
			continue
		}
	}
	if failed {
		return 1
	}
	return 0
}

// joinPkgTypes renders package types as a comma-separated string.
func joinPkgTypes(types []pkgmgr.PackageType) string {
	out := ""
	for i, t := range types {
		if i > 0 {
			out += ", "
		}
		out += string(t)
	}
	return out
}
