package main

import (
	"errors"
	"fmt"
	"io"
	"maps"
	"net/url"
	"os"
	"path"
	"strings"

	"github.com/spf13/cobra"
	"go.yaml.in/yaml/v4"

	incus "github.com/lxc/incus/v7/client"
	"github.com/lxc/incus/v7/cmd/incus/color"
	u "github.com/lxc/incus/v7/cmd/incus/usage"
	"github.com/lxc/incus/v7/internal/i18n"
	"github.com/lxc/incus/v7/shared/api"
	config "github.com/lxc/incus/v7/shared/cliconfig"
	cli "github.com/lxc/incus/v7/shared/cmd"
	"github.com/lxc/incus/v7/shared/termios"
)

type cmdCreate struct {
	global *cmdGlobal

	flagConfig          []string
	flagDevice          []string
	flagEnvironmentFile string
	flagEphemeral       bool
	flagNetwork         string
	flagProfile         []string
	flagStorage         string
	flagTarget          string
	flagType            string
	flagNoProfiles      bool
	flagEmpty           bool
	flagVM              bool
	flagDescription     string
}

var cmdCreateUsage = u.Usage{u.Either(u.Flag("empty"), u.RemoteImage), u.NewName(u.Instance).Optional().Remote()}

func (c *cmdCreate) command() *cobra.Command {
	cmd := &cobra.Command{}
	cmd.Use = cli.U("create", cmdCreateUsage...)
	cmd.Short = i18n.G("Create instances from images")
	cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Create instances from images`))
	cmd.Example = cli.FormatSection("", i18n.G(`incus create images:debian/12 u1
    Create the instance u1

incus create images:debian/12 u1 < config.yaml
    Create the instance with configuration from config.yaml

incus launch images:debian/12 v2 --vm -d root,size=50GiB -d root,io.bus=nvme
    Create and start a virtual machine, overriding the disk size and bus`))

	cmd.Aliases = []string{"init"}
	cmd.RunE = c.run
	cli.AddStringArrayFlag(cmd.Flags(), &c.flagConfig, "config|c", i18n.G("Config key/value to apply to the new instance"))
	cli.AddStringArrayFlag(cmd.Flags(), &c.flagProfile, "profile|p", i18n.G("Profile to apply to the new instance"))
	cli.AddStringArrayFlag(cmd.Flags(), &c.flagDevice, "device|d", i18n.G("New key/value to apply to a specific device"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagEphemeral, "ephemeral|e", i18n.G("Ephemeral instance"))
	cli.AddStringFlag(cmd.Flags(), &c.flagEnvironmentFile, "environment-file", "", "", i18n.G("Include environment variables from file"))
	cli.AddStringFlag(cmd.Flags(), &c.flagNetwork, "network|n", "", "", i18n.G("Network name"))
	cli.AddStringFlag(cmd.Flags(), &c.flagStorage, "storage|s", "", "", i18n.G("Storage pool name"))
	cli.AddStringFlag(cmd.Flags(), &c.flagType, "type|t", "", "", i18n.G("Instance type"))
	cli.AddStringFlag(cmd.Flags(), &c.flagTarget, "target", "", "", i18n.G("Cluster member name"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagNoProfiles, "no-profiles", i18n.G("Create the instance with no profiles applied"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagEmpty, "empty", i18n.G("Create an empty instance"))
	cli.AddBoolFlag(cmd.Flags(), &c.flagVM, "vm", i18n.G("Create a virtual machine"))
	cli.AddStringFlag(cmd.Flags(), &c.flagDescription, "description", "", "", i18n.G("Instance description"))

	cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
		if len(args) != 0 {
			return nil, cobra.ShellCompDirectiveNoFileComp
		}

		return c.global.cmpImages(toComplete)
	}

	return cmd
}

func (c *cmdCreate) run(cmd *cobra.Command, args []string) error {
	parsed, err := c.global.Parse(cmdCreateUsage, cmd, args)
	if err != nil {
		return err
	}

	_, err = c.create(c.global.conf, parsed, false)
	return err
}

func (c *cmdCreate) create(conf *config.Config, parsed []*u.Parsed, launch bool) (*u.Parsed, error) {
	p := parsed[1]
	d := p.RemoteServer
	remoteName := p.RemoteName
	hasInstance := !p.RemoteObject.Skipped
	instanceName := p.RemoteObject.String

	// If stdin isn't a terminal, read text from it
	var stdinData api.InstancePut
	if !termios.IsTerminal(getStdinFd()) {
		loader, err := yaml.NewLoader(os.Stdin)
		if err != nil {
			return nil, err
		}

		err = loader.Load(&stdinData)
		if err != nil && !errors.Is(err, io.EOF) {
			return nil, err
		}
	}

	// Overwrite profiles.
	var profiles []string
	if c.flagProfile != nil {
		profiles = c.flagProfile
	} else if c.flagNoProfiles {
		profiles = []string{}
	}

	if !c.global.flagQuiet {
		if d.HasExtension("instance_create_start") && launch {
			if hasInstance {
				fmt.Printf(i18n.G("Launching %s")+"\n", formatRemote(conf, p))
			} else {
				fmt.Print(i18n.G("Launching the instance") + "\n")
			}
		} else {
			if hasInstance {
				fmt.Printf(i18n.G("Creating %s")+"\n", formatRemote(conf, p))
			} else {
				fmt.Print(i18n.G("Creating the instance") + "\n")
			}
		}
	}

	var devicesMap map[string]map[string]string
	if len(stdinData.Devices) > 0 {
		devicesMap = stdinData.Devices
	} else {
		devicesMap = map[string]map[string]string{}
	}

	if c.flagNetwork != "" {
		network, _, err := d.GetNetwork(c.flagNetwork)
		if err != nil {
			return nil, fmt.Errorf(i18n.G("Failed loading network %q: %w"), c.flagNetwork, err)
		}

		// Prepare the instance's NIC device entry.
		var device map[string]string

		if network.Managed && d.HasExtension("instance_nic_network") {
			// If network is managed, use the network property rather than nictype, so that the
			// network's inherited properties are loaded into the NIC when started.
			device = map[string]string{
				"name":    "eth0",
				"type":    "nic",
				"network": network.Name,
			}
		} else {
			// If network is unmanaged default to using a macvlan connected to the specified interface.
			device = map[string]string{
				"name":    "eth0",
				"type":    "nic",
				"nictype": "macvlan",
				"parent":  c.flagNetwork,
			}

			if network.Type == "bridge" {
				// If the network type is an unmanaged bridge, use bridged NIC type.
				device["nictype"] = "bridged"
			}
		}

		devicesMap["eth0"] = device
	}

	var configMap map[string]string
	if len(stdinData.Config) > 0 {
		configMap = stdinData.Config
	} else {
		configMap = map[string]string{}
	}

	if c.flagEnvironmentFile != "" {
		envMap, err := readEnvironmentFile(c.flagEnvironmentFile)
		if err != nil {
			return nil, err
		}

		for k, v := range envMap {
			configMap["environment."+k] = v
		}
	}

	for _, entry := range c.flagConfig {
		key, value, found := strings.Cut(entry, "=")
		if !found {
			return nil, fmt.Errorf(i18n.G("Bad key=value pair: %q"), entry)
		}

		configMap[key] = value
	}

	// Check if the specified storage pool exists.
	if c.flagStorage != "" {
		_, _, err := d.GetStoragePool(c.flagStorage)
		if err != nil {
			return nil, fmt.Errorf(i18n.G("Failed loading storage pool %q: %w"), c.flagStorage, err)
		}

		devicesMap["root"] = map[string]string{
			"type": "disk",
			"path": "/",
			"pool": c.flagStorage,
		}
	}

	// Decide whether we are creating a container or a virtual machine.
	instanceDBType := api.InstanceTypeContainer
	if c.flagVM {
		instanceDBType = api.InstanceTypeVM
	}

	// Set the target if provided.
	if c.flagTarget != "" {
		d = d.UseTarget(c.flagTarget)
	}

	// Setup instance creation request
	req := api.InstancesPost{
		Name:         instanceName,
		InstanceType: c.flagType,
		Type:         instanceDBType,
		Start:        launch,
	}

	req.Config = configMap

	if c.flagEphemeral {
		req.Ephemeral = c.flagEphemeral
	}

	if c.flagDescription != "" {
		req.Description = c.flagDescription
	} else {
		req.Description = stdinData.Description
	}

	if !c.flagNoProfiles && len(profiles) == 0 {
		if len(stdinData.Profiles) > 0 {
			req.Profiles = stdinData.Profiles
		} else {
			req.Profiles = nil
		}
	} else {
		req.Profiles = profiles
	}

	// Handle device overrides.
	deviceOverrides, err := parseDeviceOverrides(c.flagDevice)
	if err != nil {
		return nil, err
	}

	// Check to see if any of the overridden devices are for devices that are not yet defined in the
	// local devices (and thus maybe expected to be coming from profiles).
	profileDevices := make(map[string]map[string]string)
	needProfileExpansion := false
	for deviceName := range deviceOverrides {
		_, isLocalDevice := devicesMap[deviceName]
		if !isLocalDevice {
			needProfileExpansion = true
			break
		}
	}

	// If there are device overrides that are expected to be applied to profile devices then load the profiles
	// that would be applied server-side.
	if needProfileExpansion {
		// If the list of profiles is empty then the default profile would be applied on the server side.
		serverSideProfiles := req.Profiles
		if len(serverSideProfiles) == 0 {
			serverSideProfiles = []string{"default"}
		}

		// Get the effective expanded devices by overlaying each profile's devices in order.
		for _, profileName := range serverSideProfiles {
			profile, _, err := d.GetProfile(profileName)
			if err != nil {
				return nil, fmt.Errorf(i18n.G("Failed loading profile %q for device override: %w"), profileName, err)
			}

			maps.Copy(profileDevices, profile.Devices)
		}
	}

	// Apply device overrides.
	for deviceName := range deviceOverrides {
		_, isLocalDevice := devicesMap[deviceName]
		if isLocalDevice {
			// Apply overrides to local device.
			maps.Copy(devicesMap[deviceName], deviceOverrides[deviceName])
		} else {
			// Check device exists in expanded profile devices.
			profileDeviceConfig, found := profileDevices[deviceName]
			if !found {
				return nil, fmt.Errorf(i18n.G("Cannot override config for device %q: Device not found in profile devices"), deviceName)
			}

			maps.Copy(profileDeviceConfig, deviceOverrides[deviceName])

			// Add device to local devices.
			devicesMap[deviceName] = profileDeviceConfig
		}
	}

	req.Devices = devicesMap

	var opInfo api.Operation

	// If an image is provided, use it.
	if parsed[0].BranchID == 1 {
		imgRemoteName := parsed[0].List[0].Get(conf.DefaultRemote)

		imgServer, imgInfo, err := getImgInfo(d, conf, imgRemoteName, remoteName, parsed[0].List[1].String, &req.Source)
		if err != nil {
			return nil, err
		}

		if conf.Remotes[imgRemoteName].Protocol == "incus" {
			if imgInfo.Type != "virtual-machine" && c.flagVM {
				return nil, errors.New(i18n.G("Asked for a VM but image is of type container"))
			}

			req.Type = api.InstanceType(imgInfo.Type)
		}

		// Create the instance
		op, err := d.CreateInstanceFromImage(imgServer, *imgInfo, req)
		if err != nil {
			return nil, err
		}

		// Watch the background operation
		progress := cli.ProgressRenderer{
			Format: i18n.G("Retrieving image: %s"),
			Quiet:  c.global.flagQuiet,
		}

		_, err = op.AddHandler(progress.UpdateOp)
		if err != nil {
			progress.Done("")
			return nil, err
		}

		err = cli.CancelableWait(op, &progress)
		if err != nil {
			progress.Done("")
			return nil, err
		}

		progress.Done("")

		// Extract the instance name
		info, err := op.GetTarget()
		if err != nil {
			return nil, err
		}

		opInfo = *info
	} else {
		req.Source.Type = "none"

		op, err := d.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		err = op.Wait()
		if err != nil {
			return nil, err
		}

		opInfo = op.Get()
	}

	instances, ok := opInfo.Resources["instances"]
	if !ok || len(instances) == 0 {
		return nil, errors.New(i18n.G("Didn't get name of new instance from the server"))
	}

	if len(instances) == 1 && !hasInstance {
		uri, err := url.Parse(instances[0])
		if err != nil {
			return nil, err
		}

		instanceName = path.Base(uri.Path)
		fmt.Printf(i18n.G("Instance name is: %s")+"\n", instanceName)
	}

	// Validate the network setup
	c.checkNetwork(d, instanceName)

	p.RemoteObject = u.ParseString(instanceName)
	return p, nil
}

func (c *cmdCreate) checkNetwork(d incus.InstanceServer, name string) {
	ct, _, err := d.GetInstance(name)
	if err != nil {
		return
	}

	for _, d := range ct.ExpandedDevices {
		if d["type"] == "nic" {
			return
		}
	}

	fmt.Fprint(os.Stderr, "\n"+i18n.G("The instance you are starting doesn't have any network attached to it.")+"\n")
	fmt.Fprint(os.Stderr, "  "+i18n.G("To create a new network, use: incus network create")+"\n")
	fmt.Fprint(os.Stderr, "  "+i18n.G("To attach a network to an instance, use: incus network attach")+"\n\n")
}
