package cli

import (
	"errors"
	"fmt"
	"strings"

	"banger/internal/api"
	"banger/internal/model"
	"banger/internal/rpc"

	"github.com/spf13/cobra"
)

func (d *deps) newImageCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "image",
		Short: "Pull and manage banger images (rootfs + kernel + work-seed)",
		Long: strings.TrimSpace(`
A banger image bundles a rootfs.ext4, a kernel, an optional initrd
+ modules, and an optional work-seed (the snapshot used to populate
each new VM's /root). Most users only need 'banger image pull
<catalog-name>' for the cataloged paths (see internal/imagecat),
or 'banger image pull <oci-ref>' for an OCI image.

Subcommands:
  pull       fetch a bundle by catalog name OR pull an OCI image
  register   point banger at an existing local rootfs (advanced)
  promote    copy a registered image's files into banger's managed dir
  list       show what's installed
  show       print one image's full record as JSON
  delete     remove an image (no VMs may reference it)
`),
		Example: strings.TrimSpace(`
  banger image pull debian-bookworm
  banger image pull docker.io/library/alpine:3.20 --kernel-ref generic-6.12
  banger image list
`),
		RunE: helpNoArgs,
	}
	cmd.AddCommand(
		d.newImageRegisterCommand(),
		d.newImagePullCommand(),
		d.newImagePromoteCommand(),
		d.newImageListCommand(),
		d.newImageShowCommand(),
		d.newImageDeleteCommand(),
		d.newImageCacheCommand(),
	)
	return cmd
}

// newImageCacheCommand groups OCI-cache lifecycle subcommands. Today
// the only one is `prune`; future additions (size, list, etc.) plug
// in here without polluting the top-level `image` namespace.
func (d *deps) newImageCacheCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "cache",
		Short: "Manage banger's OCI layer-blob cache",
		Long: strings.TrimSpace(`
banger keeps a local copy of every OCI layer it downloads so a re-pull
of the same image (or any image that shares a base layer) skips the
network round-trip. The cache lives under the daemon's CacheDir
(see 'banger doctor' or docs/config.md). Layers accumulate forever;
'banger image cache prune' is the cheap way to reclaim disk.
`),
		Example: strings.TrimSpace(`
  banger image cache prune --dry-run
  banger image cache prune
`),
		RunE: helpNoArgs,
	}
	cmd.AddCommand(d.newImageCachePruneCommand())
	return cmd
}

func (d *deps) newImageCachePruneCommand() *cobra.Command {
	var dryRun bool
	cmd := &cobra.Command{
		Use:   "prune",
		Short: "Remove every cached OCI layer blob",
		Long: strings.TrimSpace(`
Removes every layer blob under the OCI cache. Registered banger
images are independent of the cache (each pull flattens layers into
a self-contained ext4), so prune only loses re-pull avoidance — the
next pull of the same image re-downloads the layers it needs.

Safe to run any time the daemon is idle. If you have an image pull
in flight when you run prune, that pull may fail and need a retry.

--dry-run reports the byte count without removing anything.
`),
		Args: noArgsUsage("usage: banger image cache prune [--dry-run]"),
		RunE: func(cmd *cobra.Command, args []string) error {
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			result, err := rpc.Call[api.ImageCachePruneResult](cmd.Context(), layout.SocketPath, "image.cache.prune", api.ImageCachePruneParams{DryRun: dryRun})
			if err != nil {
				return err
			}
			out := cmd.OutOrStdout()
			verb := "freed"
			if result.DryRun {
				verb = "would free"
			}
			_, err = fmt.Fprintf(out, "%s %s across %d blob(s) in %s\n",
				verb, humanSize(result.BytesFreed), result.BlobsFreed, result.CacheDir)
			return err
		},
	}
	cmd.Flags().BoolVar(&dryRun, "dry-run", false, "report the size that would be freed without deleting anything")
	return cmd
}

func (d *deps) newImageRegisterCommand() *cobra.Command {
	var params api.ImageRegisterParams
	cmd := &cobra.Command{
		Use:   "register",
		Short: "Register or update an unmanaged image",
		Args:  noArgsUsage("usage: banger image register --name <name> --rootfs <path> [--work-seed <path>] (--kernel <path> [--initrd <path>] [--modules <dir>] | --kernel-ref <name>)"),
		RunE: func(cmd *cobra.Command, args []string) error {
			if strings.TrimSpace(params.KernelRef) != "" && (params.KernelPath != "" || params.InitrdPath != "" || params.ModulesDir != "") {
				return errors.New("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
			}
			if err := absolutizeImageRegisterPaths(&params); err != nil {
				return err
			}
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.register", params)
			if err != nil {
				return err
			}
			return printImageSummary(cmd.OutOrStdout(), result.Image)
		},
	}
	cmd.Flags().StringVar(&params.Name, "name", "", "image name")
	cmd.Flags().StringVar(&params.RootfsPath, "rootfs", "", "rootfs path")
	cmd.Flags().StringVar(&params.WorkSeedPath, "work-seed", "", "work-seed path")
	cmd.Flags().StringVar(&params.KernelPath, "kernel", "", "kernel path")
	cmd.Flags().StringVar(&params.InitrdPath, "initrd", "", "initrd path")
	cmd.Flags().StringVar(&params.ModulesDir, "modules", "", "modules dir")
	cmd.Flags().StringVar(&params.KernelRef, "kernel-ref", "", "name of a cataloged kernel (see 'banger kernel list')")
	_ = cmd.RegisterFlagCompletionFunc("kernel-ref", d.completeKernelNames)
	return cmd
}

func (d *deps) newImagePullCommand() *cobra.Command {
	var (
		params  api.ImagePullParams
		sizeRaw string
	)
	cmd := &cobra.Command{
		Use:               "pull <name-or-oci-ref>",
		Short:             "Pull an image bundle (catalog name) or OCI image and register it",
		ValidArgsFunction: d.completeImageCatalogNameOnlyAtPos0,
		Long: strings.TrimSpace(`
Pull an image into banger. Two paths:

  • Catalog name  (e.g. 'debian-bookworm')
      Fetches a pre-built bundle from the embedded imagecat catalog.
      Kernel-ref comes from the catalog entry; --kernel-ref still
      overrides.

  • OCI reference (e.g. 'docker.io/library/debian:bookworm')
      Pulls the image, flattens its layers, fixes ownership, injects
      banger's guest agents. --kernel-ref or direct --kernel/--initrd/
      --modules are required.

Use 'banger image list' to see installed images.
`),
		Example: strings.TrimSpace(`
  banger image pull debian-bookworm
  banger image pull debian-bookworm --name sandbox
  banger image pull docker.io/library/debian:bookworm --kernel-ref generic-6.12
`),
		Args: exactArgsUsage(1, "usage: banger image pull <name-or-oci-ref> [--name <name>] [--kernel-ref <name>] [--kernel <path>] [--initrd <path>] [--modules <dir>] [--size <human>]"),
		RunE: func(cmd *cobra.Command, args []string) error {
			params.Ref = args[0]
			if strings.TrimSpace(params.KernelRef) != "" && (params.KernelPath != "" || params.InitrdPath != "" || params.ModulesDir != "") {
				return errors.New("--kernel-ref is mutually exclusive with --kernel/--initrd/--modules")
			}
			if strings.TrimSpace(sizeRaw) != "" {
				size, err := model.ParseSize(sizeRaw)
				if err != nil {
					return fmt.Errorf("--size: %w", err)
				}
				params.SizeBytes = size
			}
			if err := absolutizePaths(&params.KernelPath, &params.InitrdPath, &params.ModulesDir); err != nil {
				return err
			}
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			var result api.ImageShowResult
			err = withHeartbeat(cmd.ErrOrStderr(), "image pull", func() error {
				var callErr error
				result, callErr = rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.pull", params)
				return callErr
			})
			if err != nil {
				return err
			}
			return printImageSummary(cmd.OutOrStdout(), result.Image)
		},
	}
	cmd.Flags().StringVar(&params.Name, "name", "", "image name (defaults to the ref's repo+tag, sanitised)")
	cmd.Flags().StringVar(&params.KernelPath, "kernel", "", "kernel path")
	cmd.Flags().StringVar(&params.InitrdPath, "initrd", "", "initrd path")
	cmd.Flags().StringVar(&params.ModulesDir, "modules", "", "modules dir")
	cmd.Flags().StringVar(&params.KernelRef, "kernel-ref", "", "name of a cataloged kernel (see 'banger kernel list')")
	cmd.Flags().StringVar(&sizeRaw, "size", "", "ext4 image size, e.g. 4GiB, 512M, 2G (defaults to content + 25%, min 1GiB)")
	_ = cmd.RegisterFlagCompletionFunc("kernel-ref", d.completeKernelNames)
	return cmd
}

func (d *deps) newImagePromoteCommand() *cobra.Command {
	return &cobra.Command{
		Use:               "promote <id-or-name>",
		Short:             "Promote an unmanaged image to a managed artifact",
		Args:              exactArgsUsage(1, "usage: banger image promote <id-or-name>"),
		ValidArgsFunction: d.completeImageNameOnlyAtPos0,
		RunE: func(cmd *cobra.Command, args []string) error {
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.promote", api.ImageRefParams{IDOrName: args[0]})
			if err != nil {
				return err
			}
			return printImageSummary(cmd.OutOrStdout(), result.Image)
		},
	}
}

func (d *deps) newImageListCommand() *cobra.Command {
	return &cobra.Command{
		Use:     "list",
		Aliases: []string{"ls"},
		Short:   "List images",
		Args:    noArgsUsage("usage: banger image list"),
		RunE: func(cmd *cobra.Command, args []string) error {
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			result, err := rpc.Call[api.ImageListResult](cmd.Context(), layout.SocketPath, "image.list", api.Empty{})
			if err != nil {
				return err
			}
			return printImageListTable(cmd.OutOrStdout(), result.Images)
		},
	}
}

func (d *deps) newImageShowCommand() *cobra.Command {
	return &cobra.Command{
		Use:               "show <id-or-name>",
		Short:             "Show image details",
		Args:              exactArgsUsage(1, "usage: banger image show <id-or-name>"),
		ValidArgsFunction: d.completeImageNameOnlyAtPos0,
		RunE: func(cmd *cobra.Command, args []string) error {
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.show", api.ImageRefParams{IDOrName: args[0]})
			if err != nil {
				return err
			}
			return printJSON(cmd.OutOrStdout(), result.Image)
		},
	}
}

func (d *deps) newImageDeleteCommand() *cobra.Command {
	return &cobra.Command{
		Use:               "delete <id-or-name>",
		Aliases:           []string{"rm"},
		Short:             "Delete an image",
		Args:              exactArgsUsage(1, "usage: banger image delete <id-or-name>"),
		ValidArgsFunction: d.completeImageNameOnlyAtPos0,
		RunE: func(cmd *cobra.Command, args []string) error {
			layout, _, err := d.ensureDaemon(cmd.Context())
			if err != nil {
				return err
			}
			result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.delete", api.ImageRefParams{IDOrName: args[0]})
			if err != nil {
				return err
			}
			return printImageSummary(cmd.OutOrStdout(), result.Image)
		},
	}
}
