package cli

import (
	"errors"
	"fmt"
	"path/filepath"
	"strings"

	"banger/internal/api"
	"banger/internal/buildinfo"

	"github.com/spf13/cobra"
)

// NewBangerCommand builds the top-level cobra tree with production
// defaults wired into the dependency struct. Tests reach into the
// package directly — see newRootCommand + defaultDeps.
func NewBangerCommand() *cobra.Command {
	return defaultDeps().newRootCommand()
}

func (d *deps) newRootCommand() *cobra.Command {
	root := &cobra.Command{
		Use:     "banger",
		Version: formatVersionLine(buildinfo.Current()),
		Short:   "Run development sandboxes as Firecracker microVMs",
		Long: strings.TrimSpace(`
banger runs disposable development sandboxes as Firecracker microVMs.
Each sandbox boots in a few seconds, gets its own root filesystem and
network, and exits on demand.

The most common workflow is one command:

  banger vm run                          bare sandbox, drops into ssh
  banger vm run ./repo                   ships a repo into /root/repo, drops into ssh
  banger vm run ./repo -- make test      ships a repo, runs the command, exits with its status
  banger vm run --rm -- script.sh        --rm: VM auto-deletes when the session/command exits
  banger vm run --nat ./repo             --nat: outbound internet (required when .mise.toml installs tools)
  banger vm run -d ./repo --nat          -d/--detach: prep workspace + bootstrap, exit without ssh

For a longer-lived VM, use 'banger vm create' to provision and
'banger vm ssh <name>' to attach. 'banger ps' lists running VMs;
'banger vm list --all' shows stopped ones too. Guests are reachable
at <name>.vm from the host once 'banger ssh-config --install' is run.

First-time setup, in order:
  sudo banger system install             install the systemd services
  banger doctor                          confirm the host is ready
  banger image pull debian-bookworm      fetch a default image

Run 'banger <command> --help' for any subcommand. Run 'banger doctor'
to diagnose host readiness problems.
`),
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE:          helpNoArgs,
	}
	// Drop cobra's default "{{.Name}} version {{.Version}}" wrapper —
	// our Version string is already a complete sentence.
	root.SetVersionTemplate("{{.Version}}\n")
	root.AddCommand(
		d.newDaemonCommand(),
		d.newDoctorCommand(),
		d.newImageCommand(),
		d.newInternalCommand(),
		d.newKernelCommand(),
		newSSHConfigCommand(),
		d.newSystemCommand(),
		d.newUpdateCommand(),
		newVersionCommand(),
		d.newPSCommand(),
		d.newVMCommand(),
	)
	return root
}

func (d *deps) newDoctorCommand() *cobra.Command {
	var verbose bool
	cmd := &cobra.Command{
		Use:   "doctor",
		Short: "Check host and runtime readiness",
		Long: strings.TrimSpace(`
Check that the host has everything banger needs to boot guests:
required tools (mkfs.ext4, debugfs, dmsetup, ip, iptables, ...), KVM
access, daemon reachability, and per-feature preflight (NAT, DNS
routing, work-disk seeding).

Run 'banger doctor':
  - after 'banger system install' to confirm the install took
  - after upgrading the host kernel or banger itself
  - when 'banger vm run' fails with an unclear error

By default, prints failing and warning checks only and a summary
footer; a healthy host collapses to a single line. Pass --verbose to
print every check with its details. Exit code is non-zero if any
check fails. Warnings are reported but do not fail the run.
`),
		Args: noArgsUsage("usage: banger doctor"),
		RunE: func(cmd *cobra.Command, args []string) error {
			report, err := d.doctor(cmd.Context())
			if err != nil {
				return err
			}
			if err := printDoctorReport(cmd.OutOrStdout(), report, verbose); err != nil {
				return err
			}
			if report.HasFailures() {
				return errors.New("doctor found failing checks")
			}
			return nil
		},
	}
	cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "show every check (default: only failures and warnings)")
	return cmd
}

func newVersionCommand() *cobra.Command {
	return &cobra.Command{
		Use:   "version",
		Short: "Show banger build information",
		Args:  noArgsUsage("usage: banger version"),
		RunE: func(cmd *cobra.Command, args []string) error {
			_, err := fmt.Fprint(cmd.OutOrStdout(), formatBuildInfoBlock(buildinfo.Current()))
			return err
		},
	}
}

func helpNoArgs(cmd *cobra.Command, args []string) error {
	if len(args) != 0 {
		return fmt.Errorf("unknown arguments: %s", strings.Join(args, " "))
	}
	return cmd.Help()
}

func noArgsUsage(usage string) cobra.PositionalArgs {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) != 0 {
			return errors.New(usage)
		}
		return nil
	}
}

func exactArgsUsage(n int, usage string) cobra.PositionalArgs {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) != n {
			return errors.New(usage)
		}
		return nil
	}
}

func minArgsUsage(n int, usage string) cobra.PositionalArgs {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) < n {
			return errors.New(usage)
		}
		return nil
	}
}

func maxArgsUsage(n int, usage string) cobra.PositionalArgs {
	return func(cmd *cobra.Command, args []string) error {
		if len(args) > n {
			return errors.New(usage)
		}
		return nil
	}
}

func shellQuote(value string) string {
	return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}

func absolutizeImageRegisterPaths(params *api.ImageRegisterParams) error {
	return absolutizePaths(
		&params.RootfsPath,
		&params.WorkSeedPath,
		&params.KernelPath,
		&params.InitrdPath,
		&params.ModulesDir,
	)
}

func absolutizePaths(values ...*string) error {
	var err error
	for _, value := range values {
		if *value == "" || filepath.IsAbs(*value) {
			continue
		}
		*value, err = filepath.Abs(*value)
		if err != nil {
			return err
		}
	}
	return nil
}

func formatBuildInfoBlock(info buildinfo.Info) string {
	return fmt.Sprintf("version: %s\ncommit: %s\nbuilt_at: %s\n", info.Version, info.Commit, info.BuiltAt)
}

// formatVersionLine renders a buildinfo.Info as a single line —
// "banger v0.1.0 (commit abcd1234, built 2026-04-28T20:45:50Z)" — for
// the `--version` flag. Long commit strings are truncated to the
// first 8 hex chars so the line stays scannable. The verbose
// multi-line form lives on `banger version` for callers that want
// the full SHA / built_at on separate lines.
func formatVersionLine(info buildinfo.Info) string {
	commit := info.Commit
	if len(commit) > 8 {
		commit = commit[:8]
	}
	return fmt.Sprintf("banger %s (commit %s, built %s)", info.Version, commit, info.BuiltAt)
}
