<?php
require_once __DIR__ . '/common.php';

// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Monitor-page roll-ups that are not raw system metrics: partner reachability, fallback
//   state, the single-glance watchdog health summary, script run status, and rsync progress.
//   Raw hardware numbers come from common.php; this file answers "is anything wrong".
//
// DESIGN PRINCIPLES
//   One boolean has to be trustworthy.
//       vv_watchdog_summary() reduces every watchdog's state to 'healthy'. It is the only
//       thing most people look at, so it is conjunctive — healthy requires every strike set
//       empty, every level zero, the NIC up and sshd alive. Any doubt resolves to not-healthy.
//
//   State paths derive from STATE_DIR, never hardcoded.
//       The watchdogs write under STATE_DIR, which follows SCRIPTS_DIR through a storage-mode
//       migration. Hardcoding an absolute path here silently decouples the page from the
//       scripts — see OPERATIONAL SAFEGUARDS.
//
//   Summarises; does not re-derive.
//       Strike counts come from the files the watchdogs wrote. This file never recomputes
//       whether a container is unhealthy — that decision belongs to the watchdog that owns it.
//
// OPERATIONAL SAFEGUARDS
//   Missing state must not read as healthy — and once did.
//       Six state files were read from /tmp while the watchdogs write to STATE_DIR. Every
//       read returned empty, every strike set came back clear, and 'healthy' was therefore
//       always true: a permanent false all-clear on the page whose whole job is raising the
//       alarm. Fixed 2026-08-02. If a strike set ever looks suspiciously empty, verify the
//       path against where the watchdog actually writes before trusting it.
//
//   Live stability probes are cheap and time-boxed.
//       df, sensors, pgrep and ps run per render, so each is a single command with stderr
//       discarded and a scalar result. Nothing here iterates over containers or disks.
//
//   Shell arguments are escaped.
//       Paths passed to df go through escapeshellarg(); the NIC name is read from sysfs
//       rather than interpolated from user input.
//
//   Read-only. Reports on watchdogs, fallback and scripts; never starts, stops, or clears any
//   of them.
//
// EXPORTS
//   vv_partner_state()      partner reachability and identity
//   vv_fallback_state()     current fallback state for this host
//   vv_fallback_active()    whether this host is currently covering, and what
//   vv_watchdog_summary()   the health roll-up described above
//   vv_scripts_status()     last-run status per scheduled script
//   vv_rsync_status()       current/last rsync progress
//
// CONFIGURATION
//   STATE_DIR   fallback_state.db, container/resource/system/storage/network watchdog state,
//               system_watchdog_oom.db, system_watchdog_reboots.db
//   DATA_DIR    container_restart_history.db
// ═══════════════════════════════════════════════════════════════════════════════════════════════

function vv_partner_state(): array {
    $vars     = vv_conf_vars();
    $myHostId = strtoupper(vv_detect_host());

    // Parse Tailscale peer status once
    $tsData  = json_decode(shell_exec('tailscale status --json 2>/dev/null') ?: '{}', true) ?? [];
    $tsPeers = [];
    foreach ($tsData['Peer'] ?? [] as $peer) {
        // DNSName is "hostname.tailnet.ts.net." — take the first label (full, not truncated)
        $dns = $peer['DNSName'] ?? '';
        $h   = $dns ? strtolower(explode('.', $dns)[0]) : strtolower($peer['HostName'] ?? '');
        if ($h) $tsPeers[$h] = (bool)($peer['Online'] ?? false);
    }

    $hostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== '');
    sort($hostIds);

    $setupDb = vv_setup_state_read();

    $hosts = [];
    foreach ($hostIds as $id) {
        $hostname = $vars[$id] ?? '';
        if (!$hostname) continue;
        $isMe    = ($id === $myHostId);
        $isOwner = strcasecmp($id, $vars['PARTNERSHIP_OWNER_HOST'] ?? '') === 0;
        $online  = $isMe ? true : ($tsPeers[strtolower($hostname)] ?? null);
        $onboardPhase = $isMe ? null
            : (($setupDb[$id . '_PHASE2_DONE'] ?? '') === 'true' ? 2
              : (($setupDb[$id . '_PHASE1_DONE'] ?? '') === 'true' ? 1 : 0));
        $hosts[] = [
            'id'            => $id,
            'hostname'      => $hostname,
            'owner'         => $vars[$id . '_OWNER'] ?? '',
            'is_me'         => $isMe,
            'is_owner'      => $isOwner,
            'online'        => $online,
            'onboard_phase' => $onboardPhase,
        ];
    }

    return [
        'enabled'    => ($vars['PARTNERSHIP_ENABLED'] ?? 'false') === 'true',
        'owner_host' => $vars['PARTNERSHIP_OWNER_HOST'] ?? '',
        'sync_min'   => (int)($vars['PARTNERSHIP_SYNC_INTERVAL'] ?? 15),
        'hosts'      => $hosts,
    ];
}

function vv_fallback_state(): array {
    $vars      = vv_conf_vars();
    $enabled   = ($vars['FALLBACK_ENABLED']           ?? 'false') === 'true';
    $interval  = (int)($vars['FALLBACK_CHECK_INTERVAL']    ?? 30);
    $reqStrikes = (int)($vars['FALLBACK_HANDBACK_STRIKES'] ?? 3);
    $suspendAfter = (int)($vars['FALLBACK_PARTNERSHIP_SUSPEND_AFTER'] ?? 120);

    $stateFile = STATE_DIR . '/fallback_state.db';
    if (!file_exists($stateFile)) {
        return ['state' => 'UNKNOWN', 'enabled' => $enabled, 'check_interval' => $interval,
                'handback_strikes' => 0, 'handback_strikes_required' => $reqStrikes,
                'partnership_suspended' => false,
                'partner_lost_at' => 0, 'partnership_suspend_after' => $suspendAfter];
    }
    $raw = [];
    foreach (file($stateFile) ?: [] as $line) {
        [$k, $v] = array_pad(explode('=', trim($line), 2), 2, '');
        $raw[trim($k)] = trim($v);
    }
    return [
        'state'                      => $raw['state']               ?? 'UNKNOWN',
        'failover_start'             => $raw['failover_start']       ?? '0',
        'tier2_started'              => $raw['tier2_started']        ?? 'false',
        'tier3_started'              => $raw['tier3_started']        ?? 'false',
        'tier4_started'              => $raw['tier4_started']        ?? 'false',
        'handback_strikes'           => (int)($raw['handback_strikes']    ?? 0),
        'partnership_suspended'      => ($raw['partnership_suspended']    ?? 'false') === 'true',
        'partner_lost_at'            => (int)($raw['partner_lost_at']     ?? 0),
        'enabled'                    => $enabled,
        'check_interval'             => $interval,
        'handback_strikes_required'  => $reqStrikes,
        'partnership_suspend_after'  => $suspendAfter,
    ];
}

function vv_fallback_active(): array {
    $vars = vv_conf_vars();

    // Identify which HOST id we are
    $allHostIds = array_filter(array_keys($vars), fn($k) => preg_match('/^HOST\d+$/', $k) && ($vars[$k] ?? '') !== '');
    sort($allHostIds);
    $myId = strtoupper(vv_detect_host());
    if ($myId === 'UNKNOWN' || !in_array($myId, $allHostIds, true)) return [];

    // Running containers: name → image
    $running = [];
    $psOut = shell_exec("docker ps --format '{\"n\":\"{{.Names}}\",\"i\":\"{{.Image}}\"}' 2>/dev/null") ?: '';
    foreach (explode("\n", trim($psOut)) as $line) {
        $c = json_decode($line, true);
        if ($c) $running[strtolower($c['n'])] = $c['i'];
    }

    $result = [];
    foreach ($allHostIds as $covered) {
        if ($covered === $myId) continue;
        $coveredHostname = $vars[$covered] ?? '';
        if (!$coveredHostname) continue;

        // Covered host defines its own recovery profile — read from their conf
        $coveredRaw = vv_read_conf_raw(strtolower($covered) . '.conf');
        $names = [];
        for ($tier = 1; $tier <= 4; $tier++)
            $names = array_merge($names, vv_parse_bash_array($coveredRaw, "FALLBACK_{$covered}_TIER{$tier}"));

        $active = [];
        foreach ($names as $name) {
            if (isset($running[strtolower($name)]))
                $active[] = ['name' => $name, 'image' => $running[strtolower($name)]];
        }

        if ($active) $result[] = ['host_id' => $covered, 'hostname' => $coveredHostname, 'containers' => $active];
    }

    return $result;
}

function vv_watchdog_summary(): array {
    $parseKv = function(string $raw): array {
        $out = [];
        foreach (explode("\n", $raw) as $line) {
            $line = trim($line);
            if (str_contains($line, ':'))      { [$k, $v] = explode(':', $line, 2); $out[trim($k)] = trim($v); }
            elseif (str_contains($line, '='))  { [$k, $v] = explode('=', $line, 2); $out[trim($k)] = trim($v, '"\''); }
        }
        return $out;
    };

    $dock = $parseKv(@file_get_contents(STATE_DIR . '/container_watchdog_state.db')  ?: '');
    $rw   = $parseKv(@file_get_contents(STATE_DIR . '/resource_watchdog_state.db')   ?: '');

    $ctrStrikes = [];
    foreach ($dock as $k => $v) {
        if ($k !== 'daemon_strikes' && $k !== 'daemon_restarted_flag' && (int)$v > 0)
            $ctrStrikes[$k] = (int)$v;
    }

    // Recent restarts (24 h)
    $restartLog = DATA_DIR . '/container_restart_history.db';
    $restartRaw = @file_get_contents($restartLog) ?: '';
    $cutoff     = time() - 86400;
    $restarts   = [];
    foreach (explode("\n", trim($restartRaw)) as $line) {
        if (!$line || !str_contains($line, '|')) continue;
        [$name, $ts] = explode('|', $line, 2);
        if ((int)$ts >= $cutoff) $restarts[] = ['name' => trim($name), 'ts' => (int)$ts];
    }
    usort($restarts, fn($a, $b) => $b['ts'] - $a['ts']);

    // Reboots (12 h)
    $rebootRaw = @file_get_contents(STATE_DIR . '/system_watchdog_reboots.db') ?: '';
    $rbootCutoff = time() - 43200;
    $reboots = 0;
    foreach (explode("\n", trim($rebootRaw)) as $line) {
        if ((int)trim($line) >= $rbootCutoff) $reboots++;
    }

    $rwLevel       = (int)($rw['rm_action_level']  ?? 0);
    $daemonStrikes = (int)($dock['daemon_strikes']  ?? 0);
    $oomCount      = (int)trim(@file_get_contents(STATE_DIR . '/system_watchdog_oom.db') ?: '0');

    // ── Stability watchdog strikes (STATE_DIR) ───────────
    $stabRaw = @file_get_contents(STATE_DIR . '/system_watchdog_state.db') ?: '';
    $stabStrikes = [];
    foreach (explode("\n", $stabRaw) as $line) {
        $line = trim($line);
        if (!$line || !str_contains($line, ':')) continue;
        [$k, $v] = explode(':', $line, 2);
        $count = (int)trim($v);
        if ($count > 0) $stabStrikes[trim($k)] = $count;
    }

    // ── Storage watchdog strikes (STATE_DIR) ────────────
    $storRaw = @file_get_contents(STATE_DIR . '/storage_watchdog_state.db') ?: '';
    $growthStrikes = []; $logStrikes = [];
    foreach (explode("\n", $storRaw) as $line) {
        $line = trim($line);
        if (!$line || !str_contains($line, ':')) continue;
        [$k, $v] = explode(':', $line, 2);
        $count = (int)trim($v);
        if ($count <= 0) continue;
        $key = trim($k);
        if (str_starts_with($key, 'appdata_growth_'))
            $growthStrikes[substr($key, strlen('appdata_growth_'))] = $count;
        elseif (str_starts_with($key, 'appdata_log_'))
            $logStrikes[substr($key, strlen('appdata_log_'))] = $count;
    }

    // ── Network watchdog NPM strikes (STATE_DIR) ────────
    $netRaw = @file_get_contents(STATE_DIR . '/network_watchdog_state.db') ?: '';
    $npmStrikes = 0;
    foreach (explode("\n", $netRaw) as $line) {
        $line = trim($line);
        if (str_starts_with($line, 'npm:')) $npmStrikes = (int)trim(substr($line, 4));
    }

    // ── Stability live stats ──────────────────────────────────────────────────
    $dfPct = function(string $path): int {
        $out = shell_exec("df " . escapeshellarg($path) . " --output=pcent 2>/dev/null | tail -1") ?: '';
        return (int)trim(str_replace('%', '', $out));
    };

    $memRaw   = @file_get_contents('/proc/meminfo') ?: '';
    $memAvail = 0;
    if (preg_match('/^MemAvailable:\s+(\d+)/m', $memRaw, $m)) $memAvail = (int)$m[1];

    $loadRaw = @file_get_contents('/proc/loadavg') ?: '0';
    $load1   = (float)explode(' ', trim($loadRaw))[0];

    $cpuTemp = null;
    $sensorsOut = shell_exec("sensors 2>/dev/null | grep -E 'Core 0|Package id 0|Tdie|Tctl|CPU Temp' | grep -oE '[0-9]+\\.[0-9]+' | sort -n | tail -1") ?: '';
    if ($sensorsOut && is_numeric(trim($sensorsOut))) $cpuTemp = (int)round((float)trim($sensorsOut));

    $zombies = (int)trim(shell_exec("ps -eo stat 2>/dev/null | grep -c '^Z'") ?: '0');

    $fileNr  = explode("\t", trim(@file_get_contents('/proc/sys/fs/file-nr') ?: '0	0	1'));
    $fdOpen  = max(0, (int)($fileNr[0] ?? 0) - (int)($fileNr[1] ?? 0));
    $fdMax   = max(1, (int)($fileNr[2] ?? 1));
    $fdPct   = round($fdOpen / $fdMax * 100, 1);

    $nic      = trim(shell_exec("ip route show default 2>/dev/null | awk 'NR==1{print \$5}'") ?: 'eth0') ?: 'eth0';
    $nicState = trim(@file_get_contents("/sys/class/net/$nic/operstate") ?: 'unknown');
    $sshdOk   = (int)trim(shell_exec('pgrep -c sshd 2>/dev/null') ?: '0') > 0;

    $healthy = empty($ctrStrikes) && empty($stabStrikes) && empty($growthStrikes) && empty($logStrikes)
        && $rwLevel === 0 && $daemonStrikes === 0 && $oomCount === 0 && $reboots === 0
        && $npmStrikes === 0 && $nicState === 'up' && $sshdOk;

    return [
        'healthy'        => $healthy,
        'ctr_strikes'    => $ctrStrikes,
        'rw_level'       => $rwLevel,
        'rw_paused'      => array_values(array_filter(explode(',', $rw['rm_paused_containers'] ?? ''))),
        'rw_stopped'     => array_values(array_filter(explode(',', $rw['rm_stopped_containers'] ?? ''))),
        'daemon_strikes' => $daemonStrikes,
        'oom_count'      => $oomCount,
        'reboots_12h'    => $reboots,
        'restarts_24h'   => array_slice($restarts, 0, 6),
        'restart_count'  => count($restarts),
        'stability'      => [
            'rootfs_pct'  => $dfPct('/'),
            'log_pct'     => $dfPct('/var/log'),
            'tmp_pct'     => $dfPct('/tmp'),
            'ram_free_gb' => round($memAvail / 1048576, 1),
            'load_1min'   => $load1,
            'cpu_temp'    => $cpuTemp,
            'zombies'     => $zombies,
            'fd_open'     => $fdOpen,
            'fd_max'      => $fdMax,
            'fd_pct'      => $fdPct,
            'nic'         => $nic,
            'nic_state'   => $nicState,
            'sshd_ok'     => $sshdOk,
            'strikes'     => $stabStrikes,
        ],
        'storage_wd'     => [
            'growth_strikes' => $growthStrikes,
            'log_strikes'    => $logStrikes,
        ],
        'network_wd'     => [
            'npm_strikes' => $npmStrikes,
        ],
    ];
}

function vv_scripts_status(): array {
    $logDir    = LOG_DIR;
    $statFiles = array_merge(
        glob("$logDir/*.json")   ?: [],
        glob("$logDir/*/*.json") ?: []
    );

    $scripts = [];
    foreach ($statFiles as $statFile) {
        $stat   = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: [];
        $status = $stat['status'] ?? 'unknown';

        // Stale running — PID gone (crash or reboot with no cleanup)
        if ($status === 'running' && !empty($stat['pid'])) {
            if (!file_exists("/proc/{$stat['pid']}")) $status = 'error';
        }

        $id   = $stat['id'] ?? basename($statFile, '.json');
        $name = basename(preg_replace('/\.sh$/', '', $id));
        $ts   = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0);

        $scripts[] = [
            'id'       => $id,
            'name'     => $name,
            'last_ts'  => $ts,
            'status'   => $status,
            'running'  => $status === 'running',
            'exit'     => $stat['exit'] ?? null,
            'duration' => isset($stat['start'], $stat['end'])
                ? (int)$stat['end'] - (int)$stat['start'] : null,
        ];
    }

    usort($scripts, fn($a, $b) => ($b['last_ts'] ?? 0) <=> ($a['last_ts'] ?? 0));
    $scripts = array_slice($scripts, 0, 12);

    return [
        'scripts'       => $scripts,
        'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
        'ok_count'      => count(array_filter($scripts, fn($s) => $s['status'] === 'ok')),
        'warn_count'    => count(array_filter($scripts, fn($s) => $s['status'] === 'warn')),
        'error_count'   => count(array_filter($scripts, fn($s) => $s['status'] === 'error')),
    ];
}

function vv_rsync_status(): array {
    $vars = vv_conf_vars();

    $enabled = ($vars['RSYNC_ENABLED'] ?? 'true') !== 'false';

    $windows = [
        'critical'     => ($vars['CRITICAL_RSYNC_ENABLED']     ?? 'false') !== 'false',
        'daily'        => ($vars['DAILY_RSYNC_ENABLED']        ?? 'false') !== 'false',
        'intermediate' => ($vars['INTERMEDIATE_RSYNC_ENABLED'] ?? 'true')  !== 'false',
        'weekly'       => ($vars['WEEKLY_RSYNC_ENABLED']       ?? 'false') !== 'false',
        'monthly'      => ($vars['MONTHLY_RSYNC_ENABLED']      ?? 'true')  !== 'false',
    ];

    // Active rsync profiles — from lock files
    $lockDir = '/tmp/unraid_locks';
    $active  = [];
    foreach (glob("$lockDir/rsync_*.lock") ?: [] as $lf) {
        $content = trim(@file_get_contents($lf) ?: '');
        [$pid, $locked_name] = array_pad(explode(':', $content, 2), 2, '');
        if (!$pid || !file_exists("/proc/$pid")) continue;
        $profile  = preg_replace('/^rsync_/', '', $locked_name ?: basename($lf, '.lock'));
        $active[] = [
            'profile' => $profile,
            'pid'     => (int)$pid,
            'elapsed' => time() - (int)filemtime($lf),
        ];
    }

    // Last completed run per window (orchestrator log files)
    $scriptMap = [
        'critical'     => 'critical_sync_maintenance',
        'daily'        => 'daily_sync_maintenance',
        'intermediate' => 'intermediate_sync_maintenance',
        'weekly'       => 'weekly_sync_maintenance',
        'monthly'      => 'monthly_maintenance',
    ];
    $lastSync = [];
    foreach ($scriptMap as $key => $scriptName) {
        $logFile = LOG_DIR . "/Orchestrators/$scriptName.json";
        if (!file_exists($logFile)) continue;
        $stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
        $lastSync[$key] = [
            'ts'       => (int)($stat['end'] ?? $stat['start'] ?? 0),
            'status'   => $stat['status'] ?? 'unknown',
            'duration' => isset($stat['start'], $stat['end'])
                ? (int)$stat['end'] - (int)$stat['start'] : null,
        ];
    }

    // Profile activity — last 7 days, aggregated per profile
    $bwLog   = DATA_DIR . '/bandwidth_history.db';
    $cutoff7 = date('Y-m-d', strtotime('-7 days'));
    $profiles = [];
    if (file_exists($bwLog)) {
        foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
            $p = explode('|', $line);
            if (count($p) < 4 || ($p[0] ?? '') < $cutoff7) continue;
            $name = $p[2] ?? '';
            if (!$name || str_ends_with($name, '-fallback')) continue;
            if (!isset($profiles[$name])) $profiles[$name] = ['runs' => 0, 'dur' => 0, 'bytes' => 0];
            $profiles[$name]['runs']++;
            $profiles[$name]['dur']   += (int)($p[3] ?? 0);
            $profiles[$name]['bytes'] += (int)($p[5] ?? 0);
        }
    }
    arsort($profiles); // sort by run count descending
    $bwSummary = array_slice($profiles, 0, 6, true);

    return [
        'enabled'    => $enabled,
        'windows'    => $windows,
        'active'     => $active,
        'last_sync'  => $lastSync,
        'bw_summary' => $bwSummary,
    ];
}
