const std = @import("std");
const at = @import("ansi-term");
const env = @import("env.zig");
const command = @import("command.zig");
const util = @import("util.zig");

const Mutex = @import("mutex.zig").Mutex;
const Server = @import("sock/Server.zig");

const msg = @import("message.zig").msg;

pub const std_options = std.Options{
    .log_level = .debug,
    .logFn = struct {
        pub fn logFn(
            comptime msg_level: std.log.Level,
            comptime scope: @TypeOf(.enum_literal),
            comptime fmt: []const u8,
            args: anytype,
        ) void {
            const logfile = log_file orelse return;

            if (scope != .default) {
                logfile.writer().print("[{s}] ", .{@tagName(scope)}) catch return;
            }

            logfile.writer().writeAll(switch (msg_level) {
                .err => "E: ",
                .warn => "W: ",
                .info => "I: ",
                .debug => "D: ",
            }) catch return;

            logfile.writer().print(fmt ++ "\n", args) catch return;
        }
    }.logFn,
};

var log_file: ?std.fs.File = null;

pub fn main() void {
    log_file = createLogFile() catch null;
    defer if (log_file) |lf| lf.close();

    tryMain() catch |e| {
        std.log.err("FATAL ERROR: {}", .{e});
        if (@errorReturnTrace()) |trace| {
            var buf: [1024 * 8]u8 = undefined;
            const trace_s = s: {
                const deb_inf = std.debug.getSelfDebugInfo() catch break :s null;

                var fbs = std.io.fixedBufferStream(&buf);
                std.debug.writeStackTrace(
                    trace.*,
                    fbs.writer(),
                    std.heap.page_allocator,
                    deb_inf,
                    .no_color,
                ) catch break :s null;
                break :s fbs.getWritten();
            };

            if (trace_s) |s| {
                std.log.err("ERT: {s}", .{s});
            }
        }
        std.debug.print("Encountered fatal error (check log), starting emergency shell!\n", .{});

        @panic(@errorName(std.os.execveZ(
            "/bin/sh",
            &[_:null]?[*:0]const u8{"/bin/sh"},
            &[_:null]?[*:0]const u8{},
        )));
    };
}

fn tryMain() !void {
    var stdout = std.io.bufferedWriter(std.io.getStdOut().writer());

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    var launch_cmd: ?[][]const u8 = null;
    defer if (launch_cmd) |cmd| alloc.free(cmd);

    if (std.os.argv.len >= 2) {
        if (!std.mem.eql(u8, std.mem.span(std.os.argv[1]), "cmd") or std.os.argv.len < 3)
            return error.InvalidCommand;

        launch_cmd = try alloc.alloc([]const u8, std.os.argv[2..].len);
        for (launch_cmd.?, std.os.argv[2..]) |*arg, arg_in| {
            arg.* = std.mem.span(arg_in);
        }
    }

    var env_map = try std.process.getEnvMap(alloc);
    defer env_map.deinit();

    if (env_map.get("MZTEINIT")) |_| {
        try stdout.writer().writeAll("mzteinit running already, starting shell\n");
        try stdout.flush();
        var child = std.ChildProcess.init(launch_cmd orelse &.{"fish"}, alloc);
        _ = try child.spawnAndWait();
        return;
    } else {
        try env_map.put("MZTEINIT", "1");
    }

    if (try env.populateEnvironment(&env_map)) {
        try env.populateSysdaemonEnvironment(&env_map);
    }

    var env_mtx = Mutex(std.process.EnvMap){ .data = env_map };

    var srv: ?Server = null;
    if (env_map.get("XDG_RUNTIME_DIR")) |xrd| {
        var sockaddr_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
        const sockaddr = try std.fmt.bufPrintZ(
            &sockaddr_buf,
            "{s}/mzteinit-{}-{}.sock",
            .{ xrd, std.os.linux.getuid(), std.os.linux.getpid() },
        );

        try msg("starting socket server @ {s}...", .{sockaddr});

        srv = try Server.init(alloc, sockaddr, &env_mtx);
        errdefer srv.?.ss.deinit();
        (try std.Thread.spawn(.{}, Server.run, .{&srv.?})).detach();

        std.log.info("socket server started @ {s}", .{sockaddr});

        env_mtx.mtx.lock();
        defer env_mtx.mtx.unlock();

        const key_dup = try alloc.dupe(u8, "MZTEINIT_SOCKET");
        errdefer alloc.free(key_dup);
        try env_mtx.data.putMove(key_dup, sockaddr);
    } else {
        std.log.warn("XDG_RUNTIME_DIR is not set, no socket server will be started!", .{});
    }
    defer if (srv) |*s| s.ss.deinit();

    if (launch_cmd) |cmd| {
        try msg("using launch command", .{});
        var child = std.ChildProcess.init(cmd, alloc);
        {
            env_mtx.mtx.lock();
            defer env_mtx.mtx.unlock();
            child.env_map = &env_map;
            try child.spawn();
        }
        _ = try child.wait();
        return;
    }

    const entries_config_path = try std.fs.path.join(alloc, &.{
        env_map.get("XDG_CONFIG_HOME") orelse @panic("bork"),
        "mzteinit",
        "entries.cfg",
    });
    defer alloc.free(entries_config_path);

    var entries_config_file = try std.fs.cwd().openFile(entries_config_path, .{});
    defer entries_config_file.close();

    const entries_config_data = try entries_config_file.readToEndAlloc(alloc, std.math.maxInt(usize));
    defer alloc.free(entries_config_data);

    const entries = try command.parseEntriesConfig(alloc, entries_config_data);
    defer {
        for (entries) |entry|
            entry.deinit(alloc);
        alloc.free(entries);
    }

    while (true) {
        try stdout.writer().writeAll(util.ansi_clear);

        const cmd = ui(&stdout, entries) catch |e| {
            std.debug.print("Error rendering the UI: {}\n", .{e});
            break;
        };

        try stdout.writer().writeAll(util.ansi_clear);
        try stdout.flush();

        var exit = util.ExitMode.run;
        cmd.run(alloc, &exit, &env_mtx) catch |e| {
            try stdout.writer().print("Error running command: {}\n\n", .{e});
            continue;
        };

        switch (exit) {
            .run => {},
            .immediate => return,
            .delayed => {
                try stdout.writer().writeAll("Goodbye!");
                try stdout.flush();
                std.time.sleep(2 * std.time.ns_per_s);
                return;
            },
        }
    }
}

fn ui(buf_writer: anytype, entries: []command.Command) !command.Command {
    const w = buf_writer.writer();
    var style: ?at.style.Style = null;

    try @import("figlet.zig").writeFiglet(w);
    const uname = std.os.uname();
    try updateStyle(w, .{ .foreground = .Yellow }, &style);
    try w.print(
        "\n {s} {s} {s}\n\n",
        .{
            uname.nodename,
            uname.release,
            uname.machine,
        },
    );

    try updateStyle(w, .{ .font_style = .{ .bold = true } }, &style);
    try w.writeAll("     What do you want to do?\n\n");

    for (entries) |entry| {
        try updateStyle(w, .{ .foreground = .Cyan }, &style);
        try w.print("[{c}] ", .{entry.key});
        try updateStyle(w, .{ .foreground = .Green }, &style);
        try w.print("{s}\n", .{entry.label});
    }
    try at.format.resetStyle(w);
    style = .{};

    try buf_writer.flush();

    const old_termios = try std.os.tcgetattr(std.os.STDIN_FILENO);
    var new_termios = old_termios;
    new_termios.lflag.ICANON = false; // No line buffering
    new_termios.lflag.ECHO = false; // No echoing stuff
    try std.os.tcsetattr(std.os.STDIN_FILENO, .NOW, new_termios);

    var cmd: ?command.Command = null;
    var c: [1]u8 = undefined;
    while (cmd == null) {
        std.debug.assert(try std.io.getStdIn().read(&c) == 1);
        const key_upper = std.ascii.toUpper(c[0]);
        for (entries) |entry| {
            if (entry.key == key_upper) {
                cmd = entry;
                break;
            }
        } else {
            try w.print("Unknown command '{s}'\n", .{c});
            try buf_writer.flush();
        }
    }
    try std.os.tcsetattr(std.os.STDIN_FILENO, .NOW, old_termios);

    return cmd.?;
}

fn createLogFile() !std.fs.File {
    var fname_buf: [128]u8 = undefined;
    const fname = try std.fmt.bufPrintZ(
        &fname_buf,
        "/tmp/mzteinit-{}-{}.log",
        .{ std.os.linux.getuid(), std.os.linux.getpid() },
    );
    return try std.fs.createFileAbsoluteZ(fname, .{});
}

fn updateStyle(
    writer: anytype,
    new_style: at.style.Style,
    old_style: *?at.style.Style,
) !void {
    try at.format.updateStyle(writer, new_style, old_style.*);
    old_style.* = new_style;
}
