// SPDX-License-Identifier: LCL-1.0
// Copyright (c) 2026 Markus Maiwald
// Stewardship: Self Sovereign Society Foundation
//
// This file is part of the Nexus Commonwealth.
// See legal/LICENSE_COMMONWEALTH.md for license terms.

//! Project LibWeb: LWF Membrane Client
//!
//! Userland-side LWF frame handler. Runs in the Membrane where std is
//! available. Consumes validated LWF frames from the dedicated ION
//! channel (s_lwf_rx) and produces encrypted outbound frames (s_lwf_tx).
//!
//! This module bridges:
//!   - ION ring (SysTable s_lwf_rx/s_lwf_tx) for zero-copy kernel IPC
//!   - Upstream LWF codec (libertaria-stack lwf.zig) for full encode/decode
//!   - Noise Protocol for transport encryption/decryption
//!
//! Architecture:
//!   VirtIO-net → NetSwitch (validates header) → chan_lwf_rx → [this module]
//!   [this module] → chan_lwf_tx → NetSwitch → VirtIO-net
//!
//! NOTE: This file is NOT compiled freestanding. It targets the Membrane
//! (userland) and has access to std.mem.Allocator.

const std = @import("std");

// =========================================================
// ION Slab Constants (must match ion/memory.nim)
// =========================================================

const SLAB_SIZE: usize = 2048;
const SYSTABLE_ADDR: usize = if (builtin.cpu.arch == .aarch64) 0x50000000 else 0x83000000;
const ETH_HEADER_SIZE: usize = 14;

// =========================================================
// LWF Header Constants (duplicated from lwf_adapter.zig
// for use with std — adapter is freestanding, this is not)
// =========================================================

pub const HEADER_SIZE: usize = 88;
pub const TRAILER_SIZE: usize = 36;
pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE;
pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 };
pub const LWF_VERSION: u8 = 0x02;

pub const Flags = struct {
    pub const ENCRYPTED: u8 = 0x01;
    pub const SIGNED: u8 = 0x02;
    pub const RELAYABLE: u8 = 0x04;
    pub const HAS_ENTROPY: u8 = 0x08;
    pub const FRAGMENTED: u8 = 0x10;
    pub const PRIORITY: u8 = 0x20;
};

// =========================================================
// Frame Processing Result
// =========================================================

pub const FrameError = error{
    TooSmall,
    InvalidMagic,
    InvalidVersion,
    PayloadOverflow,
    DecryptionFailed,
    NoSession,
    SlabTooSmall,
};

pub const ProcessedFrame = struct {
    service_type: u16,
    payload: []const u8, // Points into slab — valid until ion_free
    session_id: [16]u8,
    dest_hint: [24]u8,
    source_hint: [24]u8,
    sequence: u32,
    flags: u8,
    encrypted: bool,
};

// =========================================================
// LWF Membrane Client
// =========================================================

pub const LwfClient = struct {
    /// Callback type for incoming LWF frames
    pub const FrameHandler = *const fn (frame: ProcessedFrame) void;

    on_frame: ?FrameHandler,

    pub fn init() LwfClient {
        return .{
            .on_frame = null,
        };
    }

    /// Register a callback for incoming LWF frames
    pub fn setHandler(self: *LwfClient, handler: FrameHandler) void {
        self.on_frame = handler;
    }

    /// Parse an LWF frame from a raw ION slab buffer.
    /// The buffer starts AFTER the Ethernet header (NetSwitch strips it
    /// to EtherType, but the ION packet still contains the full Ethernet
    /// frame — so caller must offset by 14 bytes).
    pub fn parseFrame(data: [*]const u8, len: u16) FrameError!ProcessedFrame {
        if (len < HEADER_SIZE) return error.TooSmall;

        // Magic check
        if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0)
            return error.InvalidMagic;

        // Version check (offset 77)
        if (data[77] != LWF_VERSION) return error.InvalidVersion;

        // Parse fields
        const payload_len = readU16Big(data[74..76]);
        const total_needed = HEADER_SIZE + @as(usize, payload_len) + TRAILER_SIZE;
        if (total_needed > len) return error.PayloadOverflow;

        var frame: ProcessedFrame = undefined;
        frame.service_type = readU16Big(data[72..74]);
        frame.sequence = readU32Big(data[68..72]);
        frame.flags = data[78];
        frame.encrypted = (frame.flags & Flags.ENCRYPTED) != 0;
        frame.payload = data[HEADER_SIZE .. HEADER_SIZE + payload_len];

        @memcpy(&frame.dest_hint, data[4..28]);
        @memcpy(&frame.source_hint, data[28..52]);
        @memcpy(&frame.session_id, data[52..68]);

        return frame;
    }

    /// Build an outbound LWF frame into a slab buffer.
    /// Returns the total frame size written.
    pub fn buildFrame(
        buf: []u8,
        service_type: u16,
        payload: []const u8,
        session_id: [16]u8,
        dest_hint: [24]u8,
        source_hint: [24]u8,
        sequence: u32,
        flags: u8,
    ) FrameError!usize {
        const total = HEADER_SIZE + payload.len + TRAILER_SIZE;
        if (buf.len < total) return error.SlabTooSmall;

        // Zero header + trailer regions
        @memset(buf[0..HEADER_SIZE], 0);
        @memset(buf[HEADER_SIZE + payload.len ..][0..TRAILER_SIZE], 0);

        // Magic
        buf[0] = 'L';
        buf[1] = 'W';
        buf[2] = 'F';
        buf[3] = 0;

        // Dest/Source hints
        @memcpy(buf[4..28], &dest_hint);
        @memcpy(buf[28..52], &source_hint);

        // Session ID
        @memcpy(buf[52..68], &session_id);

        // Sequence
        writeU32Big(buf[68..72], sequence);

        // Service type + payload len
        writeU16Big(buf[72..74], service_type);
        writeU16Big(buf[74..76], @truncate(payload.len));

        // Frame class (auto-select based on total size)
        buf[76] = if (total <= 128) 0x00 // micro
            else if (total <= 512) 0x01 // mini
            else if (total <= 1350) 0x02 // standard
            else if (total <= 4096) 0x03 // big
            else 0x04; // jumbo

        // Version
        buf[77] = LWF_VERSION;

        // Flags
        buf[78] = flags;

        // Payload
        @memcpy(buf[HEADER_SIZE..][0..payload.len], payload);

        return total;
    }
};

// =========================================================
// Integer Helpers
// =========================================================

fn readU16Big(bytes: *const [2]u8) u16 {
    return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]);
}

fn readU32Big(bytes: *const [4]u8) u32 {
    return (@as(u32, bytes[0]) << 24) |
        (@as(u32, bytes[1]) << 16) |
        (@as(u32, bytes[2]) << 8) |
        @as(u32, bytes[3]);
}

fn writeU16Big(buf: *[2]u8, val: u16) void {
    buf[0] = @truncate(val >> 8);
    buf[1] = @truncate(val);
}

fn writeU32Big(buf: *[4]u8, val: u32) void {
    buf[0] = @truncate(val >> 24);
    buf[1] = @truncate(val >> 16);
    buf[2] = @truncate(val >> 8);
    buf[3] = @truncate(val);
}

// =========================================================
// Tests
// =========================================================

test "parseFrame valid" {
    var buf: [512]u8 = undefined;
    const payload = "Hello Noise";
    const sz = try LwfClient.buildFrame(
        &buf,
        0x0001, // DATA_TRANSPORT
        payload,
        [_]u8{0xAA} ** 16, // session
        [_]u8{0xBB} ** 24, // dest
        [_]u8{0xCC} ** 24, // src
        42,
        0,
    );

    const frame = try LwfClient.parseFrame(&buf, @truncate(sz));
    try std.testing.expectEqual(@as(u16, 0x0001), frame.service_type);
    try std.testing.expectEqual(@as(u32, 42), frame.sequence);
    try std.testing.expectEqualSlices(u8, payload, frame.payload);
    try std.testing.expectEqual(@as(u8, 0xAA), frame.session_id[0]);
    try std.testing.expect(!frame.encrypted);
}

test "parseFrame encrypted flag" {
    var buf: [512]u8 = undefined;
    const sz = try LwfClient.buildFrame(
        &buf,
        0x0003, // IDENTITY_SIGNAL
        "encrypted_payload",
        [_]u8{0} ** 16,
        [_]u8{0} ** 24,
        [_]u8{0} ** 24,
        1,
        Flags.ENCRYPTED | Flags.SIGNED,
    );

    const frame = try LwfClient.parseFrame(&buf, @truncate(sz));
    try std.testing.expect(frame.encrypted);
    try std.testing.expectEqual(@as(u16, 0x0003), frame.service_type);
}

test "buildFrame auto frame class" {
    var buf: [2048]u8 = undefined;

    // Micro (total <= 128)
    _ = try LwfClient.buildFrame(&buf, 0, "", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
    try std.testing.expectEqual(@as(u8, 0x00), buf[76]); // micro

    // Standard (total > 512)
    var payload: [500]u8 = undefined;
    @memset(&payload, 0x42);
    _ = try LwfClient.buildFrame(&buf, 0, &payload, [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
    try std.testing.expectEqual(@as(u8, 0x02), buf[76]); // standard
}

test "parseFrame rejects bad magic" {
    var buf: [512]u8 = undefined;
    _ = try LwfClient.buildFrame(&buf, 0, "x", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0);
    buf[0] = 'X';
    try std.testing.expectError(error.InvalidMagic, LwfClient.parseFrame(&buf, 160));
}

test "buildFrame roundtrip preserves hints" {
    var buf: [512]u8 = undefined;
    const dest = [_]u8{0xDE} ** 24;
    const src = [_]u8{0x5A} ** 24;
    const sess = [_]u8{0xF0} ** 16;

    _ = try LwfClient.buildFrame(&buf, 0x0800, "audio", sess, dest, src, 99, Flags.PRIORITY);

    const frame = try LwfClient.parseFrame(&buf, 200);
    try std.testing.expectEqual(@as(u16, 0x0800), frame.service_type);
    try std.testing.expectEqual(@as(u32, 99), frame.sequence);
    try std.testing.expectEqualSlices(u8, &dest, &frame.dest_hint);
    try std.testing.expectEqualSlices(u8, &src, &frame.source_hint);
    try std.testing.expectEqualSlices(u8, &sess, &frame.session_id);
}
