package main

import (
	"encoding/json"
	"regexp"
	"strconv"
	"strings"
)

// loadJSON parses smartctl -j output. If leading noise precedes the object
// (rare, but some controllers emit warnings before the JSON), it retries from
// the first '{'.
func loadJSON(raw string) map[string]interface{} {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	var m map[string]interface{}
	if err := json.Unmarshal([]byte(raw), &m); err == nil {
		return m
	}
	if i := strings.IndexByte(raw, '{'); i >= 0 {
		if err := json.Unmarshal([]byte(raw[i:]), &m); err == nil {
			return m
		}
	}
	return nil
}

// jObj navigates nested maps by key path, returning the leaf map or nil.
func jObj(m map[string]interface{}, keys ...string) map[string]interface{} {
	cur := m
	for _, k := range keys {
		if cur == nil {
			return nil
		}
		v, ok := cur[k].(map[string]interface{})
		if !ok {
			return nil
		}
		cur = v
	}
	return cur
}

// jInt returns an *int for a numeric leaf (JSON numbers decode as float64).
func jInt(m map[string]interface{}, keys ...string) *int {
	v := jLeaf(m, keys...)
	switch t := v.(type) {
	case float64:
		n := int(t)
		return &n
	case string:
		if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil {
			return &n
		}
	}
	return nil
}

// jStr returns a trimmed string leaf, or "".
func jStr(m map[string]interface{}, keys ...string) string {
	if s, ok := jLeaf(m, keys...).(string); ok {
		return strings.TrimSpace(s)
	}
	return ""
}

// jBoolPtr returns *bool for a boolean leaf.
func jBoolPtr(m map[string]interface{}, keys ...string) *bool {
	if b, ok := jLeaf(m, keys...).(bool); ok {
		return &b
	}
	return nil
}

// jLeaf returns the raw value at the key path (the final key looked up in its
// parent map), or nil when any segment along the path is missing.
func jLeaf(m map[string]interface{}, keys ...string) interface{} {
	if len(keys) == 0 {
		return nil
	}
	parent := jObj(m, keys[:len(keys)-1]...)
	if parent == nil {
		return nil
	}
	return parent[keys[len(keys)-1]]
}

var leadingInt = regexp.MustCompile(`^\s*(\d+)`)

// firstInt extracts the leading run of digits from a string ("345 hours" -> 345).
// It stops at the first non-digit, so for comma-grouped numbers ("12,345") use
// parseIntLoose, which strips separators first.
func firstInt(s string) (int, bool) {
	m := leadingInt.FindStringSubmatch(s)
	if m == nil {
		return 0, false
	}
	n, err := strconv.Atoi(m[1])
	return n, err == nil
}

// parseIntLoose strips commas/spaces and parses an integer anywhere in s.
func parseIntLoose(s string) (int, bool) {
	s = strings.TrimSpace(strings.ReplaceAll(s, ",", ""))
	// Take the leading run of digits (and optional sign).
	m := regexp.MustCompile(`-?\d+`).FindString(s)
	if m == "" {
		return 0, false
	}
	n, err := strconv.Atoi(m)
	return n, err == nil
}
