<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Storage mode and API key management. Reports where the plugin currently lives and what
//   kind of device it boots from, migrates it between internal and flash layouts, records the
//   detected mode into conf, and reports or provisions Unraid API keys across all hosts.
//
// OPERATIONAL MODEL
//   Storage mode exists because /boot is not the same thing on every Unraid host. On a real
//   USB flash device, write wear is a genuine constraint and the plugin belongs in appdata; on
//   a host that boots from an internal SSD or a ZFS pool it does not, and living under
//   /boot/config/plugins is simpler. The mode is therefore detected from the boot device's
//   transport rather than assumed.
//
//   Detection walks from the mountpoint down to physical media, and handles two shapes. A
//   block-device /boot resolves through lsblk's parent-name to the whole disk. A ZFS /boot
//   resolves through zpool to a backing vdev first, skipping the mirror/raidz/spare topology
//   rows, and then to its whole disk. Only then is the transport read.
//
//   Migration is delegated entirely to storage_migrate.sh. It copies data, rewrites
//   varaverk.cfg and master.conf, and removes the old location — far too much to do inside a
//   web request, and it must be resumable and testable on its own.
//
// DESIGN PRINCIPLES
//   Reports detected and current separately, and never reconciles them itself.
//       current_mode is where the plugin actually is; detected is where the hardware suggests
//       it should be. A host can legitimately sit in either state, so the status action
//       presents both and lets a person decide.
//
//   A third mode, custom, is a first-class answer.
//       A SCRIPTS_DIR matching neither known path is reported as custom rather than being
//       forced into one of the two. Someone who deliberately relocated the plugin should not
//       see the UI claim it is somewhere it is not.
//
//   API key status is reported for every host, from conf.
//       Partner keys live in the partner's own conf section, so a partner's key can be
//       reported present without contacting it. Only the local host's key is validated
//       against the API, because only the local host has one to call.
//
//   Keys are previewed, never returned.
//       First 8 and last 4 characters — enough to confirm which key is in use, not enough to
//       use it.
//
// OPERATIONAL SAFEGUARDS
//   The migration target is an exact allowlist with strict comparison.
//       Only 'internal' and 'flash' reach the script, as an escapeshellarg'd --to= value. No
//       part of the request names a path, so migration cannot be pointed anywhere else.
//
//   Every shell argument is escaped, including ones derived from other shell calls.
//       The device names discovered during detection are fed back into lsblk through
//       escapeshellarg(), so a device name containing anything unexpected cannot compose a
//       command.
//
//   Every detection step degrades to 'unknown' rather than guessing.
//       A failed findmnt, lsblk, or zpool returns unknown at that step and stops. Guessing
//       here would mean recommending a migration based on a device that was never identified.
//
//   Both script runs are externally time-boxed.
//       `timeout` wraps each — set_time_limit() does not cover exec() time on Linux, so PHP's
//       own limit cannot end a stalled migration or a key renewal hung on an unreachable
//       partner. Exit 124 is reported as a timeout, distinctly from a script failure.
//
//   Missing scripts are reported, not executed.
//       file_exists() precedes both exec() calls, so a partial deploy returns a named error
//       rather than a shell failure that would look like a failed migration.
//
//   Migration and detection are POST-only. status and api_status are the only GET-reachable
//   actions, and both are pure reads.
//
//   Handle with care: the detect action writes HOST<n>_STORAGE_MODE_INTERNAL.
//       That value decides where the plugin's data lives, and on HOST1 it is deliberately
//       pinned. Running detect on a host whose boot device reports an unexpected transport
//       would rewrite it. The action exists for onboarding a new host, where nothing has been
//       decided yet — it is not a repair tool for an established one.
//
//   The conf write goes through the structured writer, so it is atomic and syntax-checked —
//   which is also why include/confform.php must be loaded here. It was previously absent and
//   the detect action fataled on an undefined function.
//
// REQUEST
//   GET|POST ?action=status         current mode, detected mode, boot device, conf value
//   GET|POST ?action=api_status     per-host API key presence and local API health
//   POST action=migrate  to=internal|flash
//   POST action=detect              write the detected mode into this host's conf
//   POST action=setup_apikeys       run unraid_api_key_renew.sh
//
// RESPONSE
//   status         {"ok":true,"current_mode","current_dir","internal_dir","flash_dir",
//                   "transport","detected","boot_disk","conf_key","conf_val","array_started"}
//   api_status     {"ok":true,"my_id","key_name","hosts":[…],"fallbacks":…}
//   migrate        {"ok":bool,"exit":int,"output":"…"}
//   detect         {"ok":bool,"detected":"true|false","transport":"…"}
//   setup_apikeys  {"ok":bool,"exit":int,"output":"…"}
//   {"ok":false,"error":"Invalid target: …"|"storage_migrate.sh not found"
//                      |"unraid_api_key_renew.sh not found"|"… timed out …"|"Unknown action"}
//
// DEPENDS ON
//   include/config.php       SCRIPTS_DIR, vv_detect_host(), vv_conf_vars()
//   include/confform.php     vv_conf_write_changes()
//   include/unraid_api.php   vv_api_get_status()   (loaded only by api_status)
//   Tools/storage_migrate.sh
//   System_Essentials/unraid_api_key_renew.sh
//   findmnt, lsblk, zpool
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
// vv_conf_write_changes() lives here — the detect action fatals without it.
require_once dirname(__DIR__) . '/include/confform.php';

$action = $_GET['action'] ?? $_POST['action'] ?? '';

// ── Boot device detection ─────────────────────────────────────────────────────
function vv_storage_detect_transport(): string {
    $src = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    if (!$src) return 'unknown';

    if (str_starts_with($src, '/dev/')) {
        // Block device partition — walk up to whole disk
        $disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($src) . " 2>/dev/null") ?: '');
        if (!$disk) return 'unknown';
    } else {
        // ZFS dataset (pool/dataset) — find a backing device via zpool
        $pool = explode('/', $src)[0];
        $dev  = trim(shell_exec("zpool list -vHp " . escapeshellarg($pool) . " 2>/dev/null | awk 'NR>1 && \$1~/^[a-z]/ && \$1!~/mirror|raidz|spare/{print \$1; exit}'") ?: '');
        if (!$dev) return 'unknown';
        // Strip partition suffix to get the whole-disk name
        $disk = trim(shell_exec("lsblk -no pkname /dev/" . escapeshellarg($dev) . " 2>/dev/null") ?: $dev);
    }

    return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown'));
}

// ── Current mode status ───────────────────────────────────────────────────────
if ($action === 'status') {
    $transport   = vv_storage_detect_transport();
    $detected    = ($transport === 'usb') ? 'flash' : 'internal';
    $currentDir  = SCRIPTS_DIR;
    $internalDir = '/boot/config/plugins/varaverk';
    $flashDir    = '/mnt/user/appdata/Varaverk';
    $currentMode = ($currentDir === $internalDir) ? 'internal'
                 : ($currentDir === $flashDir     ? 'flash' : 'custom');

    $myHost  = vv_detect_host();
    $vars    = vv_conf_vars();
    $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confVal = $vars[$confKey] ?? null;

    // Boot device name for display
    $bootSrc  = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    $bootDisk = '';
    if (str_starts_with($bootSrc, '/dev/')) {
        $bootDisk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootSrc) . " 2>/dev/null") ?: '');
        $bootDisk = $bootDisk ? '/dev/' . $bootDisk : '';
    } elseif ($bootSrc) {
        $bootDisk = $bootSrc; // ZFS: show "flash/boot"
    }

    echo json_encode([
        'ok'           => true,
        'current_mode' => $currentMode,
        'current_dir'  => $currentDir,
        'internal_dir' => $internalDir,
        'flash_dir'    => $flashDir,
        'transport'    => $transport,
        'detected'     => $detected,
        'boot_disk'    => $bootDisk ?: 'unknown',
        'conf_key'     => $confKey,
        'conf_val'     => $confVal,
        'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2,
    ]);
    exit;
}

// ── Run migration ─────────────────────────────────────────────────────────────
if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $to = trim($_POST['to'] ?? '');
    if (!in_array($to, ['internal', 'flash'], true)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']);
        exit;
    }

    $script = dirname(__DIR__) . '/Tools/storage_migrate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
        exit;
    }

    // set_time_limit() does not cover exec() time on Linux, so the bound is external.
    set_time_limit(630);
    $output = [];
    $exit   = 0;
    exec('timeout 600 bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit);
    if ($exit === 124) {
        echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh timed out after 600s', 'output' => implode("\n", $output)]);
        exit;
    }

    echo json_encode([
        'ok'     => $exit === 0,
        'exit'   => $exit,
        'output' => implode("\n", $output),
    ]);
    exit;
}

// ── Auto-detect and write to conf ─────────────────────────────────────────────
if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $transport = vv_storage_detect_transport();
    $detected  = ($transport === 'usb') ? 'false' : 'true';
    $myHost    = vv_detect_host();
    $confKey   = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confFile  = $myHost . '.conf';

    $results = vv_conf_write_changes([[
        'file'  => $confFile,
        'key'   => $confKey,
        'value' => $detected,
        'type'  => 'scalar',
    ]]);

    $ok = !in_array(false, $results, true);
    echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]);
    exit;
}

// ── Unraid API key status ─────────────────────────────────────────────────────
if ($action === 'api_status') {
    require_once dirname(__DIR__) . '/include/unraid_api.php';
    $localStatus = vv_api_get_status();
    $vars        = vv_conf_vars();
    $myHost      = vv_detect_host();
    $myId        = strtoupper($myHost);
    $hn          = trim((string)shell_exec("hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//'")) ?: 'Varaverk';
    $localKeyName = 'Varaverk ' . $hn;

    $hosts = [];
    foreach ($vars as $k => $v) {
        if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue;
        $id      = 'HOST' . $m[1];
        $keyVar  = $id . '_UNRAID_API_KEY';
        $key     = $vars[$keyVar] ?? '';
        $isLocal = ($id === $myId);
        $hosts[] = [
            'host_id'     => $id,
            'hostname'    => $v,
            'is_local'    => $isLocal,
            'key_var'     => $keyVar,
            'key_present' => !empty($key),
            'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
            'api_ok'      => $isLocal
                             ? (!$localStatus['key_missing'] && $localStatus['available'])
                             : !empty($key),
        ];
    }
    usort($hosts, fn($a, $b) => strcmp($a['host_id'], $b['host_id']));

    echo json_encode([
        'ok'        => true,
        'my_id'     => $myId,
        'key_name'  => $localKeyName,
        'hosts'     => $hosts,
        'fallbacks' => $localStatus['fallbacks'],
    ]);
    exit;
}

// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $script = SCRIPTS_DIR . '/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
    }
    // set_time_limit() does not cover exec() time on Linux — this script reaches every
    // partner over SSH, so the bound has to be external.
    set_time_limit(150);
    $output = []; $exit = 0;
    exec('timeout 120 bash ' . escapeshellarg($script) . ' 2>&1', $output, $exit);
    if ($exit === 124) {
        echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh timed out after 120s']); exit;
    }
    echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]);
    exit;
}

echo json_encode(['ok' => false, 'error' => 'Unknown action']);
