"use strict";

// Sentry integration helpers for jso-symbolicate.
//
// Sentry's built-in source-map symbolication needs line/column-aware Source
// Map v3 files. JSO's identifier map is name-only (no positions), so the
// recommended pattern is an Event Processor that runs *before* the event ships
// to Sentry: demangle function names in every stack frame, then let Sentry's
// own UI display the cleaned-up names. No upload step, no Sentry artifact
// management, the substitution lives entirely in your runtime.
//
// Usage:
//
//   const Sentry = require("@sentry/browser");
//   const symbolicate = require("jso-symbolicate");
//   const { createSentryEventProcessor } = require("jso-symbolicate/sentry");
//
//   const lookup = symbolicate.buildLookup(
//       require("./report.json").Report.GlobalIdentifierMap,
//       require("./report.json").Report.MemberIdentifierMap
//   );
//
//   Sentry.init({
//       dsn: "...",
//       beforeSend(event) {
//           return createSentryEventProcessor(lookup)(event);
//       }
//   });
//
// The processor walks event.exception.values[].stacktrace.frames[] and
// event.threads.values[].stacktrace.frames[] and rewrites `function` and
// `module` strings. It also runs over event.message and event.breadcrumbs[]
// because real-world Sentry events often have the function name in the
// message body, not just the stack.

const { demangleStack } = require("./index.js");

function rewriteFrames(frames, lookup) {
    if (!Array.isArray(frames)) return;
    for (const f of frames) {
        if (!f || typeof f !== "object") continue;
        if (typeof f.function === "string") f.function = demangleStack(f.function, lookup);
        if (typeof f.module === "string") f.module = demangleStack(f.module, lookup);
        // pre_context / context_line / post_context are source lines from the
        // protected bundle. Demangle those too so the snippet shown alongside
        // the frame is readable.
        if (typeof f.context_line === "string") f.context_line = demangleStack(f.context_line, lookup);
        if (Array.isArray(f.pre_context)) f.pre_context = f.pre_context.map(s => demangleStack(s, lookup));
        if (Array.isArray(f.post_context)) f.post_context = f.post_context.map(s => demangleStack(s, lookup));
    }
}

function rewriteException(exception, lookup) {
    if (!exception || !Array.isArray(exception.values)) return;
    for (const v of exception.values) {
        if (!v) continue;
        if (typeof v.value === "string") v.value = demangleStack(v.value, lookup);
        if (typeof v.type === "string") v.type = demangleStack(v.type, lookup);
        if (v.stacktrace && Array.isArray(v.stacktrace.frames)) {
            rewriteFrames(v.stacktrace.frames, lookup);
        }
    }
}

function rewriteThreads(threads, lookup) {
    if (!threads || !Array.isArray(threads.values)) return;
    for (const t of threads.values) {
        if (t && t.stacktrace && Array.isArray(t.stacktrace.frames)) {
            rewriteFrames(t.stacktrace.frames, lookup);
        }
    }
}

function rewriteBreadcrumbs(breadcrumbs, lookup) {
    if (!Array.isArray(breadcrumbs)) return;
    for (const b of breadcrumbs) {
        if (b && typeof b.message === "string") {
            b.message = demangleStack(b.message, lookup);
        }
    }
}

/**
 * Build a Sentry `beforeSend` / Event Processor function that demangles
 * mangled identifiers throughout the event in-place. Returns the event so
 * the result can be threaded directly into Sentry.init({ beforeSend }).
 *
 * Pass `options.preserveOriginal: true` to attach a copy of the un-demangled
 * stack as a Sentry extra for forensic comparison.
 */
function createSentryEventProcessor(lookup, options) {
    if (!lookup || typeof lookup !== "object") {
        // No-op when there's nothing to demangle. Returning the event unchanged
        // keeps Sentry working even if the caller passed in an invalid lookup.
        return (event) => event;
    }
    const opts = options || {};

    return function processEvent(event) {
        if (!event || typeof event !== "object") return event;

        try {
            if (opts.preserveOriginal) {
                event.extra = event.extra || {};
                event.extra.jso_mangled_event = JSON.parse(JSON.stringify({
                    message: event.message,
                    exception: event.exception
                }));
            }

            if (typeof event.message === "string") {
                event.message = demangleStack(event.message, lookup);
            }
            rewriteException(event.exception, lookup);
            rewriteThreads(event.threads, lookup);
            rewriteBreadcrumbs(event.breadcrumbs, lookup);

            event.tags = event.tags || {};
            event.tags.jso_symbolicated = "true";
        } catch (e) {
            // Never let a symbolication error block crash reporting. Sentry
            // takes whatever we return; an exception here would mean the
            // event never reaches the dashboard, which is much worse than a
            // partially-demangled stack.
            event.tags = event.tags || {};
            event.tags.jso_symbolicate_error = String(e && e.message || e);
        }
        return event;
    };
}

module.exports = { createSentryEventProcessor };
