<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Arr data layer for the arrs page. Discovers every Sonarr / Radarr / Lidarr instance
//   declared in the host confs, fetches live library counts and queue state from each, and
//   attaches the cleanup / discovery / sync / recovery statistics the scripts have recorded.
//
// DESIGN PRINCIPLES
//   Each host builds its own payload; nobody queries a partner's arrs.
//       vv_arrs_local_node() is what remote_arr_cache_writer.sh invokes over SSH on the
//       partner, so the partner assembles its own node using its own local URLs and keys.
//       This host therefore never holds credentials for a remote's arrs, and no path mapping
//       between hosts is involved.
//
//   Live for local, cache for remote.
//       vv_arrs_all() calls the local node live and reads remote nodes from the JSON the
//       cache writer left behind. Cross-host work happens on a 2h timer, never in a page load.
//
//   Instances come from conf, not from probing.
//       vv_discover_arrs() enumerates what the host confs declare. An arr that exists but is
//       not configured is intentionally invisible — conf is the source of truth.
//
// OPERATIONAL SAFEGUARDS
//   Every arr HTTP call is time-boxed.
//       vv_arr_http() defaults to a 4s timeout and returns null on any failure. One
//       unreachable instance costs four seconds, not the page.
//
//   A missing remote cache is reported, not faked.
//       No cache file yields an explicit cache_miss => true with an empty arrs list, so the
//       page can say "not yet collected" rather than implying the partner has no libraries.
//
//   Cached remote nodes carry their own age.
//       cache_age is attached to every cached node so the UI can show staleness instead of
//       presenting 2-hour-old counts as current.
//
//   Read-only. Statistics are parsed from the databases the scripts write; nothing here
//   triggers a scan, cleanup, or import.
//
// EXPORTS
//   Discovery   vv_discover_arrs(), vv_arr_known_hosts(), vv_arr_node_names()
//   Fetch       vv_arr_http(), vv_fetch_arr_live()
//   Statistics  vv_arr_cleanup_stats(), vv_arr_discovery_stats(), vv_arr_sync_stats(),
//               vv_arr_recovery_stats()
//   Assembly    vv_arrs_local_node()  ← called over SSH by remote_arr_cache_writer.sh
//               vv_arrs_all()         ← local live + remote cached
//
// CONFIGURATION
//   HOST*_SONARR_URL / _RADARR_URL / _LIDARR_URL              per-instance endpoints
//   HOST*_SONARR_API_KEY / _RADARR_API_KEY / _LIDARR_API_KEY  per-instance keys
//   HOST*_SONARR_TV_ROOT / _RADARR_MOVIES_ROOT / _LIDARR_MUSIC_ROOT
//   ARR_SYNC_SONARR_PORT / _RADARR_PORT / _LIDARR_PORT        used to reach partner instances
//   DATA_DIR      arr_cleanup_stats.db, arr_recovery_stats.db, <type>_discovery_history.db
//   VV_CACHE_DIR  arrs_remote_<host>.json — written by remote_arr_cache_writer.sh
// ═══════════════════════════════════════════════════════════════════════════════════════════════

require_once __DIR__ . '/config.php';

// ── Conf helpers ──────────────────────────────────────────────────────────────

function vv_arr_scalar(string $raw, string $key): string {
    return vv_parse_conf_scalar($raw, $key);
}

function vv_arr_known_hosts(): array {
    return vv_known_hosts();
}

function vv_arr_node_names(): array {
    return vv_arr_known_hosts();
}

// ── Discovery ─────────────────────────────────────────────────────────────────

function vv_discover_arrs(): array {
    $nodes = [];
    $defs  = [
        'sonarr' => ['SONARR_URL', 'SONARR_API_KEY', 'SONARR_TV_ROOT',     'v3'],
        'radarr' => ['RADARR_URL', 'RADARR_API_KEY', 'RADARR_MOVIES_ROOT', 'v3'],
        'lidarr' => ['LIDARR_URL', 'LIDARR_API_KEY', 'LIDARR_MUSIC_ROOT',  'v1'],
    ];
    foreach (array_keys(vv_arr_known_hosts()) as $h) {
        $raw = vv_read_conf_raw($h . '.conf');
        if (!$raw) continue;
        $pfx  = strtoupper($h) . '_';
        $get  = fn($k) => vv_arr_scalar($raw, $pfx . $k);
        $arrs = [];
        foreach ($defs as $type => [$uk, $ak, $rk, $api]) {
            $url = $get($uk);
            $key = $get($ak);
            if ($url && $key && !str_contains($key, 'your-')) {
                $arrs[] = ['type' => $type, 'url' => $url, 'key' => $key,
                           'root' => $get($rk), 'api' => $api];
            }
        }
        if ($arrs) $nodes[] = ['host' => $h, 'arrs' => $arrs];
    }
    return $nodes;
}

// ── HTTP ──────────────────────────────────────────────────────────────────────

function vv_arr_http(string $url, string $apiKey, string $path, int $timeout = 4): ?array {
    $ctx = stream_context_create(['http' => [
        'timeout'       => $timeout,
        'header'        => "X-Api-Key: $apiKey\r\nAccept: application/json\r\n",
        'ignore_errors' => true,
    ]]);
    $raw = @file_get_contents(rtrim($url, '/') . $path, false, $ctx);
    return $raw ? (json_decode($raw, true) ?: null) : null;
}

// ── Live arr data ─────────────────────────────────────────────────────────────

function vv_fetch_arr_live(array $arr): array {
    $url  = $arr['url'];
    $key  = $arr['key'];
    $base = '/api/' . $arr['api'];
    $type = $arr['type'];

    $out = ['online' => false, 'version' => null, 'health' => [],
            'queue'  => ['dl' => 0, 'warn' => 0, 'err' => 0], 'disk' => []];

    $sys = vv_arr_http($url, $key, "$base/system/status");
    if (!$sys) return $out;
    $out['online']  = true;
    $out['version'] = $sys['version'] ?? null;

    if ($type === 'sonarr') {
        $data = vv_arr_http($url, $key, "$base/series");
        if (is_array($data)) {
            $out['total']     = count($data);
            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
            $out['episodes']  = array_sum(array_map(
                fn($x) => $x['statistics']['episodeFileCount'] ?? $x['episodeFileCount'] ?? 0, $data));
        }
    } elseif ($type === 'radarr') {
        $data = vv_arr_http($url, $key, "$base/movie");
        if (is_array($data)) {
            $out['total']     = count($data);
            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
            $out['files']     = count(array_filter($data, fn($x) => !empty($x['hasFile'])));
        }
    } elseif ($type === 'lidarr') {
        $data = vv_arr_http($url, $key, "$base/artist");
        if (is_array($data)) {
            $out['total']     = count($data);
            $out['monitored'] = count(array_filter($data, fn($x) => !empty($x['monitored'])));
            $out['albums']    = array_sum(array_map(
                fn($a) => $a['statistics']['albumCount'] ?? $a['albumCount'] ?? 0, $data));
        }
    }

    $q = vv_arr_http($url, $key, "$base/queue?page=1&pageSize=500");
    if (is_array($q)) {
        foreach (($q['records'] ?? $q) as $r) {
            if (!is_array($r)) continue;
            $s   = $r['status'] ?? '';
            $tds = strtolower($r['trackedDownloadStatus'] ?? '');
            $tst = strtolower($r['trackedDownloadState']  ?? '');
            if ($s === 'downloading')                           $out['queue']['dl']++;
            if ($tds === 'warning' || $tst === 'downloadingstalled') $out['queue']['warn']++;
            if ($tds === 'error')                               $out['queue']['err']++;
        }
    }

    $h = vv_arr_http($url, $key, "$base/health");
    if (is_array($h)) $out['health'] = $h;

    $d = vv_arr_http($url, $key, "$base/diskspace");
    if (is_array($d)) $out['disk'] = $d;

    return $out;
}

// ── Log stats ─────────────────────────────────────────────────────────────────

function vv_arr_cleanup_stats(string $type): array {
    $slugs = ['sonarr' => 'Arrs_Stack/sonarr_cleanup',
              'radarr' => 'Arrs_Stack/radarr_cleanup',
              'lidarr' => 'Arrs_Stack/lidarr_cleanup'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'end' => null, 'status' => null,
             'tracked'  => null, 'total' => null,
             'orphans'  => 0,    'orphans_sz' => '0B', 'junk' => 0];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['end']      = $meta['end']    ?? null;
        $out['status']   = $meta['status'] ?? null;

        $lf = $base . '.log';
        if (file_exists($lf)) {
            $log   = file_get_contents($lf);
            $parts = preg_split('/━{3,}[^\n]*SUMMARY[^\n]*/u', $log);
            $blk   = count($parts) > 1 ? end($parts) : $log;

            if (preg_match('/Tracked:\s*([\d,]+)\s*files\s*\(([\d,]+)/u', $blk, $m)) {
                $out['tracked'] = (int)str_replace(',', '', $m[1]);
                $out['total']   = (int)str_replace(',', '', $m[2]);
            }
            if (preg_match('/Orphans:\s*([\d,]+)\s*files\s*\(([^)]+)\)/u', $blk, $m)) {
                $out['orphans']    = (int)str_replace(',', '', $m[1]);
                $out['orphans_sz'] = trim($m[2]);
            }
            if (preg_match('/Junk:\s*([\d,]+)\s*files/u', $blk, $m)) {
                $out['junk'] = (int)str_replace(',', '', $m[1]);
            }
        }
    }

    // Fallback: daily aggregate db — date|arr|orphan_count|orphan_bytes|junk_count|junk_bytes|recent_count|tracked_count
    if ($out['last_run'] === null) {
        $dbFile = DATA_DIR . '/arr_cleanup_stats.db';
        if (file_exists($dbFile)) {
            $last = null;
            foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $p = explode('|', $line);
                if (count($p) >= 8 && $p[1] === $type) $last = $p;
            }
            if ($last) {
                $out['last_run'] = strtotime($last[0] . ' 23:59:00') ?: null;
                $out['status']   = 'ok';
                $out['orphans']  = (int)$last[2];
                $out['junk']     = (int)$last[4];
                $out['tracked']  = (int)$last[7];
            }
        }
    }

    return $out;
}

function vv_arr_discovery_stats(string $type): array {
    $slugs = ['sonarr' => 'Arrs_Stack/playback_aware_sonarr_discovery',
              'radarr' => 'Arrs_Stack/playback_aware_radarr_discovery',
              'lidarr' => 'Arrs_Stack/playback_aware_lidarr_discovery'];
    $base = LOG_DIR . '/' . ($slugs[$type] ?? '');
    $out  = ['last_run' => null, 'status' => null, 'added' => null];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['status']   = $meta['status'] ?? null;

        $lf = $base . '.log';
        if (file_exists($lf)) {
            $log = file_get_contents($lf);
            if (preg_match('/Added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
            elseif (preg_match('/(\d+)\s+added/i', $log, $m)) $out['added'] = (int)$m[1];
        }
    }

    // Fallback: per-title history db — status|id|date[|title]
    if ($out['last_run'] === null) {
        $dbFile = DATA_DIR . '/' . $type . '_discovery_history.db';
        if (file_exists($dbFile)) {
            $lastDate = null; $added = 0;
            foreach (file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $p = explode('|', $line);
                if (count($p) < 3) continue;
                $date = $p[2];
                if ($date !== $lastDate) { $lastDate = $date; $added = 0; }
                if ($p[0] === 'ACCEPT') $added++;
            }
            if ($lastDate) {
                $out['last_run'] = strtotime($lastDate . ' 23:59:00') ?: null;
                $out['status']   = 'ok';
                $out['added']    = $added;
            }
        }
    }

    return $out;
}

function vv_arr_sync_stats(): array {
    $base = LOG_DIR . '/Arrs_Stack/arr_sync';
    $out  = ['last_run' => null, 'status' => null, 'added' => null,
             'nodes'    => null, 'blocklist_count' => null];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['status']   = $meta['status'] ?? null;
    }

    $master = vv_read_conf_raw('master.conf');
    if (preg_match('/ARR_SYNC_BLOCKLIST\s*=\s*"?([^"\n#]+)"?/m', $master, $m)) {
        $blPath = trim($m[1]);
        if (file_exists($blPath)) {
            $out['blocklist_count'] = count(array_filter(
                file($blPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)));
        }
    }

    $lf = $base . '.log';
    if (file_exists($lf)) {
        $log = file_get_contents($lf);
        if (preg_match('/Total added[:\s]+(\d+)/i', $log, $m)) $out['added'] = (int)$m[1];
        if (preg_match('/Nodes?[:\s]+(\d+)/i',      $log, $m)) $out['nodes'] = (int)$m[1];
    }
    return $out;
}

function vv_arr_recovery_stats(): array {
    $base = LOG_DIR . '/Arrs_Stack/arrs_failed_stalled_recovery';
    $out  = ['last_run' => null, 'status' => null, 'fixed' => 0, 'searched' => 0];

    $jf = $base . '.json';
    if (file_exists($jf)) {
        $meta = json_decode(file_get_contents($jf), true) ?: [];
        $out['last_run'] = $meta['start']  ?? null;
        $out['status']   = $meta['status'] ?? null;

        $lf = $base . '.log';
        if (file_exists($lf)) {
            $log = file_get_contents($lf);
            if (preg_match('/Removed[:\s]+(\d+)/i',   $log, $m)) $out['fixed']    = (int)$m[1];
            if (preg_match('/Re-searched[:\s]+(\d+)/i',$log, $m)) $out['searched'] = (int)$m[1];
        }
    }

    // Fallback: daily aggregate db — date|time|count|bytes
    if ($out['last_run'] === null) {
        $dbFile = DATA_DIR . '/arr_recovery_stats.db';
        if (file_exists($dbFile)) {
            $lines = file($dbFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            $last  = $lines ? end($lines) : null;
            if ($last) {
                $p = explode('|', $last);
                if (count($p) >= 3) {
                    $ts = strtotime(($p[0] ?? '') . ' ' . ($p[1] ?? '00:00')) ?: null;
                    if ($ts) {
                        $out['last_run'] = $ts;
                        $out['status']   = 'ok';
                        $out['fixed']    = (int)($p[2] ?? 0);
                    }
                }
            }
        }
    }

    return $out;
}

// ── Local node only — called via SSH by remote_arr_cache_writer.sh on remote hosts ───────────

function vv_arrs_local_node(): array {
    $host  = vv_detect_host();
    $names = vv_arr_node_names();
    $arrs  = [];

    foreach (vv_discover_arrs() as $node) {
        if ($node['host'] !== $host) continue;
        foreach ($node['arrs'] as $arr) {
            $entry = ['type' => $arr['type'], 'root' => $arr['root']];
            $entry = array_merge($entry, vv_fetch_arr_live($arr));
            $entry['cleanup']   = vv_arr_cleanup_stats($arr['type']);
            $entry['discovery'] = vv_arr_discovery_stats($arr['type']);
            $arrs[] = $entry;
        }
        break;
    }

    return [
        'host'  => $host,
        'name'  => $names[$host] ?? strtoupper($host),
        'local' => true,
        'arrs'  => $arrs,
    ];
}

// ── Entry point ───────────────────────────────────────────────────────────────

function vv_arrs_all(): array {
    $currentHost = vv_detect_host();
    $names       = vv_arr_node_names();

    // Local node — full live data
    $result = [vv_arrs_local_node()];

    // Remote nodes — read from file cache written by remote_arr_cache_writer.sh
    foreach (array_keys($names) as $h) {
        if ($h === $currentHost) continue;
        $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $h . '.json';
        if (file_exists($cacheFile)) {
            $node = json_decode(file_get_contents($cacheFile), true) ?: [];
            $node['cached']    = true;
            $node['cache_age'] = time() - (int)filemtime($cacheFile);
            $result[] = $node;
        } else {
            $result[] = [
                'host'       => $h,
                'name'       => $names[$h],
                'local'      => false,
                'cached'     => false,
                'cache_miss' => true,
                'arrs'       => [],
            ];
        }
    }

    $vars = vv_conf_vars();
    $myId = strtoupper($currentHost);

    return [
        'nodes'    => $result,
        'sync'     => vv_arr_sync_stats(),
        'recovery' => vv_arr_recovery_stats(),
        'host'     => $currentHost,
        'settings' => [
            'arr_sync_enabled'   => ($vars['ARR_SYNC_ENABLED']               ?? 'true') !== 'false',
            'sonarr_recovery'    => ($vars[$myId . '_SONARR_RECOVERY']       ?? 'true') !== 'false',
            'radarr_recovery'    => ($vars[$myId . '_RADARR_RECOVERY']       ?? 'true') !== 'false',
            'lidarr_recovery'    => ($vars[$myId . '_LIDARR_RECOVERY']       ?? 'true') !== 'false',
            'recovery_age_hours' => (int)($vars['ARR_IMPORT_RECOVERY_AGE']   ?? 6),
            'sonarr_port'        => (int)($vars['ARR_SYNC_SONARR_PORT']      ?? 8989),
            'radarr_port'        => (int)($vars['ARR_SYNC_RADARR_PORT']      ?? 7878),
            'lidarr_port'        => (int)($vars['ARR_SYNC_LIDARR_PORT']      ?? 8686),
            'my_host'            => $currentHost,
            'my_id'              => $myId,
        ],
        'ts'       => time(),
    ];
}
