/**
 * DB factor — Node worker thread.
 *
 * Owns the SQLite handle and FileMessageStore. Serves the UI-read surface
 * (getFolders, getMessages, getUnifiedInbox, getMessage, searchMessages,
 * searchContacts, getThreadMessages) over `mailx-bus`. Subscribes to write
 * events from the sync factor (Phase 2) and emits `db:messageAdded` /
 * `db:messageRemoved` / `db:flagChanged` so the UI can update incrementally
 * without re-asking.
 *
 * **Why a worker.** Until now every UI click had to traverse the same Node
 * event loop the sync code runs on. A 14-minute synchronous freeze in the
 * sync path (Dovecot SELECT hang, Prefirst.OIA backfill loop) starved every
 * UI read. The IPC pipe handler couldn't even be scheduled. From the user's
 * perspective: Sent shows empty, message bodies never load, autocomplete
 * dies, drafts can't save. All of those have one cause: the read path
 * shares an event loop with the write/sync path.
 *
 * In this worker, the read path is on its own thread. Sync can freeze for
 * an hour and the user's click still gets a 5 ms answer from local SQLite.
 *
 * **Topology**. Main thread spawns this worker once, holds a `WorkerBus` over
 * the `parentPort`. The worker also holds a `WorkerBus` over `parentPort` and
 * registers its handlers. Two ends of the same channel, mirror APIs.
 *
 * **DB connection**. `better-sqlite3` is in-process synchronous, so the
 * worker opens its OWN connection to the same `.db` file. WAL mode (set in
 * `MailxDB`) makes concurrent readers safe; writes still happen on the main
 * thread for Phase 1 and the worker sees committed rows immediately.
 * Phase 2 moves writes to the sync worker, which routes through this worker
 * for any UI-visible mutation so the LRU/cache here can update in lock-step.
 *
 * **No mailparser here.** Body parsing is the parse-worker's job. This
 * worker reads the .eml bytes off disk and delegates parsing via
 * `parseSerial` (which already postMessages to its own worker). Two workers
 * either side of this thread; the read coordinator is purely an indexer.
 */

import { parentPort } from "node:worker_threads";
import { WorkerBus, type MessagePortLike } from "@bobfrankston/mailx-bus";
import { MailxDB, FileMessageStore, Store } from "@bobfrankston/mailx-store";
import type { MessageQuery } from "@bobfrankston/mailx-types";

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

interface InitMessage {
    kind: "init";
    dbPath: string;
    storePath: string;
}

// Wait for the init message before constructing the DB — the main thread
// owns the resolved paths and passes them in.
let store: Store | null = null;
let initialized = false;

parentPort.once("message", async (msg: InitMessage) => {
    if (msg?.kind !== "init") {
        parentPort!.postMessage({ kind: "init-error", error: `expected "init" message, got ${JSON.stringify(msg)}` });
        return;
    }
    try {
        // Read-only handle: the main thread owns all writes/migrations and has
        // already run them before spawning us. query_only forbids any write
        // here, and WAL lets us read committed rows the instant the writer
        // commits. See MailxDB readOnly branch.
        const db = new MailxDB(msg.dbPath, { readOnly: true });
        const bodyStore = new FileMessageStore(msg.storePath);
        store = new Store(db, bodyStore);
        initialized = true;
        // Now that the store is up, register handlers and ack init.
        registerHandlers(store);
        parentPort!.postMessage({ kind: "init-ok" });
    } catch (e: any) {
        parentPort!.postMessage({ kind: "init-error", error: e?.message || String(e) });
    }
});

function registerHandlers(s: Store): void {
    // The bus speaks over `parentPort`. `WorkerBus` reads the same channel
    // we just consumed `init` from — that's fine, `once()` removes the
    // single-shot listener and the bus's own `on("message")` takes over.
    const bus = new WorkerBus(parentPort as unknown as MessagePortLike);

    // ── Folders ─────────────────────────────────────────────────────
    bus.register<{ accountId: string }, any[]>(
        "db:getFolders",
        ({ accountId }) => s.getFolders(accountId),
    );

    // ── Message list ────────────────────────────────────────────────
    bus.register<MessageQuery, any>(
        "db:getMessages",
        (q) => s.getMessages(q),
    );

    bus.register<{ page?: number; pageSize?: number; flaggedOnly?: boolean }, any>(
        "db:getUnifiedInbox",
        ({ page = 1, pageSize = 50, flaggedOnly = false }) => s.getUnifiedInbox(page, pageSize, flaggedOnly),
    );

    bus.register<
        { query: string; page?: number; pageSize?: number; accountId?: string; folderId?: number; includeTrashSpam?: boolean },
        any
    >(
        "db:searchMessages",
        ({ query, page = 1, pageSize = 50, accountId, folderId, includeTrashSpam = false }) =>
            s.searchMessages(query, page, pageSize, accountId, folderId, includeTrashSpam),
    );

    // ── Single message (envelope + body) ────────────────────────────
    bus.register<{ accountId: string; uid: number; allowRemote?: boolean; folderId?: number }, any>(
        "db:getMessage",
        ({ accountId, uid, allowRemote = false, folderId }) =>
            s.getMessage(accountId, uid, allowRemote, folderId),
    );

    bus.register<{ accountId: string; threadId: string }, any[]>(
        "db:getThreadMessages",
        ({ accountId, threadId }) => (s as any).getThreadMessages?.(accountId, threadId) ?? [],
    );

    // ── Contacts read (lookups) ─────────────────────────────────────
    bus.register<{ query: string; limit?: number }, any[]>(
        "db:searchContacts",
        ({ query, limit }) => (s as any).searchContacts?.(query, limit) ?? [],
    );

    // ── Accounts (read-only DB shape) ───────────────────────────────
    bus.register<void, any[]>(
        "db:getAccounts",
        () => s.getAccounts(),
    );

    // ── Config-cache invalidation ──────────────────────────────────
    // Main thread receives `configChanged` events (allowlist.jsonc etc.)
    // and publishes them; the worker invalidates its in-Store caches
    // so the next read sees the fresh JSONC.
    bus.subscribe<{ filename?: string }>("config:changed", () => {
        s.invalidateConfigCaches();
    });
}

// Defensive: if the bus is ever asked something before init completes, the
// caller gets a clear error rather than a hang. Mirrors the init-error path.
parentPort.on("message", (msg: any) => {
    if (!initialized && msg && typeof msg === "object" && msg.kind === "request") {
        parentPort!.postMessage({
            kind: "reply",
            id: msg.id,
            error: "db-worker: not initialized — main thread must send {kind:'init'} first",
        });
    }
});
