const std = @import("std");
const io = std.io;
const os = std.os;
const mem = std.mem;
const fmt = std.fmt;
const ArrayList = std.ArrayList;

const compress = @import("compress.zig");
const flags = @import("flags.zig");
const DB = @import("DB.zig");
const File = @import("File.zig");
const PackedUser = @import("PackedUser.zig");
const PackedGroup = @import("PackedGroup.zig");
const User = @import("User.zig");

pub const std_options = struct {
    pub const keep_sigpipe = true;
};

const Mode = enum { group, passwd };

const usage =
    \\usage: turbonss-getent [OPTION]... group|passwd [key...]
    \\
    \\  -h          Print this help message and exit
    \\  --db PATH   Path to the database (default: /etc/turbonss/db.turbo)
    \\
    \\turbonss-getent resolves group or passwd (a.k.a. user) entries from the
    \\database file. If one or more key arguments are provided, then only
    \\entries that match the supplied keys will be displayed. If no key is
    \\provided, all entries will be displayed.
    \\
    \\Exit codes:
    \\
    \\  0   command completed successfully
    \\  1   invalid arguments or db could not be opened
    \\  2   one or more supplied key could not be found in the database
    \\  3   output error
    \\
;

pub fn main() !void {
    const stderr = io.getStdErr().writer();
    const stdout = io.getStdOut().writer();

    const return_code = execute(stdout, stderr, os.argv[1..]);
    os.exit(return_code);
}

fn execute(
    stdout: anytype,
    stderr: anytype,
    argv: []const [*:0]const u8,
) u8 {
    const myflags = flags.parse(argv, &[_]flags.Flag{
        .{ .name = "-h", .kind = .boolean },
        .{ .name = "--db", .kind = .arg },
    }) catch {
        stderr.writeAll(usage) catch {};
        return 1;
    };

    if (myflags.boolFlag("-h")) {
        stdout.writeAll(usage) catch return 1;
        return 0;
    }

    if (myflags.args.len == 0) {
        stderr.writeAll(usage) catch {};
        return 1;
    }

    const arg0 = mem.span(myflags.args[0]);
    const mode: Mode = blk: {
        if (mem.eql(u8, arg0, "passwd")) {
            break :blk .passwd;
        } else if (mem.eql(u8, arg0, "group")) {
            break :blk .group;
        } else {
            stderr.print("bad argument {s}: expected passwd or group\n", .{
                myflags.args[0],
            }) catch {};
            return 1;
        }
    };

    const db_file = myflags.argFlag("--db") orelse "/etc/turbonss/db.turbo";
    var file = File.open(db_file) catch |err| {
        stderr.print(
            "ERROR {s}: file '{s}' is corrupted or cannot be read\n",
            .{ @errorName(err), db_file },
        ) catch {};
        return 1;
    };
    defer file.close();

    // TODO: how can I handle error.Overflow in a single place? Can I catch on
    // a block?
    return switch (mode) {
        .passwd => passwd(stdout, &file.db, myflags.args[1..]) catch |err| switch (err) {
            error.Overflow => {
                stderr.print(
                    "ERROR {s}: file '{s}' is corrupted or cannot be read\n",
                    .{ @errorName(err), db_file },
                ) catch {};
                return 1;
            },
        },
        .group => group(stdout, &file.db, myflags.args[1..]) catch |err| switch (err) {
            error.Overflow => {
                stderr.print(
                    "ERROR {s}: file '{s}' is corrupted or cannot be read\n",
                    .{ @errorName(err), db_file },
                ) catch {};
                return 1;
            },
        },
    };
}

fn passwd(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) error{Overflow}!u8 {
    if (keys.len == 0)
        return try passwdAll(stdout, db);
    var some_notfound = false;
    const shell_reader = db.shellReader();
    for (keys) |key| {
        const keyZ = mem.span(key);
        const maybe_packed_user = try if (fmt.parseUnsigned(u32, keyZ, 10)) |uid|
            db.getUserByUid(uid)
        else |_|
            db.getUserByName(keyZ);

        const packed_user = maybe_packed_user orelse {
            some_notfound = true;
            continue;
        };

        const line = packed_user.toUser(shell_reader).toLine();
        stdout.writeAll(line.constSlice()) catch return 3;
    }

    return if (some_notfound) 2 else 0;
}

fn passwdAll(stdout: anytype, db: *const DB) error{Overflow}!u8 {
    const shell_reader = db.shellReader();
    var it = PackedUser.iterator(db.users, db.header.num_users, shell_reader);
    while (try it.next()) |packed_user| {
        const line = packed_user.toUser(db.shellReader()).toLine();
        stdout.writeAll(line.constSlice()) catch return 3;
    }
    return 0;
}

fn group(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) error{Overflow}!u8 {
    if (keys.len == 0)
        return groupAll(stdout, db);
    var some_notfound = false;

    for (keys) |key| {
        const keyZ = mem.span(key);
        const maybe_packed_group = try if (fmt.parseUnsigned(u32, keyZ, 10)) |gid|
            db.getGroupByGid(gid)
        else |_|
            db.getGroupByName(keyZ);

        const g = maybe_packed_group orelse {
            some_notfound = true;
            continue;
        };

        if (try printGroup(stdout, db, &g)) |exit_code|
            return exit_code;
    }

    return if (some_notfound) 2 else 0;
}

fn groupAll(stdout: anytype, db: *const DB) error{Overflow}!u8 {
    var it = PackedGroup.iterator(db.groups, db.header.num_groups);
    while (try it.next()) |g|
        if (try printGroup(stdout, db, &g)) |exit_code|
            return exit_code;

    return 0;
}

fn printGroup(stdout: anytype, db: *const DB, g: *const PackedGroup) error{Overflow}!?u8 {
    // not converting to Group to save a few memory allocations.
    stdout.print("{s}:x:{d}:", .{ g.name(), g.gid() }) catch return 3;

    var it = try DB.groupMembersIter(db.groupmembers[g.members_offset..]);
    // lines will be buffered, flushed on every EOL.
    var line_writer = io.bufferedWriter(stdout);
    var i: usize = 0;
    while (try it.next()) |member_offset| : (i += 1) {
        const puser = try PackedUser.fromBytes(@alignCast(8, db.users[member_offset << 3 ..]));
        const name = puser.user.name();
        if (i != 0)
            _ = line_writer.write(",") catch return 3;
        _ = line_writer.write(name) catch return 3;
    }
    _ = line_writer.write("\n") catch return 3;
    line_writer.flush() catch return 3;

    return null;
}

const testing = std.testing;

test "turbonss-getent passwd" {
    var tf = try File.TestDB.init(testing.allocator);
    defer tf.deinit();
    var stdout = ArrayList(u8).init(testing.allocator);
    defer stdout.deinit();
    var stderr = ArrayList(u8).init(testing.allocator);
    defer stderr.deinit();

    const args = &[_][*:0]const u8{
        "--db",
        tf.path,
        "passwd",
        "root",
        "doesnotexist",
        "vidmantas",
        "0",
        "1",
    };
    const got = execute(stdout.writer(), stderr.writer(), args);
    try testing.expectEqual(got, 2);
    const want_root = "root:x:0:0::/root:/bin/bash\n";
    try testing.expectEqualStrings(stdout.items[0..want_root.len], want_root);
    const want_vidmantas = "vidmantas:x:128:128:Vidmantas Kaminskas:/home/vidmantas:/bin/bash\n";
    var offset: usize = want_root.len + want_vidmantas.len;
    try testing.expectEqualStrings(stdout.items[want_root.len..offset], want_vidmantas);
    try testing.expectEqualStrings(stdout.items[offset..], want_root);
}

test "turbonss-getent passwdAll" {
    var tf = try File.TestDB.init(testing.allocator);
    defer tf.deinit();
    var stdout = ArrayList(u8).init(testing.allocator);
    defer stdout.deinit();
    var stderr = ArrayList(u8).init(testing.allocator);
    defer stderr.deinit();

    const args = &[_][*:0]const u8{
        "--db",
        tf.path,
        "passwd",
    };
    var buf: [User.max_line_len]u8 = undefined;

    const got = execute(stdout.writer(), stderr.writer(), args);
    try testing.expectEqual(got, 0);

    const want_names = &[_][]const u8{
        "Name" ** 8,
        "nobody",
        "root",
        "svc-bar",
        "vidmantas",
    };

    var i: usize = 0;
    var buf_stream = io.fixedBufferStream(stdout.items);
    var reader = buf_stream.reader();
    while (try reader.readUntilDelimiterOrEof(buf[0..], '\n')) |line| {
        var name = mem.split(u8, line, ":");
        try testing.expectEqualStrings(want_names[i], name.next().?);
        i += 1;
    }
}

test "turbonss-getent group" {
    var tf = try File.TestDB.init(testing.allocator);
    defer tf.deinit();
    var stdout = ArrayList(u8).init(testing.allocator);
    defer stdout.deinit();
    var stderr = ArrayList(u8).init(testing.allocator);
    defer stderr.deinit();

    {
        const args = &[_][*:0]const u8{
            "--db",
            tf.path,
            "group",
            "root",
            "doesnotexist",
            "service-group",
            "0",
            "1",
        };
        const got = execute(stdout.writer(), stderr.writer(), args);
        try testing.expectEqual(got, 2);
        const want =
            \\root:x:0:
            \\service-group:x:100000:root,vidmantas
            \\root:x:0:
            \\
        ;
        try testing.expectEqualStrings(want, stdout.items);
    }
}

test "turbonss-getent groupAll" {
    var tf = try File.TestDB.init(testing.allocator);
    defer tf.deinit();
    var stdout = ArrayList(u8).init(testing.allocator);
    defer stdout.deinit();
    var stderr = ArrayList(u8).init(testing.allocator);
    defer stderr.deinit();

    {
        const args = &[_][*:0]const u8{
            "--db",
            tf.path,
            "group",
        };
        const got = execute(stdout.writer(), stderr.writer(), args);
        try testing.expectEqual(got, 0);
        const want =
            \\root:x:0:
            \\vidmantas:x:128:
            \\all:x:9999:NameNameNameNameNameNameNameName,root,svc-bar,vidmantas
            \\service-group:x:100000:root,vidmantas
            \\
        ;
        try testing.expectEqualStrings(want, stdout.items);
    }
}
