"use strict";

// jso-symbolicate — demangle obfuscated JavaScript stack traces.
//
// Two inputs:
//
//   1. An identifier map produced by jso-protector / the JSO HTTP API report.
//      Two supported shapes:
//        a) Array form (from JSOHttpReport.GlobalIdentifierMap):
//           [{ Mangled: "_0x2a3b", Original: "calculateLicenseHash" }, ...]
//        b) Object form (from JSOHttpReport.GlobalIdentifierMapBlock parsed):
//           { "_0x2a3b": "calculateLicenseHash", ... }
//
//      Both Global and Member maps can be merged into a single lookup.
//
//   2. A stack trace string. Any text — function names mentioned anywhere in
//      the text that match a mangled key get rewritten.
//
// This module is intentionally pure (no fs, no network). The CLI in
// bin/symbolicate.js wires it to stdin / files.

/**
 * Build a flat lookup of {mangled: original} from one or more map sources.
 * Accepts arrays of {Mangled, Original} entries OR plain objects.
 */
function buildLookup(...sources) {
    const out = Object.create(null);
    for (const src of sources) {
        if (!src) continue;
        if (Array.isArray(src)) {
            for (const e of src) {
                if (!e) continue;
                const k = e.Mangled || e.mangled || e.from || e.short;
                const v = e.Original || e.original || e.to || e.long;
                if (k && v && !out[k]) out[k] = v;
            }
        } else if (typeof src === "object") {
            for (const k of Object.keys(src)) {
                const v = src[k];
                if (k && typeof v === "string" && !out[k]) out[k] = v;
            }
        } else if (typeof src === "string") {
            // Block form: "mangled1=original1\nmangled2=original2\n..."
            for (const line of src.split(/\r?\n/)) {
                const eq = line.indexOf("=");
                if (eq <= 0) continue;
                const k = line.slice(0, eq).trim();
                const v = line.slice(eq + 1).trim();
                if (k && v && !out[k]) out[k] = v;
            }
        }
    }
    return out;
}

/**
 * Demangle a single stack trace string. Matches whole-word mangled identifiers
 * and substitutes their originals. Lines that don't match are returned as-is.
 *
 * Optional `style`:
 *   - "replace" (default): "  at _0x2a3b (foo.js:1:10)" -> "  at calculateLicenseHash (foo.js:1:10)"
 *   - "annotate": "  at _0x2a3b /* calculateLicenseHash *\/ (foo.js:1:10)"
 */
function demangleStack(stack, lookup, options) {
    if (typeof stack !== "string" || !stack) return stack;
    const style = (options && options.style) || "replace";
    if (!lookup || Object.keys(lookup).length === 0) return stack;

    // Build one regex from all keys, escape regex specials, longest-first so
    // _0x2a3b doesn't get nibbled by _0x2.
    const keys = Object.keys(lookup).sort((a, b) => b.length - a.length);
    const escaped = keys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
    const re = new RegExp("\\b(" + escaped.join("|") + ")\\b", "g");

    return stack.replace(re, (m) => {
        const original = lookup[m];
        if (!original) return m;
        return style === "annotate" ? `${m} /* ${original} */` : original;
    });
}

/**
 * Convenience: report fingerprint + per-line count of how many tokens were
 * rewritten. Useful so callers can detect "wrong map" cases (zero hits).
 */
function demangleStackWithStats(stack, lookup, options) {
    if (typeof stack !== "string") return { stack, hits: 0, perLine: [] };
    const lines = stack.split(/\r?\n/);
    const out = [];
    const perLine = [];
    let total = 0;
    for (const line of lines) {
        const before = line;
        const after = demangleStack(line, lookup, options);
        const hits = before === after ? 0 : countDifferences(before, after, lookup);
        out.push(after);
        perLine.push(hits);
        total += hits;
    }
    return { stack: out.join("\n"), hits: total, perLine };
}

function countDifferences(before, after, lookup) {
    // Simple heuristic: count tokens in the lookup that appeared in `before`.
    // Avoids needing the regex to track substitution count itself.
    let n = 0;
    for (const k of Object.keys(lookup)) {
        const re = new RegExp("\\b" + k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b", "g");
        const m = before.match(re);
        if (m) n += m.length;
    }
    return n;
}

module.exports = { buildLookup, demangleStack, demangleStackWithStats };
