/**
 * Graceful shutdown helper for `spectral serve`.
 *
 * Why a separate module:
 *  - The shutdown flag (`shutdownState.isShuttingDown`) is read by the relay
 *    dispatcher to reject new `client_message` envelopes. Putting it on
 *    `serve.ts` would create a circular import (dispatcher → serve → relay
 *    client → …); pulling it out keeps the dependency graph linear.
 *  - The orchestration is small but has enough edge cases (idempotent on a
 *    second SIGTERM, bounded wait for in-flight streams, best-effort store
 *    close) that it's worth one focused unit test.
 *
 * Behaviour summary (matches the contract in the Batch 6 brief):
 *  1. First SIGINT/SIGTERM:
 *     - Log "Shutting down…" to stderr.
 *     - Flip `shutdownState.isShuttingDown = true`. From this point on the
 *       dispatcher refuses new `client_message` frames with an error
 *       wrapped as a `ws_event` (so the browser sees the rejection on the
 *       same stream it was using).
 *     - Wait up to `gracePeriodMs` (default 5_000 ms) for the in-flight
 *       stream count reported by `inFlightCount()` to drain to 0. Polled
 *       every 100 ms — any non-zero value indicates an active turn.
 *     - Close the relay (code 1000, reason "shutdown") and dispose the
 *       manager. The dispose order matters: relay first so the backend
 *       sees a clean close before our local state goes; manager second so
 *       any spectral processes get torn down deterministically.
 *     - Close the SQLite store (best-effort — a failure here just gets
 *       logged; the process is exiting anyway).
 *     - Exit with the supplied code (default 0).
 *  2. Second SIGINT/SIGTERM during the grace period: skip the wait, force
 *     immediate exit with code 1. We don't want a hung spectral process to
 *     prevent operators from killing the server with a second Ctrl-C.
 *
 * The function is intentionally framework-free — it takes plain callbacks
 * for everything I/O-shaped so the unit test can exercise the orchestration
 * without spawning a real relay or sqlite handle.
 */
/**
 * Process-wide shutdown flag. Read by the dispatcher to gate new
 * `client_message` envelopes; written by `gracefulShutdown` (and by tests
 * via `resetShutdownState`).
 *
 * Module-level singleton because there is exactly one server process per
 * `spectral serve` invocation; co-locating with serve.ts would require
 * threading the flag through the dispatcher's deps (already a small
 * object) and risks dispatcher tests carrying serve.ts state.
 */
export const shutdownState = {
    isShuttingDown: false,
};
/**
 * Reset the singleton flag. Tests use this in `beforeEach` so a previous
 * test's shutdown doesn't bleed into the next. Production code must NOT
 * call this — once the process is shutting down, it's shutting down.
 */
export function resetShutdownState() {
    shutdownState.isShuttingDown = false;
}
/**
 * Number of times `gracefulShutdown` has been entered in this process.
 * 0 → first signal: do the full graceful sequence.
 * ≥1 → second signal during grace: bail with code 1 immediately.
 *
 * Deliberately a module-level counter rather than a closure so the second
 * signal handler (which closes over a different invocation of the function)
 * still observes the increment.
 */
let entryCount = 0;
/** Test-only: reset the entry counter alongside `shutdownState`. */
export function resetShutdownEntryCount() {
    entryCount = 0;
}
/**
 * Run the graceful shutdown sequence. Idempotent on the entry-count
 * dimension: the second concurrent call resolves immediately after
 * triggering an immediate exit.
 *
 * Returns once the supplied `exitFn` has been invoked (or would have been
 * invoked in production where `process.exit` does not return). Tests can
 * await it to assert ordering.
 */
export async function gracefulShutdown(opts = {}) {
    const logger = opts.logger ?? console;
    const exitFn = opts.exitFn ?? ((code) => process.exit(code));
    const sleep = opts.sleep ?? defaultSleep;
    const gracePeriodMs = opts.gracePeriodMs ?? 5_000;
    const pollIntervalMs = opts.pollIntervalMs ?? 100;
    const exitCode = opts.exitCode ?? 0;
    entryCount += 1;
    // Second signal during a graceful shutdown: don't wait around, just go.
    // Code 1 signals "abnormal exit" — operators wanted out NOW, not after
    // spectral finishes its turn.
    if (entryCount > 1) {
        try {
            logger.error("Shutdown forced by repeated signal — exiting immediately.");
        }
        catch {
            // ignore — best-effort
        }
        exitFn(1);
        return;
    }
    // First signal: announce intent, flip the flag (dispatcher reads this on
    // every inbound `client_message`), then drain.
    try {
        logger.error("Shutting down…");
    }
    catch {
        // ignore — best-effort
    }
    shutdownState.isShuttingDown = true;
    // Bounded wait for in-flight turns. We don't accept new ones (the flag
    // is set), but we let the ones that already started finish so the user
    // doesn't see a half-streamed assistant message orphaned in the UI.
    if (opts.inFlightCount) {
        const start = Date.now();
        while (Date.now() - start < gracePeriodMs) {
            let count;
            try {
                count = opts.inFlightCount();
            }
            catch {
                // Defensive: a misbehaving counter shouldn't trap the shutdown.
                // Treat it as "drained" and move on.
                count = 0;
            }
            if (count <= 0)
                break;
            await sleep(pollIntervalMs);
        }
    }
    // Order matters: relay → manager → store. See module-level comment.
    if (opts.closeRelay) {
        try {
            await opts.closeRelay();
        }
        catch (err) {
            try {
                logger.warn(`Relay close failed during shutdown: ${err instanceof Error ? err.message : String(err)}`);
            }
            catch {
                // ignore
            }
        }
    }
    if (opts.disposeManager) {
        try {
            await opts.disposeManager();
        }
        catch (err) {
            try {
                logger.warn(`Manager dispose failed during shutdown: ${err instanceof Error ? err.message : String(err)}`);
            }
            catch {
                // ignore
            }
        }
    }
    if (opts.closeStore) {
        try {
            opts.closeStore();
        }
        catch (err) {
            try {
                logger.warn(`Store close failed during shutdown: ${err instanceof Error ? err.message : String(err)}`);
            }
            catch {
                // ignore
            }
        }
    }
    exitFn(exitCode);
}
function defaultSleep(ms) {
    return new Promise((resolve) => {
        const t = setTimeout(resolve, ms);
        // Don't keep the loop alive purely for the shutdown poll.
        t.unref?.();
    });
}
