// SPDX-License-Identifier: GPL-3.0-only
//
// This file is part of the distrobox project:
//    https://github.com/89luca89/distrobox
//
// Copyright (C) 2021 distrobox contributors
//
// distrobox is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License version 3
// as published by the Free Software Foundation.
//
// distrobox is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with distrobox; if not, see <http://www.gnu.org/licenses/>.

package cli

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"os"

	"github.com/urfave/cli/v3"

	"github.com/89luca89/distrobox/pkg/commands"
	"github.com/89luca89/distrobox/pkg/config"
	"github.com/89luca89/distrobox/pkg/containermanager"
	"github.com/89luca89/distrobox/pkg/ui"
)

//nolint:funlen // function length is acceptable for CLI command definition
func newCreateCommand(cfg *config.Values) *cli.Command {
	imageDefault := cfg.DefaultContainerImage
	if cfg.ContainerImage != "" {
		imageDefault = cfg.ContainerImage
	}
	nameDefault := cfg.DefaultContainerName
	if cfg.ContainerName != "" {
		nameDefault = cfg.ContainerName
	}
	hostnameDefault := cfg.ContainerHostname
	if hostnameDefault == "" {
		hostnameDefault = defaultHostname()
	}

	return &cli.Command{

		Name:  "create",
		Usage: "create a new distrobox container",
		UsageText: `distrobox create [options] [container_name]

Examples:
    distrobox create --image alpine:latest --name test --init-hooks "touch /var/tmp/test1 && touch /var/tmp/test2"
    distrobox create --image fedora:39 --name test --additional-flags "--env MY_VAR=value"
    distrobox create --image fedora:39 --name test --volume /opt/my-dir:/usr/local/my-dir:rw --additional-flags "--pids-limit 100"
    distrobox create -i docker.io/almalinux/8-init --init --name test --pre-init-hooks "dnf config-manager --enable powertools && dnf -y install epel-release"
    distrobox create --clone fedora-39 --name fedora-39-copy
    distrobox create --image alpine my-alpine-container
    distrobox create --image registry.fedoraproject.org/fedora-toolbox:latest --name fedora-toolbox-latest
    distrobox create --pull --image centos:stream9 --home ~/distrobox/centos9
    distrobox create --image alpine:latest --name test2 --additional-packages "git tmux vim"
    distrobox create --image ubuntu:22.04 --name ubuntu-nvidia --nvidia

    DBX_NON_INTERACTIVE=1 DBX_CONTAINER_NAME=test-alpine DBX_CONTAINER_IMAGE=alpine distrobox-create`,
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    "image",
				Aliases: []string{"i"},
				Value:   imageDefault,
				Usage:   fmt.Sprintf("image to use for the container (default: %s)", imageDefault),
			},
			&cli.StringFlag{
				Name:    "name",
				Aliases: []string{"n"},
				Value:   nameDefault,
				Usage:   fmt.Sprintf("name for the distrobox (default: %s)", nameDefault),
			},
			&cli.StringFlag{
				Name:  "hostname",
				Value: hostnameDefault,
				Usage: fmt.Sprintf("hostname for the distrobox (default: %s)", hostnameDefault),
			},
			&cli.BoolFlag{
				Name:    "pull",
				Aliases: []string{"p"},
				Value:   cfg.ContainerAlwaysPull,
				Usage:   "pull the image even if it exists locally (implies --yes)",
			},
			&cli.BoolFlag{
				Name:    "yes",
				Aliases: []string{"Y"},
				Value:   cfg.NonInteractive,
				Usage:   "non-interactive, pull images without asking",
			},
			&cli.StringFlag{
				Name:    "clone",
				Aliases: []string{"c"},
				Usage: `name of the distrobox container to use as base for a new container
this will be useful to either rename an existing distrobox or have multiple copies
of the same environment.`,
			},
			&cli.StringFlag{
				Name:    "home",
				Aliases: []string{"H"},
				Value:   cfg.ContainerCustomHome,
				Usage:   "select a custom HOME directory for the container. Useful to avoid host's home littering with temp files.",
			},
			&cli.StringSliceFlag{
				Name:  "volume",
				Usage: "additional volumes to add to the container",
			},
			&cli.StringSliceFlag{
				Name:    "additional-flags",
				Aliases: []string{"a"},
				Usage:   "additional flags to pass to the container manager command",
			},
			&cli.StringSliceFlag{
				Name:    "additional-packages",
				Aliases: []string{"ap"},
				Usage:   "additional packages to install during initial container setup",
			},
			&cli.StringFlag{
				Name:  "init-hooks",
				Usage: "additional commands to execute at the end of container initialization",
			},
			&cli.StringFlag{
				Name:  "pre-init-hooks",
				Usage: "additional commands to execute at the start of container initialization",
			},
			&cli.BoolFlag{
				Name:    "init",
				Aliases: []string{"I"},
				Usage: `use init system (like systemd) inside the container.
this will make host's processes not visible from within the container. (assumes --unshare-process)
may require additional packages depending on the container image: https://github.com/89luca89/distrobox/blob/main/docs/useful_tips.md#using-init-system-inside-a-distrobox`,
			},
			&cli.BoolFlag{
				Name:  "nvidia",
				Usage: "try to integrate host's nVidia drivers in the guest",
			},
			&cli.StringFlag{
				Name:  "platform",
				Usage: "specify which platform to use, eg: linux/arm64",
			},
			&cli.BoolFlag{
				Name:  "unshare-devsys",
				Usage: "do not share host devices and sysfs dirs from host",
			},
			&cli.BoolFlag{
				Name:  "unshare-groups",
				Usage: "do not forward user's additional groups into the container",
			},
			&cli.BoolFlag{
				Name:  "unshare-ipc",
				Usage: "do not share ipc namespace with host",
			},
			&cli.BoolFlag{
				Name:  "unshare-netns",
				Usage: "do not share the net namespace with host",
			},
			&cli.BoolFlag{
				Name:  "unshare-process",
				Usage: "do not share process namespace with host",
			},
			&cli.BoolFlag{
				Name:  "unshare-all",
				Usage: "activate all the unshare flags below",
			},
			&cli.BoolFlag{
				Name:  "no-entry",
				Usage: "do not generate a container entry in the application list",
			},
			&cli.BoolFlag{
				Name:    "dry-run",
				Aliases: []string{"d"},
				Usage:   "only print the container manager command generated",
			},
			&cli.BoolFlag{
				Name:  "absolutely-disable-root-password-i-am-really-positively-sure",
				Usage: `⚠️ ⚠️ when setting up a rootful distrobox, this will skip user password setup, leaving it blank. ⚠️ ⚠️`,
			},
			&cli.BoolFlag{
				Name:    "compatibility",
				Aliases: []string{"C"},
				Usage:   "show compatibility information and exit",
			},
		},
		Action: func(ctx context.Context, cmd *cli.Command) error {
			return createAction(ctx, cmd, cfg)
		},
	}
}

func createAction(ctx context.Context, cmd *cli.Command, cfg *config.Values) error {
	if cmd.Bool("compatibility") {
		err := showCompatibility(ctx)
		if err != nil {
			return fmt.Errorf("compatibility check failed: %w", err)
		}
		return nil
	}

	containerManager, ok := ctx.Value(containerManagerKey).(containermanager.ContainerManager)
	if !ok {
		return errors.New("container manager not found in context")
	}

	generateEntry := cfg.GenerateEntry && !cmd.Bool("no-entry")

	opts := commands.CreateOptions{
		ContainerImage:          cmd.String("image"),
		ContainerName:           cmd.String("name"),
		ContainerHostname:       cmd.String("hostname"),
		ContainerHomePrefix:     cfg.ContainerHomePrefix,
		ContainerClone:          cmd.String("clone"),
		UnshareNetNs:            cmd.Bool("unshare-netns") || cmd.Bool("unshare-all"),
		UnshareDevsys:           cmd.Bool("unshare-devsys") || cmd.Bool("unshare-all"),
		UnshareGroups:           cmd.Bool("unshare-groups") || cmd.Bool("unshare-all") || cmd.Bool("init"),
		UnshareIpc:              cmd.Bool("unshare-ipc") || cmd.Bool("unshare-all"),
		UnshareProcess:          cmd.Bool("unshare-process") || cmd.Bool("unshare-all") || cmd.Bool("init"),
		AdditionalFlags:         cmd.StringSlice("additional-flags"),
		AdditionalVolumes:       cmd.StringSlice("volume"),
		AdditionalPackages:      cmd.StringSlice("additional-packages"),
		Nopasswd:                cmd.Bool("absolutely-disable-root-password-i-am-really-positively-sure"),
		ContainerUserCustomHome: cmd.String("home"),
		Init:                    cmd.Bool("init"),
		Nvidia:                  cmd.Bool("nvidia"),
		ContainerInitHook:       cmd.String("init-hooks"),
		ContainerPreInitHook:    cmd.String("pre-init-hooks"),
		ContainerPlatform:       cmd.String("platform"),
		DryRun:                  cmd.Bool("dry-run"),
		GenerateEntry:           generateEntry,
		Rootful:                 cmd.Bool("root"),
		ContainerAlwaysPull:     cmd.Bool("pull"),
		NonInteractive:          cmd.Bool("yes"),
	}

	// Setting Value: to the resolved default (so --help is honest) means
	// cmd.String("name"/"hostname") returns that default even when the user
	// passed nothing on the CLI. Two shell branches rely on the option being
	// empty to fire — basename(image) in makeContainerName
	// (distrobox-create:495-497) and the `<name>.<host>` prefix under
	// --unshare-netns in makeContainerHostname (lines 503-505). When neither
	// the flag nor the env var was supplied, restore "" so they still fire.
	if !cmd.IsSet("name") && cfg.ContainerName == "" {
		opts.ContainerName = ""
	}
	if !cmd.IsSet("hostname") && cfg.ContainerHostname == "" {
		opts.ContainerHostname = ""
	}

	// Positional container_name overrides --name, matching the shell
	// (distrobox-create:453-461) so `distrobox create --image alpine my-box`
	// names the box `my-box`.
	if positional := cmd.Args().First(); positional != "" {
		opts.ContainerName = positional
	}

	progress := ui.NewProgress(os.Stderr)
	prompter := ui.NewPrompter(*bufio.NewReader(os.Stdin), os.Stdout)

	createCmd := commands.NewCreateCommand(cfg, containerManager, progress, prompter)
	result, err := createCmd.Execute(ctx, opts)

	var containerAlreadyExistsErr *commands.ContainerAlreadyExistsError
	if errors.As(err, &containerAlreadyExistsErr) {
		printContainerAlreadyExists(progress, containerAlreadyExistsErr.ContainerName, opts.Rootful)
	}

	if errors.Is(err, commands.ErrImagePullAbortedByUser) {
		progress.Finalize("next time, pull the image first")
		return nil
	}

	if err != nil {
		return fmt.Errorf("create command failed: %w", err)
	}

	if !opts.DryRun {
		printCreateCompleted(progress, result.ContainerName, opts.Rootful)
	}

	return nil
}

func printCreateCompleted(progress *ui.Progress, containerName string, rootful bool) {
	rootFlag := ""
	if rootful {
		rootFlag = "--root "
	}

	msg := "Distrobox '%s' successfully created.\nTo enter, run:\n\ndistrobox enter %s%s\n\n"

	progress.Finalize(msg, containerName, rootFlag, containerName)
}

// defaultHostname mirrors the shell's `default: $(uname -n)` hint shown in
// distrobox-create's --hostname help line. Evaluated once at command-build
// time; an unreachable hostname call falls back to a literal hint.
func defaultHostname() string {
	if h, err := os.Hostname(); err == nil && h != "" {
		return h
	}
	return "$(uname -n)"
}

func printContainerAlreadyExists(progress *ui.Progress, containerName string, rootful bool) {
	rootFlag := ""
	if rootful {
		rootFlag = "--root "
	}

	msg := `Distrobox named '%s' already exists.
To enter, run:

distrobox enter %s%s

`

	progress.Finalize(msg, containerName, rootFlag, containerName)
}
