package storage

import (
	"encoding/json"
	"strconv"

	"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)

// smartctlJSON is the lenient subset of `smartctl -a -j` output we read. Pointers detect
// presence so an absent section (e.g. NVMe fields on a SATA disk, or no SMART at all on a
// USB bridge) decodes cleanly to nil and we degrade to UNKNOWN.
type smartctlJSON struct {
	SmartStatus *struct {
		Passed bool `json:"passed"`
	} `json:"smart_status"`
	Temperature *struct {
		Current *int `json:"current"`
	} `json:"temperature"`
	PowerOnTime *struct {
		Hours *int `json:"hours"`
	} `json:"power_on_time"`
	// SATA/ATA attribute table.
	ATA *struct {
		Table []struct {
			ID  int `json:"id"`
			Raw struct {
				Value int64 `json:"value"`
			} `json:"raw"`
		} `json:"table"`
	} `json:"ata_smart_attributes"`
	// NVMe health log.
	NVMe *struct {
		CriticalWarning *int   `json:"critical_warning"`
		MediaErrors     *int64 `json:"media_errors"`
		PercentageUsed  *int   `json:"percentage_used"`
		Temperature     *int   `json:"temperature"`
	} `json:"nvme_smart_health_information_log"`
}

// SATA attribute IDs we surface.
const (
	ataReallocatedSectorCt = 5
	ataCurrentPending      = 197
	ataOfflineUncorrect    = 198
)

// parseSMART maps smartctl JSON to a hub.SmartSummary, handling SATA + NVMe and degrading
// to UNKNOWN when health is not reported. A device populates only its own attribute set.
func parseSMART(raw []byte) hub.SmartSummary {
	s := hub.SmartSummary{Health: hub.SmartUnknown}
	if len(raw) == 0 {
		return s
	}
	var j smartctlJSON
	if err := json.Unmarshal(raw, &j); err != nil {
		return s // unparseable → UNKNOWN (never an error to the report)
	}

	if j.SmartStatus != nil {
		if j.SmartStatus.Passed {
			s.Health = hub.SmartPassed
		} else {
			s.Health = hub.SmartFailing
		}
	}
	if j.Temperature != nil && j.Temperature.Current != nil {
		s.TemperatureC = j.Temperature.Current
	}
	if j.PowerOnTime != nil && j.PowerOnTime.Hours != nil {
		s.PowerOnHours = j.PowerOnTime.Hours
	}

	// SATA attributes.
	if j.ATA != nil {
		for _, a := range j.ATA.Table {
			switch a.ID {
			case ataReallocatedSectorCt:
				s.ReallocatedSectors = intPtr(int(a.Raw.Value))
			case ataCurrentPending:
				s.PendingSectors = intPtr(int(a.Raw.Value))
			case ataOfflineUncorrect:
				s.OfflineUncorrectable = intPtr(int(a.Raw.Value))
			}
		}
	}

	// NVMe attributes.
	if j.NVMe != nil {
		s.CriticalWarning = j.NVMe.CriticalWarning
		if j.NVMe.MediaErrors != nil {
			s.MediaErrors = intPtr(int(*j.NVMe.MediaErrors))
		}
		s.PercentageUsed = j.NVMe.PercentageUsed
		// NVMe reports temperature in its own log when the top-level block is absent.
		if s.TemperatureC == nil && j.NVMe.Temperature != nil {
			s.TemperatureC = j.NVMe.Temperature
		}
	}
	return s
}

// lvsReport is the lenient subset of `lvs --reportformat json` output.
type lvsReport struct {
	Report []struct {
		LV []struct {
			LVName          string `json:"lv_name"`
			DataPercent     string `json:"data_percent"`
			MetadataPercent string `json:"metadata_percent"`
		} `json:"lv"`
	} `json:"report"`
}

// parseThinPoolMetadata extracts the metadata-used fraction (0..1) from lvs JSON. lvs
// reports percentages as decimal strings (e.g. "10.50"); an empty string means "not a thin
// pool / not applicable" → ok=false.
func parseThinPoolMetadata(raw []byte) (float64, bool) {
	if len(raw) == 0 {
		return 0, false
	}
	var r lvsReport
	if err := json.Unmarshal(raw, &r); err != nil {
		return 0, false
	}
	for _, rep := range r.Report {
		for _, lv := range rep.LV {
			if lv.MetadataPercent == "" {
				continue
			}
			pct, err := strconv.ParseFloat(lv.MetadataPercent, 64)
			if err != nil {
				continue
			}
			return pct / 100, true
		}
	}
	return 0, false
}

func intPtr(v int) *int { return &v }
