// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const lp = @import("lightpanda");

const js = @import("js.zig");

const v8 = js.v8;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;

const TryCatch = @This();

handle: v8.TryCatch,
local: *const js.Local,

pub fn init(self: *TryCatch, l: *const js.Local) void {
    self.local = l;
    v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
}

pub fn hasCaught(self: TryCatch) bool {
    return v8.v8__TryCatch__HasCaught(&self.handle);
}

pub fn rethrow(self: *TryCatch) void {
    if (comptime IS_DEBUG) {
        std.debug.assert(self.hasCaught());
    }
    _ = v8.v8__TryCatch__ReThrow(&self.handle);
}

pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
    if (self.hasCaught() == false) {
        return null;
    }

    const l = self.local;
    const line: ?u32 = blk: {
        const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
        const line = v8.v8__Message__GetLineNumber(handle, l.handle);
        break :blk if (line < 0) null else @intCast(line);
    };

    const exception: ?[]const u8 = blk: {
        const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
        var js_val = js.Value{ .local = l, .handle = handle };

        // If it's an Error object, try to get the message property
        if (js_val.isObject()) {
            const js_obj = js_val.toObject();
            if (js_obj.has("message")) {
                js_val = js_obj.get("message") catch break :blk null;
            }
        }

        if (js_val.isString()) |js_str| {
            break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
        }
        break :blk null;
    };

    const stack: ?[]const u8 = blk: {
        const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
        var js_val = js.Value{ .local = l, .handle = handle };

        // If it's an Error object, try to get the stack property
        if (js_val.isObject()) {
            const js_obj = js_val.toObject();
            if (js_obj.has("stack")) {
                js_val = js_obj.get("stack") catch break :blk null;
            }
        }

        if (js_val.isString()) |js_str| {
            break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
        }
        break :blk null;
    };

    return .{
        .line = line,
        .stack = stack,
        .caught = true,
        .exception = exception,
    };
}

pub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {
    return self.caught(allocator) orelse .{
        .caught = false,
        .line = null,
        .stack = null,
        .exception = @errorName(err),
    };
}

pub fn deinit(self: *TryCatch) void {
    v8.v8__TryCatch__DESTRUCT(&self.handle);
}

pub const Caught = struct {
    line: ?u32 = null,
    caught: bool = false,
    stack: ?[]const u8 = null,
    exception: ?[]const u8 = null,

    pub fn format(self: Caught, writer: *std.Io.Writer) !void {
        const separator = lp.log.separator();
        try writer.print("{s}exception: {?s}", .{ separator, self.exception });
        try writer.print("{s}stack: {?s}", .{ separator, self.stack });
        try writer.print("{s}line: {?d}", .{ separator, self.line });
        try writer.print("{s}caught: {any}", .{ separator, self.caught });
    }

    pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {
        try writer.write(prefix ++ ".exception", self.exception orelse "???");
        try writer.write(prefix ++ ".stack", self.stack orelse "na");
        try writer.write(prefix ++ ".line", self.line);
        try writer.write(prefix ++ ".caught", self.caught);
    }

    pub fn jsonStringify(self: Caught, jw: anytype) !void {
        try jw.beginObject();
        try jw.objectField("exception");
        try jw.write(self.exception);
        try jw.objectField("stack");
        try jw.write(self.stack);
        try jw.objectField("line");
        try jw.write(self.line);
        try jw.objectField("caught");
        try jw.write(self.caught);
        try jw.endObject();
    }
};
