<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Arr data endpoint. Serves the arrs tab: local Sonarr/Radarr/Lidarr statistics gathered
//   live, partner statistics served from the background cache, plus an on-demand refresh of
//   one partner's cache.
//
// OPERATIONAL MODEL
//   Two modes on one URL. Without parameters it answers from a 300s cache and only builds a
//   fresh payload on a miss. With ?action=refresh_remote&host=hostN it shells out to
//   Tools/remote_arr_cache_writer.sh for that one host, returns the newly written node, and
//   busts the main cache so the next ordinary poll picks the change up.
//
// DESIGN PRINCIPLES
//   The refresh branch runs before the cache read, and exits.
//       It is a distinct operation, not a cache-control flag, so it never falls through into
//       the normal load path and cannot return a stale payload labelled as refreshed.
//
//   Cache first for the ordinary case.
//       vv_arrs_all() contacts every configured arr instance. At the tab's poll rate that is
//       far too expensive to repeat, so the 300s cache is the default path and the live build
//       is the exception.
//
//   include/arrs.php is required only on a cache miss.
//       A cache hit answers without loading the library at all, which is the difference
//       between a poll that costs a file read and one that costs an autoload.
//
// OPERATIONAL SAFEGUARDS
//   The host parameter is matched against a pattern, never used as a path.
//       ^host\d+$ is enforced before the value goes anywhere. It reaches the script only as
//       an escapeshellarg'd --host= value and the cache filename it composes, so neither a
//       shell metacharacter nor a traversal sequence can survive the check.
//
//   The refresh is externally time-boxed.
//       set_time_limit() does not count time spent inside exec() on Linux, so PHP's own limit
//       cannot end a hung SSH call — the child is wrapped in `timeout` instead. Exit 124 is
//       reported as a timeout rather than a generic failure, so the UI can distinguish an
//       unreachable partner from a broken script.
//
//   A missing script is reported, not executed.
//       The file_exists() check runs before exec(), so a partial deploy returns a named error
//       rather than a shell "command not found" surfacing as an empty refresh.
//
//   The cache is busted after the write, not before.
//       @unlink() of arrs.json follows the script run, so a failed refresh leaves the previous
//       good payload in place instead of forcing every subsequent poll onto the live path.
//
//   Read-only with respect to the arrs themselves. Statistics come from the databases the
//   scripts write; nothing here triggers a scan, cleanup, or import.
//
// REQUEST
//   GET                                          cached (300s) full payload
//   POST action=refresh_remote  host=host<n>     re-run the partner cache writer for one host
//                                                (POST, so Unraid's CSRF guard applies)
//
// RESPONSE
//   normal   vv_arrs_all() verbatim — local node live, remote nodes cached with cache_age
//   refresh  {"ok":bool,"node":object|null,"output":string}
//   errors   {"ok":false,"error":string}  — invalid host, missing script, or timeout
//
// DEPENDS ON
//   include/config.php            vv_cache_read(), VV_CACHE_DIR
//   include/arrs.php             vv_arrs_all()          (loaded only on a cache miss)
//   Tools/remote_arr_cache_writer.sh                     (refresh branch only)
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

// Bound on the partner refresh child process. set_time_limit() does not cover exec() time
// on Linux, so this is enforced by `timeout`, not by PHP.
define('VV_ARRS_REFRESH_TIMEOUT', 120);

// ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
// POST only. The refresh executes a script, and Unraid's CSRF prepend validates POSTs while
// ignoring GETs entirely — so reaching this over GET would mean running it with no token
// check. A GET naming the action is refused rather than falling through to the cached read,
// so a stale caller fails visibly instead of silently appearing to succeed.
if (trim($_GET['action'] ?? '') === 'refresh_remote') {
    http_response_code(405);
    echo json_encode(['ok' => false, 'error' => 'POST only']); exit;
}
$_action = ($_SERVER['REQUEST_METHOD'] === 'POST') ? trim($_POST['action'] ?? '') : '';
if ($_action === 'refresh_remote') {
    $host = strtolower(trim($_POST['host'] ?? ''));
    if (!preg_match('/^host\d+$/', $host)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
    }
    $script = dirname(__DIR__) . '/Tools/remote_arr_cache_writer.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
    }
    set_time_limit(VV_ARRS_REFRESH_TIMEOUT + 30);
    $out = []; $exit = 0;
    exec('timeout ' . VV_ARRS_REFRESH_TIMEOUT . ' bash ' . escapeshellarg($script)
         . ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);

    if ($exit === 124) {
        echo json_encode([
            'ok'     => false,
            'error'  => 'Refresh timed out after ' . VV_ARRS_REFRESH_TIMEOUT . 's',
            'output' => implode("\n", $out),
        ]);
        exit;
    }

    $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $host . '.json';
    $node = null;
    if (file_exists($cacheFile)) {
        $node = json_decode(file_get_contents($cacheFile), true) ?: null;
        if ($node) {
            $node['cached']    = true;
            $node['cache_age'] = time() - (int)filemtime($cacheFile);
        }
    }
    // Bust the main arrs cache so next poll gets fresh data
    @unlink(VV_CACHE_DIR . '/arrs.json');
    echo json_encode(['ok' => $exit === 0, 'node' => $node, 'output' => implode("\n", $out)]);
    exit;
}
unset($_action);

// ── Normal data load ──────────────────────────────────────────────────────────
$_vv_cached = vv_cache_read('arrs', 300);
if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
unset($_vv_cached);

require_once dirname(__DIR__) . '/include/arrs.php';
echo json_encode(vv_arrs_all());
