//! Domain names in wire form (RFC 1035 §3.1, §4.1.4). Pure: no allocation,
//! no `std.Io` beyond writing encoded bytes to a caller's writer.
//!
//! A `Name` always holds the decoded, uncompressed form: a sequence of
//! length-prefixed labels ended by a zero byte, which `len` counts.

const std = @import("std");
const types = @import("types.zig");
const Writer = std.Io.Writer;

pub const Name = struct {
    bytes: [types.max_name_len]u8,
    len: u8,

    /// The uncompressed wire bytes, terminating zero included.
    pub fn wire(self: *const Name) []const u8 {
        return self.bytes[0..self.len];
    }

    pub fn isRoot(self: Name) bool {
        return self.len == 1 and self.bytes[0] == 0;
    }

    /// Labels excluding the root. The root name has zero labels.
    pub fn labelCount(self: Name) usize {
        var count: usize = 0;
        var i: usize = 0;
        while (i < self.len and self.bytes[i] != 0) : (i += 1 + self.bytes[i]) count += 1;
        return count;
    }
};

pub const ParseError = error{
    Truncated,
    LabelTooLong,
    NameTooLong,
    BadPointer,
    TooManyJumps,
};

pub const Parsed = struct {
    name: Name,
    /// Offset just past the name as it appears at `offset`: past the first
    /// compression pointer, or past the terminating zero when there is none.
    end: usize,
};

/// Decodes the name at `offset`, following compression pointers within
/// `packet`. Every pointer must target an offset strictly lower than the
/// pointer's own offset, which makes a chain strictly decreasing and therefore
/// finite; `types.max_compression_jumps` bounds the work regardless.
pub fn parse(packet: []const u8, offset: usize) ParseError!Parsed {
    var name: Name = .{ .bytes = undefined, .len = 0 };
    var pos = offset;
    var end: ?usize = null;
    var jumps: usize = 0;

    while (true) {
        if (pos >= packet.len) return error.Truncated;
        const control = packet[pos];
        switch (control & 0xc0) {
            0x00 => {
                if (control == 0) {
                    std.debug.assert(name.len < types.max_name_len);
                    name.bytes[name.len] = 0;
                    name.len += 1;
                    return .{ .name = name, .end = end orelse pos + 1 };
                }
                const label_len: usize = control;
                if (pos + 1 + label_len > packet.len) return error.Truncated;
                // The terminating zero still has to fit.
                if (name.len + 1 + label_len + 1 > types.max_name_len) return error.NameTooLong;
                name.bytes[name.len] = control;
                @memcpy(name.bytes[name.len + 1 ..][0..label_len], packet[pos + 1 ..][0..label_len]);
                name.len += @intCast(1 + label_len);
                pos += 1 + label_len;
            },
            0xc0 => {
                if (pos + 1 >= packet.len) return error.Truncated;
                const target = (@as(usize, control & 0x3f) << 8) | packet[pos + 1];
                if (end == null) end = pos + 2;
                if (target >= pos) return error.BadPointer;
                jumps += 1;
                if (jumps > types.max_compression_jumps) return error.TooManyJumps;
                pos = target;
            },
            // 0x40 and 0x80 are reserved label types (RFC 6891 §3 retired the
            // only assignment). A label of 64 bytes or more encodes into this
            // range, which is the case worth naming.
            else => return error.LabelTooLong,
        }
    }
}

/// Writes the name uncompressed. Compression on encode is out of scope: at
/// household query volumes the saved bytes do not pay for the offset
/// bookkeeping, and uncompressed output is always valid (RFC 1035 §4.1.4).
pub fn encode(name: Name, w: *Writer) Writer.Error!void {
    try w.writeAll(name.wire());
}

pub const FromTextError = error{
    EmptyLabel,
    LabelTooLong,
    NameTooLong,
};

/// Parses presentation form. A single trailing dot is optional; "" and "."
/// both denote the root. Bytes inside labels pass through opaquely, so no
/// escape sequences and no punycode.
pub fn fromText(text: []const u8) FromTextError!Name {
    var name: Name = .{ .bytes = undefined, .len = 0 };

    var rest = text;
    if (rest.len > 0 and rest[rest.len - 1] == '.') rest = rest[0 .. rest.len - 1];
    if (rest.len == 0) {
        name.bytes[0] = 0;
        name.len = 1;
        return name;
    }

    var it = std.mem.splitScalar(u8, rest, '.');
    while (it.next()) |label| {
        if (label.len == 0) return error.EmptyLabel;
        if (label.len > types.max_label_len) return error.LabelTooLong;
        if (name.len + 1 + label.len + 1 > types.max_name_len) return error.NameTooLong;
        name.bytes[name.len] = @intCast(label.len);
        @memcpy(name.bytes[name.len + 1 ..][0..label.len], label);
        name.len += @intCast(1 + label.len);
    }

    name.bytes[name.len] = 0;
    name.len += 1;
    return name;
}

pub const NormalizeError = error{BadName};

/// Lowercases over ASCII into `buf`, strips one trailing dot, and checks the
/// result is a name `fromText` accepts. Returns the normalized text, borrowed
/// from `buf`.
///
/// A byte ≥ 0x80 is rejected: query names arrive ASCII-lowercased, so a high
/// byte here could never be matched, and configuration that can never match is
/// worth reporting rather than storing. The root name is rejected for the same
/// reason — a configured entry that matches everything, or nothing, is not what
/// either caller means.
///
/// `filter/rules.zig`, `filter/compiler.zig` and `cache/dns_cache.zig` keep
/// their own variants on purpose; each says why beside its copy.
pub fn normalizeText(text: []const u8, buf: *[types.max_name_len]u8) NormalizeError![]const u8 {
    var rest = text;
    if (rest.len > 0 and rest[rest.len - 1] == '.') rest = rest[0 .. rest.len - 1];
    if (rest.len == 0 or rest.len > types.max_name_len) return error.BadName;

    for (rest, 0..) |byte, i| {
        if (byte >= 0x80) return error.BadName;
        buf[i] = std.ascii.toLower(byte);
    }
    const normalized = buf[0..rest.len];
    _ = fromText(normalized) catch return error.BadName;
    return normalized;
}

/// Writes presentation form: labels joined by dots, no trailing dot. The root
/// name writes as ".".
pub fn formatText(name: Name, w: *Writer) Writer.Error!void {
    if (name.isRoot()) {
        try w.writeByte('.');
        return;
    }
    var i: usize = 0;
    var first = true;
    while (i < name.len and name.bytes[i] != 0) {
        const label_len = name.bytes[i];
        if (!first) try w.writeByte('.');
        try w.writeAll(name.bytes[i + 1 ..][0..label_len]);
        first = false;
        i += 1 + @as(usize, label_len);
    }
}

/// Names compare case-insensitively over ASCII only (RFC 1035 §2.3.3); bytes
/// outside A-Z and a-z compare exactly.
pub fn eqlIgnoreCase(a: Name, b: Name) bool {
    if (a.len != b.len) return false;
    var i: usize = 0;
    while (i < a.len and a.bytes[i] != 0) {
        const label_len = a.bytes[i];
        if (b.bytes[i] != label_len) return false;
        const end = i + 1 + @as(usize, label_len);
        if (!std.ascii.eqlIgnoreCase(a.bytes[i + 1 .. end], b.bytes[i + 1 .. end])) return false;
        i = end;
    }
    return i < b.len and b.bytes[i] == 0;
}

const testing = std.testing;

fn expectText(expected: []const u8, name: Name) !void {
    var buf: [512]u8 = undefined;
    var w = Writer.fixed(&buf);
    try formatText(name, &w);
    try testing.expectEqualStrings(expected, w.buffered());
}

fn expectWire(expected: []const u8, name: Name) !void {
    var buf: [512]u8 = undefined;
    var w = Writer.fixed(&buf);
    try encode(name, &w);
    try testing.expectEqualStrings(expected, w.buffered());
}

test "fromText builds wire form" {
    const n = try fromText("example.com.");
    try expectWire("\x07example\x03com\x00", n);
    try testing.expectEqual(@as(u8, 13), n.len);
    try testing.expectEqual(@as(usize, 2), n.labelCount());
    try testing.expect(!n.isRoot());
}

test "fromText accepts a missing trailing dot" {
    const with_dot = try fromText("example.com.");
    const without_dot = try fromText("example.com");
    try testing.expectEqualSlices(u8, with_dot.wire(), without_dot.wire());
}

test "root name" {
    for ([_][]const u8{ "", "." }) |text| {
        const n = try fromText(text);
        try testing.expect(n.isRoot());
        try testing.expectEqual(@as(usize, 0), n.labelCount());
        try expectWire("\x00", n);
        try expectText(".", n);
    }
}

test "text round-trips" {
    for ([_][]const u8{ ".", "com", "example.com", "a.b.c.d.e.f" }) |text| {
        try expectText(text, try fromText(text));
    }
}

test "fromText rejects empty labels" {
    try testing.expectError(error.EmptyLabel, fromText("example..com"));
    try testing.expectError(error.EmptyLabel, fromText(".example.com"));
    try testing.expectError(error.EmptyLabel, fromText(".."));
}

test "fromText rejects an oversize label" {
    const ok = "a" ** types.max_label_len;
    _ = try fromText(ok ++ ".com");
    const too_long = "a" ** (types.max_label_len + 1);
    try testing.expectError(error.LabelTooLong, fromText(too_long ++ ".com"));
}

test "fromText rejects an oversize name" {
    // Four labels of 63 bytes encode to 4 * 64 + 1 = 257 bytes.
    const label = "a" ** types.max_label_len;
    try testing.expectError(
        error.NameTooLong,
        fromText(label ++ "." ++ label ++ "." ++ label ++ "." ++ label),
    );
    // Three of those (3 * 64 = 192) plus a 61-byte label (62) plus the
    // terminating zero reach exactly 255.
    const last = "b" ** 61;
    const max = try fromText(label ++ "." ++ label ++ "." ++ label ++ "." ++ last);
    try testing.expectEqual(@as(u8, types.max_name_len), max.len);

    const one_over = "b" ** 62;
    try testing.expectError(
        error.NameTooLong,
        fromText(label ++ "." ++ label ++ "." ++ label ++ "." ++ one_over),
    );
}

test "parse decodes an uncompressed name" {
    const packet = "\x07example\x03com\x00";
    const r = try parse(packet, 0);
    try expectText("example.com", r.name);
    try testing.expectEqual(@as(usize, 13), r.end);
}

test "parse follows a compression pointer chain" {
    // 0: "com" root at the top, then "example" pointing at it, then "www"
    // pointing at "example.com".
    const packet =
        "\x03com\x00" ++ // offset 0, ends at 5
        "\x07example\xc0\x00" ++ // offset 5, ends at 15
        "\x03www\xc0\x05"; // offset 15, ends at 21

    const com = try parse(packet, 0);
    try expectText("com", com.name);
    try testing.expectEqual(@as(usize, 5), com.end);

    const example = try parse(packet, 5);
    try expectText("example.com", example.name);
    try testing.expectEqual(@as(usize, 15), example.end);

    const www = try parse(packet, 15);
    try expectText("www.example.com", www.name);
    try testing.expectEqual(@as(usize, 21), www.end);
    try testing.expectEqualSlices(u8, (try fromText("www.example.com")).wire(), www.name.wire());
}

test "parse rejects a pointer to itself" {
    const packet = "\xc0\x00";
    try testing.expectError(error.BadPointer, parse(packet, 0));
}

test "parse rejects a two-pointer loop" {
    // The pointer at offset 2 targets 0, which points forward to 2.
    const packet = "\xc0\x02\xc0\x00";
    try testing.expectError(error.BadPointer, parse(packet, 0));
    try testing.expectError(error.BadPointer, parse(packet, 2));
}

test "parse rejects a forward pointer" {
    const packet = "\xc0\x04\x00\x00\x03com\x00";
    try testing.expectError(error.BadPointer, parse(packet, 0));
}

test "parse rejects a pointer to its own offset" {
    const packet = "\x00\x00\xc0\x02";
    try testing.expectError(error.BadPointer, parse(packet, 2));
}

test "parse caps the jump count" {
    // A descending chain of two-byte pointers: each entry at offset 2*i points
    // to 2*(i-1). Entry 0 is the root label, so a chain of n entries costs n-1
    // jumps from the last one.
    const chain_len = types.max_compression_jumps + 2;
    var packet: [chain_len * 2]u8 = undefined;
    packet[0] = 0;
    packet[1] = 0;
    var i: usize = 1;
    while (i < chain_len) : (i += 1) {
        packet[i * 2] = 0xc0;
        packet[i * 2 + 1] = @intCast((i - 1) * 2);
    }

    const at_limit = try parse(&packet, (types.max_compression_jumps) * 2);
    try testing.expect(at_limit.name.isRoot());

    try testing.expectError(
        error.TooManyJumps,
        parse(&packet, (types.max_compression_jumps + 1) * 2),
    );
}

test "parse rejects a reserved label type" {
    try testing.expectError(error.LabelTooLong, parse("\x40abc\x00", 0));
    try testing.expectError(error.LabelTooLong, parse("\x80abc\x00", 0));
}

test "parse rejects truncation" {
    // No terminating zero at all.
    try testing.expectError(error.Truncated, parse("\x03com", 0));
    // Label claims more bytes than the packet holds.
    try testing.expectError(error.Truncated, parse("\x07exa", 0));
    // Pointer's second byte is missing.
    try testing.expectError(error.Truncated, parse("\x00\xc0", 1));
    // Offset past the end.
    try testing.expectError(error.Truncated, parse("\x00", 1));
    try testing.expectError(error.Truncated, parse("", 0));
}

test "parse rejects a name longer than the limit" {
    // Five 63-byte labels through pointers would exceed 255 bytes.
    const label = "\x3f" ++ "a" ** types.max_label_len;
    const packet = label ++ label ++ label ++ label ++ label ++ "\x00";
    try testing.expectError(error.NameTooLong, parse(packet, 0));
}

test "parse round-trips the maximum length name" {
    const label = "\x3f" ++ "a" ** types.max_label_len;
    const last = "\x3d" ++ "b" ** 61;
    const packet = label ++ label ++ label ++ last ++ "\x00";
    const r = try parse(packet, 0);
    try testing.expectEqual(@as(u8, types.max_name_len), r.name.len);
    try testing.expectEqual(@as(usize, packet.len), r.end);
    try expectWire(packet, r.name);
}

test "parse then encode round-trips through a pointer" {
    const packet = "\x03com\x00\x07example\xc0\x00";
    const r = try parse(packet, 5);
    try expectWire("\x07example\x03com\x00", r.name);
}

test "eqlIgnoreCase folds ASCII only" {
    const lower = try fromText("example.com");
    const upper = try fromText("EXAMPLE.COM");
    const mixed = try fromText("ExAmPlE.cOm");
    try testing.expect(eqlIgnoreCase(lower, upper));
    try testing.expect(eqlIgnoreCase(lower, mixed));
    try testing.expect(eqlIgnoreCase(lower, lower));

    const other = try fromText("example.net");
    try testing.expect(!eqlIgnoreCase(lower, other));

    const shorter = try fromText("com");
    try testing.expect(!eqlIgnoreCase(lower, shorter));

    const root = try fromText(".");
    try testing.expect(eqlIgnoreCase(root, try fromText("")));
    try testing.expect(!eqlIgnoreCase(root, lower));

    // Same length, different label split: "aa.b" versus "a.ab".
    try testing.expect(!eqlIgnoreCase(try fromText("aa.b"), try fromText("a.ab")));

    // Byte 0xc0 is not an ASCII letter and must compare exactly.
    const high_a = try fromText("\xc0");
    const high_b = try fromText("\xe0");
    try testing.expect(!eqlIgnoreCase(high_a, high_b));
}

test "labelCount" {
    try testing.expectEqual(@as(usize, 0), (try fromText(".")).labelCount());
    try testing.expectEqual(@as(usize, 1), (try fromText("com")).labelCount());
    try testing.expectEqual(@as(usize, 3), (try fromText("www.example.com")).labelCount());
}

test "normalizeText lowercases and strips one trailing dot" {
    var buf: [types.max_name_len]u8 = undefined;
    try testing.expectEqualStrings("example.com", try normalizeText("Example.COM", &buf));
    try testing.expectEqualStrings("example.com", try normalizeText("example.com.", &buf));
    try testing.expectEqualStrings("a", try normalizeText("A", &buf));
}

test "normalizeText rejects the root, high bytes and names fromText refuses" {
    var buf: [types.max_name_len]u8 = undefined;
    try testing.expectError(error.BadName, normalizeText("", &buf));
    try testing.expectError(error.BadName, normalizeText(".", &buf));
    try testing.expectError(error.BadName, normalizeText("caf\xc3\xa9.example.com", &buf));
    try testing.expectError(error.BadName, normalizeText("a..b", &buf));
    try testing.expectError(error.BadName, normalizeText("a." ** 200 ++ "com", &buf));

    // A label of 64 bytes is one over the wire limit.
    try testing.expectError(error.BadName, normalizeText("a" ** 64 ++ ".com", &buf));
}
