"use strict";

// Electron V8 bytecode compilation adapter for jso-protector.
//
// The pipeline:
//
//   src/*.js
//     |   jso-protector  (HTTP API: obfuscate + sign + watermark)
//     v
//   dist-protected/*.js   <-- existing JSO output
//     |   jso-protector-electron compile
//     v
//   dist-bytecode/*.jsc   <-- V8 bytecode artefacts that Electron loads
//                              directly (no source visible at runtime)
//
// Why both? JSO obfuscation makes the source unreadable; V8 bytecode
// makes the source unrecoverable. An attacker who pulls the .jsc out
// of the .asar archive gets a V8-specific bytecode blob that's tied
// to a single Electron version's V8 build and is far harder to
// decompile than minified-but-shaped JavaScript.
//
// Two execution modes
// -------------------
//   "embedded" (preferred): call this module from inside an Electron
//   main process. We use the renderer's vm.Script + cachedData /
//   produceCachedData (an Electron-exposed API) to emit bytecode for
//   the running V8 version. Requires the user's CI to run on the
//   same Electron release the app ships against.
//
//   "scaffold-only": when invoked from plain Node (e.g. unit tests,
//   pre-flight checks), we DON'T fake the bytecode - we just emit a
//   detailed loader stub + manifest documenting which .jsc files
//   would be produced in Electron mode. CI integrations that don't
//   have Electron in the build environment should adopt the "build
//   in Electron container" pattern documented in README.md.
//
// Public API
// ----------
//   const electron = require("jso-protector-electron");
//   const result = await electron.compileDirectory({
//     input:  "dist-protected",
//     output: "dist-bytecode",
//     extensions: [".js"],
//     mode: "auto",                   // "auto" | "embedded" | "scaffold-only"
//     bytecodeFooter: true,           // append .bytecode-meta footer
//   });
//   // result: { mode, processed: [{ in, out, bytes, jscBytes }], skipped: [...] }

const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");

const SCHEMA_VERSION = 1;

// Detect whether we're running inside an Electron process. Electron
// sets process.versions.electron and bridges vm.Script.createCachedData.
function detectElectron() {
    try {
        if (process && process.versions && typeof process.versions.electron === "string") {
            return { available: true, version: process.versions.electron };
        }
    } catch (e) {}
    return { available: false, version: null };
}

async function compileDirectory(opts) {
    opts = opts || {};
    if (!opts.input)  throw new Error("compileDirectory: opts.input is required");
    if (!opts.output) throw new Error("compileDirectory: opts.output is required");

    const extensions = (Array.isArray(opts.extensions) && opts.extensions.length) ? opts.extensions : [".js"];
    const mode = opts.mode || "auto";
    const detected = detectElectron();
    const resolvedMode = mode === "auto"
        ? (detected.available ? "embedded" : "scaffold-only")
        : mode;

    if (resolvedMode === "embedded" && !detected.available) {
        throw new Error("jso-protector-electron: mode 'embedded' requires the process to be running under Electron (run `electron jso-protector-electron ...` instead of `node`).");
    }

    fs.mkdirSync(opts.output, { recursive: true });
    const files = walk(opts.input).filter(function (p) {
        return extensions.some(function (e) { return p.endsWith(e); });
    });

    const processed = [];
    const skipped = [];
    for (const inFile of files) {
        const rel = path.relative(opts.input, inFile);
        // Embedded mode appends .jsc to mark the file as bytecode.
        // Scaffold-only preserves the original extension so the dist
        // directory can be reviewed by a human.
        const outFile = resolvedMode === "embedded"
            ? path.join(opts.output, rel) + ".jsc"
            : path.join(opts.output, rel);
        fs.mkdirSync(path.dirname(outFile), { recursive: true });
        const src = fs.readFileSync(inFile, "utf8");
        const srcBytes = Buffer.byteLength(src, "utf8");

        if (resolvedMode === "embedded") {
            // Real bytecode emission via vm.Script.createCachedData().
            try {
                const vm = require("node:vm");
                const script = new vm.Script(src, { filename: rel });
                // Some Node versions: createCachedData. Some Electron versions: cachedDataProduced via produceCachedData.
                let cached;
                if (typeof script.createCachedData === "function") {
                    cached = script.createCachedData();
                } else {
                    // Legacy: produceCachedData via constructor option (deprecated).
                    const sc2 = new vm.Script(src, { filename: rel, produceCachedData: true });
                    cached = sc2.cachedData;
                }
                if (!cached || !cached.length) {
                    skipped.push({ in: inFile, reason: "v8 declined to emit cachedData (script too small or syntax error)" });
                    continue;
                }
                const header = buildJscHeader(rel, detected.version, srcBytes, cached.length);
                const payload = Buffer.concat([header, cached]);
                fs.writeFileSync(outFile, payload);
                processed.push({ in: inFile, out: outFile, bytes: srcBytes, jscBytes: payload.length, electronVersion: detected.version });
            } catch (x) {
                skipped.push({ in: inFile, reason: "compile threw: " + x.message });
            }
        } else {
            // Scaffold-only: emit the original JS bytes + a sibling
            // .bytecode-meta.json describing what would be produced
            // when an Electron-aware build environment is available.
            fs.writeFileSync(outFile, src, "utf8");
            const sha256 = crypto.createHash("sha256").update(src).digest("hex");
            const meta = {
                v: SCHEMA_VERSION,
                kind: "jso-protector-electron-scaffold",
                source: rel,
                sourceSha256: sha256,
                sourceBytes: srcBytes,
                generatedAt: opts.now || new Date().toISOString(),
                advice: "Re-run under Electron (mode: 'embedded') to emit real .jsc bytecode bound to the target Electron release.",
            };
            const metaFile = outFile + ".bytecode-meta.json";
            fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2) + "\n", "utf8");
            processed.push({ in: inFile, out: outFile, bytes: srcBytes, jscBytes: null });
        }
    }

    // Write a top-level manifest so downstream pipelines (Asar packer,
    // electron-builder, electron-vite) can verify coverage.
    const manifest = {
        v: SCHEMA_VERSION,
        kind: "jso-protector-electron",
        mode: resolvedMode,
        electronVersion: detected.version,
        generatedAt: opts.now || new Date().toISOString(),
        input: path.resolve(opts.input),
        output: path.resolve(opts.output),
        processed: processed.map(function (p) { return { in: path.relative(opts.input, p.in), out: path.relative(opts.output, p.out), bytes: p.bytes, jscBytes: p.jscBytes }; }),
        skipped: skipped.map(function (s) { return { in: path.relative(opts.input, s.in), reason: s.reason }; }),
    };
    fs.writeFileSync(path.join(opts.output, "bytecode.manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
    return { mode: resolvedMode, processed: processed, skipped: skipped, manifestPath: path.join(opts.output, "bytecode.manifest.json") };
}

function buildJscHeader(filename, electronVersion, srcBytes, jscBytes) {
    // Tiny header so the .jsc file is self-describing: magic +
    // electron version + source size + jsc size. Real loader is the
    // standard vm.Script({ cachedData }) pattern; the header is for
    // forensics / integrity reporting, not load-time gating.
    const magic = Buffer.from("JSOJSC\x01\x00", "utf8"); // 8 bytes
    const header = {
        v: SCHEMA_VERSION,
        filename: filename,
        electronVersion: electronVersion,
        srcBytes: srcBytes,
        jscBytes: jscBytes,
    };
    const jsonBytes = Buffer.from(JSON.stringify(header), "utf8");
    const lenBytes = Buffer.alloc(4);
    lenBytes.writeUInt32LE(jsonBytes.length, 0);
    return Buffer.concat([magic, lenBytes, jsonBytes]);
}

function walk(dir) {
    const out = [];
    function rec(d) {
        for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
            const full = path.join(d, entry.name);
            if (entry.isDirectory()) rec(full);
            else if (entry.isFile()) out.push(full);
        }
    }
    rec(dir);
    return out;
}

module.exports = {
    SCHEMA_VERSION: SCHEMA_VERSION,
    compileDirectory: compileDirectory,
    detectElectron: detectElectron,
    _buildJscHeader: buildJscHeader,
};
