//! system/services/fat — the FAT filesystem server. Spawned as a boot service, it
//! opens the block device (a USB stick via usb-storage) under `.block`, mounts the
//! FAT filesystem on it (the pure engine in engine.zig), and mounts itself into
//! the VFS at /mnt/usb. From then on the VFS forwards every open/read/write/
//! status/readdir/close under /mnt/usb to this server, which serves the same
//! vfs-protocol as a backend — turning block reads into file reads.
//!
//! The block data path never crosses IPC: a DMA bounce buffer is handed to the
//! block driver by physical address, and the engine copies sectors in and out of
//! it.

const std = @import("std");
const runtime = @import("runtime");
const engine = @import("engine.zig");
const on_disk = @import("on-disk.zig");
const protocol = runtime.vfs_protocol;
const dma = runtime.dma;

const mount_point = "/mnt/usb";

// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
// buffer the driver reads/writes by physical address.
const IpcBlock = struct {
    device: runtime.block.Device,
    bounce: dma.Region,

    fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
        const self: *IpcBlock = @ptrCast(@alignCast(context));
        if (!self.device.read(lba, 1, self.bounce.physical)) return false;
        const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
        @memcpy(buffer[0..512], source[0..512]);
        return true;
    }
    fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
        const self: *IpcBlock = @ptrCast(@alignCast(context));
        const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
        @memcpy(destination[0..512], buffer[0..512]);
        if (!self.device.write(lba, 1, self.bounce.physical)) return false;
        device_dirty = true; // a block reached the device; a close will flush it
        return true;
    }
};

var ipc_block: IpcBlock = undefined;
// Set whenever a block is written, cleared when the device cache is flushed on a
// file close — so writes are committed to stable media before a power-off.
var device_dirty: bool = false;
var filesystem: engine.FileSystem = undefined;

// Open handles the VFS holds against this backend: each maps a node id to a
// resolved engine node.
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
var open_nodes = [_]OpenNode{.{}} ** 32;

fn allocOpen() ?usize {
    for (&open_nodes, 0..) |*o, i| {
        if (!o.used) return i;
    }
    return null;
}

fn openAt(id: u64) ?*OpenNode {
    if (id >= open_nodes.len) return null;
    const o = &open_nodes[@intCast(id)];
    return if (o.used) o else null;
}

fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
    @memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
    const n = @min(payload.len, out.len - protocol.reply_size);
    @memcpy(out[protocol.reply_size..][0..n], payload[0..n]);
    return protocol.reply_size + n;
}

fn fail(out: []u8) usize {
    return writeReply(out, .{ .status = -1 }, &.{});
}

/// How often to look for a block device while none is mounted. Storage arriving
/// is EVENT-shaped (the usb chain registering, possibly after a driver restart),
/// but the registry has no subscription — a slow poll from our own harness loop
/// keeps the service responsive (ping, terminate) while it waits, and keeps it
/// alive to catch storage that appears LATE (a restarted usb-storage after a
/// transient failure — the resilience half of docs/logging.md's storage story).
const mount_retry_ms = 500;

var mounted = false;
var service_endpoint: runtime.ipc.Handle = 0;

fn initialise(endpoint: runtime.ipc.Handle) bool {
    service_endpoint = endpoint;
    _ = runtime.system.write("/system/services/fat: starting, waiting for a block device\n");
    // With the router in the kernel, clients hold OUR node ids directly; sweep
    // a dead client's open handles via the published exit events (the pattern
    // the old userspace router used for its own table).
    _ = runtime.process.subscribeExits(endpoint);
    tryBringUp();
    if (!mounted) _ = runtime.system.timerOnce(endpoint, mount_retry_ms);
    return true; // serve regardless: requests fail politely until storage mounts
}

/// One storage bring-up attempt: block device -> FAT mount -> VFS mounts. Sets
/// `mounted` on success; a failure leaves everything untouched for the next tick.
fn tryBringUp() void {
    if (mounted) return;
    const device = runtime.block.tryOpen() orelse return;
    const geometry = device.geometry() orelse {
        _ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
        return;
    };
    const bounce = dma.alloc(4096, dma.coherent) orelse return;
    ipc_block = .{ .device = device, .bounce = bounce };

    const block_device = engine.BlockDevice{
        .context = &ipc_block,
        .block_size = geometry.block_size,
        .block_count = geometry.block_count,
        .readBlockFn = IpcBlock.readBlock,
        .writeBlockFn = IpcBlock.writeBlock,
    };
    filesystem = engine.FileSystem.mount(block_device) orelse {
        _ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
        return;
    };
    std.log.info("mounted FAT ({s}, {d} clusters, partition lba {d})", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });

    // Mount ourselves into the kernel VFS at /mnt/usb — and serve /var from the
    // volume's /var subtree, so FHS paths (the logger's /var/log) stay decoupled
    // from which volume carries them.
    if (runtime.fs.mount(mount_point, endpointForMount())) {
        std.log.info("mounted {s}", .{mount_point});
    } else {
        _ = runtime.system.write("/system/services/fat: could not mount /mnt/usb\n");
    }
    if (runtime.fs.mountRewritten("/var", endpointForMount(), "/var")) {
        std.log.info("mounted /var", .{});
    } else {
        _ = runtime.system.write("/system/services/fat: could not mount /var\n");
    }
    mounted = true;
}

fn endpointForMount() runtime.ipc.Handle {
    return service_endpoint;
}

/// A subscribed process-exit event: release every open handle the dead client
/// held, so a crashed reader can't pin table slots (or, later, locks).
fn onNotification(badge: u64) void {
    const got = runtime.ipc.Received{ .len = 0, .badge = badge, .cap = null };
    if (got.isTimer()) {
        tryBringUp();
        if (!mounted) _ = runtime.system.timerOnce(service_endpoint, mount_retry_ms);
        return;
    }
    if (!got.isChildExit()) return;
    const dead = got.childProcessId();
    var released: u32 = 0;
    for (&open_nodes) |*o| {
        if (o.used and o.owner == dead) {
            o.* = .{};
            released += 1;
        }
    }
    if (released != 0) std.log.info("released {d} handle(s) for dead client {d}", .{ released, dead });
}

const ParentLeaf = struct { parent: []const u8, leaf: []const u8 };

// Split a path into its parent directory and final component: "/a/b" -> ("/a",
// "b"); "/b" -> ("/", "b"); "b" -> ("/", "b").
fn splitParent(path: []const u8) ParentLeaf {
    const slash = std.mem.lastIndexOfScalar(u8, path, '/');
    return .{
        .parent = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/",
        .leaf = if (slash) |s| path[s + 1 ..] else path,
    };
}

fn handleOpen(out: []u8, path: []const u8, flags: u32, sender: u32) usize {
    var node = filesystem.resolve(path);
    if (node == null and flags & protocol.create != 0) {
        const split = splitParent(path);
        const parent = filesystem.resolve(split.parent) orelse return fail(out);
        node = filesystem.createFile(parent, split.leaf);
    }
    var resolved = node orelse return fail(out);
    // O_TRUNC: replace an existing file's contents rather than overwriting in place
    // (frees the old chain, so a shorter rewrite leaves no stale tail).
    if (flags & protocol.truncate != 0 and !resolved.is_directory) {
        filesystem.truncate(&resolved);
    }
    const index = allocOpen() orelse return fail(out);
    open_nodes[index] = .{ .used = true, .node = resolved, .owner = sender };
    return writeReply(out, .{ .status = 0, .node = index }, &.{});
}

fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
    _ = capability;
    if (!mounted) return fail(out); // storage not up (yet): fail politely, clients retry
    if (message.len < protocol.request_size) return fail(out);
    const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
    const payload = message[protocol.request_size..];

    // Stamp create/write with the current wall-clock time (mtime). Cheap, and it
    // keeps the engine pure (it takes the time as data, not a syscall).
    filesystem.current_time_epoch = runtime.system.wallClock();

    switch (request.operation) {
        .open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags, sender),
        .read => {
            const o = openAt(request.node) orelse return fail(out);
            var buffer: [protocol.maximum_payload]u8 = undefined;
            const want = @min(@as(usize, request.len), buffer.len);
            const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]);
            return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]);
        },
        .write => {
            const o = openAt(request.node) orelse return fail(out);
            const data = payload[0..@min(payload.len, request.len)];
            const n = filesystem.writeFile(&o.node, @intCast(request.offset), data);
            return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
        },
        .status => {
            const o = openAt(request.node) orelse return fail(out);
            const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
            const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime };
            return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status));
        },
        .readdir => {
            const o = openAt(request.node) orelse return fail(out);
            if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
            const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
            const kind: protocol.NodeKind = if (listing.is_directory) .directory else .regular;
            const header = protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size };
            var buffer: [protocol.maximum_payload]u8 = undefined;
            @memcpy(buffer[0..protocol.directory_entry_size], std.mem.asBytes(&header));
            const nlen = @min(listing.name_len, buffer.len - protocol.directory_entry_size);
            @memcpy(buffer[protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]);
            const total = protocol.directory_entry_size + nlen;
            return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]);
        },
        .close => {
            if (openAt(request.node)) |o| o.used = false;
            // Durable-on-close: if any block reached the device since the last
            // flush, commit its cache to stable media now (best-effort). This is
            // what makes init's shutdown log flush survive a real power-off, and is
            // the right default for removable media the user may unplug.
            if (device_dirty) {
                _ = ipc_block.device.flush();
                device_dirty = false;
            }
            return writeReply(out, .{ .status = 0 }, &.{});
        },
        .mkdir => {
            const path = payload[0..@min(payload.len, request.len)];
            if (filesystem.resolve(path) != null) return fail(out); // already exists — no duplicate entries
            const split = splitParent(path);
            const parent = filesystem.resolve(split.parent) orelse return fail(out);
            if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out);
            return writeReply(out, .{ .status = 0 }, &.{});
        },
        .unlink => {
            const split = splitParent(payload[0..@min(payload.len, request.len)]);
            const parent = filesystem.resolve(split.parent) orelse return fail(out);
            if (!filesystem.removeFile(parent, split.leaf)) return fail(out);
            return writeReply(out, .{ .status = 0 }, &.{});
        },
        .rename => {
            const both = payload[0..@min(payload.len, request.len)];
            const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
            const old_split = splitParent(both[0..sep]);
            const new_split = splitParent(both[sep + 1 ..]);
            // Same-directory rename only.
            if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return fail(out);
            const parent = filesystem.resolve(old_split.parent) orelse return fail(out);
            if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return fail(out);
            return writeReply(out, .{ .status = 0 }, &.{});
        },
        // A backend is never itself a mount target.
        .mount, .unmount => return fail(out),
    }
}

pub fn main() void {
    runtime.service.run(protocol.message_maximum, .{
        .service = .fat,
        .init = initialise,
        .on_message = onMessage,
        .on_notification = onNotification,
    });
}
