package storage

import (
	"fmt"
	"regexp"
	"strings"
)

// This file is the security boundary for the privileged host surface (slice 5 Phase B).
// EVERY argument that will reach a root shell-out is validated HERE, before any command
// is constructed — the SudoHostOps methods refuse on a validation error and never build an
// arg vector, let alone exec. The adversarial matrix in validate_test.go is the proof that
// the "aggressive write side" is not a loose one: shell metacharacters, path traversal, and
// malformed inputs are rejected up front. Combined with arg-vector exec (never a shell
// string), a validated input cannot inject.

var (
	// fs-UUIDs: ext/xfs are 8-4-4-4-12 lowercase hex; FAT/vFAT are "XXXX-XXXX" (upper
	// hex); others vary. Accept hex groups joined by single hyphens, length-bounded.
	// This rejects '/', '.', whitespace, and every shell metacharacter by construction.
	reUUID = regexp.MustCompile(`^[A-Fa-f0-9]{4,}(-[A-Fa-f0-9]+){0,4}$`)

	// SMART device: a strict whitelist of real block-disk patterns under /dev. No
	// /dev/disk/by-* symlinks, no device-mapper, no traversal — just the raw disks
	// smartctl is run against. Anything else is refused.
	reSMARTDevice = regexp.MustCompile(`^/dev/(sd[a-z]+|nvme[0-9]+n[0-9]+|hd[a-z]+|vd[a-z]+)$`)

	// Block device for inspection / mkfs: a raw disk OR a partition under /dev. Like the SMART
	// whitelist but also allows the trailing partition number (sda1, nvme0n1p2, vdb3). No
	// by-* symlinks, no device-mapper, no traversal. This is the mkfs/inspect target — it is
	// validated AND the agent device-inspects it before any destructive decision (8C).
	reBlockDevice = regexp.MustCompile(`^/dev/(sd[a-z]+[0-9]*|nvme[0-9]+n[0-9]+(p[0-9]+)?|hd[a-z]+[0-9]*|vd[a-z]+[0-9]*)$`)

	// Filesystem types the agent will mkfs. Deliberately tiny — the sudoers mkfs entries are
	// per-fstype binaries (mkfs.ext4 / mkfs.xfs), so this set MUST match those entries.
	reFSType = regexp.MustCompile(`^(ext4|xfs)$`)

	// LVM VG / pool names: LVM permits [A-Za-z0-9._+-]; we forbid leading '-' (would look
	// like a flag) and cap the length.
	reLVMName = regexp.MustCompile(`^[A-Za-z0-9_+.][A-Za-z0-9_+.-]*$`)

	// A single safe path segment (for mountpoint validation). No metacharacters; "." and
	// ".." are rejected separately as traversal.
	rePathSegment = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
)

const (
	maxUUIDLen  = 40
	maxPathLen  = 255
	maxLVMLen   = 128
	byUUIDDir   = "/dev/disk/by-uuid"
	maxMountSeg = 32 // a sane cap on mountpoint depth
)

// ValidateUUID accepts a filesystem UUID for use in a by-uuid device path. It is the
// load-bearing check (the UUID is the DR re-attach key AND a shell-out argument).
func ValidateUUID(uuid string) error {
	if uuid == "" {
		return fmt.Errorf("storage: empty UUID")
	}
	if len(uuid) > maxUUIDLen {
		return fmt.Errorf("storage: UUID too long (%d > %d)", len(uuid), maxUUIDLen)
	}
	if !reUUID.MatchString(uuid) {
		return fmt.Errorf("storage: invalid UUID %q (want hex groups, no metacharacters)", uuid)
	}
	return nil
}

// ByUUIDDevicePath returns the validated /dev/disk/by-uuid/<uuid> path for a mount unit's
// What=. Device paths for mounting are ALWAYS confined to this directory — we never accept
// an arbitrary device path from any source.
func ByUUIDDevicePath(uuid string) (string, error) {
	if err := ValidateUUID(uuid); err != nil {
		return "", err
	}
	return byUUIDDir + "/" + uuid, nil
}

// ValidateMountPath accepts an absolute mountpoint with no traversal and no metacharacters.
// Each segment must be a safe token; "." / ".." segments are rejected; the bare root "/"
// is rejected (we never manage a mount at root).
func ValidateMountPath(path string) error {
	if path == "" || path[0] != '/' {
		return fmt.Errorf("storage: mount path must be absolute, got %q", path)
	}
	if len(path) > maxPathLen {
		return fmt.Errorf("storage: mount path too long (%d > %d)", len(path), maxPathLen)
	}
	if strings.ContainsAny(path, "\x00\n\r\t") {
		return fmt.Errorf("storage: mount path contains control characters")
	}
	segs := nonEmptySegments(path)
	if len(segs) == 0 {
		return fmt.Errorf("storage: refusing to manage a mount at %q", path)
	}
	if len(segs) > maxMountSeg {
		return fmt.Errorf("storage: mount path too deep")
	}
	for _, s := range segs {
		if s == "." || s == ".." {
			return fmt.Errorf("storage: mount path traversal segment %q in %q", s, path)
		}
		if !rePathSegment.MatchString(s) {
			return fmt.Errorf("storage: invalid mount path segment %q in %q", s, path)
		}
	}
	return nil
}

// ValidateSMARTDevice accepts only a raw block-disk path (sdX/nvmeXnY/hdX/vdX) under /dev.
func ValidateSMARTDevice(device string) error {
	if !reSMARTDevice.MatchString(device) {
		return fmt.Errorf("storage: refusing smartctl on non-whitelisted device %q", device)
	}
	return nil
}

// ValidateBlockDevice accepts only a raw disk or partition path under /dev (the mkfs / inspect
// target). The same strict-whitelist discipline as ValidateSMARTDevice: no symlinks, no
// device-mapper, no traversal — refused before any command is built.
func ValidateBlockDevice(device string) error {
	if !reBlockDevice.MatchString(device) {
		return fmt.Errorf("storage: refusing to operate on non-whitelisted block device %q", device)
	}
	return nil
}

// ValidateFSType accepts only a filesystem type the agent is configured to mkfs (ext4|xfs). The
// set MUST match the per-fstype sudoers entries.
func ValidateFSType(fstype string) error {
	if !reFSType.MatchString(fstype) {
		return fmt.Errorf("storage: unsupported filesystem type %q (want ext4|xfs)", fstype)
	}
	return nil
}

// ValidateLVMName accepts an LVM VG or LV (pool) name.
func ValidateLVMName(name string) error {
	if name == "" {
		return fmt.Errorf("storage: empty LVM name")
	}
	if len(name) > maxLVMLen {
		return fmt.Errorf("storage: LVM name too long")
	}
	if !reLVMName.MatchString(name) {
		return fmt.Errorf("storage: invalid LVM name %q", name)
	}
	return nil
}

// UnitNameForMount returns the systemd .mount unit name for a (validated) mountpoint. A
// .mount unit's name MUST be the systemd-escaped mountpoint — this is computed
// deterministically from the already-validated path, so the result is inherently safe to
// pass in an arg vector (no shell).
func UnitNameForMount(where string) (string, error) {
	if err := ValidateMountPath(where); err != nil {
		return "", err
	}
	return systemdEscapePath(where) + ".mount", nil
}

// nonEmptySegments splits a path on '/', dropping empties (so "//a///b/" → [a b]).
func nonEmptySegments(path string) []string {
	parts := strings.Split(path, "/")
	out := parts[:0]
	for _, p := range parts {
		if p != "" {
			out = append(out, p)
		}
	}
	return out
}

// systemdEscapePath replicates `systemd-escape --path`: strip leading/trailing slashes and
// collapse internal repeats, then escape each char — '/' → '-', alnum/'_' kept, '.' kept
// (except a leading '.'), everything else (including a literal '-') → '\xNN'. The empty
// path / "/" escapes to "-". Computed in-process so no `systemd-escape` shell-out / sudoers
// entry is needed.
func systemdEscapePath(path string) string {
	segs := nonEmptySegments(path)
	if len(segs) == 0 {
		return "-"
	}
	joined := strings.Join(segs, "/")
	var b strings.Builder
	for i := 0; i < len(joined); i++ {
		c := joined[i]
		switch {
		case c == '/':
			b.WriteByte('-')
		case i == 0 && c == '.':
			b.WriteString(`\x2e`)
		case isAlnum(c) || c == '_':
			b.WriteByte(c)
		case c == '.':
			b.WriteByte('.')
		default:
			fmt.Fprintf(&b, `\x%02x`, c)
		}
	}
	return b.String()
}

func isAlnum(c byte) bool {
	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
}
