const std = @import("std");
const fs = std.fs;
const io = std.io;
const mem = std.mem;
const os = std.os;
const heap = std.heap;
const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator;

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

const flags = @import("flags.zig");
const User = @import("User.zig");
const Group = @import("Group.zig");
const Corpus = @import("Corpus.zig");
const DB = @import("DB.zig");
const ErrCtx = @import("ErrCtx.zig");

const usage =
    \\usage: turbonss-unix2db [OPTION]...
    \\
    \\Options:
    \\  -h          Print this help message and exit
    \\  --passwd    Path to passwd file (default: passwd)
    \\  --group     Path to group file (default: group)
    \\  --output    Path to output file (default: db.turbo)
    \\
;

pub fn main() !void {
    const gpa = heap.raw_c_allocator;

    const stderr = io.getStdErr().writer();
    const stdout = io.getStdOut().writer();

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

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

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

    if (result.args.len != 0) {
        stderr.print("ERROR: unknown option '{s}'\n", .{result.args[0]}) catch {};
        stderr.writeAll(usage) catch {};
        return 1;
    }

    const passwd_fname = result.argFlag("--passwd") orelse "passwd";
    const group_fname = result.argFlag("--group") orelse "group";
    const out_fname = result.argFlag("--output") orelse "db.turbo";

    // to catch an error set file.OpenError, wait for
    // https://github.com/ziglang/zig/issues/2473
    var errc = ErrCtx{};
    var passwd_file = fs.cwd().openFile(passwd_fname, .{ .mode = .read_only }) catch |err|
        return fail(errc.wrapf("open '{s}'", .{passwd_fname}), stderr, err);
    defer passwd_file.close();

    var group_file = fs.cwd().openFile(group_fname, .{ .mode = .read_only }) catch |err|
        return fail(errc.wrapf("open '{s}'", .{group_fname}), stderr, err);
    defer group_file.close();

    var passwd_buf = io.bufferedReader(passwd_file.reader());
    var passwd_reader = passwd_buf.reader();
    var users = User.fromReader(allocator, &errc, passwd_reader) catch |err|
        return fail(errc.wrap("read users"), stderr, err);
    defer {
        for (users) |*user| user.deinit(allocator);
        allocator.free(users);
    }

    var group_buf = io.bufferedReader(group_file.reader());
    var group_reader = group_buf.reader();
    var groups = Group.fromReader(allocator, group_reader) catch |err|
        return fail(errc.wrap("read groups"), stderr, err);
    defer {
        for (groups) |*group| group.deinit(allocator);
        allocator.free(groups);
    }

    var corpus = Corpus.init(allocator, users, groups, &errc) catch |err|
        return fail(errc.wrap("init corpus"), stderr, err);

    defer corpus.deinit();
    var db = DB.fromCorpus(allocator, &corpus, &errc) catch |err|
        return fail(errc.wrap("construct DB from corpus"), stderr, err);
    defer db.deinit(allocator);

    const fd = os.open(out_fname, os.O.WRONLY | os.O.TRUNC | os.O.CREAT, 0o644) catch |err|
        return fail(errc.wrapf("open for writing {s}", .{out_fname}), stderr, err);
    errdefer os.close(fd);

    const len = os.writev(fd, db.iov().constSlice()) catch |err|
        return fail(errc.wrapf("writev to {s}", .{out_fname}), stderr, err);
    os.fsync(fd) catch |err|
        return fail(errc.wrapf("fsync {s}", .{out_fname}), stderr, err);
    os.close(fd);

    stderr.print("total {d} bytes. groups={d} users={d}\n", .{
        len,
        users.len,
        groups.len,
    }) catch return 1;

    return 0;
}

fn fail(errc: *ErrCtx, stderr: anytype, err: anytype) u8 {
    const err_chain = errc.unwrap().constSlice();
    stderr.print("ERROR {s}: {s}\n", .{ @errorName(err), err_chain }) catch {};
    return 1;
}

const testing = std.testing;

test "turbonss-unix2db invalid argument" {
    const allocator = testing.allocator;
    const args = &[_][*:0]const u8{"--invalid-argument"};
    var stderr = ArrayList(u8).init(allocator);
    defer stderr.deinit();
    var stdout = ArrayList(u8).init(allocator);
    defer stdout.deinit();

    const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args[0..]);
    try testing.expectEqual(@as(u8, 1), exit_code);
    try testing.expect(mem.startsWith(
        u8,
        stderr.items,
        "ERROR: unknown option '--invalid-argument'",
    ));
}

test "turbonss-unix2db trivial error: missing passwd file" {
    const allocator = testing.allocator;
    const args = &[_][*:0]const u8{
        "--passwd",
        "/does/not/exist/passwd",
        "--group",
        "/does/not/exist/group",
    };
    var stderr = ArrayList(u8).init(allocator);
    defer stderr.deinit();
    var stdout = ArrayList(u8).init(allocator);
    defer stdout.deinit();

    const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args[0..]);
    try testing.expectEqual(@as(u8, 1), exit_code);
    try testing.expectEqualStrings(stderr.items, "ERROR FileNotFound: open '/does/not/exist/passwd'\n");
}

test "turbonss-unix2db fail" {
    var errc = ErrCtx{};
    var buf = ArrayList(u8).init(testing.allocator);
    defer buf.deinit();
    var wr = buf.writer();
    const exit_code = fail(errc.wrapf("invalid user 'foo'", .{}), wr, error.NotSure);
    try testing.expectEqual(exit_code, 1);
    try testing.expectEqualStrings(buf.items, "ERROR NotSure: invalid user 'foo'\n");
}

test "turbonss-unix2db smoke test" {
    const allocator = testing.allocator;
    var stderr = ArrayList(u8).init(allocator);
    defer stderr.deinit();
    var stdout = ArrayList(u8).init(allocator);
    defer stdout.deinit();

    var corpus = try Corpus.testCorpus(allocator);
    defer corpus.deinit();

    var tmp = testing.tmpDir(.{});
    defer tmp.cleanup();

    const tmp_path = blk: {
        const relative_path = try fs.path.join(allocator, &[_][]const u8{
            "zig-cache",
            "tmp",
            tmp.sub_path[0..],
        });
        const real_path = try fs.realpathAlloc(allocator, relative_path);
        allocator.free(relative_path);
        break :blk real_path;
    };
    defer allocator.free(tmp_path);

    const passwdPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "passwd" });
    defer allocator.free(passwdPath);
    const groupPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "group" });
    defer allocator.free(groupPath);
    const outPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "tmp.turbo" });
    defer allocator.free(outPath);

    const passwd_fd = try os.open(passwdPath, os.O.CREAT | os.O.WRONLY, 0o644);
    const group_fd = try os.open(groupPath, os.O.CREAT | os.O.WRONLY, 0o644);

    var i: usize = 0;
    const users = corpus.users.slice();
    while (i < users.len) : (i += 1)
        _ = try os.write(passwd_fd, users.get(i).toLine().constSlice());
    os.close(passwd_fd);

    var group_writer = (fs.File{ .handle = group_fd }).writer();
    i = 0;
    const groups = corpus.groups.slice();
    while (i < groups.len) : (i += 1)
        try groups.get(i).writeTo(group_writer);
    os.close(group_fd);

    const args = &[_][*:0]const u8{
        "--passwd", passwdPath,
        "--group",  groupPath,
        "--output", outPath,
    };

    const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args);
    try testing.expectEqualStrings("total 1664 bytes. groups=5 users=4\n", stderr.items);
    try testing.expectEqual(@as(u8, 0), exit_code);
}
