package storage

import (
	"bufio"
	"os"
	"path/filepath"
	"strings"
)

// HostReader is the non-privileged host-read seam the observer and watchdog need. All of
// it is read-only and root-free: the active mount table, fs-UUID resolution via the
// /dev/disk/by-uuid symlinks, block-device presence, and the rotational/removable sysfs
// flags. Production is *ProcHostReader; tests inject a fake.
//
// Anything that needs root (smartctl, lvs, blkid) is NOT here — it lands on Phase B's
// privileged HostOps surface. Keep this seam root-free.
type HostReader interface {
	// Mounts parses the active mount table (/proc/mounts).
	Mounts() ([]Mount, error)
	// ResolveUUID returns the filesystem UUID of a block device, derived from the
	// /dev/disk/by-uuid symlinks. ok=false when the device has no by-uuid entry.
	ResolveUUID(device string) (uuid string, ok bool)
	// DeviceExists reports whether a block-device node is present (the fast USB-drop
	// signal for the watchdog).
	DeviceExists(device string) bool
	// Rotational reads the backing disk's rotational flag (true=HDD/slow, false=SSD/fast).
	// ok=false when it cannot be determined (network fs, missing sysfs, device-mapper).
	Rotational(device string) (rotational bool, ok bool)
	// Removable reads the backing disk's removable flag (true => a USB/hot-plug device).
	// ok=false when it cannot be determined.
	Removable(device string) (removable bool, ok bool)
}

// Mount is one active-mount-table entry.
type Mount struct {
	Device     string // e.g. "/dev/sdb1", "server:/export", "//server/share"
	MountPoint string
	FSType     string
}

// ProcHostReader is the production HostReader: it reads the host's /proc, /dev, and /sys.
// The paths are fields so tests CAN point it at fixtures, though most tests use a fully
// fake HostReader instead.
type ProcHostReader struct {
	ProcMounts string // default "/proc/mounts"
	ByUUIDDir  string // default "/dev/disk/by-uuid"
	SysClass   string // default "/sys/class/block"
}

// NewProcHostReader builds a ProcHostReader with the standard host paths.
func NewProcHostReader() *ProcHostReader {
	return &ProcHostReader{
		ProcMounts: "/proc/mounts",
		ByUUIDDir:  "/dev/disk/by-uuid",
		SysClass:   "/sys/class/block",
	}
}

// Mounts parses /proc/mounts. The format is space-separated, octal-escaped fields:
// device mountpoint fstype options dump pass. We unescape the first three.
func (r *ProcHostReader) Mounts() ([]Mount, error) {
	f, err := os.Open(r.procMounts())
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var out []Mount
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		fields := strings.Fields(sc.Text())
		if len(fields) < 3 {
			continue
		}
		out = append(out, Mount{
			Device:     unescapeMount(fields[0]),
			MountPoint: unescapeMount(fields[1]),
			FSType:     fields[2],
		})
	}
	return out, sc.Err()
}

// ResolveUUID reverse-maps a device path to its fs-UUID by reading the /dev/disk/by-uuid
// symlinks and matching the canonical target of each against the device.
func (r *ProcHostReader) ResolveUUID(device string) (string, bool) {
	if device == "" {
		return "", false
	}
	want := canonPath(device)
	entries, err := os.ReadDir(r.byUUIDDir())
	if err != nil {
		return "", false
	}
	for _, e := range entries {
		link := filepath.Join(r.byUUIDDir(), e.Name())
		target, err := os.Readlink(link)
		if err != nil {
			continue
		}
		if !filepath.IsAbs(target) {
			target = filepath.Join(r.byUUIDDir(), target)
		}
		if canonPath(target) == want {
			return e.Name(), true
		}
	}
	return "", false
}

// DeviceExists stats the device node (after resolving symlinks like /dev/disk/by-uuid/X).
func (r *ProcHostReader) DeviceExists(device string) bool {
	if device == "" {
		return false
	}
	_, err := os.Stat(device)
	return err == nil
}

// Rotational reads /sys/block/<parent-disk>/queue/rotational for the device's backing
// disk. "1" => rotational (HDD/slow), "0" => SSD/fast.
func (r *ProcHostReader) Rotational(device string) (bool, bool) {
	disk, ok := r.parentDisk(device)
	if !ok {
		return false, false
	}
	b, err := os.ReadFile(filepath.Join(r.sysBlockDir(), disk, "queue", "rotational"))
	if err != nil {
		return false, false
	}
	switch strings.TrimSpace(string(b)) {
	case "1":
		return true, true
	case "0":
		return false, true
	}
	return false, false
}

// Removable reads /sys/block/<parent-disk>/removable. "1" => removable (USB/hot-plug).
func (r *ProcHostReader) Removable(device string) (bool, bool) {
	disk, ok := r.parentDisk(device)
	if !ok {
		return false, false
	}
	b, err := os.ReadFile(filepath.Join(r.sysBlockDir(), disk, "removable"))
	if err != nil {
		return false, false
	}
	switch strings.TrimSpace(string(b)) {
	case "1":
		return true, true
	case "0":
		return false, true
	}
	return false, false
}

// parentDisk maps a device path (possibly a partition like /dev/sdb1 or /dev/nvme0n1p2)
// to its parent disk's sysfs name (sdb / nvme0n1). It uses /sys/class/block/<name>, whose
// real path ends in .../<disk>/<partition> for a partition and .../<disk> for a whole disk.
func (r *ProcHostReader) parentDisk(device string) (string, bool) {
	name := filepath.Base(strings.TrimSpace(device))
	if name == "" || name == "." || name == "/" {
		return "", false
	}
	real, err := filepath.EvalSymlinks(filepath.Join(r.sysClass(), name))
	if err != nil {
		return "", false
	}
	// If <name> is a partition, /sys/class/block/<name>/partition exists and its parent
	// directory is the disk. Otherwise <name> IS the disk.
	if _, err := os.Stat(filepath.Join(real, "partition")); err == nil {
		return filepath.Base(filepath.Dir(real)), true
	}
	return filepath.Base(real), true
}

// sysBlockDir derives /sys/block from the configured /sys/class/block.
func (r *ProcHostReader) sysBlockDir() string {
	return filepath.Join(filepath.Dir(filepath.Dir(r.sysClass())), "block")
}

func (r *ProcHostReader) procMounts() string {
	if r.ProcMounts != "" {
		return r.ProcMounts
	}
	return "/proc/mounts"
}
func (r *ProcHostReader) byUUIDDir() string {
	if r.ByUUIDDir != "" {
		return r.ByUUIDDir
	}
	return "/dev/disk/by-uuid"
}
func (r *ProcHostReader) sysClass() string {
	if r.SysClass != "" {
		return r.SysClass
	}
	return "/sys/class/block"
}

// canonPath resolves symlinks for a best-effort canonical comparison, falling back to the
// cleaned path when the target can't be resolved (e.g. the device just disappeared).
func canonPath(p string) string {
	if real, err := filepath.EvalSymlinks(p); err == nil {
		return real
	}
	return filepath.Clean(p)
}

// unescapeMount decodes the octal \040-style escapes /proc/mounts uses for spaces, tabs,
// newlines and backslashes in the device/mountpoint fields.
func unescapeMount(s string) string {
	if !strings.Contains(s, `\`) {
		return s
	}
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		if s[i] == '\\' && i+3 < len(s) && isOctal(s[i+1]) && isOctal(s[i+2]) && isOctal(s[i+3]) {
			v := (int(s[i+1]-'0') << 6) | (int(s[i+2]-'0') << 3) | int(s[i+3]-'0')
			b.WriteByte(byte(v))
			i += 3
			continue
		}
		b.WriteByte(s[i])
	}
	return b.String()
}

func isOctal(c byte) bool { return c >= '0' && c <= '7' }
