package main

import "strconv"

// colKind is a column's value type. It drives formatting (CSV), typing (InfluxDB
// "i" suffix / JSON number / bool), and the tag-vs-field and label-vs-gauge
// splits the outputs derive from the table.
type colKind int

const (
	kindStr colKind = iota
	kindInt
	kindFloat
	kindBool
)

// column is the single source of truth for one output column: its name, value
// type, where it appears, and how to read it from a Drive. Every output (CSV,
// InfluxDB line protocol/JSON, Prometheus) is driven by this one table, so a new
// column is added in exactly one place.
//
// The output partitions are derived, not duplicated:
//   - InfluxDB tag      : kindStr column with influxTag set.
//   - InfluxDB field    : any non-csvOnly column that is not a tag.
//   - Prometheus label  : every non-csvOnly kindStr column.
//   - Prometheus gauge  : every non-csvOnly numeric (non-kindStr) column.
//   - csvOnly           : present in CSV only (timestamp, risk_reasons).
type column struct {
	name      string
	kind      colKind
	influxTag bool
	csvOnly   bool
	// value reads the column from d. It returns the typed Go value (string,
	// *int, int, *float64, float64, or bool); a nil pointer means "unknown" and
	// renders blank / is skipped by the metric outputs.
	value func(d *Drive) any
}

// columns is the ordered output schema. CSV emits these in this order; the
// metric outputs sort their own tag/field sets independently.
var columns = []column{
	{name: "collected_at", kind: kindStr, csvOnly: true, value: func(d *Drive) any { return d.CollectedAt }},
	{name: "hostname", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Hostname }},
	{name: "device_path", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.DevicePath }},
	{name: "dtype", kind: kindStr, value: func(d *Drive) any { return d.Dtype }},
	{name: "enclosure_slot", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.enclosureSlot() }},
	{name: "device_id", kind: kindStr, value: func(d *Drive) any { return d.DeviceID }},
	{name: "serial", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Serial }},
	{name: "model", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Model }},
	{name: "firmware", kind: kindStr, value: func(d *Drive) any { return d.Firmware }},
	{name: "capacity", kind: kindStr, value: func(d *Drive) any { return d.Capacity }},
	{name: "rotation", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Rotation }},
	{name: "smart_health", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.SmartHealth }},
	{name: "defect_total", kind: kindInt, value: func(d *Drive) any { return d.DefectTotal }},
	{name: "udma_crc_errors", kind: kindInt, value: func(d *Drive) any { return d.UdmaCrc }},
	{name: "media_errors_ctrl", kind: kindInt, value: func(d *Drive) any { return d.MediaErrCtrl }},
	{name: "other_errors_ctrl", kind: kindInt, value: func(d *Drive) any { return d.OtherErrCtrl }},
	{name: "predictive_failure_ctrl", kind: kindInt, value: func(d *Drive) any { return d.PredictiveFailureCtrl }},
	{name: "smart_alert_ctrl", kind: kindBool, value: func(d *Drive) any { return d.SmartAlertCtrl }},
	{name: "fw_state", kind: kindStr, value: func(d *Drive) any { return d.FwState }},
	{name: "wear_pct_consumed", kind: kindInt, value: func(d *Drive) any { return d.WearPctConsumed }},
	{name: "wear_src", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.WearSrc }},
	{name: "unused_reserve_pct", kind: kindInt, value: func(d *Drive) any { return d.UnusedReservePct }},
	{name: "host_written_tb", kind: kindFloat, value: func(d *Drive) any { return d.HostWrittenTB }},
	{name: "nvme_critical_warning", kind: kindInt, value: func(d *Drive) any { return d.NvmeCriticalWarning }},
	{name: "nvme_avail_spare", kind: kindInt, value: func(d *Drive) any { return d.NvmeAvailSpare }},
	{name: "nvme_avail_spare_thresh", kind: kindInt, value: func(d *Drive) any { return d.NvmeAvailSpareThresh }},
	{name: "power_on_years", kind: kindFloat, value: func(d *Drive) any { return d.PowerOnYears }},
	{name: "power_cycle_count", kind: kindInt, value: func(d *Drive) any { return d.PowerCycleCount }},
	{name: "temp_c", kind: kindInt, value: func(d *Drive) any { return d.TempC }},
	{name: "risk_score", kind: kindInt, value: func(d *Drive) any { return d.RiskScore }},
	{name: "recommendation", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Recommendation }},
	{name: "risk_reasons", kind: kindStr, csvOnly: true, value: func(d *Drive) any { return d.RiskReasons }},
}

// raw returns the column's typed value for d, or nil when the value is unknown
// (a nil pointer) or a blank string. Callers that need a presence test treat a
// nil return as "absent".
func (c column) raw(d *Drive) any {
	switch t := c.value(d).(type) {
	case nil:
		return nil
	case *int:
		if t == nil {
			return nil
		}
		return *t
	case *float64:
		if t == nil {
			return nil
		}
		return *t
	case string:
		if t == "" {
			return nil
		}
		return t
	default:
		return t // int, float64, bool — always present.
	}
}

// format renders a raw value as its display string ("" for an absent value),
// matching the CSV/line-protocol textual form.
func format(v any) string {
	switch t := v.(type) {
	case nil:
		return ""
	case string:
		return t
	case int:
		return strconv.Itoa(t)
	case float64:
		return strconv.FormatFloat(t, 'f', -1, 64)
	case bool:
		if t {
			return "true"
		}
		return "false"
	default:
		return ""
	}
}

// field returns the column's display string for d, or "" when unknown. It is the
// canonical text form shared by the CSV output and the test helpers.
func (d *Drive) field(name string) string {
	for _, c := range columns {
		if c.name == name {
			return format(c.raw(d))
		}
	}
	return ""
}

// labelColumns returns the string columns carried as Prometheus labels and (the
// influxTag subset) as InfluxDB tags — every non-csvOnly kindStr column.
func labelColumns() []column {
	var out []column
	for _, c := range columns {
		if c.kind == kindStr && !c.csvOnly {
			out = append(out, c)
		}
	}
	return out
}

// gaugeColumns returns the numeric columns emitted as Prometheus gauges and
// InfluxDB fields — every non-csvOnly column that is not a string.
func gaugeColumns() []column {
	var out []column
	for _, c := range columns {
		if c.kind != kindStr && !c.csvOnly {
			out = append(out, c)
		}
	}
	return out
}
