/**
 * mailparser worker thread.
 *
 * Runs simpleParser off the main event loop so a 14-25 second cold-start
 * (or any slow parse) can't block IPC. The main thread's `parse-serial.ts`
 * spawns this worker once at first use, then dispatches every parse here
 * via postMessage and awaits the reply.
 *
 * Protocol:
 *   main → worker: { id: number, source: Buffer | string }
 *   worker → main: { id: number, ok: true,  result: ParsedMail }
 *                | { id: number, ok: false, error: string }
 *
 * The worker holds its own mailparser module instance — JIT cost and
 * libmime/iconv-lite loading happen exactly once per worker process. The
 * main thread sees only the postMessage round-trip (~1 ms on local
 * structured-clone of a small Buffer + parsed object).
 */
import { parentPort } from "node:worker_threads";
import { simpleParser, type Source } from "mailparser";

if (!parentPort) {
    throw new Error("parse-worker: must be spawned as a worker, parentPort is null");
}

interface ParseRequest {
    id: number;
    source: Source;
}

// Self-warmup: parse a synthetic message at worker startup so V8 JIT,
// libmime / iconv-lite loading, and mailparser's lazy init all complete
// *before* the worker accepts its first real request.
//
// CRITICAL (Bob 2026-05-15): the warmup message MUST exercise every heavy
// code path a real email hits, or the cold-start cost just moves to the
// first real parse. The previous warmup was a trivial text/plain message
// — it JIT'd the basic path but NOT the HTML pipeline, quoted-printable
// decode, multipart boundary handling, or attachment extraction. Result:
// the first real parse (a 96 KB multipart/alternative) took 33 SECONDS
// in the log — the cold start was never actually absorbed. This warmup
// message is multipart/mixed → multipart/alternative(text + QP-encoded
// HTML) + a base64 attachment, with HTML entities and a non-ASCII char,
// so the first real parse rides warm JIT for all of them.
const _warmupT0 = Date.now();
const _warmupMessage =
    "From: warmup@mailx.local\r\nTo: warmup@mailx.local\r\n"
    + "Subject: =?UTF-8?Q?warm=E2=80=91up?=\r\nMIME-Version: 1.0\r\n"
    + "Content-Type: multipart/mixed; boundary=\"MIX\"\r\n\r\n"
    + "--MIX\r\n"
    + "Content-Type: multipart/alternative; boundary=\"ALT\"\r\n\r\n"
    + "--ALT\r\n"
    + "Content-Type: text/plain; charset=UTF-8\r\n\r\n"
    + "parse-worker cold-start absorber — discard.\r\n"
    + "--ALT\r\n"
    + "Content-Type: text/html; charset=UTF-8\r\n"
    + "Content-Transfer-Encoding: quoted-printable\r\n\r\n"
    + "<html><body><p>warm=E2=80=91up &amp; discard</p>"
    + "<blockquote>quoted</blockquote><a href=3D\"http://x\">link</a></body></html>\r\n"
    + "--ALT--\r\n"
    + "--MIX\r\n"
    + "Content-Type: application/octet-stream; name=\"w.bin\"\r\n"
    + "Content-Transfer-Encoding: base64\r\n"
    + "Content-Disposition: attachment; filename=\"w.bin\"\r\n\r\n"
    + "d2FybXVw\r\n"
    + "--MIX--\r\n";
const _warmupPromise = simpleParser(Buffer.from(_warmupMessage, "utf8")).then(() => {
    // Heartbeat so the main thread can log the absorbed cost.
    parentPort!.postMessage({ warmupMs: Date.now() - _warmupT0 });
}).catch(() => { /* warmup is best-effort; fall through to real handling */ });

parentPort.on("message", async (msg: ParseRequest) => {
    const { id, source } = msg;
    // If a real parse arrives during warmup, queue behind it. The await
    // resolves immediately once warmup is done; trivially fast on hot
    // worker since _warmupPromise is already resolved.
    await _warmupPromise;
    try {
        // CRITICAL: `Buffer` sent across `worker_threads.postMessage` arrives
        // as plain `Uint8Array` (Node docs: "Buffer instances passed to
        // Worker.postMessage() are passed as Uint8Array instances when
        // received"). mailparser's `simpleParser` uses `Buffer.isBuffer(input)`
        // to discriminate Buffer / string / Stream. A Uint8Array fails that
        // check and gets routed to the Stream branch, which calls
        // `input.once("end", …)` and immediately throws
        //   `input.once is not a function`
        // Re-wrap with `Buffer.from(src.buffer, src.byteOffset, src.byteLength)`
        // — zero-copy view over the same memory, but with the Buffer prototype
        // so `Buffer.isBuffer` is true.
        let normalized: Source = source;
        if (source instanceof Uint8Array && !Buffer.isBuffer(source)) {
            const u = source as Uint8Array;
            normalized = Buffer.from(u.buffer, u.byteOffset, u.byteLength);
        }
        const _t0 = Date.now();
        const result = await simpleParser(normalized);
        const _ms = Date.now() - _t0;
        // Worker-side parse time. If THIS is slow, the bug is in simpleParser
        // on a given input; if this is fast but the main thread still saw a
        // multi-second `[parse]` figure, the cost is queue wait / respawn —
        // splitting the two was the open question of the 2026-05-15 hunt.
        if (_ms > 1000) {
            const size = Buffer.isBuffer(normalized) ? normalized.byteLength : String(normalized).length;
            console.warn(`  [parse-worker] simpleParser took ${_ms}ms for ${(size / 1024).toFixed(0)} KB (worker-side, not queue wait)`);
        }
        parentPort!.postMessage({ id, ok: true, result });
    } catch (e: unknown) {
        const error = e instanceof Error ? e.message : String(e);
        parentPort!.postMessage({ id, ok: false, error });
    }
});
