package cryptsetup

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"git.wntrmute.dev/kyle/arca/internal/verbose"
)

// Open opens a LUKS device using cryptsetup with token-based unlock.
func Open(devicePath, mapperName string) error {
	args := withTokenPluginEnv([]string{"cryptsetup", "open", devicePath, mapperName, "--token-only"})
	args = withPrivilege(args)

	verbose.Printf("exec: %s", strings.Join(args, " "))

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("cryptsetup open --token-only: %w", err)
	}
	return nil
}

// Close closes a LUKS mapping.
func Close(mapperName string) error {
	args := []string{"cryptsetup", "close", mapperName}
	args = withPrivilege(args)

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("cryptsetup close: %w", err)
	}
	return nil
}

// Mount mounts a device at the given mountpoint using privileged mount.
// If mountpoint is empty, mounts under /mnt/<basename>.
func Mount(devicePath, mountpoint string) (string, error) {
	if mountpoint == "" {
		mountpoint = "/mnt/" + filepath.Base(devicePath)
	}

	mkdirArgs := withPrivilege([]string{"mkdir", "-p", mountpoint})
	if err := exec.Command(mkdirArgs[0], mkdirArgs[1:]...).Run(); err != nil {
		return "", fmt.Errorf("creating mountpoint: %w", err)
	}

	args := withPrivilege([]string{"mount", devicePath, mountpoint})
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("mount %s: %w", devicePath, err)
	}
	return mountpoint, nil
}

// Unmount unmounts the given mountpoint using privileged umount.
func Unmount(mountpoint string) error {
	args := withPrivilege([]string{"umount", mountpoint})
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("umount %s: %w", mountpoint, err)
	}
	return nil
}

// MapperName returns "arca-<dev>" from a device path, e.g. "/dev/sda1" -> "arca-sda1".
func MapperName(devicePath string) string {
	return "arca-" + filepath.Base(devicePath)
}

// withTokenPluginEnv wraps the command with `env LD_LIBRARY_PATH=...` if
// the systemd cryptsetup token plugins are found.
//
// On NixOS, cryptsetup uses dlopen to load token plugins, but glibc only
// searches the binary's baked-in RUNPATH — which doesn't include the
// systemd plugin directory. LD_LIBRARY_PATH is the only reliable way to
// make dlopen find them.
//
// We inject it via `env` rather than cmd.Env so it survives privilege
// escalation through doas/sudo.
func withTokenPluginEnv(args []string) []string {
	pluginDir := findTokenPluginDir()
	if pluginDir == "" {
		return args
	}
	return append([]string{"env", "LD_LIBRARY_PATH=" + pluginDir}, args...)
}

// findTokenPluginDir locates the directory containing libcryptsetup-token-*.so.
// It checks the NixOS system profile first, then falls back to resolving
// from the systemd-cryptenroll binary.
func findTokenPluginDir() string {
	// NixOS stable symlink — survives rebuilds.
	const nixSystemPath = "/run/current-system/sw/lib/cryptsetup"
	if hasTokenPlugins(nixSystemPath) {
		verbose.Printf("token plugin dir: %s", nixSystemPath)
		return nixSystemPath
	}

	// Fallback: derive from systemd-cryptenroll location.
	if bin, err := exec.LookPath("systemd-cryptenroll"); err == nil {
		if resolved, err := filepath.EvalSymlinks(bin); err == nil {
			dir := filepath.Join(filepath.Dir(filepath.Dir(resolved)), "lib", "cryptsetup")
			if hasTokenPlugins(dir) {
				verbose.Printf("token plugin dir (from systemd-cryptenroll): %s", dir)
				return dir
			}
		}
	}

	verbose.Printf("no token plugin directory found")
	return ""
}

func hasTokenPlugins(dir string) bool {
	matches, _ := filepath.Glob(filepath.Join(dir, "libcryptsetup-token-*.so"))
	return len(matches) > 0
}

func withPrivilege(args []string) []string {
	if _, err := exec.LookPath("doas"); err == nil {
		return append([]string{"doas"}, args...)
	}
	if _, err := exec.LookPath("sudo"); err == nil {
		return append([]string{"sudo"}, args...)
	}
	return args
}
