package main

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

// parseSmartText parses `smartctl -a` PLAIN TEXT output. This is the path used
// on CentOS 6/7 where smartmontools (5.x/6.x) predates `--json` (7.0, 2019).
// Handles ATA/SATA, NVMe, and SAS/SCSI layouts.
func parseSmartText(text string, d *Drive) {
	if strings.TrimSpace(text) == "" {
		return
	}
	lines := strings.Split(text, "\n")

	d.Rotation = "SSD"
	val := func(prefix string) string {
		for _, ln := range lines {
			if i := strings.Index(ln, prefix); i >= 0 {
				return strings.TrimSpace(ln[i+len(prefix):])
			}
		}
		return ""
	}

	// ---- Identity ----
	d.Model = first(val("Device Model:"), val("Model Number:"), val("Product:"), val("Model Family:"))
	d.Serial = first(val("Serial Number:"), val("Serial number:"))
	d.Firmware = first(val("Firmware Version:"), val("Revision:"))

	if uc := val("User Capacity:"); uc != "" {
		// "1,920,383,410,176 bytes [1.92 TB]"
		if m := regexp.MustCompile(`([\d,]+)\s*bytes`).FindStringSubmatch(uc); m != nil {
			if n, ok := parseIntLoose(m[1]); ok && n > 0 {
				d.Capacity = fmt.Sprintf("%.2f TB", float64(n)/1e12)
			}
		}
	}

	if rr := val("Rotation Rate:"); rr != "" {
		if strings.Contains(strings.ToLower(rr), "solid state") {
			d.Rotation = "SSD"
		} else if m := regexp.MustCompile(`(\d+)\s*rpm`).FindStringSubmatch(strings.ToLower(rr)); m != nil {
			d.Rotation = m[1] + " rpm"
		}
	}
	isNVMe := false
	for _, ln := range lines {
		if strings.Contains(ln, "NVMe Log") || strings.Contains(ln, "SMART/Health Information (NVMe") {
			isNVMe = true
			break
		}
	}
	if isNVMe {
		d.Rotation = "NVMe"
	}

	// ---- SMART overall health ----
	// ATA: "SMART overall-health self-assessment test result: PASSED"
	// SCSI: "SMART Health Status: OK"
	if h := val("self-assessment test result:"); h != "" {
		up := strings.ToUpper(h)
		if strings.Contains(up, "PASS") {
			d.SmartHealth = "PASSED"
		} else if strings.Contains(up, "FAIL") {
			d.SmartHealth = "FAILED"
		} else {
			d.SmartHealth = "UNKNOWN"
		}
	} else if h := val("SMART Health Status:"); h != "" {
		if strings.Contains(strings.ToUpper(h), "OK") {
			d.SmartHealth = "PASSED"
		} else {
			d.SmartHealth = "FAILED"
		}
	} else {
		d.SmartHealth = "UNKNOWN"
	}

	if isNVMe {
		parseNVMeText(val, d)
		return
	}
	if attrs := parseATAAttrTable(lines); len(attrs) > 0 {
		applyATAAttrs(attrs, d)
		return
	}
	// SAS/SCSI fallback fields.
	parseSCSIText(val, d)
	parseSCSIErrors(lines, d)
}

// parseATAAttrTable parses the "Vendor Specific SMART Attributes" table:
//
//	ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
//	  5 Reallocated_Sector_Ct   0x0033   100   100   010    Pre-fail  Always       -       0
func parseATAAttrTable(lines []string) map[int]ataAttr {
	attrs := map[int]ataAttr{}
	inTable := false
	for _, ln := range lines {
		if strings.Contains(ln, "ATTRIBUTE_NAME") && strings.Contains(ln, "RAW_VALUE") {
			inTable = true
			continue
		}
		if !inTable {
			continue
		}
		if strings.TrimSpace(ln) == "" {
			break
		}
		f := strings.Fields(ln)
		if len(f) < 10 {
			continue
		}
		id, ok := firstInt(f[0])
		if !ok {
			continue
		}
		ta := ataAttr{whenFailed: f[8]}
		if v, ok := firstInt(f[3]); ok {
			ta.value = &v
		}
		if w, ok := firstInt(f[4]); ok {
			ta.worst = &w
		}
		// RAW_VALUE is the remainder from field 9 onward; take leading int.
		if r, ok := firstInt(strings.Join(f[9:], " ")); ok {
			ta.raw = &r
		}
		attrs[id] = ta
	}
	return attrs
}

// applyATAAttrs maps the parsed ATA attribute table onto d: the health-by-
// attribute verdict when the overall result was unknown, the shared defect/wear/
// reserve fields, and the text-path-only power-cycle and temperature fallbacks.
func applyATAAttrs(attrs map[int]ataAttr, d *Drive) {
	// Health-by-attribute when the overall verdict was unknown.
	if d.SmartHealth == "UNKNOWN" && len(attrs) > 0 {
		if attrsFailed(attrs) {
			d.SmartHealth = "FAILED"
		} else {
			d.SmartHealth = "PASSED_BY_ATTR"
		}
	}

	// Defect, wear, reserve, and host-write fields shared with the JSON path.
	applyAtaCounters(attrs, d)

	// Text-path fallbacks: JSON reads these from dedicated fields instead.
	if d.PowerCycleCount == nil {
		d.PowerCycleCount = attrRaw(attrs, 12)
	}
	if d.TempC == nil {
		d.TempC = attrRaw(attrs, 194)
	}
}

// parseNVMeText fills NVMe health fields from the "SMART/Health Information
// (NVMe Log)" section using the shared val() prefix lookup.
func parseNVMeText(val func(string) string, d *Drive) {
	if cw := val("Critical Warning:"); cw != "" {
		// "0x00"
		if n, ok := parseHexOrInt(cw); ok {
			d.NvmeCriticalWarning = &n
		}
	}
	if as := val("Available Spare:"); as != "" {
		if n, ok := firstInt(as); ok {
			d.NvmeAvailSpare = &n
		}
	}
	if at := val("Available Spare Threshold:"); at != "" {
		if n, ok := firstInt(at); ok {
			d.NvmeAvailSpareThresh = &n
		}
	}
	if pu := val("Percentage Used:"); pu != "" {
		if n, ok := firstInt(pu); ok {
			d.WearPctConsumed = &n
			d.WearPctRemaining = pInt(100 - n)
			d.WearSrc = "NVMe/percentage_used"
		}
	}
	if me := val("Media and Data Integrity Errors:"); me != "" {
		if n, ok := parseIntLoose(me); ok {
			d.NvmeMediaErrors = &n
			if d.Uncorrectable == nil {
				d.Uncorrectable = &n
			}
		}
	}
	if d.PowerOnHours == nil {
		if n, ok := parseIntLoose(val("Power On Hours:")); ok {
			d.PowerOnHours = &n
		}
	}
	if d.PowerCycleCount == nil {
		if n, ok := parseIntLoose(val("Power Cycles:")); ok {
			d.PowerCycleCount = &n
		}
	}
	if d.TempC == nil {
		if n, ok := firstInt(val("Temperature:")); ok {
			d.TempC = &n
		}
	}
}

// parseSCSIText fills SAS/SCSI fields (temperature, grown defect list,
// endurance, power-on hours) that smartctl prints as "Label: value" lines.
func parseSCSIText(val func(string) string, d *Drive) {
	if t := val("Current Drive Temperature:"); t != "" {
		if n, ok := firstInt(t); ok {
			d.TempC = &n
		}
	}
	if g := val("Elements in grown defect list:"); g != "" {
		if n, ok := parseIntLoose(g); ok {
			d.Reallocated = &n
		}
	}
	if e := val("Percentage used endurance indicator:"); e != "" {
		if n, ok := firstInt(e); ok {
			d.WearPctConsumed = &n
			d.WearPctRemaining = pInt(100 - n)
			d.WearSrc = "SCSI/endurance"
		}
	}
	if h := val("number of hours powered up"); h != "" {
		// "= 12345.67"
		if m := regexp.MustCompile(`([\d,]+)`).FindString(h); m != "" {
			if n, ok := parseIntLoose(m); ok {
				d.PowerOnHours = &n
			}
		}
	}
	// Newer smartctl prints "Accumulated power on time, hours:minutes 2487:44".
	if d.PowerOnHours == nil {
		if h := val("Accumulated power on time, hours:minutes"); h != "" {
			if n, ok := firstInt(h); ok {
				d.PowerOnHours = &n
			}
		}
	}
}

// parseSCSIErrors handles the SAS "Error counter log" and pending-defect count,
// the SAS analog of ATA uncorrectable/pending sectors. These need the full line
// list (the error log is a multi-line table), not just the val() prefix lookup.
func parseSCSIErrors(lines []string, d *Drive) {
	if d.Uncorrectable == nil {
		// Each of read:/write:/verify: ends in a "total uncorrected errors" count.
		sum, any := 0, false
		for _, ln := range lines {
			f := strings.Fields(ln)
			if len(f) < 7 {
				continue
			}
			switch f[0] {
			case "read:", "write:", "verify:":
				if n, ok := parseIntLoose(f[len(f)-1]); ok {
					any = true
					sum += n
				}
			}
		}
		if any {
			d.Uncorrectable = pInt(sum)
		}
	}
	if d.Pending == nil {
		// "  Pending defect count:0 Pending Defects" (no space after the colon).
		for _, ln := range lines {
			if i := strings.Index(ln, "Pending defect count:"); i >= 0 {
				if n, ok := firstInt(ln[i+len("Pending defect count:"):]); ok {
					d.Pending = pInt(n)
				}
				break
			}
		}
	}
}

// parseHexOrInt parses s as hex when it carries a 0x/0X prefix, otherwise as a
// loose decimal int. The NVMe critical-warning field arrives as "0x00".
func parseHexOrInt(s string) (int, bool) {
	s = strings.TrimSpace(s)
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
		var n int
		if _, err := fmt.Sscanf(s, "0x%x", &n); err == nil {
			return n, true
		}
		if _, err := fmt.Sscanf(s, "0X%x", &n); err == nil {
			return n, true
		}
		return 0, false
	}
	return parseIntLoose(s)
}
