/**
 * Worker-thread-backed simpleParser dispatcher.
 *
 * mailparser's `simpleParser` is declared `async` but its work is CPU-bound
 * (Node Streams + libmime decoding). When it runs on the main event loop:
 *   - The first parse after a fresh process takes 14-25 seconds (cold V8
 *     JIT + libmime + iconv-lite + charset-table loading). See log
 *     2026-05-14 01:11:44 — simpleParser 14524ms for 5 KB.
 *   - Every parse, cold or warm, blocks the IPC stdin pump for its
 *     duration. A 200 ms parse delays every queued IPC message by 200 ms.
 *
 * Moving the parse to a `worker_threads` Worker fixes both problems:
 *   - The main event loop stays responsive — IPC, sync, timers all run
 *     during the parse. Click-to-render latency is bounded by post-message
 *     RTT (sub-ms) + parse time on the worker, but the rest of the app
 *     stays alive.
 *   - The worker absorbs cold-start once. Subsequent parses ride the
 *     worker's warm JIT and run in their natural 50-500 ms budget.
 *
 * Priority queue: foreground (UI clicks) jumps over background (sync /
 * prefetch) when more than one parse is pending. Each parse still runs
 * sequentially on the worker — the single-worker design intentionally
 * mirrors the prior in-process serialization to keep CPU bounded.
 *
 * Lazy spawn: the worker is created on first call. boot/setup code that
 * runs no parses pays nothing.
 *
 * Worker lifecycle: the worker is process-singleton, never explicitly
 * terminated. Node exits cleanly because the worker is `unref()`d so it
 * doesn't keep the event loop alive.
 *
 * Lives in mailx-store rather than mailx-service so both the UI-hot path
 * (mailx-service/local-store.ts) and the sync path (mailx-imap) share one
 * worker — otherwise each module would spawn its own (parallel workers
 * would defeat the bound on CPU, and each pays its own cold start).
 */

import { Worker } from "node:worker_threads";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import type { ParsedMail, Source } from "mailparser";

type Pending = {
    id: number;
    source: Source;
    resolve: (m: ParsedMail) => void;
    reject: (e: unknown) => void;
};
const _head: Pending[] = [];
const _tail: Pending[] = [];
let _pumping = false;
let _worker: Worker | null = null;
let _nextId = 1;
const _inflight = new Map<number, Pending>();

function getWorker(): Worker {
    if (_worker) return _worker;
    // Resolve parse-worker.js alongside the compiled parse-serial.js.
    // import.meta.url is the running .js URL after tsc compilation, so
    // the worker resolves cleanly from the package install location.
    const here = dirname(fileURLToPath(import.meta.url));
    const workerPath = join(here, "parse-worker.js");
    const w = new Worker(workerPath);
    w.on("message", (msg: { id?: number; ok?: boolean; result?: ParsedMail; error?: string; warmupMs?: number }) => {
        // Warmup heartbeat (no `id`). Logs the worker cold-start cost so
        // we can confirm the absorber is working on each boot.
        if (typeof msg.warmupMs === "number") {
            console.log(`  [parse-worker] cold-start absorbed in worker in ${msg.warmupMs}ms`);
            return;
        }
        if (typeof msg.id !== "number") return;
        const entry = _inflight.get(msg.id);
        if (!entry) return;
        _inflight.delete(msg.id);
        if (msg.ok) entry.resolve(msg.result as ParsedMail);
        else entry.reject(new Error(msg.error || "parse-worker error"));
        signalCompletion();
    });
    w.on("error", (e: Error) => {
        // Fatal worker crash — fail every in-flight parse, then null the
        // singleton so the next call respawns. Logged loudly: a silent
        // respawn loop forces every Nth parse to eat a fresh cold-start,
        // which reads as a random multi-second "loading body" stall.
        console.error(`  [parse-worker] CRASHED (${e.message}) — ${_inflight.size} in-flight parse(s) failed, will respawn`);
        for (const entry of _inflight.values()) entry.reject(e);
        _inflight.clear();
        _worker = null;
        signalCompletion();
    });
    w.on("exit", (code: number) => {
        // code 0 = clean exit (shouldn't happen — parentPort keeps it alive);
        // non-zero = crash. Either way the next parse respawns + re-warms.
        console.warn(`  [parse-worker] exited (code=${code}), ${_inflight.size} in-flight — next parse respawns`);
        if (_inflight.size > 0) {
            const err = new Error(`parse-worker exited (code=${code}) with ${_inflight.size} in-flight`);
            for (const entry of _inflight.values()) entry.reject(err);
            _inflight.clear();
        }
        _worker = null;
        signalCompletion();
    });
    // Don't let the worker prevent process exit.
    w.unref();
    _worker = w;
    return w;
}

/** In-main-thread fallback. Used when the worker spawn itself fails (e.g.,
 *  a packaged build where parse-worker.js can't be resolved). Falls back
 *  to the original behavior — blocks the event loop, but at least works.
 *  Lazy-imports mailparser so the main thread doesn't pay the (heavy)
 *  module-init cost unless the fallback actually fires. */
async function pumpInMain(): Promise<void> {
    if (_pumping) return;
    _pumping = true;
    try {
        const { simpleParser } = await import("mailparser");
        for (;;) {
            const next = _head.shift() ?? _tail.shift();
            if (!next) break;
            try {
                const parsed = await simpleParser(next.source);
                next.resolve(parsed);
            } catch (e) {
                next.reject(e);
            }
        }
    } finally {
        _pumping = false;
    }
}

// Notifier the pump awaits to drive the next iteration. When a reply
// arrives from the worker, completing the in-flight parse, this fires
// and unblocks the pump so it can pop the next queued parse. Avoids the
// busy-wait setTimeout-poll version.
let _completionSignal: (() => void) | null = null;
function signalCompletion(): void {
    const fn = _completionSignal;
    _completionSignal = null;
    if (fn) fn();
}

async function pumpViaWorker(): Promise<void> {
    if (_pumping) return;
    _pumping = true;
    try {
        for (;;) {
            const next = _head.shift() ?? _tail.shift();
            if (!next) break;
            _inflight.set(next.id, next);
            try {
                getWorker().postMessage({ id: next.id, source: next.source });
            } catch (e) {
                _inflight.delete(next.id);
                next.reject(e);
                continue;
            }
            // Wait for THIS parse to complete before sending the next.
            // Preserves the priority order — a foreground entry that arrives
            // after we've already postMessage'd a background parse will be
            // next in line, NOT after several already-queued backgrounds.
            await new Promise<void>(resolve => { _completionSignal = resolve; });
        }
    } finally {
        _pumping = false;
    }
}

/** Spawn the parse worker early so its cold-start (mailparser module
 *  loading, V8 JIT, libmime / iconv-lite tables — empirically 14-25 s)
 *  runs in parallel with the rest of boot. Safe to call multiple times;
 *  the worker is a process-singleton. Returns immediately; the actual
 *  cold-start completes in the worker's internal self-warm. */
export function prewarmParseWorker(): void {
    try { getWorker(); } catch { /* fallback path handles it on first parseSerial */ }
}

/** Serialized wrapper around `mailparser.simpleParser` running in a worker
 *  thread. `priority` defaults to "foreground" — only sync/background
 *  callers should pass "background" so user clicks aren't stuck behind a
 *  back-fill. If the worker can't be spawned (rare), falls back to an
 *  in-main-thread serial pump so the system still functions. */
export async function parseSerial(
    source: Source,
    priority: "foreground" | "background" = "foreground",
): Promise<ParsedMail> {
    return new Promise<ParsedMail>((resolve, reject) => {
        const entry: Pending = { id: _nextId++, source, resolve, reject };
        if (priority === "foreground") _head.push(entry);
        else _tail.push(entry);
        // Try the worker first; fall back transparently if the spawn fails.
        try {
            getWorker();
            pumpViaWorker();
        } catch (e: unknown) {
            console.error(`  [parse-serial] worker unavailable, falling back to main-thread parse: ${e instanceof Error ? e.message : String(e)}`);
            pumpInMain();
        }
    });
}
