//! 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 /volumes/usb. From then on the VFS forwards every open/read/write/
//! status/readdir/close under /volumes/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 ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const time = @import("time");
const block = @import("block");
const file_system = @import("file-system");
const memory = @import("memory");
const logging = @import("logging");
const engine = @import("engine.zig");
const on_disk = @import("on-disk.zig");
const envelope = @import("envelope");
const vfs_protocol = @import("vfs-protocol");

/// The generated vfs dispatch, bound to this server. There is one FAT volume per
/// process, so the handler context is empty and the state stays where it was: in
/// this file's globals.
const Serve = vfs_protocol.Protocol.Provider(void);

const Invocation = envelope.Invocation;
const Answer = envelope.Answer;

const mount_point = "/volumes/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: block.Device,
    bounce: memory.DmaRegion, // engine.max_transfer_sectors * 512 bytes

    fn readBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool {
        const self: *IpcBlock = @ptrCast(@alignCast(context));
        if (count == 0 or count > engine.max_transfer_sectors) return false;
        const len = count * 512;
        if (!self.device.read(lba, count, self.bounce.physical)) return false;
        const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
        @memcpy(buffer[0..len], source[0..len]);
        return true;
    }
    fn writeBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool {
        const self: *IpcBlock = @ptrCast(@alignCast(context));
        if (count == 0 or count > engine.max_transfer_sectors) return false;
        const len = count * 512;
        const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
        @memcpy(destination[0..len], buffer[0..len]);
        if (!self.device.write(lba, count, 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 clients hold against this backend: each maps a node id to a
// resolved engine node, and to the client that opened it. `owner` is the
// kernel-stamped badge of the opening task — the only source identity there is.
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;
}

/// The open node `id` names **for `owner`** — null unless the id is in range, in
/// use, and this client's own. Node ids are small integers drawn from a table of
/// thirty-two, so they are trivially guessable; before this check every client
/// honoured every other client's ids, which is the hole
/// docs/os-development/protocol-namespace.md names ("handles must be scoped per
/// client — validated against the badge"). Nothing else about them changed: they
/// are still per-session, still swept when their owner dies.
///
/// The owner is a *task*, not a process, because the badge is: a threaded client
/// reads and writes a node from the thread that opened it, exactly as the exit
/// sweep already released a worker thread's handles when that thread died.
fn openFor(id: u64, owner: u32) ?*OpenNode {
    if (id >= open_nodes.len) return null;
    const o = &open_nodes[@intCast(id)];
    if (!o.used or o.owner != owner) return null;
    return o;
}

/// What a handler returns when the thing asked for is not there — a bad node id,
/// a node that is someone else's, a path that does not resolve, a mutation the
/// volume refused. One errno for all of them, because a filesystem's failures are
/// all "no such thing" as far as the file API can act on them — and because
/// *someone else's* must be indistinguishable from *nobody's*, or the refusal
/// would itself tell a prober which ids are live (the same discipline the
/// protocol namespace's refused open follows).
const refused: isize = -envelope.ENOENT;

/// 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: ipc.Handle = 0;

fn initialise(endpoint: ipc.Handle) bool {
    service_endpoint = endpoint;
    _ = logging.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).
    _ = process.subscribeExits(endpoint);
    tryBringUp();
    if (!mounted) _ = time.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 = block.tryOpen() orelse return;
    const geometry = device.geometry() orelse {
        _ = logging.write("/system/services/fat: block geometry unavailable\n");
        return;
    };
    // Shareable so the buffer's capability can be attached down the chain (block server
    // -> controller), making its physical addresses reachable by the device under an
    // enforcing IOMMU. No-op binding otherwise.
    const bounce = memory.dmaAlloc(engine.max_transfer_sectors * 512, memory.dma_coherent | memory.dma_shareable) orelse return;
    if (bounce.handle) |handle| {
        if (!device.attach(handle)) {
            _ = logging.write("/system/services/fat: could not attach the DMA bounce buffer\n");
            return;
        }
        _ = ipc.close(handle); // the binding holds its own reference now
    }
    ipc_block = .{ .device = device, .bounce = bounce };

    const block_device = engine.BlockDevice{
        .context = &ipc_block,
        .block_size = geometry.block_size,
        .block_count = geometry.block_count,
        .readBlocksFn = IpcBlock.readBlocks,
        .writeBlocksFn = IpcBlock.writeBlocks,
    };
    filesystem = engine.FileSystem.mount(block_device) orelse {
        _ = logging.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 /volumes/usb — and serve
    // /system/configuration and /system/logs from the volume's identically-named
    // subtrees (the boot volume is hierarchy-shaped, so rewrite == prefix), so
    // hierarchy paths (the logger's /system/logs) stay decoupled from which
    // volume carries them.
    if (file_system.mount(mount_point, endpointForMount())) {
        std.log.info("mounted {s}", .{mount_point});
    } else {
        _ = logging.write("/system/services/fat: could not mount /volumes/usb\n");
    }
    if (file_system.mountRewritten("/system/configuration", endpointForMount(), "/system/configuration")) {
        std.log.info("mounted /system/configuration", .{});
    } else {
        _ = logging.write("/system/services/fat: could not mount /system/configuration\n");
    }
    if (file_system.mountRewritten("/system/logs", endpointForMount(), "/system/logs")) {
        std.log.info("mounted /system/logs", .{});
    } else {
        _ = logging.write("/system/services/fat: could not mount /system/logs\n");
    }
    mounted = true;
}

fn endpointForMount() 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 = ipc.Received{ .len = 0, .badge = badge, .cap = null };
    if (got.isTimer()) {
        tryBringUp();
        if (!mounted) _ = time.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 onOpen(_: void, invocation: Invocation(vfs_protocol.Open), answer: Answer(vfs_protocol.Opened)) isize {
    const path = invocation.tail;
    const flags = invocation.request.flags;
    var node = filesystem.resolve(path);
    if (node == null and flags & vfs_protocol.create != 0) {
        const split = splitParent(path);
        const parent = filesystem.resolve(split.parent) orelse return refused;
        node = filesystem.createFile(parent, split.leaf);
    }
    var resolved = node orelse return refused;
    // 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 & vfs_protocol.truncate != 0 and !resolved.is_directory) {
        filesystem.truncate(&resolved);
    }
    const index = allocOpen() orelse return refused;
    open_nodes[index] = .{ .used = true, .node = resolved, .owner = invocation.sender };
    answer.set(.{ .node = index });
    return 0;
}

fn onRead(_: void, invocation: Invocation(vfs_protocol.Read), answer: Answer(void)) isize {
    const o = openFor(invocation.target, invocation.sender) orelse return refused;
    const into = answer.tail();
    const want = @min(@as(usize, invocation.request.len), into.len);
    return @intCast(filesystem.readFile(o.node, @intCast(invocation.request.offset), into[0..want]));
}

fn onWrite(_: void, invocation: Invocation(vfs_protocol.Write), answer: Answer(vfs_protocol.Written)) isize {
    const o = openFor(invocation.target, invocation.sender) orelse return refused;
    const data = invocation.tail[0..@min(invocation.tail.len, invocation.request.len)];
    const n = filesystem.writeFile(&o.node, @intCast(invocation.request.offset), data);
    answer.set(.{ .count = @intCast(n) });
    return 0;
}

fn onStatus(_: void, invocation: Invocation(void), answer: Answer(vfs_protocol.FileStatus)) isize {
    const o = openFor(invocation.target, invocation.sender) orelse return refused;
    const kind: vfs_protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
    answer.set(.{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime });
    return 0;
}

/// One entry per call. End of directory — a node that is not a directory, or a
/// cursor past the last child — is an entry with no name, which is how the
/// protocol spells it now that the reply's length always counts the fixed part.
fn onReaddir(_: void, invocation: Invocation(vfs_protocol.Readdir), answer: Answer(vfs_protocol.DirectoryEntry)) isize {
    const o = openFor(invocation.target, invocation.sender) orelse return refused;
    if (!o.node.is_directory) {
        answer.set(.{});
        return 0;
    }
    const listing = filesystem.listEntry(o.node, @intCast(invocation.request.cursor)) orelse {
        answer.set(.{});
        return 0;
    };
    const kind: vfs_protocol.NodeKind = if (listing.is_directory) .directory else .regular;
    const into = answer.tail();
    const name_len = @min(listing.name_len, into.len);
    @memcpy(into[0..name_len], listing.name_buffer[0..name_len]);
    answer.set(.{ .kind = @intFromEnum(kind), .name_len = @intCast(name_len), .size = listing.size });
    return @intCast(name_len);
}

/// Closing is an operation on a node like any other, so it is scoped like any
/// other: a client may release its own handles and nobody else's. An id that is
/// not the caller's — free, out of range, or another client's — is refused
/// identically, so a close cannot be used to ask which ids are live either.
fn onClose(_: void, invocation: Invocation(void), _: Answer(void)) isize {
    const o = openFor(invocation.target, invocation.sender) orelse return refused;
    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 0;
}

fn onMakeDirectory(_: void, invocation: Invocation(void), _: Answer(void)) isize {
    const path = invocation.tail;
    if (filesystem.resolve(path) != null) return refused; // already exists — no duplicate entries
    const split = splitParent(path);
    const parent = filesystem.resolve(split.parent) orelse return refused;
    if (filesystem.createDirectory(parent, split.leaf) == null) return refused;
    return 0;
}

fn onUnlink(_: void, invocation: Invocation(void), _: Answer(void)) isize {
    const split = splitParent(invocation.tail);
    const parent = filesystem.resolve(split.parent) orelse return refused;
    if (!filesystem.removeFile(parent, split.leaf)) return refused;
    return 0;
}

fn onRename(_: void, invocation: Invocation(void), _: Answer(void)) isize {
    const both = invocation.tail;
    const separator = std.mem.indexOfScalar(u8, both, 0) orelse return refused;
    const old_split = splitParent(both[0..separator]);
    const new_split = splitParent(both[separator + 1 ..]);
    // Same-directory rename only.
    if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return refused;
    const parent = filesystem.resolve(old_split.parent) orelse return refused;
    if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return refused;
    return 0;
}

/// The verbs this backend implements. The three it leaves out — `mount`,
/// `unmount`, `bind` — answer `-ENOSYS` from the generated dispatch, which is
/// exactly right: path routing is the kernel's now, and only init implements
/// `bind` (docs/os-development/protocol-namespace.md). `describe` is the
/// envelope's own.
const handlers = Serve.Handlers{
    .open = onOpen,
    .close = onClose,
    .read = onRead,
    .write = onWrite,
    .status = onStatus,
    .readdir = onReaddir,
    .mkdir = onMakeDirectory,
    .unlink = onUnlink,
    .rename = onRename,
};

/// The vfs protocol has no operation that takes a capability, so `arrived` is
/// never claimed here — which, under the harness's ownership rule, means the
/// loop closes whatever a caller attached. That is the point of the rule: this
/// callback used to discard a `?ipc.Handle` and every request carrying one — a
/// legal thing for any client to do — spent a slot of the VFS server's
/// thirty-two until it could accept no capability at all.
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
    _ = arrived;
    // Storage not up (yet): fail politely, whatever was asked — clients retry.
    if (!mounted) {
        const status = envelope.Status{ .status = refused, .len = 0 };
        @memcpy(out[0..envelope.prefix_size], std.mem.asBytes(&status));
        return envelope.prefix_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 = time.wallClock();
    return Serve.dispatch({}, handlers, message, sender, null, out);
}

pub fn main() void {
    service.run(vfs_protocol.message_maximum, .{
        .service = "vfs",
        .init = initialise,
        .on_message = onMessage,
        .on_notification = onNotification,
    });
}
