package platform

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"
)

// InstallDisk describes a candidate disk for installation.
type InstallDisk struct {
	Device       string // e.g. /dev/sda
	Model        string
	Size         string   // human-readable, e.g. "500G"
	SizeBytes    int64    // raw byte count from lsblk
	MountedParts []string // partition mount points currently active
}

const squashfsPath = "/run/live/medium/live/filesystem.squashfs"

// ListInstallDisks returns block devices suitable for installation.
// Excludes the current live boot medium but includes USB drives.
func (s *System) ListInstallDisks() ([]InstallDisk, error) {
	out, err := exec.Command("lsblk", "-dn", "-o", "NAME,MODEL,SIZE,TYPE,TRAN").Output()
	if err != nil {
		return nil, fmt.Errorf("lsblk: %w", err)
	}

	bootDev := findLiveBootDevice()

	var disks []InstallDisk
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		fields := strings.Fields(line)
		// NAME MODEL SIZE TYPE TRAN  — model may have spaces so we parse from end
		if len(fields) < 4 {
			continue
		}
		// Last field: TRAN, second-to-last: TYPE, third-to-last: SIZE
		typ := fields[len(fields)-2]
		size := fields[len(fields)-3]
		name := fields[0]
		model := strings.Join(fields[1:len(fields)-3], " ")

		if typ != "disk" {
			continue
		}

		device := "/dev/" + name
		if device == bootDev {
			continue
		}

		sizeBytes := diskSizeBytes(device)
		mounted := mountedParts(device)

		disks = append(disks, InstallDisk{
			Device:       device,
			Model:        strings.TrimSpace(model),
			Size:         size,
			SizeBytes:    sizeBytes,
			MountedParts: mounted,
		})
	}
	return disks, nil
}

// diskSizeBytes returns the byte size of a block device using lsblk.
func diskSizeBytes(device string) int64 {
	out, err := exec.Command("lsblk", "-bdn", "-o", "SIZE", device).Output()
	if err != nil {
		return 0
	}
	n, _ := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64)
	return n
}

// mountedParts returns a list of "<part> at <mountpoint>" strings for any
// mounted partitions on the given device.
func mountedParts(device string) []string {
	out, err := exec.Command("lsblk", "-n", "-o", "NAME,MOUNTPOINT", device).Output()
	if err != nil {
		return nil
	}
	var result []string
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}
		mp := fields[1]
		if mp == "" || mp == "[SWAP]" {
			continue
		}
		result = append(result, "/dev/"+strings.TrimLeft(fields[0], "└─├─")+" at "+mp)
	}
	return result
}

// findLiveBootDevice returns the block device backing /run/live/medium (if any).
func findLiveBootDevice() string {
	out, err := exec.Command("findmnt", "-n", "-o", "SOURCE", "/run/live/medium").Output()
	if err != nil {
		return ""
	}
	src := strings.TrimSpace(string(out))
	if src == "" {
		return ""
	}
	// Strip partition suffix to get the whole disk device.
	// e.g. /dev/sdb1 → /dev/sdb,  /dev/nvme0n1p1 → /dev/nvme0n1
	out2, err := exec.Command("lsblk", "-no", "PKNAME", src).Output()
	if err != nil || strings.TrimSpace(string(out2)) == "" {
		return src
	}
	return "/dev/" + strings.TrimSpace(string(out2))
}

func mountSource(target string) string {
	out, err := exec.Command("findmnt", "-n", "-o", "SOURCE", target).Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func mountFSType(target string) string {
	out, err := exec.Command("findmnt", "-n", "-o", "FSTYPE", target).Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func blockDeviceType(device string) string {
	if strings.TrimSpace(device) == "" {
		return ""
	}
	out, err := exec.Command("lsblk", "-dn", "-o", "TYPE", device).Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func blockDeviceTransport(device string) string {
	if strings.TrimSpace(device) == "" {
		return ""
	}
	out, err := exec.Command("lsblk", "-dn", "-o", "TRAN", device).Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

func inferLiveBootKind(fsType, source, deviceType, transport string) string {
	switch {
	case strings.EqualFold(strings.TrimSpace(fsType), "tmpfs"):
		return "ram"
	case strings.EqualFold(strings.TrimSpace(deviceType), "rom"):
		return "cdrom"
	case strings.EqualFold(strings.TrimSpace(transport), "usb"):
		return "usb"
	case strings.HasPrefix(strings.TrimSpace(source), "/dev/sr"):
		return "cdrom"
	case strings.HasPrefix(strings.TrimSpace(source), "/dev/"):
		return "disk"
	default:
		return "unknown"
	}
}

// MinInstallBytes returns the minimum recommended disk size for installation:
// squashfs size × 1.5 to allow for extracted filesystem and bootloader.
// Returns 0 if the squashfs is not available (non-live environment).
func MinInstallBytes() int64 {
	fi, err := os.Stat(squashfsPath)
	if err != nil {
		return 0
	}
	return fi.Size() * 3 / 2
}

// toramActive returns true when the live system was booted with toram.
func toramActive() bool {
	data, err := os.ReadFile("/proc/cmdline")
	if err != nil {
		return false
	}
	return strings.Contains(string(data), "toram")
}

// freeMemBytes returns MemAvailable from /proc/meminfo.
func freeMemBytes() int64 {
	data, err := os.ReadFile("/proc/meminfo")
	if err != nil {
		return 0
	}
	for _, line := range strings.Split(string(data), "\n") {
		if strings.HasPrefix(line, "MemAvailable:") {
			fields := strings.Fields(line)
			if len(fields) >= 2 {
				n, _ := strconv.ParseInt(fields[1], 10, 64)
				return n * 1024 // kB → bytes
			}
		}
	}
	return 0
}

// DiskWarnings returns advisory warning strings for a disk candidate.
func DiskWarnings(d InstallDisk) []string {
	var w []string
	if len(d.MountedParts) > 0 {
		w = append(w, "has mounted partitions: "+strings.Join(d.MountedParts, ", "))
	}
	min := MinInstallBytes()
	if min > 0 && d.SizeBytes > 0 && d.SizeBytes < min {
		w = append(w, fmt.Sprintf("disk may be too small (need ≥ %s, have %s)",
			humanBytes(min), humanBytes(d.SizeBytes)))
	}
	if toramActive() {
		sqFi, err := os.Stat(squashfsPath)
		if err == nil {
			free := freeMemBytes()
			if free > 0 && free < sqFi.Size()*2 {
				w = append(w, "toram mode — low RAM, extraction may be slow or fail")
			}
		}
	}
	return w
}

func humanBytes(b int64) string {
	const unit = 1024
	if b < unit {
		return fmt.Sprintf("%d B", b)
	}
	div, exp := int64(unit), 0
	for n := b / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
}

// InstallToDisk runs bee-install <device> <logfile> and streams output to logFile.
// The context can be used to cancel.
func (s *System) InstallToDisk(ctx context.Context, device string, logFile string) error {
	cmd := exec.CommandContext(ctx, "bee-install", device, logFile)
	return cmd.Run()
}

// InstallLogPath returns the default install log path for a given device.
func InstallLogPath(device string) string {
	safe := strings.NewReplacer("/", "_", " ", "_").Replace(device)
	return "/tmp/bee-install" + safe + ".log"
}

// Label returns a display label for a disk.
func (d InstallDisk) Label() string {
	model := d.Model
	if model == "" {
		model = "Unknown"
	}
	return fmt.Sprintf("%s  %s  %s", d.Device, d.Size, model)
}
