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 compress = @import("compress.zig");
const DB = @import("DB.zig");
const File = @import("File.zig");
const PackedUser = @import("PackedUser.zig");
const PackedGroup = @import("PackedGroup.zig");
const Header = @import("header.zig").Header;
const HeaderHost = @import("header.zig").Host;
const section_length_bits = @import("header.zig").section_length_bits;

const usage =
    \\usage: turbonss-analyze [OPTION]... <PATH>
    \\
    \\Arguments:
    \\  <PATH>  Path to the database (default: /etc/turbonss/db.turbo)
    \\
    \\Options:
    \\  -h      Print this help message and exit
    \\
;

const Info = struct {
    fname: []const u8,
    size_file: []const u8,
    version: meta.fieldInfo(Header, .version).type,
    endian: []const u8,
    ptr_size: meta.fieldInfo(HeaderHost, .ptr_size).type,
    getgr_bufsize: meta.fieldInfo(Header, .getgr_bufsize).type,
    getpw_bufsize: meta.fieldInfo(Header, .getpw_bufsize).type,
    users: meta.fieldInfo(Header, .num_users).type,
    groups: meta.fieldInfo(Header, .num_groups).type,
    shells: meta.fieldInfo(Header, .num_shells).type,
};

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 },
    }) catch {
        stderr.writeAll(usage) catch return 3;
        return 1;
    };

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

    const db_file = switch (myflags.args.len) {
        0 => "/etc/turbonss/db.turbo",
        1 => mem.span(myflags.args[0]),
        else => {
            stderr.print("ERROR: too many arguments\n", .{}) catch return 3;
            stderr.writeAll(usage) catch return 3;
            return 1;
        },
    };

    const file_size_bytes = blk: {
        const fd = os.open(db_file, os.O.RDONLY, 0) catch |err| {
            stderr.print("ERROR: failed to open '{s}': {s}\n", .{
                db_file,
                @errorName(err),
            }) catch {};
            return 1;
        };
        defer os.close(fd);
        const stat = os.fstat(fd) catch |err| {
            stderr.print("ERROR: fstat '{s}': {s}\n", .{
                db_file,
                @errorName(err),
            }) catch {};
            return 1;
        };
        break :blk stat.size;
    };

    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();
    const db = file.db;

    const info = Info{
        .fname = db_file,
        .size_file = splitInt(@intCast(u64, file_size_bytes)).constSlice(),
        .version = db.header.version,
        .endian = @tagName(db.header.host.endian),
        .ptr_size = db.header.host.ptr_size,
        .getgr_bufsize = db.header.getgr_bufsize,
        .getpw_bufsize = db.header.getpw_bufsize,
        .users = db.header.num_users,
        .groups = db.header.num_groups,
        .shells = db.header.num_shells,
    };

    const template =
        \\File:               {[fname]s}
        \\Size:               {[size_file]s} bytes
        \\Version:            {[version]d}
        \\Endian:             {[endian]s}
        \\Pointer size:       {[ptr_size]} bytes
        \\getgr buffer size:  {[getgr_bufsize]d}
        \\getpw buffer size:  {[getpw_bufsize]d}
        \\Users:              {[users]d}
        \\Groups:             {[groups]d}
        \\Shells:             {[shells]d}
        \\
    ;
    stdout.print(template, info) catch return 3;

    // Popularity contest:
    // - user with most groups
    // - TODO: group with most users. Not trivial, because
    //   group memberships do not include users whose primary
    //   gid is the target one.
    if (db.header.num_users > 0) {
        const Name = BoundedArray(u8, 32);
        const Popular = struct {
            name: Name = Name.init(0) catch unreachable,
            score: u64 = 0,
        };

        var popUser = Popular{};
        var it = PackedUser.iterator(
            db.users,
            db.header.num_users,
            db.shellReader(),
        );
        while (it.next() catch |err| switch (err) {
            error.Overflow => {
                stderr.print("ERROR: file '{s}' is corrupted\n", .{db_file}) catch {};
                return 1;
            },
        }) |packed_user| {
            const offset = packed_user.additional_gids_offset;
            const additional_gids = db.additional_gids[offset..];
            const vit = compress.varintSliceIterator(additional_gids) catch |err| {
                stderr.print(
                    "ERROR {s}: file '{s}' is corrupted or cannot be read\n",
                    .{ @errorName(err), db_file },
                ) catch {};
                return 1;
            };
            // the primary gid of the user is never in "additional gids"
            const ngroups = vit.remaining + 1;
            if (ngroups > popUser.score) {
                const name = packed_user.name();
                popUser.name = Name.fromSlice(name) catch unreachable;
                popUser.score = ngroups;
            }
        }

        stdout.print("Most memberships:   {s} ({d})\n", .{
            popUser.name.constSlice(),
            popUser.score,
        }) catch return 3;
    }

    var lengths = DB.fieldLengths(db.header);
    var offsets = DB.fieldOffsets(lengths);
    stdout.writeAll(
        \\Sections:
        \\    Name                 Begin    End          Size bytes
        \\
    ) catch return 3;

    inline for (meta.fields(DB.DBNumbers)) |field| {
        const length = @field(lengths, field.name);
        const start = @field(offsets, field.name);
        const end = start + @field(lengths, field.name);
        stdout.print("    {s:<21}{x:0>8} {x:0>8} {s:>14}\n", .{
            field.name,
            start << section_length_bits,
            end << section_length_bits,
            splitInt(length << section_length_bits).constSlice(),
        }) catch return 3;
    }

    return 0;
}

const testing = std.testing;

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

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

const max_len = fmt.count("{d}", .{math.maxInt(u64)}) * 2;
fn splitInt(n: u64) BoundedArray(u8, max_len) {
    var result = BoundedArray(u8, max_len).init(0) catch unreachable;
    var buf: [max_len]u8 = undefined;
    const str = fmt.bufPrint(buf[0..], "{d}", .{n}) catch unreachable;
    for (str, str.len..0) |c, remaining| {
        result.append(c) catch unreachable;
        if (remaining > 0 and remaining % 3 == 0)
            result.append(',') catch unreachable;
    }
    return result;
}

test "turbonss-analyze separators" {
    const tests = [_]struct {
        input: u64,
        want: []const u8,
    }{
        .{ .input = 0, .want = "0" },
        .{ .input = 999, .want = "999" },
        .{ .input = 1000, .want = "1,000" },
        .{ .input = 9999, .want = "9,999" },
        .{ .input = 19999, .want = "19,999" },
        .{ .input = 19999, .want = "19,999" },
        .{ .input = 18446744073709551615, .want = "18,446,744,073,709,551,615" },
    };
    for (tests) |tt| {
        const got = splitInt(tt.input);
        try testing.expectEqualStrings(tt.want, got.constSlice());
    }
}
