const std = @import("std");
const ascii = std.ascii;
const BoundedArray = std.BoundedArray;

const ErrCtx = @import("ErrCtx.zig");

pub fn downCast(comptime T: type, n: u64) error{InvalidRecord}!T {
    return std.math.cast(T, n) orelse return error.InvalidRecord;
}

pub fn utf8(s: []const u8) error{InvalidRecord}!void {
    if (!std.unicode.utf8ValidateSlice(s)) {
        return error.InvalidRecord;
    }
}

fn debugChar(c: u8) BoundedArray(u8, 4) {
    var res = BoundedArray(u8, 4).init(0) catch unreachable;
    if (ascii.isPrint(c)) {
        res.writer().print("'{c}'", .{c}) catch unreachable;
    } else res.writer().print("0x{X}", .{c}) catch unreachable;
    return res;
}

// # adduser žmogus
// adduser: To avoid problems, the username should consist only of letters,
// digits, underscores, periods, at signs and dashes, and not start with a dash
// (as defined by IEEE Std 1003.1-2001). For compatibility with Samba machine
// accounts $ is also supported at the end of the username
pub fn name(s: []const u8, err: *ErrCtx) error{InvalidRecord}!void {
    if (s.len == 0)
        return err.returnf("cannot be empty", .{}, error.InvalidRecord);

    const c0 = s[0];
    if (!(ascii.isAlphanumeric(c0) or c0 == '_' or c0 == '.' or c0 == '@'))
        return err.returnf(
            "invalid character {s} at position 0",
            .{debugChar(c0).constSlice()},
            error.InvalidRecord,
        );

    for (s[1..], 0..) |c, i| {
        if (!(ascii.isAlphanumeric(c) or c == '_' or c == '.' or c == '@' or c == '-'))
            return err.returnf(
                "invalid character {s} at position {d}",
                .{ debugChar(c).constSlice(), i + 2 },
                error.InvalidRecord,
            );
    }
}

// gecos must be utf8 without ':'
pub fn gecos(s: []const u8, errc: *ErrCtx) error{InvalidRecord}!void {
    var utf8view = std.unicode.Utf8View.init(s) catch |err| switch (err) {
        error.InvalidUtf8 => return errc.returnf("invalid utf8", .{}, error.InvalidRecord),
    };

    var it = utf8view.iterator();
    while (it.nextCodepoint()) |codepoint| {
        if (codepoint == ':')
            return errc.returnf("colon is not allowed", .{}, error.InvalidRecord);
    }
}

pub fn path(s: []const u8, err: *ErrCtx) error{InvalidRecord}!void {
    if (s.len == 0)
        return err.returnf("cannot be empty", .{}, error.InvalidRecord);

    if (s[0] != '/')
        return err.returnf("must start with /", .{}, error.InvalidRecord);

    for (s[1..], 0..) |c, i| {
        if (!(ascii.isAlphanumeric(c) or c == '/' or c == '_' or c == '.' or c == '@' or c == '-'))
            return err.returnf(
                "invalid character 0xD at position {d}",
                .{i + 2},
                error.InvalidRecord,
            );
    }
}

const testing = std.testing;

test "validate name" {
    const examples = [_]struct {
        name: []const u8,
        wantErr: ?[]const u8 = null,
    }{
        .{ .name = "all-good" },
        .{ .name = "..." },
        .{ .name = "", .wantErr = "cannot be empty" },
        .{ .name = "-no-start-dash", .wantErr = "invalid character '-' at position 0" },
        .{ .name = "Herbasž", .wantErr = "invalid character 0xC5 at position 7" },
    };
    for (examples) |tt| {
        var err = ErrCtx{};

        const got = name(tt.name, &err);
        if (tt.wantErr) |wantErr| {
            try testing.expectError(error.InvalidRecord, got);
            try testing.expectEqualStrings(wantErr, err.unwrap().constSlice());
        } else {
            if (got) |_| {} else |_| return error.TestUnExpectedError;
            try testing.expectEqualStrings("", err.unwrap().constSlice());
        }
    }
}

test "validate gecos" {
    const examples = [_]struct {
        gecos: []const u8,
        wantErr: ?[]const u8 = null,
    }{
        .{ .gecos = "all-good" },
        .{ .gecos = "..." },
        .{ .gecos = "" },
        .{ .gecos = "Vidmantas Kaminskas Ž" },
        .{ .gecos = "Not\xffUnicode", .wantErr = "invalid utf8" },
        .{ .gecos = "Has:Colon", .wantErr = "colon is not allowed" },
    };
    for (examples) |tt| {
        var err = ErrCtx{};

        const got = gecos(tt.gecos, &err);
        if (tt.wantErr) |wantErr| {
            try testing.expectError(error.InvalidRecord, got);
            try testing.expectEqualStrings(wantErr, err.unwrap().constSlice());
        } else {
            if (got) |_| {} else |_| return error.TestUnExpectedError;
            try testing.expectEqualStrings("", err.unwrap().constSlice());
        }
    }
}

test "validate path" {
    const examples = [_]struct {
        path: []const u8,
        wantErr: ?[]const u8 = null,
    }{
        .{ .path = "/path/ok" },
        .{ .path = "/" },
        .{ .path = "foo", .wantErr = "must start with /" },
        .{ .path = "", .wantErr = "cannot be empty" },
        .{ .path = "/path:motiejus", .wantErr = "invalid character 0xD at position 6" },
        .{ .path = "/Herbasž", .wantErr = "invalid character 0xD at position 8" },
    };
    for (examples) |tt| {
        var err = ErrCtx{};

        const got = path(tt.path, &err);
        if (tt.wantErr) |wantErr| {
            try testing.expectError(error.InvalidRecord, got);
            try testing.expectEqualStrings(wantErr, err.unwrap().constSlice());
        } else {
            if (got) |_| {} else |_| return error.TestUnExpectedError;
            try testing.expectEqualStrings("", err.unwrap().constSlice());
        }
    }
}
