package main

import (
	"regexp"
	"strconv"
	"strings"
)

// smartTool holds the resolved smartctl binary and whether it can emit JSON
// (smartmontools >= 7.0). On CentOS 6/7 jsonCapable is false and we parse text.
type smartTool struct {
	bin         string
	jsonCapable bool
}

// newSmartTool resolves the smartctl binary and detects whether it can emit JSON
// (smartmontools >= 7.0); older builds fall back to text parsing.
func newSmartTool() smartTool {
	bin := lookPath("smartctl", "/usr/sbin/smartctl", "/sbin/smartctl", "/usr/local/sbin/smartctl")
	if bin == "" {
		bin = "smartctl"
	}
	st := smartTool{bin: bin}
	ver := run(bin, "--version")
	// "smartctl 7.2 2020-12-30 r5155 ..."
	if m := regexp.MustCompile(`smartctl\s+(\d+)\.(\d+)`).FindStringSubmatch(ver); m != nil {
		major, _ := strconv.Atoi(m[1])
		if major >= 7 {
			st.jsonCapable = true
		}
	}
	return st
}

// scanned describes one device from `smartctl --scan-open`.
type scanned struct {
	path      string
	dtype     string
	megaraidN string // megaraidN is empty when the device is not a megaraid passthrough.
	comment   string
}

var scanLine = regexp.MustCompile(`^(\S+)\s+-d\s+(\S+)\s*#?(.*)$`)
var megaraidIdx = regexp.MustCompile(`megaraid,(\d+)`)

// scan enumerates physical drives from `smartctl --scan-open`, keeping direct
// SATA/SAS/NVMe devices and megaraid passthroughs while skipping iSCSI virtual
// disks.
func (s smartTool) scan() []scanned {
	out := run(s.bin, "--scan-open")
	var res []scanned
	for _, ln := range strings.Split(out, "\n") {
		ln = strings.TrimSpace(ln)
		if ln == "" || strings.HasPrefix(ln, "#") {
			continue
		}
		m := scanLine.FindStringSubmatch(ln)
		if m == nil {
			continue
		}
		path, dtype, comment := m[1], m[2], m[3]
		if strings.Contains(strings.ToUpper(comment), "VIRTUAL-DISK") {
			continue // Skip iSCSI IET virtual disks; they are not physical drives.
		}
		sc := scanned{path: path, dtype: dtype, comment: comment}
		if mn := megaraidIdx.FindStringSubmatch(dtype); mn != nil {
			sc.megaraidN = mn[1]
		}
		res = append(res, sc)
	}
	return res
}

// querySmart runs smartctl against a device. With a JSON-capable smartctl it
// parses -j; otherwise it parses -a text. On the megaraid path, when no
// explicit -d works it tries the common passthrough type variants.
func (s smartTool) querySmart(path, dtype string, d *Drive) bool {
	args := func(extra ...string) []string {
		a := []string{}
		if s.jsonCapable {
			a = append(a, "-j")
		}
		a = append(a, "-a")
		if dtype != "" {
			a = append(a, "-d", dtype)
		}
		a = append(a, extra...)
		a = append(a, path)
		return a
	}

	raw := run(s.bin, args()...)
	if s.jsonCapable {
		j := loadJSON(raw)
		// Capture identity + transport even when SMART is unusable, so pseudo-
		// device filtering can recognize SMART-less controller VDs (e.g. "DELL
		// RAID") and iSCSI LUNs that expose no usable SMART.
		if d.Model == "" {
			d.Model = first(jStr(j, "model_name"), jStr(j, "scsi_model_name"))
		}
		if d.Transport == "" {
			d.Transport = jStr(j, "scsi_transport_protocol", "name")
		}
		if jsonUsable(j) {
			parseSmartJSON(j, d)
			d.DevicePath = path
			d.Dtype = dtype
			return true
		}
		return false
	}
	// Text path: usability check is "did we get a model + some health/attrs".
	if looksLikeSmartText(raw) {
		parseSmartText(raw, d)
		d.DevicePath = path
		d.Dtype = dtype
		return d.Model != "" || d.SmartHealth != "UNKNOWN"
	}
	return false
}

// megaraidDtypes lists the megaraid passthrough type variants to try when the
// scan didn't pin one.
var megaraidDtypes = []string{"sat+megaraid,%s", "megaraid,%s", "scsi+megaraid,%s"}

// pseudoDeviceModels lists lowercase model substrings that identify devices
// which are not physical drives: iSCSI targets and RAID controller virtual
// disks. `smartctl --scan-open` presents these as plain "-d scsi" with no
// VIRTUAL-DISK hint in the scan comment, so they are filtered after identity is
// read. Extend this list as new controller families appear in the fleet.
var pseudoDeviceModels = []string{
	"virtual-disk", "virtual disk", // iSCSI IET LUNs (e.g. "IET VIRTUAL-DISK").
	"lio-org", // Linux-IO iSCSI target LUNs (text-path fallback).
	// RAID controller virtual disks report the HBA vendor/model as their
	// identity (e.g. "AVAGO MR9363-4i", "BROADCOM MR9560-16i", "DELL PERC
	// H730", "DELL RAID"). These tokens appear on controllers, never on bare
	// drives.
	"avago", "broadcom", "lsi", "megaraid", "perc", "adaptec", "microsemi",
	"dell raid",
}

// isPseudoDevice reports whether a queried device is an iSCSI target or a RAID
// controller virtual disk rather than a physical drive. An iSCSI SCSI transport
// is the authoritative signal (covers LIO, IET, any target software); the model
// token list catches RAID virtual disks and the legacy text path that has no
// transport field.
func isPseudoDevice(d *Drive) bool {
	if strings.EqualFold(d.Transport, "iSCSI") {
		return true
	}
	m := strings.ToLower(d.Model)
	if m == "" {
		return false
	}
	for _, p := range pseudoDeviceModels {
		if strings.Contains(m, p) {
			return true
		}
	}
	return false
}

// looksLikeSmartText reports whether raw is real smartctl text output worth
// parsing, keyed off identity and health section markers. It guards the text
// path from acting on error messages or empty output.
func looksLikeSmartText(raw string) bool {
	if strings.TrimSpace(raw) == "" {
		return false
	}
	for _, marker := range []string{
		"=== START OF INFORMATION SECTION ===",
		"Device Model:", "Model Number:", "Product:",
		"SMART overall-health", "SMART Health Status",
	} {
		if strings.Contains(raw, marker) {
			return true
		}
	}
	return false
}
