const std = @import("std");
const logger = @import("logger.zig");
const root = @import("root");
const bun = @import("global.zig");
const string = bun.string;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const CLI = @import("./cli.zig").Cli;
const Features = @import("./analytics/analytics_thread.zig").Features;
const Platform = @import("./analytics/analytics_thread.zig").GenerateHeader.GeneratePlatform;
const HTTP = @import("http").AsyncHTTP;
const CrashReporter = @import("crash_reporter");

const Report = @This();

var crash_report_writer: CrashReportWriter = CrashReportWriter{ .file = null };
const CrashWritable = if (Environment.isWasm) std.io.fixedBufferStream([2048]u8) else std.fs.File.Writer;
var crash_reporter_path: [1024]u8 = undefined;
pub const CrashReportWriter = struct {
    file: ?std.io.BufferedWriter(4096, CrashWritable) = null,
    file_path: []const u8 = "",

    pub fn printFrame(_: ?*anyopaque, frame: CrashReporter.StackFrame) void {
        const function_name = if (frame.function_name.len > 0) frame.function_name else "[function ?]";
        const filename = if (frame.filename.len > 0) frame.function_name else "[file ?]";
        crash_report_writer.print("[0x{X}] - <b>{s}<r> {s}:{d}\n", .{ frame.pc, function_name, filename, frame.line_number });
    }

    pub fn dump() void {
        if (comptime !Environment.isWasm)
            CrashReporter.print();
    }

    pub fn done() void {
        if (comptime !Environment.isWasm)
            CrashReporter.print();
    }

    pub fn print(this: *CrashReportWriter, comptime fmt: string, args: anytype) void {
        Output.prettyError(fmt, args);
        if (this.file) |*file| {
            var writer = file.writer();
            writer.print(comptime Output.prettyFmt(fmt, false), args) catch {};
        }
    }

    pub fn flush(this: *CrashReportWriter) void {
        if (this.file) |*file| {
            file.flush() catch {};
        }
        Output.flush();
    }

    pub fn generateFile(this: *CrashReportWriter) void {
        if (this.file != null) return;

        var base_dir: []const u8 = ".";
        if (std.os.getenv("BUN_INSTALL")) |install_dir| {
            base_dir = std.mem.trimRight(u8, install_dir, std.fs.path.sep_str);
        } else if (std.os.getenv("HOME")) |home_dir| {
            base_dir = std.mem.trimRight(u8, home_dir, std.fs.path.sep_str);
        }
        const file_path = std.fmt.bufPrintZ(
            &crash_reporter_path,
            "{s}/.bun-crash/v{s}-{d}.crash",
            .{ base_dir, Global.package_json_version, @intCast(u64, @maximum(std.time.milliTimestamp(), 0)) },
        ) catch return;

        std.fs.cwd().makeDir(std.fs.path.dirname(std.mem.span(file_path)).?) catch {};
        var file = std.fs.cwd().createFileZ(file_path, .{ .truncate = true }) catch return;
        this.file = std.io.bufferedWriter(
            file.writer(),
        );
        this.file_path = std.mem.span(file_path);
    }

    pub fn printPath(this: *CrashReportWriter) void {
        var display_path = this.file_path;

        if (this.file_path.len > 0) {
            var tilda = false;
            if (std.os.getenv("HOME")) |home_dir| {
                if (strings.hasPrefix(display_path, home_dir)) {
                    display_path = display_path[home_dir.len..];
                    tilda = true;
                }
            }

            if (tilda) {
                Output.prettyError("\nCrash report saved to:\n  <b>~{s}<r>\n", .{display_path});
            } else {
                Output.prettyError("\nCrash report saved to:\n  <b>{s}<r>\n", .{display_path});
            }
        }
    }
};

pub fn printMetadata() void {
    @setCold(true);
    crash_report_writer.generateFile();

    const cmd_label: string = if (CLI.cmd) |tag| @tagName(tag) else "Unknown";

    const platform = if (Environment.isMac) "macOS" else "Linux";
    const arch = if (Environment.isAarch64)
        if (Environment.isMac) "Silicon" else "arm64"
    else
        "x64";

    var analytics_platform = Platform.forOS();

    crash_report_writer.print(
        \\
        \\<r>----- bun meta -----
    ++ "\nBun v" ++ Global.package_json_version_with_sha ++ " " ++ platform ++ " " ++ arch ++ " {s}\n" ++
        \\{s}: {}
        \\
    , .{
        analytics_platform.version,
        cmd_label,
        Features.formatter(),
    });

    const http_count = HTTP.active_requests_count.loadUnchecked();
    if (http_count > 0)
        crash_report_writer.print(
            \\HTTP: {d}
            \\
        , .{http_count});

    if (comptime bun.use_mimalloc) {
        var elapsed_msecs: usize = 0;
        var user_msecs: usize = 0;
        var system_msecs: usize = 0;
        var current_rss: usize = 0;
        var peak_rss: usize = 0;
        var current_commit: usize = 0;
        var peak_commit: usize = 0;
        var page_faults: usize = 0;
        const mimalloc = @import("allocators/mimalloc.zig");
        mimalloc.mi_process_info(
            &elapsed_msecs,
            &user_msecs,
            &system_msecs,
            &current_rss,
            &peak_rss,
            &current_commit,
            &peak_commit,
            &page_faults,
        );
        crash_report_writer.print("Elapsed: {d}ms | User: {d}ms | Sys: {d}ms\nRSS: {:<3.2} | Peak: {:<3.2} | Commit: {:<3.2} | Faults: {d}\n", .{
            elapsed_msecs,
            user_msecs,
            system_msecs,
            std.fmt.fmtIntSizeDec(current_rss),
            std.fmt.fmtIntSizeDec(peak_rss),
            std.fmt.fmtIntSizeDec(current_commit),
            page_faults,
        });
    }

    crash_report_writer.print("----- bun meta -----\n", .{});
}
var has_printed_fatal = false;
var has_printed_crash = false;
pub fn fatal(err_: ?anyerror, msg_: ?string) void {
    const had_printed_fatal = has_printed_fatal;
    if (!has_printed_fatal) {
        has_printed_fatal = true;
        crash_report_writer.generateFile();

        if (err_) |err| {
            if (Output.isEmojiEnabled()) {
                crash_report_writer.print(
                    "\n<r><red>error<r><d>:<r> <b>{s}<r>\n",
                    .{@errorName(err)},
                );
            } else {
                crash_report_writer.print(
                    "\n<r>error: {s}\n\n",
                    .{@errorName(err)},
                );
            }
        }

        if (msg_) |msg| {
            const msg_ptr = @ptrToInt(msg.ptr);
            if (msg_ptr > 0) {
                const len = @maximum(@minimum(msg.len, 1024), 0);

                if (len > 0) {
                    if (Output.isEmojiEnabled()) {
                        crash_report_writer.print(
                            "\n<r><red>uh-oh<r><d>:<r> <b>{s}<r>\n",
                            .{msg[0..len]},
                        );
                    } else {
                        crash_report_writer.print(
                            "\n<r>an uh-oh: {s}\n\n",
                            .{msg[0..len]},
                        );
                    }
                }
            }
        }

        if (err_ == null) {
            if (Output.isEmojiEnabled()) {
                if (msg_ == null and err_ == null) {
                    crash_report_writer.print("<r><red>", .{});
                } else {
                    crash_report_writer.print("<r>", .{});
                }
                crash_report_writer.print("bun will crash now<r> 😭😭😭\n", .{});
            } else {
                crash_report_writer.print("bun has crashed :'(\n", .{});
            }
        }
        crash_report_writer.flush();

        printMetadata();

        crash_report_writer.flush();

        // It only is a real crash report if it's not coming from Zig

        CrashReportWriter.dump();
        crash_report_writer.flush();

        crash_report_writer.printPath();
    }

    if (!had_printed_fatal) {
        crash_report_writer.print("\nSearch GitHub issues https://bun.sh/issues or ask for #help in https://bun.sh/discord\n\n", .{});
        crash_report_writer.flush();
    }
}

var globalError_ranOnce = false;

pub noinline fn handleCrash(signal: i32, addr: usize) void {
    const had_printed_fatal = has_printed_fatal;
    if (has_printed_fatal) return;
    has_printed_fatal = true;

    crash_report_writer.generateFile();

    const name = switch (signal) {
        std.os.SIG.SEGV => error.SegmentationFault,
        std.os.SIG.ILL => error.InstructionError,
        std.os.SIG.BUS => error.BusError,
        else => error.Crash,
    };

    crash_report_writer.print(
        "\n<r><red>{s}<d> at 0x{any}\n\n",
        .{ @errorName(name), std.fmt.fmtSliceHexUpper(std.mem.asBytes(&addr)) },
    );
    printMetadata();
    CrashReportWriter.dump();

    if (!had_printed_fatal) {
        crash_report_writer.print("\nAsk for #help in https://bun.sh/discord or go to https://bun.sh/issues\n\n", .{});
    }
    crash_report_writer.flush();

    if (crash_report_writer.file) |file| {
        // don't handle return codes here
        _ = std.os.system.close(file.unbuffered_writer.context.handle);
    }

    crash_report_writer.file = null;
    if (comptime Environment.isDebug) {
        if (@errorReturnTrace()) |stack| {
            std.debug.dumpStackTrace(stack.*);
        }
    }

    std.c._exit(128 + @truncate(u8, @intCast(u8, @maximum(signal, 0))));
}

pub noinline fn globalError(err: anyerror) noreturn {
    @setCold(true);

    if (@atomicRmw(bool, &globalError_ranOnce, .Xchg, true, .Monotonic)) {
        Global.exit(1);
    }

    switch (err) {
        error.SyntaxError => {
            Output.prettyError(
                "\n<r><red>SyntaxError<r><d>:<r> An error occurred while parsing code",
                .{},
            );
            Global.exit(1);
        },
        error.OutOfMemory => {
            Output.prettyError(
                "\n<r><red>OutOfMemory<r><d>:<r> There might be an infinite loop somewhere",
                .{},
            );
            printMetadata();
            Global.exit(1);
        },
        error.CurrentWorkingDirectoryUnlinked => {
            Output.prettyError(
                "\n<r><red>error: <r>The current working directory was deleted, so that command didn't work. Please cd into a different directory and try again.",
                .{},
            );
            Global.exit(1);
        },
        error.BundleFailed => {
            Output.prettyError(
                "\n<r><red>BundleFailed<r>",
                .{},
            );
            Global.exit(1);
        },
        error.InvalidArgument, error.InstallFailed => {
            Global.exit(1);
        },
        error.SystemFdQuotaExceeded => {
            const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit);
            if (comptime Environment.isMac) {
                Output.prettyError(
                    \\
                    \\<r><red>error<r>: Your computer ran out of file descriptors <d>(<red>SystemFdQuotaExceeded<r><d>)<r>
                    \\
                    \\<d>Current limit: {d}<r>
                    \\
                    \\To fix this, try running:
                    \\ 
                    \\  <cyan>sudo launchctl limit maxfiles 2147483646<r>
                    \\  <cyan>ulimit -n 2147483646<r>
                    \\
                    \\That will only work until you reboot.
                    \\
                ,
                    .{
                        limit.cur,
                    },
                );
            } else {
                Output.prettyError(
                    \\
                    \\<r><red>error<r>: Your computer ran out of file descriptors <d>(<red>SystemFdQuotaExceeded<r><d>)<r>
                    \\
                    \\<d>Current limit: {d}<r>
                    \\
                    \\To fix this, try running:
                    \\ 
                    \\  <cyan>sudo echo -e "\nfs.file-max=2147483646\n" >> /etc/sysctl.conf<r>
                    \\  <cyan>sudo sysctl -p<r>
                    \\  <cyan>ulimit -n 2147483646<r>
                    \\
                ,
                    .{
                        limit.cur,
                    },
                );

                if (std.os.getenv("USER")) |user| {
                    if (user.len > 0) {
                        Output.prettyError(
                            \\
                            \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf:
                            \\
                            \\ <cyan>{s} soft nofile 2147483646<r>
                            \\ <cyan>{s} hard nofile 2147483646<r>
                            \\
                        ,
                            .{ user, user },
                        );
                    }
                }
            }

            Global.exit(1);
        },
        error.@"Invalid Bunfig" => {
            Global.exit(1);
        },
        error.ProcessFdQuotaExceeded => {
            const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit);
            if (comptime Environment.isMac) {
                Output.prettyError(
                    \\
                    \\<r><red>error<r>: bun ran out of file descriptors <d>(<red>ProcessFdQuotaExceeded<r><d>)<r>
                    \\
                    \\<d>Current limit: {d}<r>
                    \\
                    \\To fix this, try running:
                    \\ 
                    \\  <cyan>ulimit -n 2147483646<r>
                    \\
                    \\You may also need to run:
                    \\
                    \\  <cyan>sudo launchctl limit maxfiles 2147483646<r>
                    \\
                ,
                    .{
                        limit.cur,
                    },
                );
            } else {
                Output.prettyError(
                    \\
                    \\<r><red>error<r>: bun ran out of file descriptors <d>(<red>ProcessFdQuotaExceeded<r><d>)<r>
                    \\
                    \\<d>Current limit: {d}<r>
                    \\
                    \\To fix this, try running:
                    \\ 
                    \\  <cyan>ulimit -n 2147483646<r>
                    \\
                    \\That will only work for the current shell. To fix this for the entire system, run:
                    \\
                    \\  <cyan>sudo echo -e "\nfs.file-max=2147483646\n" >> /etc/sysctl.conf<r>
                    \\  <cyan>sudo sysctl -p<r>
                    \\
                ,
                    .{
                        limit.cur,
                    },
                );

                if (std.os.getenv("USER")) |user| {
                    if (user.len > 0) {
                        Output.prettyError(
                            \\
                            \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf:
                            \\
                            \\ <cyan>{s} soft nofile 2147483646<r>
                            \\ <cyan>{s} hard nofile 2147483646<r>
                            \\
                        ,
                            .{ user, user },
                        );
                    }
                }
            }

            Global.exit(1);
        },
        // The usage of `unreachable` in Zig's std.os may cause the file descriptor problem to show up as other errors
        error.NotOpenForReading, error.Unexpected => {
            const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit);

            if (limit.cur > 0 and limit.cur < (8096 * 2)) {
                Output.prettyError(
                    \\
                    \\<r><red>error<r>: An unknown error ocurred, possibly due to low max file descriptors <d>(<red>Unexpected<r><d>)<r>
                    \\
                    \\<d>Current limit: {d}<r>
                    \\
                    \\To fix this, try running:
                    \\ 
                    \\  <cyan>ulimit -n 2147483646<r>
                    \\
                ,
                    .{
                        limit.cur,
                    },
                );

                if (Environment.isLinux) {
                    if (std.os.getenv("USER")) |user| {
                        if (user.len > 0) {
                            Output.prettyError(
                                \\
                                \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf:
                                \\
                                \\ <cyan>{s} soft nofile 2147483646<r>
                                \\ <cyan>{s} hard nofile 2147483646<r>
                                \\
                            ,
                                .{
                                    user,
                                    user,
                                },
                            );
                        }
                    }
                } else if (Environment.isMac) {
                    Output.prettyError(
                        \\
                        \\If that still doesn't work, you may need to run:
                        \\
                        \\  <cyan>sudo launchctl limit maxfiles 2147483646<r>
                        \\
                    ,
                        .{},
                    );
                }

                Global.exit(1);
            }
        },
        error.FileNotFound => {
            Output.prettyError(
                "\n<r><red>error<r><d>:<r> <b>FileNotFound<r>\nbun could not find a file, and the code that produces this error is missing a better error.\n",
                .{},
            );
            Output.flush();

            Report.printMetadata();

            Output.flush();

            print_stacktrace: {
                var debug_info = std.debug.getSelfDebugInfo() catch break :print_stacktrace;
                var trace = @errorReturnTrace() orelse break :print_stacktrace;
                Output.disableBuffering();
                std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.debug.detectTTYConfig()) catch break :print_stacktrace;
            }

            Global.exit(1);
        },
        error.MissingPackageJSON => {
            Output.prettyError(
                "\n<r><red>error<r><d>:<r> <b>MissingPackageJSON<r>\nbun could not find a package.json file.\n",
                .{},
            );
            Global.exit(1);
        },
        error.MissingValue => {
            Global.exit(1);
        },
        else => {},
    }

    Report.fatal(err, null);

    print_stacktrace: {
        var debug_info = std.debug.getSelfDebugInfo() catch break :print_stacktrace;
        var trace = @errorReturnTrace() orelse break :print_stacktrace;
        Output.disableBuffering();
        std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.debug.detectTTYConfig()) catch break :print_stacktrace;
    }

    Global.exit(1);
}
