package core

/*	License: GPLv3
	Authors:
		Mirko Brombin <brombin94@gmail.com>
		Pietro di Caprio <pietro@fabricators.ltd>
		Vanilla OS Contributors <https://github.com/vanilla-os/>
	Copyright: 2024
	Description: Apx is a wrapper around multiple package managers to install packages and run commands inside a managed container.
*/

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"
	"syscall"

	"github.com/vanilla-os/apx/v3/settings"
)

type dbox struct {
	Engine       string
	EngineBinary string
	Version      string
}

type dboxContainer struct {
	ID        string
	CreatedAt string
	Status    string
	Labels    map[string]string
	Name      string
}

func NewDbox() (*dbox, error) {
	engineBinary, engine := getEngine()

	version, err := dboxGetVersion()
	if err != nil {
		return nil, err
	}

	return &dbox{
		Engine:       engine,
		EngineBinary: engineBinary,
		Version:      version,
	}, nil
}

func getEngine() (string, string) {
	podmanBinary, err := settings.LookPath("podman")
	if err == nil {
		return podmanBinary, "podman"
	}

	dockerBinary, err := settings.LookPath("docker")
	if err == nil {
		return dockerBinary, "docker"
	}

	log.Fatal("no container engine found. Please install Podman or Docker.")
	return "", ""
}

func dboxGetVersion() (version string, err error) {
	entrypoint := apx.Cnf.DistroboxPath
	finalArgs := []string{"version"}
	if settings.IsFlatpak() {
		finalArgs = append([]string{"--host", entrypoint}, finalArgs...)
		entrypoint = "flatpak-spawn"
	}
	output, err := exec.Command(entrypoint, finalArgs...).Output()
	if err != nil {
		return "", err
	}

	splitted := strings.Split(string(output), "distrobox: ")
	if len(splitted) != 2 {
		return "", errors.New("can't retrieve distrobox version")
	}

	return splitted[1], nil
}

func (d *dbox) RunCommand(command string, args []string, engineFlags []string, useEngine bool, captureOutput bool, muteOutput bool, rootFull bool, detachedMode bool) ([]byte, error) {
	entrypoint := apx.Cnf.DistroboxPath
	if useEngine {
		entrypoint = d.EngineBinary
	}

	// we need to append engineFlags after command, otherwise they will be
	// ignored in commands like "enter"
	finalArgs := []string{command}

	// NOTE: for engine-specific commands, we need to use pkexec for rootfull
	//		 containers, since podman does not offer a dedicated flag for this.
	if rootFull && useEngine {
		entrypoint = "pkexec"
		finalArgs = []string{d.EngineBinary, command}
	}

	envVars := []string{"DBX_SUDO_PROGRAM=pkexec"}

	// NOTE: the custom storage is not being used since it prevent other
	//		 utilities, like VSCode, to access the container.
	switch d.Engine {
	case "podman":
		envVars = append(envVars, "CONTAINER_STORAGE_DRIVER="+apx.Cnf.StorageDriver)
		// envVars = append(envVars, "XDG_DATA_HOME="+apx.Cnf.ApxStoragePath)
	case "docker":
		envVars = append(envVars, "DOCKER_STORAGE_DRIVER="+apx.Cnf.StorageDriver)
		// envVars = append(envVars, "DOCKER_DATA_ROOT="+apx.Cnf.ApxStoragePath)
	}

	if settings.IsFlatpak() {
		finalArgs = append([]string{entrypoint}, finalArgs...)
		for i := range envVars {
			envVars[i] = "--env=" + envVars[i]
		}
		finalArgs = append(envVars, finalArgs...)
		finalArgs = append([]string{"--host"}, finalArgs...)
		entrypoint = "flatpak-spawn"
	}

	cmd := exec.Command(entrypoint, finalArgs...)

	if detachedMode {
		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	}

	if !captureOutput && !muteOutput {
		cmd.Stdout = os.Stdout
	}
	if !muteOutput {
		cmd.Stderr = os.Stderr
	}
	cmd.Stdin = os.Stdin

	if !settings.IsFlatpak() {
		cmd.Env = append(os.Environ(), envVars...)
	}

	if len(engineFlags) > 0 {
		cmd.Args = append(cmd.Args, "--additional-flags")
		cmd.Args = append(cmd.Args, strings.Join(engineFlags, " "))
	}

	// NOTE: the root flag is not being used by the Apx CLI, but it's useful
	//		 for those using Apx as a library, e.g. VSO.
	if rootFull && !useEngine {
		cmd.Args = append(cmd.Args, "--root")
	}

	cmd.Args = append(cmd.Args, args...)

	if os.Getenv("APX_VERBOSE") == "1" {
		fmt.Println("Running a command:")
		fmt.Println("\tCommand:", cmd.String())
		fmt.Println("\tcaptureOutput:", captureOutput)
		fmt.Println("\tmuteOutput:", muteOutput)
		fmt.Println("\trootFull:", rootFull)
		fmt.Println("\tdetachedMode:", detachedMode)
	}

	if detachedMode {
		err := cmd.Start()
		if err != nil {
			return nil, err
		}
		return nil, nil
	}

	if captureOutput {
		output, err := cmd.Output()
		if err != nil {
			if exitErr, ok := err.(*exec.ExitError); ok {
				return output, errors.New(string(exitErr.Stderr))
			}
		}
		return output, err
	}

	err := cmd.Run()
	return nil, err
}

func (d *dbox) ListContainers(rootFull bool) ([]dboxContainer, error) {
	output, err := d.RunCommand("ps", []string{
		"-a",
		"--format", "{{.ID}}|{{.CreatedAt}}|{{.Status}}|{{.Labels}}|{{.Names}}",
	}, []string{}, true, true, false, rootFull, false)
	if err != nil {
		return nil, err
	}

	rows := strings.Split(string(output), "\n")
	containers := []dboxContainer{}

	for _, row := range rows {
		if row == "" {
			continue
		}

		rowItems := strings.Split(row, "|")
		if len(rowItems) != 5 {
			continue
		}

		container := dboxContainer{
			ID:        strings.Trim(rowItems[0], "\""),
			CreatedAt: strings.Trim(rowItems[1], "\""),
			Status:    strings.Trim(rowItems[2], "\""),
			Name:      strings.Trim(rowItems[4], "\""),
			Labels:    map[string]string{},
		}

		// example labels: map[manager:apx name:alpine stack:alpine]
		labels := strings.ReplaceAll(rowItems[3], "map[", "")
		labels = strings.ReplaceAll(labels, "]", "")
		labelsItems := strings.Split(labels, " ")
		for _, label := range labelsItems {
			labelItems := strings.Split(label, ":")
			if len(labelItems) != 2 {
				continue
			}

			container.Labels[labelItems[0]] = labelItems[1]
		}

		containers = append(containers, container)
	}

	return containers, nil
}

func (d *dbox) GetContainer(name string, rootFull bool) (*dboxContainer, error) {
	containers, err := d.ListContainers(rootFull)
	if err != nil {
		return nil, err
	}

	for _, container := range containers {
		// fmt.Println("found container", container.Name, "requested", name)
		if container.Name == name {
			return &container, nil
		}
	}

	return nil, errors.New("container not found")
}

func (d *dbox) ContainerDelete(name string, rootFull bool) error {
	_, err := d.RunCommand("rm", []string{
		"--force",
		name,
	}, []string{}, false, false, true, rootFull, false)
	return err
}

func (d *dbox) CreateContainer(name string, image string, additionalPackages []string, home string, labels map[string]string, withInit bool, rootFull bool, unshared bool, withNvidiaIntegration bool, hostname string, additionalArgs ...string) error {
	args := []string{
		"--image", image,
		"--name", name,
		"--no-entry",
		"--yes",
		"--pull",
	}

	if home != "" {
		args = append(args, "--home", home)
	}

	if hasNvidiaGPU() && withNvidiaIntegration {
		args = append(args, "--nvidia")
	}

	if withInit {
		args = append(args, "--init")
	}

	if unshared {
		args = append(args, "--unshare-all")
	}

	if hostname != "" {
		args = append(args, "--hostname", hostname)
	}

	if len(additionalPackages) > 0 {
		args = append(args, "--additional-packages")
		args = append(args, strings.Join(additionalPackages, " "))
	}

	args = append(args, additionalArgs...)

	engineFlags := []string{}
	for key, value := range labels {
		engineFlags = append(engineFlags, fmt.Sprintf("--label=%s=%s", key, value))
	}
	engineFlags = append(engineFlags, "--label=manager=apx")

	_, err := d.RunCommand("create", args, engineFlags, false, false, false, rootFull, false)
	// fmt.Println(string(out))
	return err
}

func (d *dbox) RunContainerCommand(name string, command []string, rootFull, detachedMode bool) error {
	args := []string{
		"--name", name,
		"--",
	}

	args = append(args, command...)

	_, err := d.RunCommand("run", args, []string{}, false, false, false, rootFull, detachedMode)
	return err
}

func (d *dbox) ContainerExec(name string, captureOutput bool, muteOutput bool, rootFull, detachedMode bool, args ...string) (string, error) {
	finalArgs := []string{
		// "--verbose",
		name,
		"--",
	}

	finalArgs = append(finalArgs, args...)
	engineFlags := []string{}

	out, err := d.RunCommand("enter", finalArgs, engineFlags, false, captureOutput, muteOutput, rootFull, detachedMode)
	// if error 130, it means that the user pressed CTRL+D, so ignore
	if err != nil && err.Error() == "exit status 130" {
		return string(out), nil
	}

	return string(out), err
}

func (d *dbox) ContainerEnter(name string, rootFull bool) error {
	finalArgs := []string{
		name,
	}

	engineFlags := []string{}

	_, err := d.RunCommand("enter", finalArgs, engineFlags, false, false, false, rootFull, false)
	// if error 130, it means that the user pressed CTRL+D, so ignore
	if err != nil && err.Error() == "exit status 130" {
		return nil
	}

	return err
}

func (d *dbox) ContainerStart(name string, rootFull bool) error {
	_, err := d.RunCommand("start", []string{
		name,
	}, []string{}, true, false, false, rootFull, false)
	return err
}

func (d *dbox) ContainerStop(name string, rootFull bool) error {
	finalArgs := []string{
		name,
		"--yes",
	}

	engineFlags := []string{}

	_, err := d.RunCommand("stop", finalArgs, engineFlags, false, false, false, rootFull, false)
	return err
}

func (d *dbox) ContainerExport(name string, delete bool, rootFull bool, args ...string) error {
	finalArgs := []string{"distrobox-export"}

	if delete {
		finalArgs = append(finalArgs, "--delete")
	}

	finalArgs = append(finalArgs, args...)

	_, err := d.ContainerExec(name, true, true, rootFull, false, finalArgs...)
	return err
}

func (d *dbox) ContainerExportDesktopEntry(containerName string, appName string, label string, rootFull bool) error {
	args := []string{"--app", appName, "--export-label", label}
	return d.ContainerExport(containerName, false, rootFull, args...)
}

func (d *dbox) ContainerUnexportDesktopEntry(containerName string, appName string, rootFull bool) error {
	args := []string{"--app", appName}
	return d.ContainerExport(containerName, true, rootFull, args...)
}

func (d *dbox) ContainerExportBin(containerName string, binary string, exportPath string, rootFull bool) error {
	args := []string{"--bin", binary, "--export-path", exportPath}
	return d.ContainerExport(containerName, false, rootFull, args...)
}

func (d *dbox) ContainerUnexportBin(containerName string, binary string, rootFull bool) error {
	args := []string{"--bin", binary}
	return d.ContainerExport(containerName, true, rootFull, args...)
}
