//! runtime.fs — the danos-native file API. A program opens, reads, writes, and
//! lists files through the kernel VFS root (resolve + redirect), each call
//! marshalling a vfs-protocol request over IPC. This is the danos-native layer
//! danos programs use directly; it is also where the file operations that later
//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces
//! the old POSIX `unistd` shim — a compatibility spelling danos does not need yet.
//!
//! Handles are *values*, not entries in a global descriptor table: a `File` /
//! `Directory` owns its VFS node id and (for files) a byte offset. So there is no
//! per-process fd limit and no shared table to synchronise — the danos-native
//! shape, unlike the POSIX fd model the old shim emulated.

const std = @import("std");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("vfs-protocol");

/// The kind of a filesystem node — re-exported so a caller need not import the
/// wire protocol.
pub const Kind = protocol.NodeKind;

/// A node's metadata (the answer to a status request).
pub const Attributes = struct {
    size: u64,
    kind: Kind,
    /// Modification time — Unix epoch seconds, UTC. 0 if the filesystem has none.
    mtime: u64 = 0,
};

// Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to
// `.regular` (the server is trusted, but a value outside the enum would be
// illegal to `@enumFromInt` directly).
fn kindFromWire(value: u32) Kind {
    return switch (value) {
        @intFromEnum(Kind.directory) => .directory,
        @intFromEnum(Kind.character_device) => .character_device,
        @intFromEnum(Kind.block_device) => .block_device,
        @intFromEnum(Kind.symbolic_link) => .symbolic_link,
        @intFromEnum(Kind.fifo) => .fifo,
        @intFromEnum(Kind.socket) => .socket,
        else => .regular,
    };
}

/// How to open a path.
pub const OpenOptions = struct {
    /// Create the file if it does not exist.
    create: bool = false,
    /// Open a directory node (for listing) rather than a file.
    directory: bool = false,
    /// Truncate an existing file to zero length on open (O_TRUNC) — replace its
    /// contents rather than overwriting in place.
    truncate: bool = false,

    fn wireFlags(self: OpenOptions) u32 {
        var f: u32 = 0;
        if (self.create) f |= protocol.create;
        if (self.directory) f |= protocol.directory;
        if (self.truncate) f |= protocol.truncate;
        return f;
    }
};

// The route to a path: the kernel resolves (fs_resolve) and either serves the
// node itself (the initrd at /system — a permanent token) or redirects us to
// the owning filesystem backend's endpoint, to which we speak the vfs-protocol
// rendezvous directly with the rewritten mount-relative path.
const Route = union(enum) {
    kernel: u64,
    backend: struct { handle: ipc.Handle, path: [224]u8, path_len: usize },

    fn backendPath(self: *const Route) []const u8 {
        return self.backend.path[0..self.backend.path_len];
    }
};

fn resolve(path: []const u8, flags: usize) ?Route {
    var out: [224]u8 = undefined;
    const route = system.fsResolve(path, flags, &out) orelse return null;
    switch (route) {
        .kernel => |token| return .{ .kernel = token },
        .backend => |b| {
            var r: Route = .{ .backend = .{ .handle = b.handle, .path = undefined, .path_len = b.path_len } };
            @memcpy(r.backend.path[0..b.path_len], out[0..b.path_len]);
            return r;
        },
    }
}

const Result = struct { reply: protocol.Reply, payload: []u8 };

// One request/reply round trip: [Request header][send payload] -> backend ->
// [Reply header][receive payload]. The receive payload lands in `out`.
fn transact(h: ipc.Handle, request: protocol.Request, send: []const u8, out: []u8) ?Result {
    var message: [protocol.message_maximum]u8 = undefined;
    @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
    const slen = @min(send.len, protocol.maximum_payload);
    @memcpy(message[protocol.request_size..][0..slen], send[0..slen]);

    var rbuf: [protocol.message_maximum]u8 = undefined;
    const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
    if (n < protocol.reply_size) return null;
    const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
    const rpl = @min(n - protocol.reply_size, out.len);
    @memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
    return .{ .reply = reply, .payload = out[0..rpl] };
}

/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
pub const File = struct {
    node: u64,
    offset: u64 = 0,
    /// The owning backend's endpoint, or null for a kernel-served node (the
    /// read-only /system tree), whose `node` is a permanent fs_node token.
    backend: ?ipc.Handle = null,

    /// Read up to `buffer.len` bytes at the current offset; returns the count, or
    /// null on error.
    pub fn read(self: *File, buffer: []u8) ?usize {
        const h = self.backend orelse {
            const n = system.fsNodeRead(self.node, self.offset, buffer) orelse return null;
            self.offset += n;
            return n;
        };
        const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
        const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
        const r = transact(h, request, &.{}, buffer) orelse return null;
        if (r.reply.status != 0) return null;
        self.offset += r.reply.len;
        return r.reply.len;
    }

    /// Write `data` at the current offset; returns the count written. A single
    /// call is capped at the VFS payload size, so the return may be short — use
    /// `writeAll` to write the whole slice. Null on error (kernel-served nodes
    /// are read-only).
    pub fn write(self: *File, data: []const u8) ?usize {
        const h = self.backend orelse return null;
        const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
        const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
        const r = transact(h, request, data[0..want], &.{}) orelse return null;
        if (r.reply.status != 0) return null;
        self.offset += r.reply.len;
        return r.reply.len;
    }

    /// Write all of `data`, looping past the per-call payload cap. Returns the
    /// total written, or null if a write failed before any progress.
    pub fn writeAll(self: *File, data: []const u8) ?usize {
        var written: usize = 0;
        while (written < data.len) {
            const n = self.write(data[written..]) orelse return if (written == 0) null else written;
            if (n == 0) return written; // no forward progress; stop rather than spin
            written += n;
        }
        return written;
    }

    /// Move the read/write cursor to an absolute byte position.
    pub fn seekTo(self: *File, position: u64) void {
        self.offset = position;
    }

    /// This file's metadata.
    pub fn attributes(self: *File) ?Attributes {
        const h = self.backend orelse {
            const a = system.fsNodeStatus(self.node) orelse return null;
            return .{ .size = a.size, .kind = if (a.kind == system.file_kind_directory) .directory else .regular, .mtime = a.mtime };
        };
        const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
        var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
        const r = transact(h, request, &.{}, &buffer) orelse return null;
        if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
        const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
        return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
    }

    /// Release the backend's open handle for this file. Kernel-served node
    /// tokens are permanent — nothing to release.
    pub fn close(self: *File) void {
        const h = self.backend orelse return;
        const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
        _ = transact(h, request, &.{}, &.{});
    }
};

/// Open (or create, with `.create`) `path`. Returns the open file, or null.
pub fn open(path: []const u8, options: OpenOptions) ?File {
    const route = resolve(path, options.wireFlags()) orelse return null;
    switch (route) {
        .kernel => |token| return .{ .node = token, .backend = null },
        .backend => |b| {
            const relative = route.backendPath();
            const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = options.wireFlags() };
            const r = transact(b.handle, request, relative, &.{}) orelse return null;
            if (r.reply.status != 0) return null;
            return .{ .node = r.reply.node, .backend = b.handle };
        },
    }
}

/// A path's metadata without keeping it open (open -> status -> close).
pub fn attributes(path: []const u8) ?Attributes {
    var file = open(path, .{}) orelse return null;
    defer file.close();
    return file.attributes();
}

/// Whether `path` resolves — handy as a readiness check (e.g. waiting for a mount
/// to come up before writing to it).
pub fn exists(path: []const u8) bool {
    return attributes(path) != null;
}

/// One entry returned by `Directory.next`.
pub const Entry = struct {
    kind: Kind = .regular,
    size: u64 = 0,
    name_buffer: [64]u8 = undefined,
    name_len: usize = 0,

    pub fn name(self: *const Entry) []const u8 {
        return self.name_buffer[0..self.name_len];
    }
};

/// An open directory being listed, cursor-advanced by `next`.
pub const Directory = struct {
    node: u64,
    cursor: u64 = 0,
    backend: ?ipc.Handle = null,

    /// Fill `entry` with the next directory entry; false at end of directory or
    /// on error.
    pub fn next(self: *Directory, entry: *Entry) bool {
        const h = self.backend orelse {
            var buffer: [@sizeOf(system.DirectoryEntryHeader) + 64]u8 = undefined;
            const n = system.fsNodeReaddir(self.node, self.cursor, &buffer) orelse return false;
            if (n < @sizeOf(system.DirectoryEntryHeader)) return false; // end
            const header = std.mem.bytesToValue(system.DirectoryEntryHeader, buffer[0..@sizeOf(system.DirectoryEntryHeader)]);
            entry.kind = if (header.kind == system.file_kind_directory) .directory else .regular;
            entry.size = header.size;
            const nlen = @min(@as(usize, header.name_len), entry.name_buffer.len);
            @memcpy(entry.name_buffer[0..nlen], buffer[@sizeOf(system.DirectoryEntryHeader)..][0..nlen]);
            entry.name_len = nlen;
            self.cursor += 1;
            return true;
        };
        const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
        var buffer: [protocol.message_maximum]u8 = undefined;
        const r = transact(h, request, &.{}, &buffer) orelse return false;
        if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
        if (r.payload.len < protocol.directory_entry_size) return false;
        const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
        entry.kind = kindFromWire(header.kind);
        entry.size = header.size;
        const source = r.payload[protocol.directory_entry_size..];
        const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
        @memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
        entry.name_len = nlen;
        self.cursor += 1;
        return true;
    }

    /// Release the backend's open handle for this directory.
    pub fn close(self: *Directory) void {
        var f = File{ .node = self.node, .backend = self.backend };
        f.close();
    }
};

/// Open `path` as a directory for listing. Returns null if it isn't one / on error.
pub fn openDirectory(path: []const u8) ?Directory {
    const file = open(path, .{ .directory = true }) orelse return null;
    return .{ .node = file.node, .backend = file.backend };
}

// A path-based request that returns only a status (mkdir, unlink). Kernel-served
// paths (the read-only /system) refuse mutation by construction: the resolve
// must land on a backend.
fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
    const route = resolve(path, 0) orelse return false;
    if (route != .backend) return false;
    const relative = route.backendPath();
    const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
    const r = transact(route.backend.handle, request, relative, &.{}) orelse return false;
    return r.reply.status == 0;
}

/// Create a directory at `path` (its parent must already exist). Returns true on
/// success. Only works under a mounted filesystem that supports directories.
pub fn makeDirectory(path: []const u8) bool {
    return pathOperation(.mkdir, path);
}

/// Create every missing directory along `path` (mkdir -p). Probes each prefix
/// with `exists` first — a FAT mkdir of an existing name is refused, and the
/// probe keeps the common "already there" case cheap. Returns true when the
/// whole path exists afterwards.
pub fn makePath(path: []const u8) bool {
    var end: usize = 0;
    while (end < path.len) {
        end += 1;
        while (end < path.len and path[end] != '/') end += 1;
        const prefix = path[0..end];
        if (prefix.len == 0 or (prefix.len == 1 and prefix[0] == '/')) continue;
        // Best-effort per prefix: components at or above a mount point ("/mnt")
        // are router names, not filesystem nodes — they neither exist as nodes
        // nor accept mkdir, and that is fine. Only the final verdict counts.
        if (!exists(prefix)) _ = makeDirectory(prefix);
    }
    return exists(path);
}

/// Remove the file at `path`. Returns true on success. Directories are refused
/// (a separate directory-removal would have to check emptiness).
pub fn remove(path: []const u8) bool {
    return pathOperation(.unlink, path);
}

/// Rename `old_path` to `new_path`. Both must resolve to the SAME filesystem
/// backend (same-directory, 8.3-name rename only for now). Returns true on
/// success.
pub fn rename(old_path: []const u8, new_path: []const u8) bool {
    const old_route = resolve(old_path, 0) orelse return false;
    const new_route = resolve(new_path, 0) orelse return false;
    if (old_route != .backend or new_route != .backend) return false;
    if (old_route.backend.handle != new_route.backend.handle) return false; // cross-filesystem
    const old_relative = old_route.backendPath();
    const new_relative = new_route.backendPath();
    const total = old_relative.len + 1 + new_relative.len;
    if (total > protocol.maximum_payload) return false;
    var payload: [protocol.maximum_payload]u8 = undefined;
    @memcpy(payload[0..old_relative.len], old_relative);
    payload[old_relative.len] = 0;
    @memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative);
    const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
    const r = transact(old_route.backend.handle, request, payload[0..total], &.{}) orelse return false;
    return r.reply.status == 0;
}

/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
/// the kernel VFS then routes everything under `target` to that backend.
/// Possession of the endpoint handle is the capability. Returns true on success.
pub fn mount(target: []const u8, backend: ipc.Handle) bool {
    return system.fsMount(target, backend, "");
}

/// As `mount`, with a backend-side rewrite prefix: a path under `target` reaches
/// the backend as `rewrite` + the mount-relative tail. How one volume serves two
/// mounts ("/mnt/usb" from its root, "/var" from its /var subtree).
pub fn mountRewritten(target: []const u8, backend: ipc.Handle, rewrite: []const u8) bool {
    return system.fsMount(target, backend, rewrite);
}
