//! `Channel` — layer L1 of the communication stack
//! (docs/os-development/communication.md) made concrete. A program holds a
//! channel that speaks a protocol; it does not hold a raw handle and marshal
//! bytes at one. The channel is the answer to "who am I talking to", decided
//! once at establishment, so nothing after that ever routes a party again:
//! every packet's `target` addresses an *object* within the peer already chosen.
//!
//! **Possession of the Channel is the connection.** There is no connect step, no
//! session id, no reconnect handshake — the endpoint capability inside is the
//! whole of the relationship, and it cannot be forged, only handed over. Which
//! also means a channel is a resource: `close` it, or it occupies a handle-table
//! slot for the life of the process.
//!
//! **A dead provider surfaces as `-EPEER`, and the recovery is to re-open.**
//! When the process on the other end exits, the kernel fails calls on its
//! endpoint rather than blocking forever; `call` returns null. The client does
//! not repair the channel — it discards it and opens the name again, which
//! reaches whatever instance the registry now points at. The restart story
//! falls out of the naming layer for free; no protocol needs a reconnect verb.
//!
//! `open` resolves a `/protocol/<name>` path through the kernel VFS router and
//! takes the provider's endpoint from the open reply's capability. The registry
//! answering it is init, PID 1, which mounts `/protocol` before it spawns anyone
//! (docs/os-development/protocol-namespace.md); `bind` below is the other half —
//! how a provider claims the name in the first place.

const std = @import("std");
const ipc = @import("ipc");
const time = @import("time");
const file_system = @import("file-system");
const vfs_protocol = @import("vfs-protocol");
const envelope = @import("envelope");

/// Longest `/protocol/...` path this client marshals. The registry's names are
/// short by construction (a contract leaf, not a file path), and the buffer is
/// on the stack of whoever opens.
pub const path_maximum: usize = 224;

/// Where the protocol namespace is rooted — the one path prefix in the system
/// that names contracts rather than files. Spelled once, here, so no caller
/// builds it by hand (docs/file-system-development/file-system-hierarchy.md).
pub const root: []const u8 = "/protocol";

/// Longest contract name — the part after `/protocol/`. Short by construction:
/// a leaf like `display`, or a subtree leaf like `test/shared-memory`.
pub const name_maximum: usize = 64;

/// What a `call` came back with: the provider's status, the reply payload (the
/// bytes after the `Status`, in the caller's own buffer), and any capability the
/// reply carried.
pub const Response = struct {
    status: envelope.Status,
    payload: []u8,
    capability: ?ipc.Handle,

    /// Whether the provider answered success. A negative status is its refusal
    /// (`-ENOSYS` for a verb it does not implement, and so on).
    pub fn succeeded(self: Response) bool {
        return self.status.status == 0;
    }
};

/// An open conversation with one provider, speaking one protocol.
pub const Channel = struct {
    /// The provider's endpoint. Sending into it is the only thing this handle
    /// can do — an endpoint is a mailbox owned by its creator, and that
    /// direction never reverses.
    endpoint: ipc.Handle,

    /// Adopt an endpoint that arrived some other way — a capability delivered
    /// in a reply, or one a supervisor wired in at spawn time (P5). The channel
    /// takes ownership of the handle.
    pub fn adopt(endpoint: ipc.Handle) Channel {
        return .{ .endpoint = endpoint };
    }

    /// Establish a channel by name: resolve `/protocol/<name>` to the registry
    /// backend, `open` the contract there, and take the provider's endpoint from
    /// the reply's capability. Null if the path does not resolve, the registry
    /// refuses (an ungranted name is refused *as* not-found), or the reply
    /// carries no capability.
    ///
    /// The path is spoken exactly once, here. Everything afterwards is integers
    /// in the packet header.
    pub fn open(path: []const u8) ?Channel {
        return .{ .endpoint = openPath(path) orelse return null };
    }

    /// Establish a channel by contract name — `open` with `/protocol/` supplied,
    /// which is how every caller in the system spells it.
    pub fn connect(name: []const u8) ?Channel {
        return .{ .endpoint = openEndpoint(name) orelse return null };
    }

    /// Send one request packet and block for the reply: `[Header][request]` out,
    /// `[Status][reply]` back. `request` is the bytes *after* the header — the
    /// protocol's fixed part plus any tail — because the header is this call's
    /// to lay down. The reply's payload lands in `into`.
    ///
    /// Null means the transport failed, which today means one of: a dead
    /// provider (`-EPEER` — discard this channel and `open` the name again), an
    /// oversized packet, or a bad handle. A provider that answered *and refused*
    /// is not a failure here: it comes back with a negative `Response.status`.
    pub fn call(self: Channel, header: envelope.Header, request: []const u8, into: []u8) ?Response {
        return self.callCapability(header, request, into, null);
    }

    /// As `call`, handing the provider a capability with the request — the only
    /// direction-crossing move kernel-ipc offers, and how `subscribe` delivers
    /// the subscriber's own endpoint.
    pub fn callCapability(
        self: Channel,
        header: envelope.Header,
        request: []const u8,
        into: []u8,
        capability: ?ipc.Handle,
    ) ?Response {
        var packet: [envelope.packet_maximum]u8 = undefined;
        const framed = frame(header, request, &packet) orelse return null;

        var reply: [envelope.packet_maximum]u8 = undefined;
        const answer = ipc.callCap(self.endpoint, framed, &reply, capability) catch return null;
        const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
        const available = @min(answer.len - envelope.prefix_size, @as(usize, status.len));
        const taken = @min(available, into.len);
        @memcpy(into[0..taken], reply[envelope.prefix_size..][0..taken]);
        return .{ .status = status, .payload = into[0..taken], .capability = answer.cap };
    }

    /// Push one event packet and return immediately — no reply owed, and a slow
    /// or dead peer can never stall the sender. Bounded by `post_maximum`: an
    /// event that does not fit is refused here rather than split, because a
    /// packet is never fragmented.
    pub fn send(self: Channel, header: envelope.Header, payload: []const u8) bool {
        var packet: [envelope.post_maximum]u8 = undefined;
        const framed = frame(header, payload, &packet) orelse return false;
        return ipc.send(self.endpoint, framed);
    }

    /// Ask the provider what it is: the reserved `describe` verb, answered by
    /// every protocol built through `envelope.Define`. The name and version come
    /// back in `into`, which the returned `Described` borrows.
    pub fn describe(self: Channel, into: []u8) ?envelope.Described {
        var request: [envelope.packet_maximum]u8 = undefined;
        const packet = envelope.encodeDescribe(&request) orelse return null;

        var reply: [envelope.packet_maximum]u8 = undefined;
        const answer = ipc.callCap(self.endpoint, packet, &reply, null) catch return null;
        const taken = @min(answer.len, into.len);
        @memcpy(into[0..taken], reply[0..taken]);
        return envelope.decodeDescribe(into[0..taken]);
    }

    /// Drop the provider's endpoint and free the handle-table slot. The
    /// conversation is over the moment the capability is gone — there is nothing
    /// else holding it open.
    pub fn close(self: Channel) void {
        _ = ipc.close(self.endpoint);
    }
};

// --- the namespace: resolving, opening, and claiming a contract name ---------

/// Where a `/protocol/...` path routed: the registry's endpoint, plus the path
/// rewritten mount-relative (`/display` for `/protocol/display`). The handle is
/// deduplicated by the kernel across resolves and shared with every other user
/// of that mount, so it is never ours to close.
const Registry = struct {
    handle: ipc.Handle,
    relative: [path_maximum]u8,
    relative_len: usize,

    fn path(self: *const Registry) []const u8 {
        return self.relative[0..self.relative_len];
    }
};

/// Route `path` to whatever backend serves it. Null when nothing is mounted
/// there — under `/protocol` that means the registry is not up yet, which is a
/// *retry*, not a refusal. A kernel-served route (the read-only `/system` tree)
/// is the wrong path, not a channel, and is refused here.
fn reach(path: []const u8) ?Registry {
    var out: Registry = .{ .handle = 0, .relative = undefined, .relative_len = 0 };
    const route = file_system.fsResolve(path, 0, &out.relative) orelse return null;
    switch (route) {
        .kernel => return null,
        .backend => |b| {
            out.handle = b.handle;
            out.relative_len = b.path_len;
            return out;
        },
    }
}

/// One vfs-protocol round trip at a backend: fixed header, inline payload, and
/// an optional capability in each direction.
fn transact(
    handle: ipc.Handle,
    operation: vfs_protocol.Operation,
    payload: []const u8,
    send_capability: ?ipc.Handle,
) ?struct { reply: vfs_protocol.Reply, capability: ?ipc.Handle } {
    var request: [vfs_protocol.message_maximum]u8 = undefined;
    if (vfs_protocol.request_size + payload.len > request.len) return null;
    const header = vfs_protocol.Request{
        .operation = operation,
        .node = 0,
        .offset = 0,
        .len = @intCast(payload.len),
        .flags = 0,
    };
    @memcpy(request[0..vfs_protocol.request_size], std.mem.asBytes(&header));
    @memcpy(request[vfs_protocol.request_size..][0..payload.len], payload);

    var reply: [vfs_protocol.message_maximum]u8 = undefined;
    const answer = ipc.callCap(handle, request[0 .. vfs_protocol.request_size + payload.len], &reply, send_capability) catch return null;
    if (answer.len < vfs_protocol.reply_size) return null;
    return .{
        .reply = std.mem.bytesToValue(vfs_protocol.Reply, reply[0..vfs_protocol.reply_size]),
        .capability = answer.cap,
    };
}

/// Resolve an absolute `/protocol/...` path and take the provider's endpoint out
/// of the open reply's capability.
fn openPath(path: []const u8) ?ipc.Handle {
    const registry = reach(path) orelse return null;
    const answered = transact(registry.handle, .open, registry.path(), null) orelse return null;
    if (answered.reply.status != 0) return null;
    // The capability *is* the channel — an open that succeeds without one was
    // answered by a file backend, which does not speak protocols.
    return answered.capability;
}

/// The provider's raw endpoint behind `/protocol/<name>`. The transitional form,
/// for the clients that still marshal their protocol's bytes by hand; P4 moves
/// them onto `Channel` proper and this shrinks back to `connect`.
///
/// Null covers both "no such contract" and "you may not have it" — deliberately
/// the same answer (protocol-namespace.md: enforcement is absence), and also
/// "the registry is not mounted yet", which is why every caller retries.
pub fn openEndpoint(name: []const u8) ?ipc.Handle {
    var path: [path_maximum]u8 = undefined;
    const full = join(name, &path) orelse return null;
    return openPath(full);
}

/// Claim `/protocol/<name>` for `endpoint`: the registry records the name
/// against this process and hands the endpoint to whoever opens it afterwards.
/// The endpoint rides the call as its capability, the one direction-crossing
/// move kernel-ipc offers.
///
/// Three-valued on purpose. **Null** is "the registry could not be reached" —
/// it is not mounted yet, which happens when a provider starts before init has
/// finished coming up, and the answer is to retry. A **value** is the registry's
/// verdict and is final: 0 bound, `-EPERM` this binary is not granted that name,
/// `-EBUSY` a live provider already holds it.
pub fn bind(name: []const u8, endpoint: ipc.Handle) ?i32 {
    const registry = reach(root) orelse return null;
    const answered = transact(registry.handle, .bind, name, endpoint) orelse return null;
    return answered.reply.status;
}

/// How long a provider keeps offering itself before giving up. The registry is
/// init, which mounts `/protocol` before it spawns anyone, so in a normal boot
/// the first try lands; a provider the kernel test harness starts may well beat
/// init to the mount, which is what the patience is for. Four seconds of 20 ms
/// tries — the same cadence every client in the tree spends finding a service.
const bind_attempts: u32 = 200;
const bind_retry_ms: u64 = 20;

/// `bind`, waiting out a registry that is not mounted yet. Only unreachability
/// is retried: a registry that *answered* has decided, and asking again cannot
/// change its mind. True when the name is ours.
pub fn bindPatiently(name: []const u8, endpoint: ipc.Handle) bool {
    var attempt: u32 = 0;
    while (attempt < bind_attempts) : (attempt += 1) {
        if (bind(name, endpoint)) |status| return status == 0;
        time.sleepMillis(bind_retry_ms);
    }
    return false;
}

/// `/protocol/` + `name`, in the caller's buffer. Null if the name is empty or
/// longer than the namespace admits.
fn join(name: []const u8, buffer: []u8) ?[]u8 {
    if (name.len == 0 or name.len > name_maximum) return null;
    const total = root.len + 1 + name.len;
    if (total > buffer.len) return null;
    @memcpy(buffer[0..root.len], root);
    buffer[root.len] = '/';
    @memcpy(buffer[root.len + 1 ..][0..name.len], name);
    return buffer[0..total];
}

/// Lay a packet down: the folded header first, then the protocol's bytes. Null
/// when it would not fit the buffer — the same rule as `envelope`'s framing,
/// applied where the buffer is the transport's, not the protocol's.
fn frame(header: envelope.Header, body: []const u8, buffer: []u8) ?[]u8 {
    const total = envelope.prefix_size + body.len;
    if (total > buffer.len) return null;
    @memcpy(buffer[0..envelope.prefix_size], std.mem.asBytes(&header));
    @memcpy(buffer[envelope.prefix_size..][0..body.len], body);
    return buffer[0..total];
}

// --- tests ------------------------------------------------------------------
//
// The syscall half cannot run on the host, and there is no registry to reach
// until P2 — so what is testable here is the framing, which is the part with
// arithmetic in it.

const testing = std.testing;

test "a framed packet is the header followed by the protocol's bytes" {
    var buffer: [envelope.packet_maximum]u8 = undefined;
    const header = envelope.Header{ .operation = envelope.first_protocol_operation, .target = 9 };
    const packet = frame(header, "body", &buffer).?;

    try testing.expectEqual(envelope.prefix_size + "body".len, packet.len);
    const decoded = envelope.headerOf(packet).?;
    try testing.expectEqual(envelope.first_protocol_operation, decoded.operation);
    try testing.expectEqual(@as(u64, 9), decoded.target);
    try testing.expectEqualStrings("body", packet[envelope.prefix_size..]);
}

test "a contract name joins the namespace root exactly once" {
    var buffer: [path_maximum]u8 = undefined;
    try testing.expectEqualStrings("/protocol/display", join("display", &buffer).?);
    try testing.expectEqualStrings("/protocol/test/shared-memory", join("test/shared-memory", &buffer).?);
    try testing.expect(join("", &buffer) == null);
    try testing.expect(join("x" ** (name_maximum + 1), &buffer) == null);
}

test "framing refuses a packet that would not fit rather than truncating it" {
    var post: [envelope.post_maximum]u8 = undefined;
    const header = envelope.Header{ .operation = envelope.first_protocol_operation };
    const body = [_]u8{0} ** (envelope.post_maximum - envelope.prefix_size);
    const one_too_many = body ++ [_]u8{0};

    try testing.expect(frame(header, &body, &post) != null);
    try testing.expect(frame(header, &one_too_many, &post) == null);
}
