<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Fallback page data layer. Reports what state each node is in (NORMAL / FALLBACK /
//   NO_INTERNET / DARK), which tiers have activated, and which covered containers are
//   actually running — for this host and for every partner.
//
// DESIGN PRINCIPLES
//   Observes fallback.sh; never participates in it.
//       State is read from the state file the running fallback process maintains. This file
//       has no opinion about whether a failover should happen and cannot trigger, advance,
//       or hand back one. The page is a window, not a lever.
//
//   Remote state is read the same way local state is.
//       vv_fb_remote_state() SSHes over and reads the partner's own fallback_state.db rather
//       than inferring the partner's state from what this host can see. A node's state is
//       whatever that node believes, not what its neighbour guesses.
//
//   Coverage lists come from conf, tier membership from FALLBACK_<HOST>_TIER<n>.
//       Named for the host being covered, not the host doing the covering — see the fallback
//       section of the top-level README for why that reads backwards at first.
//
// OPERATIONAL SAFEGUARDS
//   A missing state file parses as empty, not as NORMAL.
//       vv_fb_local_state() hands an empty string to the parser when the file is absent, so
//       the page shows unknown rather than asserting everything is fine. Reporting a healthy
//       state for a fallback process that is not running would be the worst possible lie on
//       this page.
//
//   An unreachable partner degrades to what is locally known.
//       Remote SSH failures return empty rather than propagating an error, so one dark node
//       cannot blank the whole page — which is precisely the situation this page exists for.
//
//   Read-only over SSH.
//       The only remote commands issued are a state-file read and `docker ps`. Nothing here
//       starts or stops a container on either side.
//
// EXPORTS
//   State       vv_fb_local_state(), vv_fb_remote_state(), vv_fb_parse_state()
//   Containers  vv_fb_local_running(), vv_fb_remote_running(), vv_fb_covers()
//   Assembly    vv_fb_known_hosts(), vv_fb_all()
//   Parsing     vv_fb_bash_array(), vv_fb_scalar()
//
// CONFIGURATION
//   STATE_DIR                    fallback_state.db — written by Fallback/fallback.sh
//   FALLBACK_<HOST>_TIER1..4     containers covered per tier, keyed by the covered host
//   HOST*_SSH_KEY                used to read partner state
// ═══════════════════════════════════════════════════════════════════════════════════════════════


require_once __DIR__ . '/config.php';
require_once __DIR__ . '/partnership.php';   // vv_pt_ssh(), vv_pt_ts_peers()

// ── Conf parsers ──────────────────────────────────────────────────────────────

function vv_fb_bash_array(string $raw, string $varname): array {
    return vv_parse_bash_array($raw, $varname);
}

function vv_fb_scalar(string $raw, string $varname): string {
    return vv_parse_conf_scalar($raw, $varname);
}

// ── State file ────────────────────────────────────────────────────────────────

function vv_fb_parse_state(string $text): array {
    $out = [
        'state'                  => 'UNKNOWN',
        'fallback_start'         => 0,
        'handback_strikes'       => 0,
        'tier2_started'          => false,
        'tier3_started'          => false,
        'tier4_started'          => false,
        'partnership_suspended'  => false,
        'partner_lost_at'        => 0,
    ];
    foreach (explode("\n", $text) as $line) {
        $line = trim($line);
        if (!$line || !str_contains($line, '=')) continue;
        [$k, $v] = array_pad(explode('=', $line, 2), 2, '');
        $k = trim($k); $v = trim($v, '"\'');
        switch ($k) {
            case 'state':                 $out['state']                 = $v;            break;
            case 'fallback_start':        $out['fallback_start']        = (int)$v;       break;
            case 'handback_strikes':      $out['handback_strikes']      = (int)$v;       break;
            case 'tier2_started':         $out['tier2_started']         = $v === 'true'; break;
            case 'tier3_started':         $out['tier3_started']         = $v === 'true'; break;
            case 'tier4_started':         $out['tier4_started']         = $v === 'true'; break;
            case 'partnership_suspended': $out['partnership_suspended']  = $v === 'true'; break;
            case 'partner_lost_at':       $out['partner_lost_at']       = (int)$v;       break;
        }
    }
    return $out;
}

function vv_fb_local_state(): array {
    $path = STATE_DIR . '/fallback_state.db';
    return vv_fb_parse_state(file_exists($path) ? file_get_contents($path) : '');
}

function vv_fb_remote_state(string $ip, string $sshKey): array {
    $out = vv_pt_ssh($ip, $sshKey, vv_remote_state_cmd('fallback_state.db'));
    return vv_fb_parse_state($out);
}

// ── Running containers ────────────────────────────────────────────────────────

function vv_fb_local_running(): array {
    $out = shell_exec("docker ps --format '{{.Names}}' 2>/dev/null") ?: '';
    return array_values(array_filter(explode("\n", trim($out))));
}

function vv_fb_remote_running(string $ip, string $sshKey): array {
    $out = vv_pt_ssh($ip, $sshKey, "docker ps --format '{{.Names}}' 2>/dev/null");
    return array_values(array_filter(explode("\n", trim($out))));
}

// ── Covers — what a node runs for the other when it's down ───────────────────

function vv_fb_covers(string $covering, string $remote, string $coveringRaw, string $remoteRaw): array {
    $ru = strtoupper($remote);   // HOST2 — covered host owns its own tier lists
    $tiers = [];
    for ($t = 1; $t <= 4; $t++) {
        $tiers["tier$t"] = vv_fb_bash_array($remoteRaw, "FALLBACK_{$ru}_TIER{$t}");
    }
    // Delays: how long the remote (covered) host must be down before each tier fires.
    // Stored in the *remote* host's conf as REMOTE_TIER*_DELAY.
    $tiers['delays'] = [
        'tier2' => (int)(vv_fb_scalar($remoteRaw, "{$ru}_TIER2_DELAY") ?: 240),
        'tier3' => (int)(vv_fb_scalar($remoteRaw, "{$ru}_TIER3_DELAY") ?: 720),
        'tier4' => (int)(vv_fb_scalar($remoteRaw, "{$ru}_TIER4_DELAY") ?: 1440),
    ];
    return $tiers;
}

// ── Known hosts ───────────────────────────────────────────────────────────────

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

// ── Main data builder ─────────────────────────────────────────────────────────

function vv_fb_all(): array {
    $currentHost  = vv_detect_host();
    $hosts        = vv_fb_known_hosts();
    $tsPeers      = vv_pt_ts_peers();
    $masterRaw    = vv_read_conf_raw('master.conf');
    $handbackReq  = (int)(vv_fb_scalar($masterRaw, 'FALLBACK_HANDBACK_STRIKES') ?: 3);
    $fbEnabled    = vv_fb_scalar($masterRaw, 'FALLBACK_ENABLED')       === 'true';
    $ptEnabled    = vv_fb_scalar($masterRaw, 'PARTNERSHIP_ENABLED')    === 'true';
    $rsyncEnabled = vv_fb_scalar($masterRaw, 'FALLBACK_RSYNC_ENABLED') !== 'false';
    $checkInterval = (int)(vv_fb_scalar($masterRaw, 'FALLBACK_CHECK_INTERVAL')           ?: 30);
    $suspendAfter  = (int)(vv_fb_scalar($masterRaw, 'FALLBACK_PARTNERSHIP_SUSPEND_AFTER') ?: 120);

    // Read all host conf raws upfront
    $raws = [];
    foreach (array_keys($hosts) as $slot) {
        $raws[$slot] = vv_read_conf_raw($slot . '.conf');
    }

    // SSH key — from local host conf
    $myId     = strtoupper($currentHost);
    $myRaw    = $raws[$currentHost] ?? '';
    $mySshKey = vv_fb_scalar($myRaw, $myId . '_SSH_KEY');

    $nodes = [];
    foreach ($hosts as $slot => $hostname) {
        $isMe   = ($slot === $currentHost || $currentHost === 'unknown');
        $tsLabel = strtolower($hostname);
        $ts      = $tsPeers[$tsLabel] ?? ['online' => null, 'active' => false, 'ip' => null];
        $ip      = $ts['ip'] ?? null;

        // State
        if ($isMe) {
            $state = vv_fb_local_state();
        } elseif ($ip && $mySshKey) {
            $state = vv_fb_remote_state($ip, $mySshKey);
        } else {
            $state = vv_fb_parse_state('');
            $state['state'] = $ts['online'] === false ? 'OFFLINE' : 'UNREACHABLE';
        }

        // Running containers
        if ($isMe) {
            $running = vv_fb_local_running();
        } elseif ($ip && $mySshKey && $ts['online']) {
            $running = vv_fb_remote_running($ip, $mySshKey);
        } else {
            $running = [];
        }

        // Covers: for a 2-node setup, each covers the other
        // For N nodes this would need a different approach — for now, assume 2-node
        $covers = null;
        foreach ($hosts as $otherSlot => $otherHostname) {
            if ($otherSlot === $slot) continue;
            $coveringRaw = $raws[$slot]      ?? '';
            $remoteRaw   = $raws[$otherSlot] ?? '';
            $covers = [
                'slot'     => $otherSlot,
                'id'       => strtoupper($otherSlot),
                'hostname' => $otherHostname,
            ] + vv_fb_covers($slot, $otherSlot, $coveringRaw, $remoteRaw);
            break; // 2-node only
        }

        $nodes[] = [
            'slot'         => $slot,
            'id'           => strtoupper($slot),
            'hostname'     => $hostname,
            'is_me'        => $isMe,
            'ts_online'    => $ts['online'],
            'state'        => $state,
            'running'      => $running,
            'covers'       => $covers,
        ];
    }

    return [
        'ts'                  => time(),
        'fb_enabled'          => $fbEnabled,
        'partnership_enabled' => $ptEnabled,
        'fb_rsync_enabled'    => $rsyncEnabled,
        'handback_req'        => $handbackReq,
        'check_interval'      => $checkInterval,
        'suspend_after'       => $suspendAfter,
        'nodes'               => $nodes,
    ];
}
