package sentinel

import (
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"metis/pkg/facts"
)

type nodeConfig struct {
	USBScratch *usbScratchConfig `json:"usb_scratch,omitempty"`
}

type usbScratchConfig struct {
	Mountpoint  string   `json:"mountpoint,omitempty"`
	UUID        string   `json:"uuid,omitempty"`
	Label       string   `json:"label,omitempty"`
	FS          string   `json:"fs,omitempty"`
	BindTargets []string `json:"bind_targets,omitempty"`
}

// Snapshot captures host-level facts.
type Snapshot struct {
	Hostname      string            `json:"hostname,omitempty"`
	Kernel        string            `json:"kernel,omitempty"`
	OSImage       string            `json:"os_image,omitempty"`
	K3sVersion    string            `json:"k3s_version,omitempty"`
	Containerd    string            `json:"containerd,omitempty"`
	PackageSample map[string]string `json:"package_sample,omitempty"` // small subset to detect drift
	DropInsSample map[string]string `json:"dropins_sample,omitempty"` // path->content hash/sample
	USBScratch    *facts.USBScratch `json:"usb_scratch,omitempty"`
	Notes         string            `json:"notes,omitempty"`
}

// Collect gathers a minimal set of facts; intended to run inside a DaemonSet pod with host mounts.
func Collect() *Snapshot {
	return &Snapshot{
		Hostname:      runAndTrim("hostname"),
		Kernel:        runAndTrim("uname", "-r"),
		OSImage:       osRelease(),
		K3sVersion:    runAndTrim("k3s", "version"),
		Containerd:    runAndTrim("containerd", "--version"),
		PackageSample: pkgSample(),
		USBScratch:    collectUSBScratch(),
	}
}

func collectUSBScratch() *facts.USBScratch {
	cfg, ok := loadNodeConfig()
	if !ok || cfg.USBScratch == nil {
		return nil
	}
	desired := cfg.USBScratch
	scratch := &facts.USBScratch{
		Mountpoint: desired.Mountpoint,
		UUID:       desired.UUID,
		Label:      desired.Label,
		FS:         desired.FS,
	}

	source, fsType, mounted := mountInfo(desired.Mountpoint)
	scratch.MountHealthy = mounted && strings.TrimSpace(source) != ""
	if scratch.MountHealthy && desired.FS != "" && fsType != "" {
		scratch.MountHealthy = strings.EqualFold(fsType, desired.FS)
	}
	if scratch.FS == "" && fsType != "" {
		scratch.FS = fsType
	}

	device := source
	if device == "" && desired.UUID != "" {
		device = resolveDeviceByUUID(desired.UUID)
	}
	if device == "" && desired.Label != "" {
		device = resolveDeviceByLabel(desired.Label)
	}
	if device != "" {
		export := blkidExport(device)
		if desired.UUID != "" {
			scratch.UUIDHealthy = export["UUID"] == desired.UUID
		}
		if desired.Label != "" {
			scratch.LabelHealthy = export["LABEL"] == desired.Label
		}
		if scratch.FS == "" {
			scratch.FS = export["TYPE"]
		}
	}

	healthy := true
	if len(desired.BindTargets) > 0 {
		scratch.BindTargets = make([]facts.USBBindTarget, 0, len(desired.BindTargets))
		for _, target := range desired.BindTargets {
			ok := bindHealthy(target, desired.Mountpoint)
			if !ok {
				healthy = false
			}
			scratch.BindTargets = append(scratch.BindTargets, facts.USBBindTarget{
				Path:    target,
				Healthy: ok,
			})
		}
		scratch.BindHealthy = healthy
	} else {
		scratch.BindHealthy = true
	}
	return scratch
}

func loadNodeConfig() (nodeConfig, bool) {
	raw, err := commandOutput("cat", "/etc/metis/node.json")
	if err == nil && len(strings.TrimSpace(string(raw))) > 0 {
		var cfg nodeConfig
		if err := json.Unmarshal(raw, &cfg); err == nil && cfg.USBScratch != nil {
			return cfg, true
		}
	}
	raw, err = commandOutput("cat", "/etc/fstab")
	if err != nil {
		return nodeConfig{}, false
	}
	if scratch, ok := parseUSBScratchFstab(string(raw)); ok {
		return nodeConfig{USBScratch: scratch}, true
	}
	return nodeConfig{}, false
}

func parseUSBScratchFstab(raw string) (*usbScratchConfig, bool) {
	cfg := &usbScratchConfig{}
	inBlock := false
	for _, line := range strings.Split(raw, "\n") {
		line = strings.TrimSpace(line)
		switch {
		case strings.HasPrefix(line, "# BEGIN maintenance.bstein.dev usb-scratch"):
			inBlock = true
			continue
		case strings.HasPrefix(line, "# END maintenance.bstein.dev usb-scratch"):
			inBlock = false
			continue
		case !inBlock || line == "" || strings.HasPrefix(line, "#"):
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 4 {
			continue
		}
		source, target, fsType, options := fields[0], fields[1], fields[2], fields[3]
		if fsType == "none" && hasFstabOption(options, "bind") {
			cfg.BindTargets = append(cfg.BindTargets, target)
			continue
		}
		if fsType == "tmpfs" || strings.EqualFold(source, "tmpfs") {
			continue
		}
		cfg.Mountpoint = target
		cfg.FS = fsType
		if value, ok := strings.CutPrefix(source, "UUID="); ok {
			cfg.UUID = value
		}
		if value, ok := strings.CutPrefix(source, "LABEL="); ok {
			cfg.Label = value
		}
	}
	if cfg.Mountpoint == "" {
		return nil, false
	}
	return cfg, true
}

func hasFstabOption(options, want string) bool {
	for _, option := range strings.Split(options, ",") {
		if option == want {
			return true
		}
	}
	return false
}

func runAndTrim(cmd string, args ...string) string {
	out, err := commandOutput(cmd, args...)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func osRelease() string {
	out, err := commandOutput("cat", "/etc/os-release")
	if err != nil {
		return ""
	}
	for _, line := range strings.Split(string(out), "\n") {
		if strings.HasPrefix(line, "PRETTY_NAME=") {
			return strings.Trim(line[len("PRETTY_NAME="):], "\"")
		}
	}
	return ""
}

// pkgSample grabs a tiny subset of package versions to detect drift without collecting everything.
func pkgSample() map[string]string {
	names := []string{"containerd", "k3s", "nvidia-container-toolkit", "linux-image-raspi"}
	result := map[string]string{}
	for _, n := range names {
		v := pkgVersion(n)
		if v != "" {
			result[n] = v
		}
	}
	return result
}

func pkgVersion(name string) string {
	// Try dpkg-query first.
	out, err := commandOutput("dpkg-query", "-W", "-f", "${Version}", name)
	if err == nil && len(out) > 0 {
		return strings.TrimSpace(string(out))
	}
	// Fallback rpm.
	out, err = commandOutput("rpm", "-q", "--qf", "%{VERSION}-%{RELEASE}", name)
	if err == nil && len(out) > 0 {
		return strings.TrimSpace(string(out))
	}
	return ""
}

func mountInfo(target string) (string, string, bool) {
	target = strings.TrimSpace(target)
	if target == "" {
		return "", "", false
	}
	out, err := commandOutput("findmnt", "-P", "-n", "-T", target, "-o", "SOURCE,TARGET,FSTYPE")
	if err != nil {
		return "", "", false
	}
	fields := parseKeyValues(string(out))
	source := fields["SOURCE"]
	fsType := fields["FSTYPE"]
	return source, fsType, strings.TrimSpace(fields["TARGET"]) == target
}

func bindHealthy(target, source string) bool {
	target = strings.TrimSpace(target)
	source = strings.TrimSpace(source)
	if target == "" || source == "" {
		return false
	}

	targetReal := realPath(target)
	sourceReal := realPath(source)
	if sourceReal != "" && pathWithin(targetReal, sourceReal) {
		return true
	}

	mountSource, _, mounted := mountInfo(target)
	if !mounted {
		return false
	}
	if strings.TrimSpace(mountSource) == source {
		return true
	}

	rootSource, _, rootMounted := mountInfo(source)
	if !rootMounted {
		return false
	}
	device, subpath := splitFindmntSource(mountSource)
	return device != "" && device == rootSource && subpath == filepath.Clean(target)
}

func realPath(path string) string {
	path = strings.TrimSpace(path)
	if path == "" {
		return ""
	}
	out, err := commandOutput("readlink", "-f", path)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func pathWithin(path, root string) bool {
	path = filepath.Clean(strings.TrimSpace(path))
	root = filepath.Clean(strings.TrimSpace(root))
	if path == "" || root == "" || path == "." || root == "." {
		return false
	}
	return path == root || strings.HasPrefix(path, root+string(os.PathSeparator))
}

func splitFindmntSource(source string) (string, string) {
	source = strings.TrimSpace(source)
	if source == "" {
		return "", ""
	}
	start := strings.Index(source, "[")
	if start == -1 || !strings.HasSuffix(source, "]") {
		return source, ""
	}
	return source[:start], source[start+1 : len(source)-1]
}

func resolveDeviceByUUID(uuid string) string {
	uuid = strings.TrimSpace(uuid)
	if uuid == "" {
		return ""
	}
	return runAndTrim("blkid", "-U", uuid)
}

func resolveDeviceByLabel(label string) string {
	label = strings.TrimSpace(label)
	if label == "" {
		return ""
	}
	return runAndTrim("blkid", "-L", label)
}

func blkidExport(device string) map[string]string {
	device = strings.TrimSpace(device)
	if device == "" {
		return map[string]string{}
	}
	out, err := commandOutput("blkid", "-o", "export", device)
	if err != nil {
		return map[string]string{}
	}
	return parseKeyValues(string(out))
}

func parseKeyValues(out string) map[string]string {
	result := map[string]string{}
	for _, field := range strings.Fields(strings.TrimSpace(out)) {
		key, value, ok := strings.Cut(field, "=")
		if !ok {
			continue
		}
		result[key] = strings.Trim(value, `"`)
	}
	return result
}

func commandOutput(cmd string, args ...string) ([]byte, error) {
	if os.Getenv("METIS_SENTINEL_NSENTER") == "1" {
		nsenterArgs := []string{"-t", "1", "-m", "-u", "-n", "-i", "-p", "--", cmd}
		nsenterArgs = append(nsenterArgs, args...)
		return exec.Command("nsenter", nsenterArgs...).Output()
	}
	return exec.Command(cmd, args...).Output()
}
