package storage

import (
	"context"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

// CandidateDisk is one host block device that the Impl-1 unclaimed filter proved is FREE for Felhom to
// enroll (Impl-2a discovery). The controller wizard (Impl-2b) renders these; enrollment then formats
// (guarded, Impl-1) and/or mounts + binds. Blank disks are "initialize"-only; disks already carrying a
// mountable FS are ALSO "attach" candidates.
type CandidateDisk struct {
	Device      string `json:"device"`       // whole disk, e.g. /dev/sdd
	SizeBytes   int64  `json:"size_bytes"`   // 0 when unreadable
	Model       string `json:"model,omitempty"`
	FSType      string `json:"fstype,omitempty"`      // first filesystem found ("" = blank)
	DataBearing bool   `json:"data_bearing"`          // has any FS / partition (→ wipe-confirm downstream)
	Mountable   bool   `json:"mountable"`             // carries an ext4/xfs FS the agent can mount as-is
	MountSource string `json:"mount_source,omitempty"` // the node to mount for attach (e.g. /dev/sdd1)
	DurableID   string `json:"durable_id,omitempty"`   // "uuid:<fs-uuid>" if it has an FS; "" for a blank disk
}

// mountableFSTypes are the filesystems the agent can mount as-is (EnsureMount) → an "attach" candidate.
// Others (ntfs, exfat, …) are data-bearing but not attach-mountable → "initialize" only (with wipe).
var mountableFSTypes = map[string]bool{"ext4": true, "xfs": true}

// ListCandidateDisks enumerates the host's whole disks (from /sys/block — non-privileged) and returns
// the subset the Impl-1 unclaimed filter proves is free for Felhom. Fail-safe carries through: a device
// the filter cannot prove unclaimed (incl. any read error) is simply omitted (never offered).
func (h *SudoHostOps) ListCandidateDisks(ctx context.Context) ([]CandidateDisk, error) {
	entries, err := os.ReadDir("/sys/block")
	if err != nil {
		return nil, err
	}
	var out []CandidateDisk
	for _, e := range entries {
		name := e.Name()
		dev := "/dev/" + name
		// Only real whole disks (sd*/nvme*n*/vd*/hd*). wholeDiskOf rejects loop/ram/dm-/zram/md/sr etc.
		if wd, ok := wholeDiskOf(dev); !ok || wd != dev {
			continue
		}
		unclaimed, _ := classifyClaim(h.gatherClaimFacts(ctx, dev))
		if !unclaimed {
			continue
		}
		out = append(out, h.buildCandidate(dev, name))
	}
	return out, nil
}

// buildCandidate assembles the probe info for an already-unclaimed disk. It re-reads the lsblk tree for
// per-node FS (via the same allowlisted command the claim gather uses) and /sys for size/model.
func (h *SudoHostOps) buildCandidate(dev, name string) CandidateDisk {
	c := CandidateDisk{Device: dev}
	c.SizeBytes = readSysBlockSize(name)
	c.Model = readSysBlockModel(name)

	// Per-node FS from lsblk (reuses the FELHOM_FORMAT-allowlisted command).
	lout, _, lerr := h.runner.Run(context.Background(), h.bins.Lsblk, "-J", "-o", "NAME,FSTYPE,PTTYPE,MOUNTPOINT", dev)
	if lerr == nil {
		if nodes, perr := parseLsblkNodes(lout); perr == nil {
			for _, n := range nodes {
				if n.fstype == "" {
					continue
				}
				c.DataBearing = true
				if c.FSType == "" {
					c.FSType = n.fstype // first filesystem found (partition or whole-disk)
				}
				if mountableFSTypes[n.fstype] && c.MountSource == "" {
					c.Mountable = true
					c.MountSource = "/dev/" + n.name
					if uuid, ok := h.host.ResolveUUID("/dev/" + n.name); ok {
						c.DurableID = "uuid:" + uuid
					}
				}
			}
			// A partition table with no FS is still data-bearing (must be wiped before init).
			if len(nodes) > 1 {
				c.DataBearing = true
			}
		}
	}
	return c
}

// readSysBlockSize returns the device size in bytes from /sys/block/<name>/size (512-byte sectors).
func readSysBlockSize(name string) int64 {
	b, err := os.ReadFile(filepath.Join("/sys/block", name, "size"))
	if err != nil {
		return 0
	}
	sectors, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
	if err != nil {
		return 0
	}
	return sectors * 512
}

// readSysBlockModel returns the device model from /sys/block/<name>/device/model ("" if unreadable).
func readSysBlockModel(name string) string {
	b, err := os.ReadFile(filepath.Join("/sys/block", name, "device", "model"))
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(b))
}
