//! restart_marker — broker self-restart detection (Tier-1 #8, R-T1-8 residual a;
//! survivability §4.3 "re-derive, don't persist").
//!
//! The broker holds its ownership table only in memory (deliberately — persisting
//! it would risk persisting corruption). So after a crash it must re-derive truth
//! by reconciling against the hardware. But it could not previously KNOW it had
//! restarted — reconcile was operator-triggered. This closes that: a one-byte
//! marker file records "an instance is running." On startup the broker atomically
//! checks for a marker a PRIOR instance left and (re)creates it; finding one means
//! this is a restart (the previous process is gone — a crash or a kill — without
//! a clean exit), so the broker auto-reconciles.
//!
//! ## Why a marker in a runtime dir gives the right cold-vs-restart distinction
//!
//! Put the marker beside the socket (a runtime location — typically tmpfs).
//!   * Host REBOOT → tmpfs cleared → no marker → COLD start. Correct: a reboot
//!     reset every device; there are no orphans to reconcile.
//!   * Broker CRASH / kill (host still up) → marker survives → RESTART. Correct:
//!     devices the dead broker bound (vfio-pci) are still bound — genuine orphans.
//!   * CLEAN exit → `clear` removed the marker → next start is COLD.
//!
//! Fail-safe: any filesystem error is read as "cold start" (return false), so the
//! broker never spuriously quarantines hardware on uncertainty.

const std = @import("std");
const linux = std.os.linux;

/// Atomically check for a prior instance's marker and (re)create it. Returns
/// **true** iff the marker ALREADY EXISTED — a prior instance left it without a
/// clean exit → this is a RESTART. Returns **false** on a cold/clean start (no
/// prior marker), or on any filesystem error (fail-safe: never spuriously
/// reconcile). The marker remains after this call (it represents "an instance has
/// run"); `clear` removes it on a clean exit.
///
/// `O_CREAT | O_EXCL` makes the check-and-create one atomic step — no TOCTOU
/// window where two starts could disagree.
pub fn checkAndMark(path: [:0]const u8) bool {
    const rc = linux.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .EXCL = true }, 0o600);
    switch (linux.errno(rc)) {
        .SUCCESS => {
            _ = linux.close(@intCast(rc)); // we created it → cold start
            return false;
        },
        .EXIST => return true, // a prior instance left it → restart
        else => return false, // perm / missing dir / etc. → fail-safe cold
    }
}

/// Remove the marker — call on a clean shutdown so the next start is a cold
/// start. Best-effort + idempotent (a missing marker is fine).
pub fn clear(path: [:0]const u8) void {
    _ = linux.unlink(path);
}

// ===========================================================================
// R-T1-8 (a) residual — clean-stop marker cleanup on a signal.
//
// `checkAndMark` leaves the marker in place for the running instance's
// lifetime; `clear` removes it on a clean exit. But the broker's accept loop
// never returns — in production it stops only on a signal, and a
// default-disposition signal terminates the process WITHOUT running any
// `defer clear`. So every operator-initiated stop (systemd `stop`, `kill`,
// Ctrl-C) left the marker behind, making the NEXT start look like a
// crash-restart and spuriously auto-reconcile.
//
// Fix: install SIGTERM + SIGINT handlers that remove the marker before exiting.
// Those are the deliberate, graceful stops → clean exit → cold next start. A
// crash or SIGKILL can't run a handler, so it still (correctly) leaves the
// marker → a genuine restart. The handler does exactly one async-signal-safe
// syscall (`unlink`, via `clear`) then `exit_group` — safe in signal context
// (no allocation, no stdio, no locks).
// ===========================================================================

var g_marker_buf: [256]u8 = undefined;
var g_marker: ?[:0]const u8 = null;

/// Remove the marker recorded by `installCleanupHandler` (no-op if none armed).
/// A single `unlink` — async-signal-safe, so the signal handler calls it.
pub fn clearArmedMarker() void {
    if (g_marker) |m| clear(m);
}

fn cleanupSignalHandler(sig: linux.SIG) callconv(.c) void {
    _ = sig;
    clearArmedMarker();
    linux.exit_group(0); // clean operator stop → exit 0
}

/// Record `path` as the marker the cleanup handler should remove. Split out from
/// the sigaction install so a test can exercise the clear-path plumbing without
/// installing process-wide handlers. Copies the bytes into a static buffer so
/// the pointer the handler dereferences outlives the caller's stack.
fn armMarker(path: [:0]const u8) void {
    const n = @min(path.len, g_marker_buf.len - 1);
    @memcpy(g_marker_buf[0..n], path[0..n]);
    g_marker_buf[n] = 0;
    g_marker = g_marker_buf[0..n :0];
}

/// Install SIGTERM + SIGINT handlers that remove `path` before exiting, so an
/// operator-initiated stop records a CLEAN exit (the next start is cold). A
/// crash or SIGKILL can't run a handler, so it still leaves the marker → a
/// restart. Call once, after `checkAndMark`, with the same marker path.
pub fn installCleanupHandler(path: [:0]const u8) void {
    armMarker(path);
    var sa = std.mem.zeroes(linux.Sigaction);
    sa.handler.handler = cleanupSignalHandler;
    _ = linux.sigaction(linux.SIG.TERM, &sa, null);
    _ = linux.sigaction(linux.SIG.INT, &sa, null);
}

// ===========================================================================
// R-T1-8 (a) — self-restart detection pins.
// ===========================================================================

const testing = std.testing;

test "restart_marker: first start is cold, a second (no clean exit) is a restart, clear resets" {
    const path: [:0]const u8 = "/tmp/mgf-hrb-restart-marker.zigtest";
    clear(path); // ensure a clean slate (drop any leftover from a prior run)
    defer clear(path);

    // First ever start — no marker → cold (and the marker is now created).
    try testing.expect(!checkAndMark(path));
    // A second start with the marker still present (the prior instance didn't
    // clean up — a crash) → RESTART.
    try testing.expect(checkAndMark(path));
    try testing.expect(checkAndMark(path)); // still a restart while the marker stands

    // A clean exit removes the marker → the next start is cold again.
    clear(path);
    try testing.expect(!checkAndMark(path));
}

test "restart_marker: a clean-stop cleanup clears the armed marker so the next start is cold" {
    const path: [:0]const u8 = "/tmp/mgf-hrb-restart-cleanup.zigtest";
    clear(path); // clean slate
    defer clear(path);

    // Startup: cold (creates the marker), then arm the cleanup path (the bytes
    // are what the signal handler would unlink — exercised here WITHOUT the
    // sigaction install or the exit, so the test process survives).
    try testing.expect(!checkAndMark(path));
    armMarker(path);
    // While running, the marker stands — a fresh start would read it as a restart.
    try testing.expect(checkAndMark(path));

    // An operator-initiated stop runs the handler body (minus the exit): the
    // marker is removed via the path we armed.
    clearArmedMarker();

    // So the next start is correctly COLD — no spurious restart / auto-reconcile.
    try testing.expect(!checkAndMark(path));
}
