package storage

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"

	"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
	"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
)

// HostOps is the narrow privileged host surface (slice 5 Phase B) — the ONE place the
// agent steps outside its Proxmox API token into OS-root. Production is *SudoHostOps
// (shells out via a sudoers allowlist, arg vectors, no shell); tests use a fake, so NO
// real root runs in the test suite.
//
// Every method validates its arguments (validate.go) before constructing a command. The
// surface is deliberately tiny: persistent mounts (systemd .mount units keyed by fs-UUID),
// detach (stop+disable the unit), SMART, and thin-pool metadata.
type HostOps interface {
	// EnsureMount writes + enables a systemd .mount unit for spec (idempotent: re-applying
	// an existing target is a no-op). Benign — additive, no signature.
	EnsureMount(ctx context.Context, spec MountSpec) error
	// Unmount stops + disables the .mount unit for a mountpoint (detach from service). This
	// is DESTRUCTIVE (deliberately removing a target) — the caller MUST have routed it
	// through the gate first; HostOps only performs an already-authorized op.
	Unmount(ctx context.Context, where string) error
	// SMART returns a parsed health summary for a raw block device, degrading to
	// {Health: UNKNOWN} when the device exposes no SMART (e.g. a USB-SATA bridge).
	SMART(ctx context.Context, device string) (hub.SmartSummary, error)
	// ThinPoolMetadata returns the lvmthin pool's metadata-used fraction (0..1) via lvs.
	// ok=false when it cannot be read (the field stays null in the report).
	ThinPoolMetadata(ctx context.Context, vg, pool string) (fraction float64, ok bool)
	// InspectDevice probes a block device for data-bearing evidence (filesystem signature,
	// partition table, partitions, mounted) — the AGENT-INTERNAL evidence the 8C classifier
	// uses, NEVER the caller's claim. Conservative: a failed/ambiguous probe → DataBearing()
	// true (fail-safe). This is the read that decides whether a format is benign or destructive.
	InspectDevice(ctx context.Context, device string) (DeviceProbe, error)
	// Format runs mkfs.<fstype> on a (validated) device. DESTRUCTIVE to whatever is on the
	// device — the caller MUST have classified it non-data-bearing AND/OR routed it through the
	// gate first; HostOps only performs an already-authorized format.
	Format(ctx context.Context, device, fstype string) error
}

// DeviceProbe is the result of inspecting a block device for data-bearing evidence (8C). The
// agent decides data-bearing-ness from THIS (its own device read), never from the caller's claim.
type DeviceProbe struct {
	Device            string `json:"device"`
	Probed            bool   `json:"probed"`              // false = the probe failed/was ambiguous → treat as data-bearing
	HasFilesystem     bool   `json:"has_filesystem"`      // a filesystem signature (blkid TYPE / USAGE)
	HasPartitionTable bool   `json:"has_partition_table"` // a partition table (blkid PTTYPE)
	HasPartitions     bool   `json:"has_partitions"`      // child partitions present (lsblk)
	Mounted           bool   `json:"mounted"`             // currently mounted somewhere
	FSType            string `json:"fstype,omitempty"`
}

// DataBearing is the conservative verdict: any signature / partition table / partition / mount —
// OR a probe that did not complete cleanly — makes the device data-bearing. Only a device that
// probed cleanly AND shows none of those is considered blank (benign to format).
func (p DeviceProbe) DataBearing() bool {
	if !p.Probed {
		return true // fail-safe: never call an unprobed device blank
	}
	return p.HasFilesystem || p.HasPartitionTable || p.HasPartitions || p.Mounted
}

// Reason returns a short human string for why the device is data-bearing (for the UI/audit).
func (p DeviceProbe) Reason() string {
	switch {
	case !p.Probed:
		return "device could not be reliably inspected"
	case p.Mounted:
		return "device is mounted"
	case p.HasFilesystem:
		return "device has a " + p.FSType + " filesystem"
	case p.HasPartitionTable:
		return "device has a partition table"
	case p.HasPartitions:
		return "device has partitions"
	default:
		return "device is blank"
	}
}

// MountSpec describes a persistent by-UUID mount.
type MountSpec struct {
	Name    string // storage name (unit Description only)
	UUID    string // filesystem UUID — validated; becomes What=/dev/disk/by-uuid/<UUID>
	Where   string // mountpoint — validated; the unit name is derived from it
	FSType  string // optional Type=
	Options string // optional Options=
}

// Binaries holds the absolute paths of the allow-listed binaries (overridable from config
// so the sudoers entries and the agent agree on exact paths).
type Binaries struct {
	Systemctl string
	Install   string
	Smartctl  string
	Lvs       string
	Blkid     string // device signature probe (8C data-bearing detection)
	Lsblk     string // partition/mount topology (8C)
	MkfsExt4  string // 8C format executor (ext4)
	MkfsXfs   string // 8C format executor (xfs)
}

func (b Binaries) withDefaults() Binaries {
	if b.Systemctl == "" {
		b.Systemctl = "/usr/bin/systemctl"
	}
	if b.Install == "" {
		b.Install = "/usr/bin/install"
	}
	if b.Smartctl == "" {
		b.Smartctl = "/usr/sbin/smartctl"
	}
	if b.Lvs == "" {
		b.Lvs = "/usr/sbin/lvs"
	}
	if b.Blkid == "" {
		b.Blkid = "/usr/sbin/blkid"
	}
	if b.Lsblk == "" {
		b.Lsblk = "/usr/bin/lsblk"
	}
	if b.MkfsExt4 == "" {
		b.MkfsExt4 = "/usr/sbin/mkfs.ext4"
	}
	if b.MkfsXfs == "" {
		b.MkfsXfs = "/usr/sbin/mkfs.xfs"
	}
	return b
}

// SudoHostOps is the production HostOps: it stages a unit file the agent owns, then uses
// the sudoers allowlist (`install` it into the unit dir, `systemctl` to manage it,
// `smartctl`/`lvs` to read). The Runner execs with an arg vector (no shell) — see
// proxmox.ExecRunner in RunnerSudo mode.
type SudoHostOps struct {
	runner   proxmox.Runner
	bins     Binaries
	unitDir  string // where enabled units live (e.g. /etc/systemd/system)
	stageDir string // agent-owned staging dir for unit files before install
	logger   *slog.Logger
}

// SudoHostOpsConfig configures a SudoHostOps.
type SudoHostOpsConfig struct {
	Runner   proxmox.Runner
	Bins     Binaries
	UnitDir  string // default /etc/systemd/system
	StageDir string // default <dataDir>/units; must be agent-writable
	Logger   *slog.Logger
}

// NewSudoHostOps builds the production privileged surface.
func NewSudoHostOps(cfg SudoHostOpsConfig) *SudoHostOps {
	unitDir := cfg.UnitDir
	if unitDir == "" {
		unitDir = "/etc/systemd/system"
	}
	stageDir := cfg.StageDir
	if stageDir == "" {
		stageDir = "/var/lib/felhom-agent/units"
	}
	logger := cfg.Logger
	if logger == nil {
		logger = slog.Default()
	}
	return &SudoHostOps{
		runner:   cfg.Runner,
		bins:     cfg.Bins.withDefaults(),
		unitDir:  unitDir,
		stageDir: stageDir,
		logger:   logger,
	}
}

// EnsureMount validates, renders, stages, installs and enables the .mount unit.
func (h *SudoHostOps) EnsureMount(ctx context.Context, spec MountSpec) error {
	// VALIDATE FIRST — refuse before constructing any command.
	if err := ValidateUUID(spec.UUID); err != nil {
		return err
	}
	if err := ValidateMountPath(spec.Where); err != nil {
		return err
	}
	if err := validateUnitOpt(spec.FSType); err != nil {
		return fmt.Errorf("storage: fstype: %w", err)
	}
	if err := validateUnitOpt(spec.Options); err != nil {
		return fmt.Errorf("storage: mount options: %w", err)
	}
	unitName, err := UnitNameForMount(spec.Where)
	if err != nil {
		return err
	}
	content := renderMountUnit(spec) // uses the validated fields only

	// Stage the unit file (agent-owned dir; no root needed for this write).
	if err := os.MkdirAll(h.stageDir, 0o700); err != nil {
		return fmt.Errorf("storage: staging dir: %w", err)
	}
	stagePath := filepath.Join(h.stageDir, unitName)
	if err := os.WriteFile(stagePath, []byte(content), 0o644); err != nil {
		return fmt.Errorf("storage: staging unit: %w", err)
	}

	dest := filepath.Join(h.unitDir, unitName)
	// install as root (atomic copy with fixed mode/owner) — fixed arg vector.
	if err := h.run(ctx, h.bins.Install, "-o", "root", "-g", "root", "-m", "0644", "--", stagePath, dest); err != nil {
		return fmt.Errorf("storage: installing unit %s: %w", unitName, err)
	}
	if err := h.run(ctx, h.bins.Systemctl, "daemon-reload"); err != nil {
		return fmt.Errorf("storage: daemon-reload: %w", err)
	}
	// enable --now both mounts now and persists across reboot. Idempotent.
	if err := h.run(ctx, h.bins.Systemctl, "enable", "--now", "--", unitName); err != nil {
		return fmt.Errorf("storage: enabling mount %s: %w", unitName, err)
	}
	h.logger.Info("storage: ensured mount", "name", spec.Name, "where", spec.Where, "unit", unitName)
	return nil
}

// Unmount stops + disables the unit (detach). The caller is responsible for authorization.
func (h *SudoHostOps) Unmount(ctx context.Context, where string) error {
	unitName, err := UnitNameForMount(where)
	if err != nil {
		return err
	}
	if err := h.run(ctx, h.bins.Systemctl, "stop", "--", unitName); err != nil {
		return fmt.Errorf("storage: stopping mount %s: %w", unitName, err)
	}
	if err := h.run(ctx, h.bins.Systemctl, "disable", "--", unitName); err != nil {
		return fmt.Errorf("storage: disabling mount %s: %w", unitName, err)
	}
	h.logger.Info("storage: unmounted (detached)", "where", where, "unit", unitName)
	return nil
}

// SMART runs `smartctl -a -j <device>` and parses the JSON.
func (h *SudoHostOps) SMART(ctx context.Context, device string) (hub.SmartSummary, error) {
	if err := ValidateSMARTDevice(device); err != nil {
		return hub.SmartSummary{Health: hub.SmartUnknown}, err
	}
	out, stderr, err := h.runner.Run(ctx, h.bins.Smartctl, "-a", "-j", device)
	if err != nil && len(out) == 0 {
		// smartctl uses a nonzero exit bitmask even on success; only treat empty output
		// as a hard failure. A device with no SMART → degrade to UNKNOWN, not an error.
		return hub.SmartSummary{Health: hub.SmartUnknown}, fmt.Errorf("storage: smartctl %s: %w: %s", device, err, trim(stderr))
	}
	return parseSMART(out), nil
}

// ThinPoolMetadata runs `lvs` for the pool and returns its metadata-used fraction.
func (h *SudoHostOps) ThinPoolMetadata(ctx context.Context, vg, pool string) (float64, bool) {
	if err := ValidateLVMName(vg); err != nil {
		h.logger.Warn("storage: refusing lvs on invalid vg", "vg", vg, "err", err)
		return 0, false
	}
	if err := ValidateLVMName(pool); err != nil {
		h.logger.Warn("storage: refusing lvs on invalid pool", "pool", pool, "err", err)
		return 0, false
	}
	// --reportformat json, metadata_percent for the specific LV. lv path "vg/pool".
	out, stderr, err := h.runner.Run(ctx, h.bins.Lvs, "--reportformat", "json", "--units", "b",
		"-o", "lv_name,data_percent,metadata_percent", "--", vg+"/"+pool)
	if err != nil && len(out) == 0 {
		h.logger.Warn("storage: lvs failed", "vg", vg, "pool", pool, "err", err, "stderr", trim(stderr))
		return 0, false
	}
	return parseThinPoolMetadata(out)
}

// InspectDevice probes a device for data-bearing evidence (8C). It runs `blkid -p -o export`
// (the reliable signature probe) for filesystem/partition-table signatures and `lsblk -J` for
// child partitions + mount state. The verdict defaults to data-bearing on ANY read failure
// (Probed=false), so a compromised caller cannot get a data-bearing device declared blank.
func (h *SudoHostOps) InspectDevice(ctx context.Context, device string) (DeviceProbe, error) {
	if err := ValidateBlockDevice(device); err != nil {
		return DeviceProbe{Device: device}, err // Probed=false → DataBearing()=true
	}
	probe := DeviceProbe{Device: device}

	// blkid -p -o export is the authoritative on-disk SIGNATURE probe. Its OUTPUT is the signal:
	// any TYPE/PTTYPE/USAGE line is positive data-bearing evidence. Its exit code is NOT relied
	// on (blkid exits 2 on a blank device) — output presence is what matters. A broken/empty
	// blkid simply adds no positive evidence; lsblk (below) is the read-success authority.
	bout, _, _ := h.runner.Run(ctx, h.bins.Blkid, "-p", "-o", "export", device)
	for k, v := range parseBlkidExport(bout) {
		switch k {
		case "TYPE":
			probe.HasFilesystem = true
			probe.FSType = v
		case "PTTYPE":
			probe.HasPartitionTable = true
		case "USAGE":
			if v != "" {
				probe.HasFilesystem = true // filesystem/raid/crypto member = data-bearing
			}
		}
	}

	// lsblk -J is the READ-SUCCESS authority + the partition/mount view. It exits 0 on any valid
	// device (blank or not), so a clean parse means the agent reliably read the device. If lsblk
	// fails, Probed stays false → DataBearing()=true (fail-safe — never call a device blank on a
	// failed read).
	lout, _, lerr := h.runner.Run(ctx, h.bins.Lsblk, "-J", "-o", "NAME,FSTYPE,PTTYPE,MOUNTPOINT", device)
	if lerr == nil {
		probe.Probed = true
		hasChildren, mounted, fstype, pttype := parseLsblkDevice(lout)
		if hasChildren {
			probe.HasPartitions = true
		}
		if mounted {
			probe.Mounted = true
		}
		if fstype != "" {
			probe.HasFilesystem = true
			if probe.FSType == "" {
				probe.FSType = fstype
			}
		}
		if pttype != "" {
			probe.HasPartitionTable = true
		}
	}
	return probe, nil
}

// Format runs mkfs.<fstype> on a validated device. The caller is responsible for authorization
// (8C: only after classifying the device non-data-bearing, or via a slice-10 operator signature).
func (h *SudoHostOps) Format(ctx context.Context, device, fstype string) error {
	if err := ValidateBlockDevice(device); err != nil {
		return err
	}
	if err := ValidateFSType(fstype); err != nil {
		return err
	}
	switch fstype {
	case "ext4":
		if err := h.run(ctx, h.bins.MkfsExt4, "-F", device); err != nil {
			return fmt.Errorf("storage: mkfs.ext4 %s: %w", device, err)
		}
	case "xfs":
		if err := h.run(ctx, h.bins.MkfsXfs, "-f", device); err != nil {
			return fmt.Errorf("storage: mkfs.xfs %s: %w", device, err)
		}
	default:
		return fmt.Errorf("storage: unsupported fstype %q", fstype) // unreachable after Validate
	}
	h.logger.Info("storage: formatted device", "device", device, "fstype", fstype)
	return nil
}

// run execs an allow-listed command with a fixed arg vector and wraps a nonzero exit.
func (h *SudoHostOps) run(ctx context.Context, name string, args ...string) error {
	_, stderr, err := h.runner.Run(ctx, name, args...)
	if err != nil {
		return fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, trim(stderr))
	}
	return nil
}

// validateUnitOpt rejects metacharacters / newlines in an optional unit value (FSType /
// Options) so a crafted value can't inject extra directives into the unit file. Empty is OK.
func validateUnitOpt(v string) error {
	if v == "" {
		return nil
	}
	if strings.ContainsAny(v, "\n\r\x00[]=") {
		return fmt.Errorf("storage: value %q contains forbidden characters", v)
	}
	return nil
}

func trim(b []byte) string {
	s := strings.TrimSpace(string(b))
	if len(s) > 300 {
		return s[:300] + "…"
	}
	return s
}

// parseBlkidExport parses `blkid -p -o export` output (KEY=value lines) into a map.
func parseBlkidExport(out []byte) map[string]string {
	m := map[string]string{}
	for _, line := range strings.Split(string(out), "\n") {
		line = strings.TrimSpace(line)
		if i := strings.IndexByte(line, '='); i > 0 {
			m[line[:i]] = line[i+1:]
		}
	}
	return m
}

// lsblkDevice mirrors the `lsblk -J` device shape (only the fields we read).
type lsblkDevice struct {
	Name       string        `json:"name"`
	FSType     string        `json:"fstype"`
	PTType     string        `json:"pttype"`
	MountPoint string        `json:"mountpoint"`
	Children   []lsblkDevice `json:"children"`
}

// parseLsblkDevice parses `lsblk -J -o NAME,FSTYPE,PTTYPE,MOUNTPOINT <device>` for the top device:
// whether it has child partitions, is mounted (itself or any child), and its fstype/pttype.
func parseLsblkDevice(out []byte) (hasChildren, mounted bool, fstype, pttype string) {
	var doc struct {
		BlockDevices []lsblkDevice `json:"blockdevices"`
	}
	if json.Unmarshal(out, &doc) != nil || len(doc.BlockDevices) == 0 {
		return false, false, "", ""
	}
	d := doc.BlockDevices[0]
	fstype, pttype = d.FSType, d.PTType
	hasChildren = len(d.Children) > 0
	mounted = d.MountPoint != ""
	for _, c := range d.Children {
		if c.MountPoint != "" {
			mounted = true
		}
	}
	return hasChildren, mounted, fstype, pttype
}

// NoopHostOps is the safe fallback when the privileged surface is unavailable or declined
// (a missing sudoers entry must degrade with a clear warning, not crash — slice notes). It
// reports SMART as UNKNOWN, no thin-pool metadata, and errors on any write (so a benign
// re-mount logs a clear failure rather than silently "succeeding").
type NoopHostOps struct{ Logger *slog.Logger }

func (n NoopHostOps) EnsureMount(context.Context, MountSpec) error {
	return fmt.Errorf("storage: privileged HostOps not configured; cannot mount")
}
func (n NoopHostOps) Unmount(context.Context, string) error {
	return fmt.Errorf("storage: privileged HostOps not configured; cannot unmount")
}
func (n NoopHostOps) SMART(context.Context, string) (hub.SmartSummary, error) {
	return hub.SmartSummary{Health: hub.SmartUnknown}, nil
}
func (n NoopHostOps) ThinPoolMetadata(context.Context, string, string) (float64, bool) {
	return 0, false
}
func (n NoopHostOps) InspectDevice(_ context.Context, device string) (DeviceProbe, error) {
	// Probed=false → DataBearing()=true: with no privileged surface we MUST NOT call any device
	// blank (fail-safe — a format would then be refused as destructive).
	return DeviceProbe{Device: device}, nil
}
func (n NoopHostOps) Format(context.Context, string, string) error {
	return fmt.Errorf("storage: privileged HostOps not configured; cannot format")
}
