<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Rsync tab data. Tier enablement, the currently running sync, last-run results, 30 days of
//   bandwidth history, the per-window script and share lists, and the tuning settings — plus
//   a separate action serving the live sync log.
//
// OPERATIONAL MODEL
//   Two responses on one URL. The default is the tab's full state. ?action=rsync_log is the
//   log viewer, split out because it polls far faster than the rest and returns a payload
//   nothing else needs.
//
//   The log action has two sources and prefers the live one. An active sync is identified by
//   a lock file whose pid is still in /proc, and its in-progress log is read directly. With
//   no active sync, the most recently modified .last.log is served instead — so the panel
//   shows the run that is happening, or failing that the run that just happened, without the
//   caller having to know which.
//
//   Window definitions are a table, not a set of branches. Each tier names the master.conf
//   script array and the host.conf shares array it draws from, so adding a tier is a row.
//   monthly is deliberately half-populated — monthly_maintenance.sh does ZFS scrub and SMART
//   tests and has no rsync section, so no shares variable exists for it. fallback has neither:
//   it is driven entirely by fallback.sh.
//
// DESIGN PRINCIPLES
//   Liveness is proven by the process table, not by the lock file's existence.
//       A lock whose pid is gone is a crashed run, and treating it as active would show a
//       stale log as a sync in progress forever.
//
//   Toggles default to on when the key is absent.
//       !== 'false' rather than === 'true', so a conf that predates a flag behaves as it did
//       before the flag existed. That is the correct default for a tier that was previously
//       unconditional.
//
//   History is filtered by date, not by line count.
//       The bandwidth log is append-only and unbounded; a 30-day cutoff keeps the response
//       proportional to the window the page renders rather than to the file's age.
//
//   Elapsed time comes from the lock's mtime.
//       The lock is touched when the run starts, so its age is the run's age without the
//       script having to report progress.
//
// OPERATIONAL SAFEGUARDS
//   Read-only. Nothing here starts, stops, or reconfigures a sync — this endpoint reports on
//   rsync.sh, and the interlocks that make a sync safe live there. In particular, the
//   --merge-run / --delete ordering is rsync.sh's to enforce; nothing in this payload implies
//   it has been satisfied.
//
//   The lock and log scan is bounded to a hardcoded directory.
//       glob over /tmp/unraid_locks — a literal, not a config value.
//
//   Every read degrades to empty.
//       @file_get_contents, file() with ?: [] fallbacks, and file_exists() before each read.
//       A lock caught mid-write or a log removed between the glob and the read yields an empty
//       line list rather than a fatal.
//
//   Malformed history lines are skipped, not repaired.
//       A count check before the fields are used, and an isset() on the optional bytes column,
//       so a truncated or older-format row is dropped instead of producing a row of nulls.
//
//   Output is bounded and stripped.
//       Last 200 lines, with ANSI escapes removed. rsync logs are long and coloured, and the
//       escapes would otherwise reach the page as control codes.
//
//   Every setting has a default.
//       ?? on all four tuning values and on the bandwidth warning threshold, so a conf missing
//       a key renders a usable panel rather than zeros that read as "no limit configured".
//
//   The profile name is derived from the lock's own contents, with the filename as fallback,
//   so a lock written by an older format still identifies its run.
//
// REQUEST
//   GET                      full rsync tab state
//   GET ?action=rsync_log    live sync log, or the most recent completed one
//
// RESPONSE
//   default  {"enabled","windows","active","last_sync","bw_history":[…],"bw_warn_gb",
//             "win_arrays":{<window>:{"scripts":[…],"shares":[…]}},"settings":{…},"ts"}
//   log      {"ok":true,"live":bool,"profile","elapsed","lines":[…]}
//
// DEPENDS ON
//   include/monitor.php   vv_rsync_status()
//   include/config.php    vv_conf_vars(), vv_read_conf_raw(), vv_detect_host(),
//                         vv_parse_bash_array(), DATA_DIR
//   /tmp/unraid_locks     rsync_*.lock, rsync_*.log, rsync_*.last.log — written by rsync.sh
//   DATA_DIR              bandwidth_history.db
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/monitor.php';

// ── Live sync log ─────────────────────────────────────────────────────────────
$action  = $_GET['action'] ?? '';
if ($action === 'rsync_log') {
    $lockDir = '/tmp/unraid_locks';
    $lines   = [];
    $profile = null;
    $live    = false;
    $elapsed = 0;

    // Active sync — read live log
    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'));
        $elapsed = time() - (int)filemtime($lf);
        $liveLog = "$lockDir/rsync_{$profile}.log";
        $raw     = file_exists($liveLog) ? (file($liveLog, FILE_IGNORE_NEW_LINES) ?: []) : [];
        $live    = true;
        $lines   = $raw;
        break;
    }

    // No active sync — use most recent last.log
    if (!$live) {
        $lastLogs = glob("$lockDir/rsync_*.last.log") ?: [];
        if ($lastLogs) {
            usort($lastLogs, fn($a, $b) => filemtime($b) <=> filemtime($a));
            $lastLog = $lastLogs[0];
            $profile = preg_replace('/^rsync_(.+)\.last\.log$/', '$1', basename($lastLog));
            $lines   = file($lastLog, FILE_IGNORE_NEW_LINES) ?: [];
        }
    }

    // Strip ANSI escape codes, keep last 200 lines
    $lines = array_map(fn($l) => preg_replace('/\x1b\[[0-9;]*[mGKHF]/', '', $l), $lines);
    $lines = array_slice($lines, -200);

    echo json_encode(['ok' => true, 'live' => $live, 'profile' => $profile, 'elapsed' => $elapsed, 'lines' => array_values($lines)]);
    exit;
}

$base = vv_rsync_status();
$vars = vv_conf_vars();

$base['windows']['fallback'] = ($vars['FALLBACK_RSYNC_ENABLED']  ?? 'true') !== 'false';
$base['windows']['monthly']  = ($vars['MONTHLY_RSYNC_ENABLED']   ?? 'true') !== 'false';

// Bandwidth history — last 30 days
$bwLog    = DATA_DIR . '/bandwidth_history.db';
$warnGb   = (float)($vars['BANDWIDTH_WARN_GB'] ?? 50);
$cutoff   = date('Y-m-d', strtotime('-30 days'));
$history  = [];

if (file_exists($bwLog)) {
    foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
        $p = explode('|', $line);
        if (count($p) < 5) continue;
        if ($p[0] < $cutoff) continue;
        $history[] = [
            'date'     => $p[0],
            'time'     => $p[1],
            'profile'  => $p[2],
            'duration' => (int)$p[3],
            'status'   => trim($p[4]),
            'bytes'    => isset($p[5]) ? (int)$p[5] : 0,
        ];
    }
}

// Per-window orch arrays (scripts + sync shares)
$masterRaw = vv_read_conf_raw('master.conf');
$myId      = strtoupper(vv_detect_host());
$hostRaw   = vv_read_conf_raw(vv_detect_host() . '.conf');

$winArrayDefs = [
    'critical'     => ['CRITICAL_MAINTENANCE_SCRIPTS',     "{$myId}_CRITICAL_SYNC_SHARES"],
    'intermediate' => ['INTERMEDIATE_MAINTENANCE_SCRIPTS', "{$myId}_INTERMEDIATE_SYNC_SHARES"],
    'daily'        => ['DAILY_MAINTENANCE_SCRIPTS',        "{$myId}_DAILY_SYNC_SHARES"],
    'weekly'       => ['WEEKLY_MAINTENANCE_SCRIPTS',       "{$myId}_WEEKLY_SYNC_SHARES"],
    // monthly_maintenance.sh has no rsync section (ZFS scrub/SMART tests only) — no shares var exists.
    'monthly'      => ['MONTHLY_MAINTENANCE_SCRIPTS',      null],
    'fallback'     => [null,                               null],
];
$winArrays = [];
foreach ($winArrayDefs as $win => [$sv, $shv]) {
    $winArrays[$win] = [
        'scripts' => $sv  ? vv_parse_bash_array($masterRaw, $sv)  : [],
        'shares'  => $shv ? vv_parse_bash_array($hostRaw,   $shv) : [],
    ];
}

echo json_encode([
    'enabled'    => $base['enabled'],
    'windows'    => $base['windows'],
    'active'     => $base['active'],
    'last_sync'  => $base['last_sync'],
    'bw_history' => $history,
    'bw_warn_gb' => $warnGb,
    'win_arrays' => $winArrays,
    'settings'   => [
        'bw_limit'     => (int)($vars['BW_LIMIT']          ?? 0),
        'retry_count'  => (int)($vars['RETRY_COUNT']        ?? 3),
        'sleep'        => (int)($vars['SLEEP']              ?? 300),
        'bw_warn_gb'   => (float)($vars['BANDWIDTH_WARN_GB'] ?? 50),
    ],
    'ts'         => time(),
]);
