//go:build linux

package system

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
	"time"
)

// IsMountPoint checks if a path is on a different device than its parent.
// Returns true if the path is a mount point (different device ID from parent).
func IsMountPoint(path string) bool {
	var pathStat, parentStat syscall.Stat_t
	if err := syscall.Stat(path, &pathStat); err != nil {
		return false
	}
	parent := filepath.Dir(path)
	if err := syscall.Stat(parent, &parentStat); err != nil {
		return false
	}
	return pathStat.Dev != parentStat.Dev
}

// IsWritable checks if the given path is writable by attempting to create+remove a temp file.
func IsWritable(path string) bool {
	testFile := filepath.Join(path, ".felhom-write-test")
	f, err := os.Create(testFile)
	if err != nil {
		return false
	}
	f.Close()
	os.Remove(testFile)
	return true
}

// PathsOverlap returns true if one path is a parent or child of the other.
func PathsOverlap(a, b string) bool {
	a = filepath.Clean(a)
	b = filepath.Clean(b)
	if a == b {
		return true
	}
	aSep := a + string(os.PathSeparator)
	bSep := b + string(os.PathSeparator)
	return strings.HasPrefix(aSep, bSep) || strings.HasPrefix(bSep, aSep)
}

// DiskUsageInfo holds disk usage statistics for a path.
type DiskUsageInfo struct {
	TotalGB     float64
	UsedGB      float64
	AvailGB     float64
	UsedPercent float64
	TotalHuman  string
	UsedHuman   string
}

// GetDiskUsage returns disk usage info for a path, or nil on error.
func GetDiskUsage(path string) *DiskUsageInfo {
	var stat syscall.Statfs_t
	if err := syscall.Statfs(path, &stat); err != nil {
		debugf("[DEBUG] [system] GetDiskUsage: statfs(%q) failed: %v", path, err)
		return nil
	}

	bsize := uint64(stat.Bsize)
	total := stat.Blocks * bsize
	avail := stat.Bavail * bsize
	used := total - (stat.Bfree * bsize)

	const gb = 1024 * 1024 * 1024
	info := &DiskUsageInfo{
		TotalGB: float64(total) / float64(gb),
		UsedGB:  float64(used) / float64(gb),
		AvailGB: float64(avail) / float64(gb),
	}
	if total > 0 {
		info.UsedPercent = float64(used) / float64(total) * 100
	}
	info.TotalHuman = formatGB(info.TotalGB)
	info.UsedHuman = formatGB(info.UsedGB)
	debugf("[DEBUG] [system] GetDiskUsage: path=%q total=%s used=%s avail=%.1fGB (%.1f%%)",
		path, info.TotalHuman, info.UsedHuman, info.AvailGB, info.UsedPercent)
	return info
}

func formatGB(gb float64) string {
	if gb >= 1000 {
		return fmt.Sprintf("%.1f TB", gb/1024)
	}
	return fmt.Sprintf("%.1f GB", gb)
}

// FSInfo holds filesystem type, device, and disk model info.
type FSInfo struct {
	FSType string // "ext4", "btrfs"
	Device string // "/dev/sda1"
	Model  string // "WD Elements 25A2" (best-effort from sysfs)
}

// GetFSInfo returns filesystem info for a path using findmnt, or nil on error.
func GetFSInfo(path string) *FSInfo {
	out, err := exec.Command("findmnt", "-n", "-o", "SOURCE,FSTYPE", "--target", path).Output()
	if err != nil {
		debugf("[DEBUG] [system] GetFSInfo: findmnt(%q) failed: %v", path, err)
		return nil
	}
	fields := strings.Fields(strings.TrimSpace(string(out)))
	if len(fields) < 2 {
		debugf("[DEBUG] [system] GetFSInfo: findmnt(%q) returned unexpected output: %q", path, strings.TrimSpace(string(out)))
		return nil
	}
	info := &FSInfo{
		Device: fields[0],
		FSType: fields[1],
	}
	// Try to get disk model from sysfs
	info.Model = diskModel(info.Device)
	debugf("[DEBUG] [system] GetFSInfo: path=%q device=%s fstype=%s model=%q", path, info.Device, info.FSType, info.Model)
	return info
}

// DestinationHealth holds the result of a tiered backup destination check.
type DestinationHealth struct {
	Exists      bool
	Writable    bool
	MountPoint  bool    // true if path is on a different device from its parent
	SystemDrive bool    // true if path is on the same device as /
	UsedPercent float64 // disk usage percentage (0 if unknown)
	FreeGB      float64
	Warning     string // human-readable warning message in Hungarian (empty = ok)
	Blocked     bool   // if true, backup must not run
	Severity    string // "ok", "warning", "critical"
}

// CheckBackupDestination performs tiered validation of a cross-drive backup destination.
// Returns a DestinationHealth describing any issues found.
func CheckBackupDestination(path string) DestinationHealth {
	debugf("[DEBUG] [system] CheckBackupDestination: path=%q", path)
	h := DestinationHealth{Severity: "ok"}

	// Tier 1: path must exist
	if _, err := os.Stat(path); os.IsNotExist(err) {
		h.Warning = "A cél tárhely (" + path + ") nem létezik!"
		h.Blocked = true
		h.Severity = "critical"
		log.Printf("[WARN] [system] Backup destination %s is not safe: path does not exist", path)
		debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier1 FAIL (not exists)", path)
		return h
	}
	h.Exists = true

	// Tier 2: path must be writable
	if !IsWritable(path) {
		h.Warning = "A cél tárhely (" + path + ") nem írható! Ellenőrizd a jogosultságokat."
		h.Blocked = true
		h.Severity = "critical"
		log.Printf("[WARN] [system] Backup destination %s is not safe: not writable", path)
		debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier2 FAIL (not writable)", path)
		return h
	}
	h.Writable = true

	// Tier 3: detect if source and destination are on the same block device
	// (stronger than IsMountPoint — catches e.g. bind mounts within same device)
	if isSameBlockDevice(path, "/") {
		h.SystemDrive = true
		// This is a warning, not a block — user data still protected against software errors
		h.Warning = "A cél tárhely (" + path + ") a rendszermeghajtón van. " +
			"Meghajtóhiba esetén az eredeti adat és a mentés is elveszhet. " +
			"Külső meghajtó használata javasolt."
		h.Severity = "warning"
		debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier3 WARN (same block device as /)", path)
		// Don't return early — also check disk usage
	} else {
		h.MountPoint = true
		debugf("[DEBUG] [system] CheckBackupDestination: path=%q — tier3 OK (different block device)", path)
	}

	// Tier 4: disk usage checks
	if di := GetDiskUsage(path); di != nil {
		h.UsedPercent = di.UsedPercent
		h.FreeGB = di.AvailGB
		if h.SystemDrive {
			// System drive: stricter limits to protect OS stability
			if di.AvailGB < 10 {
				h.Warning = fmt.Sprintf("A rendszermeghajtón csak %.1f GB szabad — legalább 10 GB szükséges a rendszer stabilitásához!", di.AvailGB)
				h.Blocked = true
				h.Severity = "critical"
				log.Printf("[WARN] [system] Backup destination %s is not safe: system drive low space (%.1f GB free)", path, di.AvailGB)
			} else if di.UsedPercent >= 90 {
				h.Warning = fmt.Sprintf("A rendszermeghajtó %.0f%%-ban megtelt — maximum 90%% megengedett.", di.UsedPercent)
				h.Blocked = true
				h.Severity = "critical"
				log.Printf("[WARN] [system] Backup destination %s is not safe: system drive %.0f%% full", path, di.UsedPercent)
			}
			// If neither triggers, keep the Tier 3 system-drive warning
		} else {
			// External drive: original thresholds
			if di.UsedPercent >= 95 {
				h.Warning = fmt.Sprintf("A mentési meghajtó megtelt (%.0f%% használt)!", di.UsedPercent)
				h.Blocked = true
				h.Severity = "critical"
				log.Printf("[WARN] [system] Backup destination %s is not safe: drive %.0f%% full", path, di.UsedPercent)
			} else if di.UsedPercent >= 90 {
				h.Warning = fmt.Sprintf("A mentési meghajtó majdnem megtelt (%.0f%% használt).", di.UsedPercent)
				h.Severity = "warning"
			}
		}
	}

	debugf("[DEBUG] [system] CheckBackupDestination: path=%q — result: severity=%s blocked=%v freeGB=%.1f usedPct=%.1f%%",
		path, h.Severity, h.Blocked, h.FreeGB, h.UsedPercent)
	return h
}

// isSameBlockDevice returns true if pathA and pathB are on the same block device.
func isSameBlockDevice(pathA, pathB string) bool {
	var statA, statB syscall.Stat_t
	if err := syscall.Stat(pathA, &statA); err != nil {
		return false
	}
	if err := syscall.Stat(pathB, &statB); err != nil {
		return false
	}
	return statA.Dev == statB.Dev
}

// SamePhysicalDevice reports whether two paths resolve to the same block device. Used by the Tier 2
// off-drive guard to refuse copying an app's backup onto the same physical disk as its source (the
// whole point of Tier 2 is to survive that disk failing). Returns false if either path can't be
// stat'd (fail-open to "different" would be unsafe, so callers must also verify the dest separately —
// but in practice an unstattable path fails earlier). NOTE: this is mount/device-granularity; two
// partitions on one physical disk look "different" here — the agent's durable-id is the stronger
// guarantee for that case, but for the felhom layout (external drive vs system rootfs) this suffices.
func SamePhysicalDevice(a, b string) bool {
	return isSameBlockDevice(a, b)
}

// stripPartition strips the partition suffix from a device name.
// e.g., "sda1" → "sda", "nvme0n1p1" → "nvme0n1", "mmcblk0p1" → "mmcblk0".
func stripPartition(base string) string {
	if strings.HasPrefix(base, "nvme") || strings.HasPrefix(base, "mmcblk") {
		if idx := strings.LastIndex(base, "p"); idx > 4 {
			return base[:idx]
		}
	} else {
		return strings.TrimRight(base, "0123456789")
	}
	return base
}

// diskModel reads the disk model from /sys/block/<dev>/device/model.
func diskModel(device string) string {
	// Strip bind-mount subdir suffix (e.g., "/dev/sdb1[/felhom_data]" → "/dev/sdb1")
	if idx := strings.IndexByte(device, '['); idx >= 0 {
		device = device[:idx]
	}
	disk := stripPartition(filepath.Base(device))
	modelPath := "/sys/block/" + disk + "/device/model"
	data, err := os.ReadFile(modelPath)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(data))
}

// ProbeStatus represents the result of a storage path probe.
type ProbeStatus int

const (
	ProbeConnected    ProbeStatus = iota
	ProbeDisconnected
	ProbeTimeout
)

// ProbeResult holds the outcome of a storage path probe.
type ProbeResult struct {
	Status ProbeStatus
	Err    error
}

// ProbeStoragePath checks if a storage path is responsive.
// Uses a goroutine with a 3-second timeout to avoid blocking on dead mounts.
func ProbeStoragePath(path string) ProbeResult {
	start := time.Now()

	// Quick check: does the path exist at all?
	if _, err := os.Lstat(path); os.IsNotExist(err) {
		log.Printf("[WARN] [system] Storage path %s probe failed: %v", path, err)
		debugf("[DEBUG] [system] ProbeStoragePath: path=%q — not exists (%s)", path, time.Since(start).Round(time.Millisecond))
		return ProbeResult{Status: ProbeDisconnected, Err: err}
	}

	type statResult struct {
		err error
	}
	ch := make(chan statResult, 1)
	go func() {
		var stat syscall.Statfs_t
		err := syscall.Statfs(path, &stat)
		ch <- statResult{err: err}
	}()

	select {
	case res := <-ch:
		elapsed := time.Since(start).Round(time.Millisecond)
		if res.err == nil {
			debugf("[DEBUG] [system] ProbeStoragePath: path=%q — connected (%s)", path, elapsed)
			return ProbeResult{Status: ProbeConnected}
		}
		errStr := res.err.Error()
		if strings.Contains(errStr, "transport endpoint") ||
			strings.Contains(errStr, "input/output error") ||
			strings.Contains(errStr, "no such device") {
			log.Printf("[WARN] [system] Storage path %s probe failed: %v", path, res.err)
			debugf("[DEBUG] [system] ProbeStoragePath: path=%q — disconnected: %v (%s)", path, res.err, elapsed)
			return ProbeResult{Status: ProbeDisconnected, Err: res.err}
		}
		log.Printf("[WARN] [system] Storage path %s probe failed: %v", path, res.err)
		debugf("[DEBUG] [system] ProbeStoragePath: path=%q — disconnected (other error): %v (%s)", path, res.err, elapsed)
		return ProbeResult{Status: ProbeDisconnected, Err: res.err}
	case <-time.After(3 * time.Second):
		log.Printf("[WARN] [system] Storage path %s probe failed: stat timed out after 3s", path)
		debugf("[DEBUG] [system] ProbeStoragePath: path=%q — TIMEOUT (3s)", path)
		return ProbeResult{Status: ProbeTimeout, Err: fmt.Errorf("stat timed out after 3s")}
	}
}

// IsUSBDevice checks if a block device is connected via USB.
// devicePath should be like "/dev/sdb" or "/dev/sdb1".
// Also handles bind-mount suffixes from findmnt (e.g., "/dev/sdb1[/felhom_data]").
// Checks the sysfs symlink for the disk — if the path contains "/usb", it's a USB device.
func IsUSBDevice(devicePath string) bool {
	// Strip bind-mount subdir suffix (e.g., "/dev/sdb1[/felhom_data]" → "/dev/sdb1")
	if idx := strings.IndexByte(devicePath, '['); idx >= 0 {
		devicePath = devicePath[:idx]
	}
	disk := stripPartition(filepath.Base(devicePath))
	if disk == "" {
		return false
	}
	// Try /host/sys first (Docker mount), then /sys (native)
	for _, prefix := range []string{"/host/sys", "/sys"} {
		link, err := os.Readlink(prefix + "/block/" + disk)
		if err != nil {
			continue
		}
		isUSB := strings.Contains(link, "/usb")
		debugf("[DEBUG] [system] IsUSBDevice: device=%q disk=%q sysfs=%s → usb=%v", devicePath, disk, link, isUSB)
		return isUSB
	}
	debugf("[DEBUG] [system] IsUSBDevice: device=%q disk=%q — no sysfs entry found", devicePath, disk)
	return false
}

// ParseFstabUUID extracts the UUID for a given mount point from an fstab file.
// Returns empty string if the mount point is not found or has no UUID.
func ParseFstabUUID(fstabPath, mountPath string) string {
	f, err := os.Open(fstabPath)
	if err != nil {
		return ""
	}
	defer f.Close()

	cleanMount := filepath.Clean(mountPath)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}
		if filepath.Clean(fields[1]) != cleanMount {
			continue
		}
		source := fields[0]
		if strings.HasPrefix(source, "UUID=") {
			return strings.TrimPrefix(source, "UUID=")
		}
	}
	return ""
}

// HasFelhomRawMount checks if a mount path was set up via the attach wizard
// (which uses a raw mount at /mnt/.felhom-raw/<x> + a bind mount).
// Returns the raw mount path if found, e.g., "/mnt/.felhom-raw/hdd_1".
func HasFelhomRawMount(fstabPath, mountPath string) (rawPath string, ok bool) {
	f, err := os.Open(fstabPath)
	if err != nil {
		return "", false
	}
	defer f.Close()

	cleanMount := filepath.Clean(mountPath)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) < 4 {
			continue
		}
		// Look for a bind mount line targeting our mount path
		// Format: /mnt/.felhom-raw/hdd_1/subfolder  /mnt/hdd_1  none  bind,...
		target := filepath.Clean(fields[1])
		if target != cleanMount {
			continue
		}
		source := fields[0]
		if !strings.Contains(source, ".felhom-raw") {
			continue
		}
		// Extract the raw mount path: /mnt/.felhom-raw/hdd_1/subfolder → /mnt/.felhom-raw/hdd_1
		// The raw mount is the first two components after /mnt/.felhom-raw/
		parts := strings.Split(filepath.Clean(source), string(os.PathSeparator))
		// parts: ["", "mnt", ".felhom-raw", "hdd_1", "subfolder"]
		for i, p := range parts {
			if p == ".felhom-raw" && i+1 < len(parts) {
				rawPath = string(os.PathSeparator) + filepath.Join(parts[1:i+2]...)
				return rawPath, true
			}
		}
	}
	return "", false
}
