//! SIGINT and SIGTERM, turned into one `std.Io.Event`.
//!
//! No signalfd, no self-pipe, no epoll: the handler does exactly one thing, and
//! `std.Io.Event.set` is async-signal-safe on the Threaded Linux backend — a
//! raw `futex` wake with no allocation and no lock (`Io.zig:1855` →
//! `Threaded.futexWake`). The `.mask`/`.flags` shape is the one Threaded uses
//! for its own `SIG.IO`/`SIG.PIPE` handlers (`Threaded.zig:1653`): an empty
//! mask and no `SA_RESTART`, so a blocking syscall returns `EINTR` and the
//! backend's retry loop re-reads the cancellation state.
//!
//! Everything a shutdown actually has to do — drain the query log, cancel the
//! task group, close the databases — happens on the task blocked in `wait`.
//!
//! The previous handlers are not restored. The process is leaving, and a
//! second SIGTERM during teardown should still terminate it the default way
//! only if the operator sends it before this module is armed.

const std = @import("std");
const posix = std.posix;

var event: std.Io.Event = .unset;

/// Read by the signal handler, written by `install` before the handler exists.
/// A `std.Io` is two pointers and cannot be stored atomically, so ordering is
/// what makes the read safe: the store precedes the `sigaction` syscall that
/// arms the handler, and no signal can reach the handler before that call
/// returns.
var handler_io: ?std.Io = null;

var installed: bool = false;

/// Arms the handlers for INT and TERM. Calling it again is a no-op: the process
/// has one event and one pair of handlers, and a second boot inside one process
/// (which only a test does) must not re-arm anything.
pub fn install(io: std.Io) void {
    if (installed) return;
    handler_io = io;
    installed = true;

    const act: posix.Sigaction = .{
        .handler = .{ .handler = onSignal },
        .mask = posix.sigemptyset(),
        .flags = 0,
    };
    posix.sigaction(.INT, &act, null);
    posix.sigaction(.TERM, &act, null);
}

fn onSignal(_: posix.SIG) callconv(.c) void {
    const io = handler_io orelse return;
    event.set(io);
}

/// Blocks until a shutdown is requested. A canceled wait is the caller's cue to
/// tear down as well, which is why `app.run` treats both results the same.
pub fn wait(io: std.Io) std.Io.Cancelable!void {
    return event.wait(io);
}

/// The programmatic equivalent of the signal: what a test uses to shut the app
/// down. No restart endpoint calls it — none exists, and none is planned; the
/// web API echoes `restart_required` and leaves the restart to the operator.
pub fn trigger(io: std.Io) void {
    event.set(io);
}

pub fn isRequested() bool {
    return event.isSet();
}

/// Clears the request so the next `wait` blocks again. Only a test that boots
/// the app more than once in one process needs this; a served process shuts
/// down once.
pub fn reset() void {
    event.reset();
}

const testing = std.testing;

test "trigger releases a waiter and isRequested reports it" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    reset();
    try testing.expect(!isRequested());

    trigger(io);
    try testing.expect(isRequested());
    // Already set, so this returns without blocking.
    try wait(io);

    reset();
    try testing.expect(!isRequested());
}

test "a waiting task is released by a later trigger" {
    var threaded: std.Io.Threaded = .init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    reset();

    var group: std.Io.Group = .init;
    try group.concurrent(io, waitThenSet, .{ io, &done });
    trigger(io);
    try group.await(io);

    try testing.expect(done.isSet());
    reset();
    done.reset();
}

var done: std.Io.Event = .unset;

fn waitThenSet(io: std.Io, flag: *std.Io.Event) std.Io.Cancelable!void {
    try wait(io);
    flag.set(io);
}
