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

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

const flags = @import("flags.zig");
const User = @import("User.zig");
const PackedUser = @import("PackedUser.zig");

const usage =
    \\usage: turbonss-makecorpus [OPTION]...
    \\
    \\Options:
    \\  -h             Print this help message and exit
    \\  --directory    Write files to given directory (default: .)
    \\  --num-users    Number of users (default: 10000)
    \\  --num-groups   Number of groups (default: 10000)
    \\  --max-members  Average members per group (default: 1000)
    \\
;

const shells = &[_][]const u8{
    "/bin/bash",
    "/bin/sh",
    "/bin/zsh",
    "/usr/sbin/nologin",
};

pub fn main() void {
    execute(
        io.getStdOut().writer(),
        io.getStdErr().writer(),
        os.argv[1..],
    ) catch |err| switch (err) {
        error.User => os.exit(1),
        error.IO => os.exit(3),
    };
}

const options = &[_]flags.Flag{
    .{ .name = "-h", .kind = .boolean },
    .{ .name = "--directory", .kind = .arg },
    .{ .name = "--num-users", .kind = .arg },
    .{ .name = "--num-groups", .kind = .arg },
    .{ .name = "--max-members", .kind = .arg },
};

fn execute(
    stdout: anytype,
    stderr: anytype,
    argv: []const [*:0]const u8,
) error{ User, IO }!void {
    const myflags = flags.parse(argv, options) catch {
        stderr.writeAll(usage) catch {};
        return error.User;
    };

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

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

    const num_users = parseInt(stderr, myflags, "--num-users", 10000) orelse return error.User;
    const num_groups = parseInt(stderr, myflags, "--num-groups", 10000) orelse return error.User;
    const max_members = parseInt(stderr, myflags, "--max-members", 1000) orelse return error.User;

    var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
    var fixed = std.heap.FixedBufferAllocator.init(buf[0..]);
    const fixed_a = fixed.allocator();
    const dir = myflags.argFlag("--directory") orelse ".";
    const passwd_path = fs.path.join(fixed_a, &[_][]const u8{ dir, "passwd" }) catch |err| switch (err) {
        error.OutOfMemory => {
            stderr.print("ERROR: --directory too long\n", .{}) catch {};
            return error.User;
        },
    };

    const open_flags = std.fs.File.CreateFlags{ .mode = 0o644 };
    var passwdf = fs.cwd().createFile(passwd_path, open_flags) catch |err| {
        stderr.print("ERROR: create passwd: {s}\n", .{@errorName(err)}) catch {};
        return error.User;
    };
    defer passwdf.close();
    errdefer os.unlink(passwd_path) catch {};
    try dump_passwd(passwdf.writer(), num_users);

    fixed.reset();
    // if passwd fit, group will too
    const group_path = fs.path.join(fixed_a, &[_][]const u8{ dir, "group" }) catch unreachable;
    var groupf = fs.cwd().createFile(group_path, open_flags) catch |err| {
        stderr.print("ERROR: creating group: {s}\n", .{@errorName(err)}) catch {};
        return error.User;
    };
    defer groupf.close();
    errdefer os.unlink(group_path) catch {};
    var group_wr = io.bufferedWriter(groupf.writer());
    try dump_group(group_wr.writer(), num_users, num_groups, max_members);
    group_wr.flush() catch return error.IO;

    stderr.print("wrote users={d} groups={d} max-members={d} to {s}\n", .{
        num_users,
        num_groups,
        max_members,
        dir,
    }) catch {};
}

fn dump_passwd(wr: anytype, num_users: u64) error{IO}!void {
    var buf_gecos: [PackedUser.max_gecos_len]u8 = undefined;
    var buf_name: [PackedUser.max_name_len]u8 = undefined;
    var buf_home: [PackedUser.max_home_len]u8 = undefined;
    for (1000000..1000000 + num_users) |i| {
        const name = fmt.bufPrint(buf_name[0..], "u_{d}", .{i}) catch unreachable;
        const gecos = fmt.bufPrint(buf_gecos[0..], "User {d}", .{i}) catch unreachable;
        const home = fmt.bufPrint(buf_home[0..], "/home/{s}", .{name}) catch unreachable;
        const user = User{
            .uid = @intCast(u32, i),
            .gid = 1000000,
            .name = name,
            .gecos = gecos,
            .home = home,
            .shell = shells[i % shells.len],
        };
        _ = wr.write(user.toLine().constSlice()) catch return error.IO;
    }
}

fn dump_group(wr: anytype, num_users: u64, num_groups: u64, max_members: u32) error{IO}!void {
    var lw = io.bufferedWriter(wr);
    var lwr = lw.writer();
    for (1000000..1000000 + num_groups) |i| {
        lwr.print("g_{d}:x:{d}:", .{ i, i }) catch return error.IO;
        for (0..(i % max_members)) |j| {
            const user_idx = (i + j - 1) % num_users;
            if (j != 0)
                _ = lwr.write(",") catch return error.IO;
            _ = lwr.print("u_{d}", .{1000000 + user_idx}) catch return error.IO;
        }
        _ = lwr.write("\n") catch return error.IO;
    }
    lw.flush() catch return error.IO;
}

fn parseInt(stderr: anytype, myflags: flags.ParseResult(options), option: [:0]const u8, default: u32) ?u32 {
    const arg = myflags.argFlag(option) orelse return default;
    const num = std.fmt.parseUnsigned(u32, arg, 0) catch |err| {
        stderr.print("unable to parse {s}={s}: {s}\n", .{ option, arg, @errorName(err) }) catch {};
        return null;
    };
    return num;
}

const want_group =
    \\g_1000000:x:1000000:
    \\g_1000001:x:1000001:u_1000001
    \\g_1000002:x:1000002:u_1000002,u_1000003
    \\g_1000003:x:1000003:u_1000003,u_1000004,u_1000005
    \\g_1000004:x:1000004:
    \\g_1000005:x:1000005:u_1000005
    \\g_1000006:x:1000006:u_1000006,u_1000000
    \\g_1000007:x:1000007:u_1000000,u_1000001,u_1000002
    \\
;

const want_passwd =
    \\u_1000000:x:1000000:1000000:User 1000000:/home/u_1000000:/bin/bash
    \\u_1000001:x:1000001:1000000:User 1000001:/home/u_1000001:/bin/sh
    \\u_1000002:x:1000002:1000000:User 1000002:/home/u_1000002:/bin/zsh
    \\u_1000003:x:1000003:1000000:User 1000003:/home/u_1000003:/usr/sbin/nologin
    \\u_1000004:x:1000004:1000000:User 1000004:/home/u_1000004:/bin/bash
    \\u_1000005:x:1000005:1000000:User 1000005:/home/u_1000005:/bin/sh
    \\u_1000006:x:1000006:1000000:User 1000006:/home/u_1000006:/bin/zsh
    \\
;

const testing = std.testing;

test "turbonss-makecorpus: bad flag" {
    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{ "--num-users", "x" };

    const got = execute(stdout.writer(), stderr.writer(), args);
    try testing.expectEqual(got, error.User);
    try testing.expectEqualStrings(stderr.items, "unable to parse --num-users=x: InvalidCharacter\n");
    try testing.expectEqualStrings(stdout.items, "");
}

test "turbonss-makecorpus: simple OK case" {
    const allocator = testing.allocator;
    var stdout = ArrayList(u8).init(allocator);
    defer stdout.deinit();
    var stderr = ArrayList(u8).init(allocator);
    defer stderr.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..],
        });
        defer allocator.free(relative_path);
        const real_path = try fs.realpathAlloc(allocator, relative_path);
        const real_pathZ = try allocator.dupeZ(u8, real_path);
        allocator.free(real_path);
        break :blk real_pathZ;
    };
    defer allocator.free(tmp_path);

    const args = &[_][*:0]const u8{
        "--num-users",
        "1",
        "--num-groups",
        "1",
        "--max-members",
        "1",
        "--directory",
        tmp_path,
    };

    try execute(stdout.writer(), stderr.writer(), args);
    try testing.expectStringStartsWith(stderr.items, "wrote users=1 groups=1 max-members=1 to /");
    try testing.expectEqualStrings(stdout.items, "");

    const passwd_fname = try fs.path.join(allocator, &[_][]const u8{ tmp_path, "passwd" });
    defer allocator.free(passwd_fname);
    var passwd_file = try fs.cwd().openFile(passwd_fname, .{ .mode = .read_only });
    defer passwd_file.close();
    var group_file = try fs.cwd().openFile(passwd_fname, .{ .mode = .read_only });
    defer group_file.close();
}

test "turbonss-makecorpus: dump_passwd" {
    var wr = ArrayList(u8).init(testing.allocator);
    defer wr.deinit();

    try dump_passwd(wr.writer(), 7);
    try testing.expectEqualStrings(want_passwd, wr.items);
}

test "turbonss-makecorpus: dump_group" {
    var wr = ArrayList(u8).init(testing.allocator);
    defer wr.deinit();

    try dump_group(wr.writer(), 7, 8, 4);
    try testing.expectEqualStrings(want_group, wr.items);
}
