package generators

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

	lxdShared "github.com/canonical/lxd/shared"

	"github.com/canonical/lxd-imagebuilder/image"
	"github.com/canonical/lxd-imagebuilder/shared"
)

var lxdAgentSetupScript = `#!/bin/sh
set -eu
PREFIX="/run/lxd_agent"
CDROM="/dev/disk/by-id/scsi-0QEMU_QEMU_CD-ROM_lxd_agent"

# Functions.
mount_virtiofs() {
    mount -t virtiofs config "${PREFIX}.mnt" >/dev/null 2>&1
}

mount_9p() {
    modprobe 9pnet_virtio >/dev/null 2>&1 || true
    mount -t 9p config "${PREFIX}.mnt" -o access=0,trans=virtio,size=1048576 >/dev/null 2>&1
}

mount_cdrom() {
    mount "${CDROM}" "${PREFIX}.mnt" >/dev/null 2>&1
}

fail() {
    # Check if we already have an agent in place.
    # This will typically be true during restart in the case of a cdrom-based setup.
    if [ -x "${PREFIX}/lxd-agent" ]; then
        echo "${1}, re-using existing agent"
        exit 0
    fi

    # Cleanup and fail.
    umount -l "${PREFIX}" >/dev/null 2>&1 || true
    eject "${CDROM}" >/dev/null 2>&1 || true
    rmdir "${PREFIX}" >/dev/null 2>&1 || true
    echo "${1}, failing"

    exit 1
}

# Try getting an agent drive.
mkdir -p "${PREFIX}.mnt"
mount_9p || mount_virtiofs || mount_cdrom || fail "Couldn't mount 9p or cdrom"

# Setup the mount target.
umount -l "${PREFIX}" >/dev/null 2>&1 || true
mkdir -p "${PREFIX}"
mount -t tmpfs tmpfs "${PREFIX}" -o mode=0700,size=50M

# Copy the data.
cp -Ra "${PREFIX}.mnt/"* "${PREFIX}"

# Unmount the temporary mount.
umount "${PREFIX}.mnt"
rmdir "${PREFIX}.mnt"

# Eject the cdrom in case it's present.
eject "${CDROM}" >/dev/null 2>&1 || true

# Fix up permissions.
chown -R root:root "${PREFIX}"

# Attempt to restore SELinux labels.
restorecon -R "${PREFIX}" >/dev/null 2>&1 || true

exit 0
`

type lxdAgent struct {
	common
}

// RunLXC is not supported.
func (g *lxdAgent) RunLXC(img *image.LXCImage, target shared.DefinitionTargetLXC) error {
	return ErrNotSupported
}

// RunLXD creates systemd unit files for the agent.
func (g *lxdAgent) RunLXD(img *image.LXDImage, target shared.DefinitionTargetLXD) error {
	initFile := filepath.Join(g.sourceDir, "sbin", "init")

	fi, err := os.Lstat(initFile)
	if err != nil {
		return fmt.Errorf("Failed to stat file %q: %w", initFile, err)
	}

	if fi.Mode()&os.ModeSymlink != 0 {
		linkTarget, err := os.Readlink(initFile)
		if err != nil {
			return fmt.Errorf("Failed to read link %q: %w", initFile, err)
		}

		if strings.Contains(linkTarget, "systemd") {
			return g.handleSystemd()
		}

		if strings.Contains(linkTarget, "busybox") {
			return g.getInitSystemFromInittab()
		}

		return nil
	}

	return g.getInitSystemFromInittab()
}

// Run does nothing.
func (g *lxdAgent) Run() error {
	return nil
}

func (g *lxdAgent) handleSystemd() error {
	systemdPath := filepath.Join("/", "lib", "systemd")
	if !lxdShared.PathExists(filepath.Join(g.sourceDir, systemdPath)) {
		systemdPath = filepath.Join("/", "usr", "lib", "systemd")
	}

	lxdAgentServiceUnit := fmt.Sprintf(`[Unit]
Description=LXD - agent
Documentation=https://documentation.ubuntu.com/lxd
Before=multi-user.target cloud-init.target cloud-init.service cloud-init-local.service
DefaultDependencies=no

[Service]
Type=notify
WorkingDirectory=-/run/lxd_agent
ExecStartPre=%s/lxd-agent-setup
ExecStart=/run/lxd_agent/lxd-agent
Restart=on-failure
RestartSec=5s
StartLimitInterval=60
StartLimitBurst=10
`, systemdPath)

	path := filepath.Join(g.sourceDir, systemdPath, "system", "lxd-agent.service")

	err := os.WriteFile(path, []byte(lxdAgentServiceUnit), 0644)
	if err != nil {
		return fmt.Errorf("Failed to write file %q: %w", path, err)
	}

	path = filepath.Join(g.sourceDir, systemdPath, "lxd-agent-setup")

	err = os.WriteFile(path, []byte(lxdAgentSetupScript), 0755)
	if err != nil {
		return fmt.Errorf("Failed to write file %q: %w", path, err)
	}

	udevPath := filepath.Join("/", "lib", "udev", "rules.d")
	stat, err := os.Lstat(filepath.Join(g.sourceDir, "lib", "udev"))
	if err == nil && stat.Mode()&os.ModeSymlink != 0 || !lxdShared.PathExists(filepath.Dir(filepath.Join(g.sourceDir, udevPath))) {
		udevPath = filepath.Join("/", "usr", "lib", "udev", "rules.d")
	}

	lxdAgentRules := `SYMLINK=="virtio-ports/com.canonical.lxd", TAG+="systemd", ENV{SYSTEMD_WANTS}+="lxd-agent.service"

# Legacy.
SYMLINK=="virtio-ports/org.linuxcontainers.lxd", TAG+="systemd", ENV{SYSTEMD_WANTS}+="lxd-agent.service"
`
	err = os.WriteFile(filepath.Join(g.sourceDir, udevPath, "99-lxd-agent.rules"), []byte(lxdAgentRules), 0400)
	if err != nil {
		return fmt.Errorf("Failed to write file %q: %w", filepath.Join(g.sourceDir, udevPath, "99-lxd-agent.rules"), err)
	}

	return nil
}

func (g *lxdAgent) handleOpenRC() error {
	lxdAgentScript := `#!/sbin/openrc-run

description="LXD - agent"
command=/run/lxd_agent/lxd-agent
command_background=true
pidfile=/run/lxd-agent.pid
start_stop_daemon_args="--chdir /run/lxd_agent"
required_dirs=/run/lxd_agent

depend() {
	need lxd-agent-setup
	after lxd-agent-setup
	before cloud-init
	before cloud-init-local
}
`

	err := os.WriteFile(filepath.Join(g.sourceDir, "/etc/init.d/lxd-agent"), []byte(lxdAgentScript), 0755)
	if err != nil {
		return fmt.Errorf("Failed to write file %q: %w", filepath.Join(g.sourceDir, "/etc/init.d/lxd-agent"), err)
	}

	err = os.Symlink("/etc/init.d/lxd-agent", filepath.Join(g.sourceDir, "/etc/runlevels/default/lxd-agent"))
	if err != nil {
		return fmt.Errorf("Failed to create symlink %q: %w", filepath.Join(g.sourceDir, "/etc/runlevels/default/lxd-agent"), err)
	}

	lxdConfigShareMountScript := `#!/sbin/openrc-run

description="LXD - agent - setup"
command=/usr/local/bin/lxd-agent-setup
required_dirs=/dev/virtio-ports/
`

	err = os.WriteFile(filepath.Join(g.sourceDir, "/etc/init.d/lxd-agent-setup"), []byte(lxdConfigShareMountScript), 0755)
	if err != nil {
		return fmt.Errorf("Failed to write file %q: %w", filepath.Join(g.sourceDir, "/etc/init.d/lxd-agent-setup"), err)
	}

	err = os.Symlink("/etc/init.d/lxd-agent-setup", filepath.Join(g.sourceDir, "/etc/runlevels/default/lxd-agent-setup"))
	if err != nil {
		return fmt.Errorf("Failed to create symlink %q: %w", filepath.Join(g.sourceDir, "/etc/runlevels/default/lxd-agent-setup"), err)
	}

	path := filepath.Join(g.sourceDir, "/usr/local/bin", "lxd-agent-setup")

	err = os.WriteFile(path, []byte(lxdAgentSetupScript), 0755)
	if err != nil {
		return fmt.Errorf("Failed to write file %q: %w", path, err)
	}

	return nil
}

func (g *lxdAgent) getInitSystemFromInittab() error {
	f, err := os.Open(filepath.Join(g.sourceDir, "etc", "inittab"))
	if err != nil {
		return fmt.Errorf("Failed to open file %q: %w", filepath.Join(g.sourceDir, "etc", "inittab"), err)
	}

	defer f.Close()

	scanner := bufio.NewScanner(f)

	for scanner.Scan() {
		if strings.Contains(scanner.Text(), "sysinit") && strings.Contains(scanner.Text(), "openrc") {
			return g.handleOpenRC()
		}
	}

	return errors.New("Failed to determine init system")
}
