const std = @import("std");
const boot_handoff = @import("boot-handoff");
const abi = @import("abi");
const parameters = @import("parameters");
const architecture = @import("architecture");
const console = @import("console.zig");
const log = @import("log.zig");
const wall_clock = @import("wall-clock.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const scheduler = @import("scheduler.zig");
const sync = @import("sync.zig");
const process = @import("process.zig");
const devices_broker = @import("devices-broker.zig");
const irq = @import("irq.zig");
const platform = @import("platform");
const tests = @import("tests.zig");
const build_options = @import("build_options");
const BootInformation = boot_handoff.BootInformation;

/// The calling convention used to enter the kernel. Pinned to SystemV explicitly:
/// the bootloader is built for the UEFI target, whose C convention is Microsoft
/// x64 (first argument in RCX), while the kernel is SystemV (first argument in
/// RDI). Both sides reference this so the `boot_information` pointer lands in the
/// register the other expects. `boot_handoff.kernel_abi` re-exports it to the loader.
pub const kernel_abi = boot_handoff.kernel_abi;

// POST/checkpoint codes emitted to I/O port 0x80 at boot milestones — the
// last-resort progress signal on a machine with no text output at all.
const cp_entry = 0x10;
const cp_paging = 0x20;
const cp_heap = 0x30;
const cp_discovery = 0x40;
const cp_scheduler = 0x50;
const cp_timer = 0x60;
const cp_running = 0x70;
const cp_exception = 0xE0;
const cp_panic = 0xEE;

/// Physical address of the low page reserved at boot for the AP trampoline (0 = none
/// was available). Claimed right after the frame allocator comes up, before paging
/// and the heap consume the scarce sub-1 MiB frames.
var ap_trampoline_page: u64 = 0;

/// Kernel entry point. The bootloader jumps here after `ExitBootServices` with a
/// pointer to the handoff data. There is no runtime, no stack unwinding, and no
/// caller to return to, so this never returns.
/// The real entry (`_start`, in isr.s) installs a kernel-owned stack in .bss
/// then calls this with the loader's `boot_information` pointer in RDI. We can't keep
/// running on the loader's stack: it's a low physical address that the identity
/// map covers only transitionally, and vanishes once the kernel drops the low
/// half. `boot_information` (also low) is reached through the physmap — its base is the
/// same under the loader's bootstrap tables and the kernel's own.
export fn kmainEntry(boot_information: *const BootInformation) callconv(kernel_abi) noreturn {
    kmain(@ptrFromInt(boot_handoff.physicalToVirtual(@intFromPtr(boot_information))));
}

fn kmain(boot_information: *const BootInformation) noreturn {
    // The **log** is the machine-readable diagnostic stream: it fans out to every
    // *diagnostic* channel that exists (serial, the 0xE9 debug console, and later a
    // file on a ramdisk/USB/SSD), so a message survives as long as any is present.
    // A headless, serial-less machine still boots correctly — it just goes quiet,
    // with port-0x80 checkpoints as the only progress signal.
    //
    // Serial is compiled in only under -Dserial (build.zig): a real machine often
    // has no live legacy COM1, and the log survives in the RAM buffer (below) and
    // is flushed to disk — so serial is now a QEMU/dev convenience the flashable
    // image leaves out. When it *is* built in, `serialInit`'s loopback probe still
    // guards against a dead port (so a -Dserial image is safe on real hardware).
    if (build_options.serial) {
        architecture.serialInit();
        log.addSink(architecture.serialWrite);
    }
    if (architecture.debugconPresent()) log.addSink(architecture.debugconWrite);
    // Retain the whole stream in a RAM buffer too, so a user program can later
    // read it back (klog_read) and persist the boot log to disk — the only way to
    // see it on a headless/real machine with no host capturing serial.
    // (Retention is the tagged ring inside log.zig — not a sink.)

    // The **framebuffer** is deliberately *not* a log sink. It's a separate output
    // surface — a bootstrap text console today, a graphics device driver later — so
    // we never assume the OS is text-based. Only a few user-facing status lines
    // (via `status`) and panics are mirrored to it; the verbose log stays out.
    //
    // The console is brought up *after* paging (below), not here: its one-time
    // full-screen clear then runs on the kernel's **write-combining** mapping of the
    // framebuffer instead of the loader's uncached one — a fast burst rather than
    // millions of uncached writes on real hardware. Until then, on-screen output is
    // absent (an early panic still lands in the serial/RAM log); the trade is worth
    // a near-instant boot. `console.write` is a safe no-op while the console is down.
    const fb = boot_information.framebuffer;

    log.checkpoint(cp_entry);

    // Catch CPU exceptions before doing anything that might fault: install our
    // reporter, then bring up the GDT + IDT.
    architecture.setFaultHandler(onException);
    architecture.init();

    status("/system/kernel: initialising kernel...\n");
    if (build_options.serial) log.write(if (architecture.serialPresent())
        "/system/kernel: serial console online (COM1)\n"
    else
        "/system/kernel: no serial UART (COM1 absent) -> log kept in RAM/debugcon\n");
    log.write("/system/kernel: cpu tables online (GDT, IDT, TSS)\n");
    log.print("  resolution : {d}x{d}\n", .{ fb.width, fb.height });
    log.print("  pitch      : {d} bytes\n", .{fb.pitch});
    log.print("  format     : {s}\n", .{@tagName(fb.format)});
    log.print("  framebuffer: 0x{x:0>16}\n", .{fb.base});
    log.print(" footprint  : {d} MiB\n", .{(fb.pitch * fb.height) / (1024 * 1024)});

    // Summarise the physical memory the loader handed us. The array is danos's
    // own MemoryRegion, so this is a plain slice — no firmware layout in sight.
    const regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.memory_map.regions)))[0..boot_information.memory_map.len];
    var usable_pages: u64 = 0;
    var reserved_pages: u64 = 0; // reserved RAM only — MMIO is device space, not RAM
    for (regions) |r| {
        switch (r.kind) {
            .usable => usable_pages += r.pages,
            .reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages,
            .mmio => {},
        }
    }
    const total_pages = usable_pages + reserved_pages;
    const total_bytes = total_pages * abi.page_size;
    const gib = 1 << 30;

    log.write("\n/system/kernel: physical memory\n");
    log.print("  total RAM  : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) });
    log.print("  usable     : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)});
    log.print("  reserved   : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)});
    log.print("  regions    : {d} - entries in the firmware memory map\n", .{regions.len});

    // Bring up the physical frame allocator over that map, and prove it works:
    // allocate three frames, then hand them back.
    pmm.init(boot_information.memory_map);
    // Claim the AP trampoline's low (<1 MiB) page *now*, before paging and the heap
    // draw down sub-1 MiB frames (the allocator scans upward from frame 0). Held
    // until SMP bring-up; 0 means none was available (we stay uniprocessor).
    ap_trampoline_page = pmm.allocBelow(0x100000) orelse 0;
    const s1 = pmm.stats();
    log.print("\n/system/kernel: frame allocator online\n", .{});
    log.print("  free frames: {d} ({d} MiB)\n", .{ s1.free_frames, mib(s1.free_frames) });
    const f0 = pmm.alloc();
    const f1 = pmm.alloc();
    const f2 = pmm.alloc();
    log.print("  alloc x3   : 0x{x} 0x{x} 0x{x}\n", .{ f0 orelse 0, f1 orelse 0, f2 orelse 0 });
    if (f0) |p| pmm.free(p);
    if (f1) |p| pmm.free(p);
    if (f2) |p| pmm.free(p);
    log.print("  after free : {d} frames free\n", .{pmm.stats().free_frames});

    // Switch off the firmware's page tables onto our own (with real permissions).
    architecture.enablePaging(pmm.alloc, pmm.free, boot_information);
    log.checkpoint(cp_paging);
    log.print("\n/system/kernel: paging enabled\n", .{});
    log.print("  page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
    log.print("  kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});

    // Now on our own tables, the framebuffer window is write-combining: bring up the
    // on-screen console and clear it to a blank canvas (a fast burst here, not the loader's
    // uncached crawl). Routine boot output goes only to the log; this console now exists for
    // early-boot and fatal (`fatal`/panic) output, until the display service takes over.
    console.init(fb);
    // The console joins the log sinks: the boot transcript — kernel AND
    // userspace lines, each timestamped by the renderer — shows on screen
    // until the display service claims the framebuffer (which flips the
    // console's `suppressed` and silences this sink). On a machine with no
    // serial this is the only live view of the boot, and a slow boot becomes
    // diagnosable by eye: the timeline is right there.
    if (console.present()) log.addSink(console.write);
    log.write(if (console.present())
        "/system/kernel: framebuffer ready (early-boot + fatal fallback; the display service drives it in normal operation)\n"
    else
        "/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");

    // Bring up the kernel heap (dynamic allocation), built on the VMM.
    heap.init();
    log.checkpoint(cp_heap);
    log.write("\n/system/kernel: kernel heap online\n");
    // Measure the amount of resources the kernel is actually using
    const s2 = pmm.stats();
    log.print("  Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)});

    // Enumerate hardware from the firmware tables (ACPI here) into a generic
    // device tree, then list it. Discovery walks ACPI memory directly (identity-
    // mapped) and maps PCIe configuration space on demand via the VMM. A failure here is
    // not fatal yet — log it and carry on.
    const hal = platform.Hal{
        .mapMmio = architecture.mapMmio,
        .pioRead = architecture.pioRead,
        .pioWrite = architecture.pioWrite,
    };
    if (platform.discover(boot_information, heap.allocator(), hal)) |devtree| {
        var device_tree = devtree;
        log.write("\n/system/kernel: device discovery online\n");
        device_tree.dump(log.write);

        // Snapshot the device tree for user-space drivers (device_enumerate/claim/
        // mmio_map operate on this flat, id-indexed table + claim map).
        devices_broker.init(&device_tree);
        if (devices_broker.dropped > 0) {
            // Otherwise entirely silent: drivers would just never see that hardware.
            log.print("/system/kernel: WARNING {d} device(s) dropped — table full\n", .{devices_broker.dropped});
        }

        // Publish the loader's framebuffer as a claimable `display` device, so a
        // user-space display service can take it over the same claim + mmio_map path as
        // any other hardware (it is not firmware-discovered; it rides the boot handoff).
        if (devices_broker.seedDisplay(fb.base, fb.width, fb.height, fb.pitch, @intFromEnum(fb.format), fb.refresh_hz)) |display_id| {
            log.print("/system/kernel: framebuffer device {d} seeded ({d}x{d}, pitch {d}, {d} Hz, write-combining)\n", .{ display_id, fb.width, fb.height, fb.pitch, fb.refresh_hz });
        }

        // Install the device-IRQ trampolines, so a driver's irq_bind has vectors to
        // land on. Every line stays masked until something binds it (ioapic.init).
        irq.init();

        // Power register map, from the FADT (the SLP_TYP sleep values live in AML,
        // which the kernel doesn't parse — the ring-3 acpi service owns soft-off).
        const pw = platform.powerInformation();
        log.write("/system/kernel: power\n");
        log.print("  pm1a_cnt   : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width });
        log.print("  reset      : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value });

        // Feed the architecture layer the discovered addresses/facts so it makes no legacy
        // assumptions — the point of all this on UEFI Class 3 firmware. MMIO bases
        // (HPET, I/O APIC) come from the device tree; scalar facts from ACPI.
        const pinfo = platform.platformInformation();
        const hpet_base: u64 = if (device_tree.firstOfClass(.timer)) |t|
            (if (t.firstResource(.memory)) |r| r.start else 0)
        else
            0;
        var ioapic_base: u64 = 0;
        var ioapic_gsi: u32 = 0;
        if (device_tree.firstOfClass(.interrupt_controller)) |ic| {
            if (ic.firstResource(.memory)) |r| ioapic_base = r.start;
            if (ic.firstResource(.irq)) |r| ioapic_gsi = @intCast(r.start);
        }
        var isos: [16]architecture.IsoEntry = undefined;
        const iso_n = @min(pinfo.override_count, isos.len);
        for (0..iso_n) |i| isos[i] = .{
            .source = pinfo.overrides[i].source,
            .gsi = pinfo.overrides[i].gsi,
            .flags = pinfo.overrides[i].flags,
        };
        const pm_timer: ?architecture.PmTimer = if (pinfo.pm_timer.present())
            .{ .mmio = pinfo.pm_timer.mmio, .address = pinfo.pm_timer.address, .is_32bit = pinfo.pm_timer_32bit }
        else
            null;
        architecture.configurePlatform(.{
            .pic_present = pinfo.pic_present,
            .hpet_base = hpet_base,
            .pm_timer = pm_timer,
            .ioapic_base = ioapic_base,
            .ioapic_gsi_base = ioapic_gsi,
            .overrides = isos[0..iso_n],
        });
        if (pinfo.spcr_uart) |u| architecture.serialReconfigure(u.mmio, u.address);

        log.write("/system/kernel: platform\n");
        log.print("  8259 PIC   : {s}\n", .{if (pinfo.pic_present) "present" else "absent"});
        log.print("  lapic base : 0x{x}\n", .{pinfo.lapic_base});
        log.print("  hpet base  : 0x{x}\n", .{hpet_base});
        log.print("  pm timer   : {s} 0x{x} ({s})\n", .{ if (pinfo.pm_timer.mmio) "mmio" else "io", pinfo.pm_timer.address, if (pinfo.pm_timer_32bit) "32-bit" else "24-bit" });
        if (pinfo.spcr_uart) |u| {
            log.print("  console UART: {s} 0x{x} (SPCR type {d})\n", .{ if (u.mmio) "mmio" else "io", u.address, pinfo.spcr_kind });
        } else {
            log.write("  console UART: none in SPCR -> legacy COM1\n");
        }
        log.print("  ioapic     : base 0x{x}, {d} inputs (masked); route0 raw 0x{x}\n", .{ ioapic_base, architecture.irqRouteCount(), architecture.irqRouteRaw(0) });
        const cores = platform.cpus();
        log.print("  cpus       : {d} usable core(s); 1 running (BSP), {d} AP(s) parked (SMP bring-up pending)\n", .{ cores.len, if (cores.len > 0) cores.len - 1 else 0 });
        if (platform.cpusDropped() > 0)
            log.print("  cpus       : WARNING {d} core(s) beyond pool cap dropped\n", .{platform.cpusDropped()});
    } else |err| {
        log.print("\n/system/kernel: device discovery failed: {s}\n", .{@errorName(err)});
    }
    log.checkpoint(cp_discovery);

    // Install the system_call handler (int 0x80 gate + system_call stub) once, before any
    // user code runs.
    process.init();

    // Register the current context as the first task before enabling preemption.
    scheduler.init(4);
    log.checkpoint(cp_scheduler);
    log.write("\n/system/kernel: scheduler online\n");

    // Start the timer and unmask interrupts — the kernel now has a heartbeat, and
    // the timer preempts among tasks.
    architecture.startTimer();
    architecture.enableInterrupts();
    log.checkpoint(cp_timer);
    log.print("/system/kernel: timer online ({d} Hz tick; timer clock {d} MHz, clock {d} MHz; calibrated via {s})\n", .{ architecture.timer_hz, architecture.timerClockHz() / 1_000_000, architecture.clockHz() / 1_000_000, architecture.timerCalibrationSource() });

    // The tsc-sync test forces the TSC clocksource on before the cores come up, so the
    // TSC + warp-check path is exercised even under TCG (which won't advertise an
    // invariant TSC). Inert in a normal build (docs/timers.md).
    if (build_options.test_case) |tc| {
        if (std.mem.eql(u8, tc, "tsc-sync")) architecture.forceTscClocksourceForTest();
    }

    // Wake the other cores (application processors). A no-op on a single-core
    // machine; on SMP each AP climbs to long mode and reports in (docs/smp.md). The
    // per-core TSC warp check rides this: each AP is vetted before it joins the run
    // loop (docs/timers.md).
    bringUpSecondaries();

    // Report the monotonic clock's final reliability, now the warp check has run on
    // every core. On real Intel/AMD this is the invariant, synchronized TSC; a bare
    // VM (no invariant bit) or a machine whose cores' TSCs skew uses the HPET instead.
    log.print("/system/kernel: clocksource {s} (TSC invariant: {s}, synchronized: {s})\n", .{
        architecture.clockSourceName(),
        if (architecture.clockInvariant()) "yes" else "no",
        if (architecture.clockSynchronized()) "yes" else "no",
    });
    if (!architecture.clockSynchronized())
        log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n");

    // Anchor wall-clock time: read the RTC once, now the monotonic clock is final.
    wall_clock.init();
    log.print("/system/kernel: wall clock {d} (Unix epoch seconds, UTC, from the RTC)\n", .{wall_clock.nowSeconds()});

    // In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
    // Normal builds fall through to the idle halt.
    if (build_options.test_case) |case| {
        tests.run(case, boot_information);
        architecture.halt();
    }

    log.checkpoint(cp_running);
    status("/system/kernel: initialised.\n");

    // Publish the initial-ramdisk so user space can `system_spawn` its bundled
    // binaries by name. The kernel no longer launches them itself: init is the
    // service supervisor and the device manager spawns the drivers it discovers.
    publishInitialRamdisk(boot_information);

    // Hand over to user space: spawn /system/services/init out of the ramdisk as a
    // real ring-3 process, PID 1 — it rides the same table as every other binary.
    // As the supervisor it brings up the system services (the VFS server, the device
    // manager); the device manager then discovers the hardware and spawns each
    // driver. init runs on its own address space, preemptively — this boot context
    // becomes the BSP's idle loop.
    status("/system/kernel: starting /system/services/init...\n");
    process.spawnBundled("/system/services/init") catch |err| {
        statusPrint("/system/kernel: /system/services/init failed to start: {s}\n", .{@errorName(err)});
    };

    // Become the idle task: drop below every real task and halt until an
    // interrupt. The timer keeps preempting into init and any other work.
    scheduler.setPriority(0);
    status("\n/system/kernel: kernel idle; user space is running.\n");
    architecture.halt();
}

/// Publish the initial-ramdisk image to the process layer so user space can
/// `system_spawn` its bundled binaries by name. The kernel used to spawn every
/// bundled program here; now init (the service supervisor) and the device manager
/// (drivers) own that, so this only hands the image over — nothing is launched from
/// the kernel.
fn publishInitialRamdisk(boot_information: *const boot_handoff.BootInformation) void {
    if (boot_information.initial_ramdisk_len == 0) return;
    const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
    process.setInitialRamdisk(image);
}

/// Wake the application processors the firmware left parked. Allocates the low
/// trampoline page (and makes it executable), then wakes each non-boot core in turn,
/// handing it a fresh kernel stack and its per-CPU slot. Cores that don't report in
/// are left parked — the running system is unaffected. See docs/smp.md.
fn bringUpSecondaries() void {
    const cores = platform.cpus();
    if (cores.len <= 1) return;

    // A low (<1 MiB) frame was reserved at boot for the real-mode trampoline (a SIPI
    // vector addresses it). It's kept for the system's life — armed only during a
    // wake, inert (zeroed, non-executable) otherwise — so cores can be re-woken later.
    if (ap_trampoline_page == 0) {
        log.write("/system/kernel: smp: no low page for the AP trampoline; staying uniprocessor\n");
        return;
    }
    architecture.setTrampolinePage(ap_trampoline_page);
    architecture.setSecondaryEntry(scheduler.secondaryMain); // where a woken core joins the run loop

    // Test hook: the smp-retry case forces the first wake to fail, so the retry below
    // must still bring every core online. Inert in a normal build (test_case is null).
    if (build_options.test_case) |tc| {
        if (std.mem.eql(u8, tc, "smp-retry")) architecture.testFailNextWakes(1);
    }

    log.print("\n/system/kernel: bringing up {d} application processor(s)\n", .{cores.len - 1});
    const maximum_wake_attempts = 3; // a core that misses the first INIT-SIPI-SIPI gets retried
    for (cores[1..], 1..) |core, index| {
        const stack = heap.allocator().alloc(u8, parameters.kernel_stack_size) catch {
            log.print("  cpu apic_id {d}: no stack; skipped\n", .{core.apic_id});
            continue;
        };
        const stack_top = (@intFromPtr(stack.ptr) + stack.len) & ~@as(usize, 15);
        // This core's dedicated fault stack — allocated only now that the core is
        // real, rather than reserved statically for every possible core.
        const fault_stack = heap.allocator().alloc(u8, architecture.fault_stack_size) catch {
            log.print("/system/kernel:  cpu apic_id {d}: no fault stack; skipped\n", .{core.apic_id});
            continue;
        };
        architecture.setFaultStack(index, (@intFromPtr(fault_stack.ptr) + fault_stack.len) & ~@as(usize, 15));
        const pc = scheduler.prepareSecondary(index, core.apic_id);
        var attempt: u32 = 1;
        while (attempt <= maximum_wake_attempts) : (attempt += 1) {
            if (architecture.startSecondary(core.apic_id, stack_top, @intFromPtr(pc), index)) {
                pc.online = true;
                log.print("/system/kernel:  cpu apic_id {d}: online (attempt {d})\n", .{ core.apic_id, attempt });
                break;
            }
            if (attempt == maximum_wake_attempts)
                log.print("/system/kernel:  cpu apic_id {d}: no response after {d} attempts (parked)\n", .{ core.apic_id, maximum_wake_attempts });
        }
    }
    log.print("/system/kernel: {d}/{d} cores online\n", .{ scheduler.onlineCount(), cores.len });
}

/// A user-facing status line. Now that the user-space **display service** owns the
/// framebuffer in normal operation (docs/display.md), routine kernel output goes to the
/// diagnostic `log` (serial/debugcon/RAM) *only* — never to the on-screen console, which
/// the compositor is about to paint over. For a message that must reach the screen even so
/// — a panic or a fatal fault, when the machine is going down — use `fatal`.
fn status(message: []const u8) void {
    log.write(message);
}

/// A fatal, user-facing message: to the diagnostic log *and* the on-screen console, forcing
/// the console back on (`setSuppressed(false)`) first — a dying machine's last words outrank
/// any display service holding the framebuffer. The console is otherwise silent in normal
/// operation (see `status`); it exists now only for early-boot and fatal output.
fn fatal(message: []const u8) void {
    log.appendPanic(message); // bounded lock wait: a panic never deadlocks on the log
    console.setSuppressed(false);
    console.write(message);
}

fn statusPrint(comptime fmt: []const u8, args: anytype) void {
    var buffer: [256]u8 = undefined;
    status(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}

fn fatalPrint(comptime fmt: []const u8, args: anytype) void {
    var buffer: [256]u8 = undefined;
    fatal(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}

/// Frames (4 KiB pages) to whole MiB.
fn mib(pages: u64) u64 {
    return pages * abi.page_size / (1024 * 1024);
}

fn kib(frames: u64) u64 {
    return frames * abi.page_size / (1024);
}

/// Whether a ring-3 exception is attributable to the process that raised it — and
/// therefore recoverable by killing that process. NMI (2), double fault (8), and
/// machine check (18) report machine or kernel trouble even when they arrive with a
/// user CS (an NMI interrupts whatever happens to be running), so they stay terminal.
fn recoverableFault(vector: u64) bool {
    return switch (vector) {
        2, 8, 18 => false,
        else => true,
    };
}

/// Report a CPU exception. Two outcomes (docs/resilience.md):
///
/// **A fault taken in user mode kills the faulting process, not the machine.** The
/// kernel is intact — the CPU trapped onto the task's kernel stack — so the process
/// is killed, everything it held (address space, IRQ bindings, IPC handles, device
/// grants' frames) is reclaimed, and the core reschedules. A crashing driver takes
/// itself down, never the OS.
///
/// **Everything else halts this core.** A kernel-mode fault means the trusted base
/// itself is broken — there is nothing safe to kill — and NMI/#DF/#MC report machine
/// trouble regardless of CS (`recoverableFault`). Even then the fault is *contained*:
/// on an application processor only that core stops and the rest keep running. The
/// report names the core so an AP fault is attributed, and goes to every output sink
/// plus a POST code and a persistent breadcrumb. (A ring-3 fault on a *borrowed*
/// kernel thread — process.run, the user-pf isolation probe — also lands here: there
/// is no scheduled process to kill.)
/// Classify a CPU exception vector as the ExitReason a supervisor reads — the
/// fault classes of docs/process-lifecycle.md. Faults are exit reasons, never
/// signals delivered to the faulting process: recovery is restart, not a handler.
fn exitReasonForVector(vector: u64) abi.ExitReason {
    return switch (vector) {
        14 => .segmentation_fault, // page fault
        6 => .illegal_instruction, // invalid opcode
        0, 16, 19 => .arithmetic_fault, // divide error, x87, SIMD
        13 => .protection_fault, // general protection
        else => .fault,
    };
}

fn onException(state: *const architecture.CpuState) noreturn {
    if (architecture.fromUser(state) and scheduler.currentIsUserProcess() and recoverableFault(state.vector)) {
        statusPrint("\n/system/kernel: process {d} ({s}) killed by {s} (vector {d}) on core {d}\n", .{ scheduler.currentId(), scheduler.current().name(), architecture.exceptionName(state.vector), state.vector, scheduler.currentCpuIndex() });
        statusPrint("  error code : 0x{x}\n", .{state.error_code});
        statusPrint("  IP         : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
        if (architecture.faultAddress(state)) |address| statusPrint("  fault addr : 0x{x:0>16}\n", .{address});
        process.killCurrentProcess(exitReasonForVector(state.vector)); // reclaims everything, reschedules; never returns
    }

    log.checkpoint(cp_exception);
    const core = scheduler.currentCpuIndex();
    // The machine is going down: paint the exception on screen too — `fatalPrint` forces the
    // console back on even if a display service was holding the framebuffer — on top of the
    // diagnostic log.
    fatalPrint("\nCPU EXCEPTION on core {d}: {s} (vector {d})\n", .{ core, architecture.exceptionName(state.vector), state.vector });
    // Name the culprit: which task, and whether it faulted in ring 3 (a process the
    // kernel would normally kill — landing here means it had no address space) or ring 0
    // (the trusted base itself). Without this the fatal report is anonymous.
    fatalPrint("  task       : {d} ({s}), {s}\n", .{ scheduler.currentIdSafe(), scheduler.currentNameSafe(), if (architecture.fromUser(state)) "ring 3 (user)" else "ring 0 (kernel)" });
    fatalPrint("  error code : 0x{x}\n", .{state.error_code});
    fatalPrint("  IP         : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
    fatalPrint("  SP         : 0x{x:0>16}\n", .{architecture.stackPointer(state)});
    if (architecture.faultAddress(state)) |address| fatalPrint("  fault addr : 0x{x:0>16}\n", .{address});

    var buffer: [128]u8 = undefined;
    log.recordPanic(std.fmt.bufPrint(&buffer, "CPU exception {s} (vector {d}) on core {d} at IP 0x{x}", .{ architecture.exceptionName(state.vector), state.vector, core, architecture.instructionPointer(state) }) catch "cpu exception");
    // Free the BKL if this core held it (a kernel-mode fault, or a nested fault in the
    // recovery teardown), so halting this one core doesn't deadlock every other core on
    // the lock. Only that core stops; the rest — and the supervisor — keep running.
    sync.releaseIfHeldHere();
    architecture.halt();
}

/// Freestanding has no OS to receive a panic. Emit it to every output sink, drop a
/// POST code + a persistent breadcrumb (so a post-mortem can recover it even with
/// no live console), then halt. Assumes no console — the sinks self-guard.
pub const panic = std.debug.FullPanic(struct {
    fn panic(message: []const u8, first_trace_address: ?usize) noreturn {
        _ = first_trace_address;
        log.checkpoint(cp_panic);
        log.recordPanic(message);
        fatal("\nKERNEL PANIC: "); // a panic outranks any display service holding the screen
        fatal(message);
        fatal("\n");
        fatalPrint("  task       : {d} ({s})\n", .{ scheduler.currentIdSafe(), scheduler.currentNameSafe() });
        sync.releaseIfHeldHere(); // don't deadlock the other cores on the lock we may hold
        architecture.halt();
    }
}.panic);
