<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Unraid GraphQL API client. Issues one combined query per request for OS, CPU, memory,
//   disks and array state, and records which callers had to fall back when the API is
//   unavailable.
//
// DESIGN PRINCIPLES
//   One round trip per request, not one per metric.
//       vv_api_data() runs a single combined query and caches it for the lifetime of the
//       request. A page reading eight metrics still makes one API call.
//
//   Fallback is expected, and it is tracked.
//       The unraid-api registry is ephemeral — a key that worked yesterday can be gone.
//       Every function that had to use a local path records itself via
//       vv_api_record_fallback(), so vv_api_get_status() can report precisely which data is
//       degraded rather than a single unhelpful "API down".
//
//   The API is an optimisation, never a dependency.
//       Every value it provides has a local path in common.php. Losing the API costs detail
//       and precision, not availability.
//
// OPERATIONAL SAFEGUARDS
//   Absent or invalid key degrades silently to local reads.
//       No exception, no error banner — the caller gets its value from /proc or sysfs and the
//       fallback is recorded for the status endpoint.
//
//   Request-lifetime cache only.
//       Nothing is persisted to disk here, so a stale API response cannot outlive the page
//       that fetched it.
//
//   Read-only. Queries state; issues no mutations against unraid-api.
//
// EXPORTS
//   vv_api_data()            the combined query result, cached per request
//   vv_api_get_status()      availability plus the list of functions that fell back
//   vv_api_record_fallback() called by consumers when they use a local path instead
//   vv_api_node_metrics()    per-node metric summary
//   vv_api_disk_entry()      normalised disk record
//   vv_local_host_stats()    local summary in the same shape as a remote node
//
// CONFIGURATION
//   HOST*_UNRAID_API_KEY   written every array start and every 15 min by
//                          Plugin/unraid/System_Essentials/unraid_api_key_renew.sh
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Unraid GraphQL API — single-request master fetch + per-function fallback tracking.
// All API-first functions call vv_api_data() then fall back to local reads on null.
//
// Confirmed schema (introspected 2026-05-31, Unraid 7.3):
//   InfoOs:     hostname, uptime (String), release, kernel (NEW 7.3), arch (NEW 7.3)
//               — also new but unused: fqdn, build, codename, platform, distro, serial, uefi
//               — no version/uptime-as-int
//   InfoCpu:    brand, threads, cores — also new but unused: manufacturer, model, speed,
//               speedmin, speedmax, processors, socket, topology, packages
//   InfoMemory: layout only — NO usage fields; memory usage via metrics.memory
//   ArrayDisk:  used for parities/disks/caches (ArrayParity/ArrayCache named types removed in 7.3)
//               fields: name, device, type (ArrayDiskType enum), status (ArrayDiskStatus enum),
//               size (BigInt), fsSize, fsFree, fsUsed (BigInt), temp, transport (String),
//               rotational, isSpinning, fsType (String) — also new: numReads, numWrites,
//               numErrors (BigInt), idx, warning, critical, color, exportable, comment, format
//   MemoryUtilization: percentTotal, total, used, available, swapTotal, swapUsed —
//               also new: free, active, buffcache, swapFree, percentSwapTotal
//   VmDomain:   name, state (VmState enum) — no memory/vcpus

require_once __DIR__ . '/config.php';

// ── Fallback tracking ─────────────────────────────────────────────────────────

function &_vv_api_fallbacks(): array { static $f = []; return $f; }
function &_vv_api_key_missing(): bool { static $m = false; return $m; }

function vv_api_record_fallback(string $fn): void {
    $f   = &_vv_api_fallbacks();
    $f[] = $fn;
}

function vv_api_get_status(): array {
    $fallbacks = array_unique(_vv_api_fallbacks());
    return [
        'available'   => empty($fallbacks),
        'key_missing' => _vv_api_key_missing(),
        'fallbacks'   => $fallbacks,
    ];
}

// ── Master fetch ──────────────────────────────────────────────────────────────

// Single combined query — one HTTP round-trip, cached for the request lifetime.
// Returns null if API is unreachable, key missing, or any query error occurs.
function vv_api_data(): ?array {
    static $cache = null, $fetched = false;
    if ($fetched) return $cache;
    $fetched = true;

    $hostId = vv_detect_host();
    $vars   = vv_conf_vars();
    $key    = $vars[strtoupper($hostId) . '_UNRAID_API_KEY'] ?? '';
    if (!$key) {
        $m = &_vv_api_key_missing();
        $m = true;
        $cache = null;
        return null;
    }

    // Fields verified against live schema introspection (2026-05-31, Unraid 7.3).
    // array.parities / .disks / .caches are SEPARATE lists — .disks is DATA only.
    // ArrayParity/ArrayCache are no longer named types in 7.3 but the query structure is unchanged.
    // metrics.cpu.percentTotal and metrics.memory.* provide real-time utilisation.
    $gql = <<<'GQL'
{
  info {
    os  { hostname uptime release kernel }
    cpu { brand threads cores }
  }
  metrics {
    cpu    { percentTotal }
    memory { percentTotal total used available swapTotal swapUsed }
  }
  array {
    state
    parities { name device type status size fsSize fsFree fsUsed temp transport rotational isSpinning }
    disks    { name device type status size fsSize fsFree fsUsed temp transport rotational isSpinning }
    caches   { name device type status size fsSize fsFree fsUsed temp transport rotational isSpinning }
  }
  vms {
    domains { name state }
  }
}
GQL;

    $cache = vv_unraid_api_query($hostId, $gql, 5, $key);
    return $cache;
}

// ── Disk data helpers ─────────────────────────────────────────────────────────

// API size fields (BigInt) are in bytes on this schema.
// Heuristic: if raw > 100 billion → bytes; else → KB (covers both possible encodings).
function _vv_api_bytes_to_gb(float $raw): float {
    return $raw > 100_000_000_000
        ? round($raw / (1024 ** 3), 1)
        : round($raw / (1024 ** 2), 1);
}

// Map ArrayDiskType enum → role string used by the rest of the plugin.
// Unraid 6.9 used CACHE; 6.10+ renamed pools to POOL. FLASH is the USB boot drive (skip).
// Anything unrecognised (not DATA/PARITY*/FLASH) is treated as a pool.
function _vv_api_disk_role(string $type): string {
    $t = strtoupper($type);
    if (str_contains($t, 'PARITY')) return 'parity';
    if ($t === 'DATA')               return 'data';
    if ($t === 'FLASH')              return 'flash';   // USB boot — excluded from both views
    return 'cache';                                     // CACHE, POOL, or future variants
}

// Build a normalised disk entry from API data, matching the shape vv_disk_entry() produces.
// $role may be overridden; if empty it is derived from the disk's type field.
// $ini_used_kb: last-known fsUsed in KB from disks.ini — used when the disk is
// spun down and the API returns fsUsed=0 because the filesystem is unmounted.
function vv_api_disk_entry(array $d, string $role = '', int $ini_used_kb = 0): ?array {
    if (!$role) $role = _vv_api_disk_role((string)($d['type'] ?? 'DATA'));

    if ($role === 'parity') {
        // Parity disks have no filesystem — use raw size only.
        $sizeRaw = (float)($d['size'] ?? 0);
        if ($sizeRaw <= 0) return null;
        $sizeGb  = _vv_api_bytes_to_gb($sizeRaw);
        $usedGb  = 0.0;
        $pct     = null;
    } else {
        // Data/cache disks: prefer fsSize/fsUsed; fall back to size if unmounted.
        $sizeRaw = (float)($d['fsSize'] ?? $d['size'] ?? 0);
        if ($sizeRaw <= 0) return null;
        $usedRaw = (float)($d['fsUsed'] ?? 0);
        $sizeGb  = _vv_api_bytes_to_gb($sizeRaw);
        if ($usedRaw > 0.0) {
            $usedGb = _vv_api_bytes_to_gb($usedRaw);
        } elseif (!($d['isSpinning'] ?? true) && $ini_used_kb > 0) {
            $usedGb = round($ini_used_kb / 1048576, 1);
        } else {
            $usedGb = 0.0;
        }
        $pct     = $sizeGb > 0 ? round($usedGb / $sizeGb * 100, 1) : null;
    }

    $temp      = isset($d['temp']) && is_numeric($d['temp']) ? (int)$d['temp'] : null;
    $spinning  = (bool)($d['isSpinning'] ?? true);
    $transport = $d['transport'] ?? (str_contains(strtolower($d['device'] ?? ''), 'nvme') ? 'nvme' : 'ata');

    return [
        'name'      => $d['name']   ?? '',
        'device'    => $d['device'] ?? '',
        'role'      => $role,
        'size_gb'   => $sizeGb,
        'used_gb'   => $usedGb,
        'pct'       => $pct,
        'temp'      => $temp,
        'transport' => strtolower($transport),
        'mounted'   => $spinning,
        'status'    => $d['status'] ?? 'DISK_OK',
    ];
}

// ── Node metrics extractor ────────────────────────────────────────────────────

// Parse CPU%, RAM, array storage, disk temps, and VM count from a raw API response.
// Used by vv_remote_hosts_stats() and the local vv_api_data() path — one parser, no duplication.
// GQL must include: metrics.cpu.percentTotal, metrics.memory.{total,used},
//   array.{disks,caches,parities}.{fsSize,fsUsed,temp}, vms.domains.
function vv_api_node_metrics(?array $d): array {
    if (!$d) return [];
    $cpu = (int)round((float)($d['metrics']['cpu']['percentTotal'] ?? 0));
    $mem = $d['metrics']['memory'] ?? [];
    $ramUsed = isset($mem['used'])  ? _vv_api_bytes_to_gb((float)$mem['used'])  : null;
    $ramTot  = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : null;

    $disks  = $d['array']['disks']    ?? [];
    $caches = $d['array']['caches']   ?? [];
    $pars   = $d['array']['parities'] ?? [];
    $usedGb = 0.0; $totGb = 0.0;
    foreach (array_merge($disks, $caches) as $dk) {
        $sz = (float)($dk['fsSize'] ?? 0);
        if ($sz <= 0) continue;
        $totGb  += _vv_api_bytes_to_gb($sz);
        $usedGb += _vv_api_bytes_to_gb((float)($dk['fsUsed'] ?? 0));
    }
    $temps = array_filter(
        array_merge(array_column($disks,'temp'), array_column($caches,'temp'), array_column($pars,'temp')),
        fn($t) => is_numeric($t) && $t > 0
    );
    return [
        'cpu_pct'        => $cpu,
        'ram_used_gb'    => $ramUsed !== null ? round($ramUsed, 1) : null,
        'ram_total_gb'   => $ramTot  !== null ? round($ramTot,  1) : null,
        'array_used_tb'  => $totGb > 0 ? round($usedGb / 1000, 1) : null,
        'array_total_tb' => $totGb > 0 ? round($totGb  / 1000, 1) : null,
        'max_disk_temp'  => $temps ? (int)max($temps) : null,
        'vm_count'       => count($d['vms']['domains'] ?? []),
    ];
}

// ── Local host stats snapshot — same shape as one entry from vv_remote_hosts_stats() ─────────
// Called locally and via SSH by remote_arr_cache_writer.sh on remote hosts.
function vv_local_host_stats(): array {
    $host = vv_detect_host();
    $myId = strtoupper($host);
    $vars = vv_conf_vars();
    $name = $vars[$myId] ?? gethostname();

    $api     = vv_api_data();
    $metrics = vv_api_node_metrics($api);

    if (!$api) {
        return ['available' => false, 'host_id' => $myId, 'hostname' => $name,
                'no_api_key' => _vv_api_key_missing()];
    }

    $os  = $api['info']['os']  ?? [];
    $cpu = $api['info']['cpu'] ?? [];
    $mem = $api['metrics']['memory'] ?? [];

    $memPct = round((float)($mem['percentTotal'] ?? 0));
    if ($memPct === 0) {
        $tot = (float)($mem['total'] ?? 0); $avail = (float)($mem['available'] ?? 0);
        $memPct = $tot > 0 ? (int)round(($tot - $avail) / $tot * 100) : 0;
    }
    $memTotalGb = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : 0;

    $uptimeRaw = $os['uptime'] ?? '';
    if (is_numeric($uptimeRaw)) {
        $s      = (int)$uptimeRaw;
        $uptime = vv_format_uptime($s);
    } else {
        $s = 0; $uptime = $uptimeRaw ?: '—';
    }

    return array_merge([
        'available'    => true,
        'host_id'      => $myId,
        'hostname'     => $os['hostname'] ?? $name,
        'version'      => $os['release']  ?? '',
        'uptime'       => $uptime,
        'uptime_sec'   => $s,
        'cpu_load'     => $metrics['cpu_pct']   ?? 0,
        'cpu_threads'  => (int)($cpu['threads'] ?? 0),
        'mem_total_gb' => $memTotalGb,
        'mem_used_pct' => $memPct,
        'array_state'  => $api['array']['state'] ?? 'UNKNOWN',
    ], $metrics);
}

// ── Confirmed schema (Unraid 7.3, introspected 2026-05-31) ───────────────────
// Adding a new host: add HOSTn="hostname" to master.conf and HOSTn_UNRAID_API_KEY
// to hostn.conf, then push and pull as usual. No schema work needed.
//
// If a future Unraid version renames a field, the affected function falls back
// to local reads and the api banner lists the fallback — fix by updating the GQL.
