//! The service harness (docs/process-lifecycle.md): one replyWait loop that
//! folds protocol requests, signals, and subscribed notifications into
//! callbacks — so the lifecycle contract ("answers ping, exits on terminate")
//! is satisfied by construction and a service author writes domain logic only.
//! Nothing is asynchronous inside the process: a callback runs at a point the
//! loop chose, never on a hijacked stack — the whole reason signals are
//! messages.
//!
//! The liveness probe: a **zero-length request is the universal ping**, answered
//! with a zero-length reply by the harness itself. No protocol's requests start
//! at length zero, so the encoding cannot collide, and there is nothing for a
//! service author to implement — a wedged service simply fails to answer, which
//! is the diagnosis (see docs/ipc.md).

const abi = @import("abi");
const ipc = @import("ipc.zig");
const process = @import("process.zig");

pub const Callbacks = struct {
    /// Called once with the service's endpoint before the loop starts — the
    /// place to subscribe to exit events, bind IRQs, or announce readiness.
    /// Return false to abort startup (the process exits).
    init: ?*const fn (endpoint: ipc.Handle) bool = null,
    /// One protocol request from `sender` (a task id): write the reply into
    /// `reply`, return its length. `capability` is the handle the request
    /// carried, if any (M13 cap passing — how a subscriber hands over its
    /// endpoint). The zero-length ping never reaches this.
    on_message: *const fn (message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize,
    /// A notification that is not a signal — a subscribed exit event, a bound
    /// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
    on_notification: ?*const fn (badge: u64) void = null,
    /// The reload signal. Default: ignored.
    on_reload: ?*const fn () void = null,
    /// The terminate signal, called before the loop returns. The clean exit is
    /// the return itself — never put *necessary* work here (iron rule 1: a kill
    /// arrives with no warning; this is for graceful extras only).
    on_terminate: ?*const fn () void = null,
    /// Publish the endpoint under a well-known service id at startup.
    service: ?abi.ServiceId = null,
};

/// Run the service: create and (optionally) register the endpoint, bind signals
/// to it, call `init`, then serve until `terminate` arrives — at which point the
/// loop returns and main's return is the clean exit the supervisor reads as
/// `ExitReason.exited`. `maximum_message` sizes the receive and reply buffers
/// (a service passes its protocol's message maximum).
pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
    const endpoint = ipc.createIpcEndpoint() orelse return;
    if (callbacks.service) |id| {
        if (!ipc.register(id, endpoint)) return;
    }
    _ = process.bindSignals(endpoint);
    if (callbacks.init) |initialise| {
        if (!initialise(endpoint)) return;
    }

    var reply_buffer: [maximum_message]u8 = undefined;
    var reply_len: usize = 0;
    var receive: [maximum_message]u8 = undefined;
    while (true) {
        const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
        if (got.isNotification()) {
            reply_len = 0; // nothing owed for a notification
            if (process.signalsFrom(got.badge)) |signals| {
                if (signals.has(.reload)) {
                    if (callbacks.on_reload) |onReload| onReload();
                }
                if (signals.has(.terminate)) {
                    if (callbacks.on_terminate) |onTerminate| onTerminate();
                    return; // the loop's return IS the clean exit
                }
                continue;
            }
            if (callbacks.on_notification) |onNotification| onNotification(got.badge);
            continue;
        }
        if (got.len == 0) {
            reply_len = 0; // the universal ping: a zero-length reply, from the harness
            continue;
        }
        reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId(), got.cap);
    }
}
