/**
 * JSON-RPC dispatcher for MailxService.
 * Maps mailxapi action names to MailxService method calls.
 * Used by msger's bidirectional IPC (Phase 3) and can also
 * serve as a stdio JSON-RPC interface for testing.
 */
import { readFileSync } from "node:fs";
import { join } from "node:path";
const rootPkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "..", "package.json"), "utf-8"));
// ── Debug eval return channel ──
// The `--debug-server` /api/eval endpoint pushes a `debugEval` event to the
// main window; app.ts evaluates the code and answers via the
// `debugEvalResult` action below. bin/mailx.ts registers this sink to
// correlate answers back to the waiting HTTP request. Dev tooling only —
// nothing fires unless --debug-server pushed an eval event.
let debugEvalSink = null;
export function setDebugEvalSink(fn) {
    debugEvalSink = fn;
}
/** Dispatch an incoming mailxapi call to the appropriate MailxService method */
export async function dispatch(svc, req) {
    const { _action, _cbid, ...params } = req;
    try {
        const result = await dispatchAction(svc, _action, params);
        return { _cbid, result };
    }
    catch (e) {
        return { _cbid, error: formatRpcError(e) };
    }
}
/** Turn any thrown value into a useful one-line message for the UI.
 *  Critically, unwraps AggregateError — Node throws it for a failed
 *  connection that tried multiple addresses (IPv6 + IPv4), and its `.message`
 *  is the useless bare string "AggregateError" while the real reasons
 *  (ECONNREFUSED / ETIMEDOUT / connection-limit) live in `.errors`. Surfacing
 *  those turns "Failed: AggregateError" into "Failed: couldn't connect —
 *  ETIMEDOUT" (Bob 2026-06-12). Also appends `.code` and the `.cause` chain. */
function formatRpcError(e) {
    if (e == null)
        return "Unknown error";
    let msg = (typeof e === "string") ? e : (e.message || "");
    if (Array.isArray(e?.errors) && e.errors.length) {
        const inner = e.errors
            .map((x) => (x?.message || x?.code || String(x)))
            .filter(Boolean)
            .join("; ");
        msg = (msg && msg !== "AggregateError") ? `${msg}: ${inner}` : inner;
    }
    if (!msg)
        msg = String(e);
    if (e?.code && !msg.includes(e.code))
        msg += ` (${e.code})`;
    if (e?.cause) {
        const c = e.cause.message || e.cause.code || String(e.cause);
        if (c && !msg.includes(c))
            msg += ` ← ${c}`;
    }
    return msg;
}
async function dispatchAction(svc, action, p) {
    switch (action) {
        // Accounts
        case "getAccounts":
            return svc.getAccounts();
        case "getFolders":
            return svc.getFolders(p.accountId);
        // Messages
        case "getMessages":
            return svc.getMessages(p.accountId, p.folderId, p.page, p.pageSize, p.sort, p.sortDir, p.search, p.flaggedOnly, p.dateBasis);
        case "getUnifiedInbox":
            return svc.getUnifiedInbox(p.page || 1, p.pageSize || 50, !!p.flaggedOnly, p.dateBasis);
        case "getMessage":
            return svc.getMessage(p.accountId, p.uid, p.allowRemote, p.folderId);
        // Flags & actions
        case "updateFlags":
            await svc.updateFlags(p.accountId, p.uid, p.flags, p.folderId);
            return { ok: true };
        case "deleteMessage":
            await svc.deleteMessage(p.accountId, p.uid, p.folderId);
            return { ok: true };
        case "deleteMessages":
            await svc.deleteMessages(p.accountId, p.uids, p.folderIds);
            return { ok: true, count: p.uids.length };
        case "undeleteMessage":
            await svc.undeleteMessage(p.accountId, p.uid, p.folderId);
            return { ok: true };
        case "moveMessage":
            await svc.moveMessage(p.accountId, p.uid, p.targetFolderId, p.targetAccountId);
            return { ok: true };
        case "moveMessages":
            await svc.moveMessages(p.accountId, p.uids, p.targetFolderId, p.folderIds);
            return { ok: true, count: p.uids.length };
        case "copyMessages":
            await svc.copyMessages(p.accountId, p.uids, p.folderIds, p.targetFolderId);
            return { ok: true, count: p.uids.length };
        case "markAsSpamMessages":
            return await svc.markAsSpamMessages(p.accountId, p.uids, p.folderIds);
        // Folders
        case "markFolderRead":
            svc.markFolderRead(p.folderId);
            return { ok: true };
        case "createFolder":
            await svc.createFolder(p.accountId, p.parentPath, p.name);
            return { ok: true };
        case "renameFolder":
            await svc.renameFolder(p.accountId, p.folderId, p.newName, p.newParentId);
            return { ok: true };
        case "deleteFolder":
            await svc.deleteFolder(p.accountId, p.folderId);
            return { ok: true };
        case "moveFolderToTrash":
            await svc.moveFolderToTrash(p.accountId, p.folderId);
            return { ok: true };
        case "emptyFolder":
            await svc.emptyFolder(p.accountId, p.folderId);
            return { ok: true };
        // Compose. sendMessage validates synchronously (fast — regex check on
        // recipient strings), then writes to disk synchronously inside the
        // service. Validation errors come back immediately to the user; once
        // we've ACK'd, the message is durable on disk and the worker drains it.
        // The whole call should be sub-100ms; if it ever isn't, the [send]
        // timing log shows which step ate the budget.
        case "sendMessage":
            await svc.send(p);
            return { ok: true };
        case "saveDraft":
            return svc.saveDraft(p.accountId, p.subject, p.bodyHtml, p.bodyText, p.to, p.cc, p.previousDraftUid, p.draftId, p.bcc);
        case "deleteDraft":
            await svc.deleteDraft(p.accountId, p.draftUid, p.draftId);
            return { ok: true };
        // Sync
        case "syncAll":
            await svc.syncAll();
            return { ok: true };
        case "syncAccount":
            await svc.syncAccount(p.accountId);
            return { ok: true };
        case "syncFolderNow":
            await svc.syncFolderNow(p.accountId, p.folderId);
            return { ok: true };
        case "getSyncPending":
            return svc.getSyncPending();
        case "getDiagnostics":
            return svc.getDiagnostics();
        case "getPrimaryAccount":
            return svc.getPrimaryAccount(p?.feature);
        // Calendar / Tasks: two-way cache. Reads return local immediately
        // and kick a background refresh that emits updates when done.
        case "getCalendarEvents":
            return await svc.getCalendarEvents(p.fromMs, p.toMs);
        case "getCalendars":
            return await svc.getCalendars();
        case "createCalendarEvent":
            return { uuid: await svc.createCalendarEventLocal(p) };
        case "updateCalendarEvent":
            await svc.updateCalendarEventLocal(p.uuid, p.patch);
            return { ok: true };
        case "deleteCalendarEvent":
            await svc.deleteCalendarEventLocal(p.uuid);
            return { ok: true };
        case "getTasks":
            return await svc.getTasks(!!p.includeCompleted);
        case "createTask":
            return { uuid: await svc.createTaskLocal(p) };
        case "updateTask":
            await svc.updateTaskLocal(p.uuid, p.patch);
            return { ok: true };
        case "deleteTask":
            await svc.deleteTaskLocal(p.uuid);
            return { ok: true };
        case "drainStoreSync":
            await svc.drainStoreSync();
            return { ok: true };
        case "recordSpamReport":
            return await svc.recordSpamReport(p.accountId, p.uid, p.folderId);
        case "getOutboxStatus":
            return svc.getOutboxStatus();
        case "listQueuedOutgoing":
            return svc.listQueuedOutgoing();
        case "cancelQueuedOutgoing":
            return svc.cancelQueuedOutgoing(p.path);
        case "reauthenticate":
            return { ok: await svc.reauthenticate(p.accountId) };
        case "reauthGoogleScopes":
            return svc.reauthGoogleScopes();
        // Search & contacts
        case "searchMessages":
            return svc.search(p.query, p.page, p.pageSize, p.scope, p.accountId, p.folderId, !!p.includeTrashSpam);
        case "cancelServerSearch":
            svc.cancelServerSearch();
            return { ok: true };
        case "searchContacts":
            return svc.searchContacts(p.query);
        case "hasCcHistoryTo":
            return { hasCc: svc.hasCcHistoryTo(p.email) };
        case "hasBccHistoryTo":
            return { hasBcc: svc.hasBccHistoryTo(p.email) };
        case "addContact":
            return { ok: svc.addContact(p.name, p.email) };
        case "listContacts":
            return svc.listContacts(p.query || "", p.page || 1, p.pageSize || 100);
        case "upsertContact":
            return svc.upsertContact(p.name, p.email);
        case "deleteContact":
            return svc.deleteContact(p.email);
        case "addPreferredContact":
            await svc.addPreferredContact({ name: p.name, email: p.email, source: p.source, organization: p.organization });
            return { ok: true };
        case "addToDenylist":
            await svc.addToDenylist(p.email);
            return { ok: true };
        case "getPriorityLists":
            return svc.getPriorityLists();
        case "setPrioritySender":
            await svc.setPrioritySender(p.email, !!p.value, p.name);
            return { ok: true };
        case "setPriorityDomain":
            await svc.setPriorityDomain(p.domain, !!p.value);
            return { ok: true };
        case "loadContactsConfig":
            return { result: await svc.loadContactsConfig() };
        case "openLocalPath":
            return await svc.openLocalPath(p.which);
        case "openInTextEditor":
            return await svc.openInTextEditor(p.path);
        case "getThreadMessages":
            return svc.getThreadMessages(p.accountId, p.threadId);
        case "readJsoncFile":
            return { content: await svc.readJsoncFile(p.name) };
        case "writeJsoncFile":
            await svc.writeJsoncFile(p.name, p.content);
            return { ok: true };
        case "formatJsonc":
            return { content: await svc.formatJsonc(p.content) };
        case "readConfigHelp":
            return { content: await svc.readConfigHelp(p.name) };
        case "unsubscribeOneClick":
            return await svc.unsubscribeOneClick(p.url);
        case "openInWord":
            return await svc.openInWord(p.editId, p.html);
        case "closeWordEdit":
            return await svc.closeWordEdit(p.editId);
        case "showReminderPopup":
            return await svc.showReminderPopup(p);
        case "consumePendingMailto":
            return svc.consumePendingMailto();
        // Client-side tracing — lets webview / iframe code ship events to the
        // Node log so a "compose→send→vanished" report can be diagnosed without
        // opening devtools. Every call shows up as `[client] <tag> <data>` in
        // the main log. Keep it on the top of the switch so it's cheap + first
        // to dispatch.
        // Answer leg of the --debug-server /api/eval round trip (see
        // setDebugEvalSink above).
        case "debugEvalResult":
            debugEvalSink?.(p.id, p.result, p.error);
            return { ok: true };
        case "logClientEvent": {
            // 2026-05-27: bumped truncation from 400 → 2000 chars. The new
            // installConsoleCapture routes console.error and window.error
            // through this same channel, and 400 chars truncated stacks to
            // ~3 frames — too short to diagnose the unhandled rejection that
            // was leaving the message list empty.
            const tag = p.tag || "?";
            // Errors deserve a louder log level than info traces.
            const isErr = tag === "console.error" || tag === "window.error" || tag === "window.unhandledrejection";
            const fn = isErr ? console.error : console.log;
            fn(`  [client] ${tag}${p.data ? " " + JSON.stringify(p.data).slice(0, 2000) : ""}`);
            return { ok: true };
        }
        // Settings
        case "getSettings":
            return svc.getSettings();
        case "saveSettingsData":
            await svc.saveSettings(p);
            return { ok: true };
        case "allowRemoteContent":
            // Was missing `await` — the IPC ack fired before saveAllowlist
            // committed, so the immediate getMessage(allowRemote=true) ran
            // against a stale list. Functionally OK because that follow-up
            // call passes allowRemote=true explicitly, but a refresh /
            // background poll could re-block until the write landed.
            await svc.allowRemoteContent(p.type, p.value);
            return { ok: true };
        case "getUserDict":
            return await svc.getUserDict();
        case "addUserDictWord":
            return await svc.addUserDictWord(p.word);
        case "addUserDictWords":
            return await svc.addUserDictWords(p.words);
        case "removeUserDictWord":
            return await svc.removeUserDictWord(p.word);
        case "flagSenderOrDomain":
            return await svc.flagSenderOrDomain(p.type, p.value);
        case "getVersion": {
            try {
                const settings = svc.getSettings();
                const storage = svc.getStorageInfo();
                return { version: rootPkg.version || "dev", theme: settings.ui?.theme || "system", storage };
            }
            catch (e) {
                console.error(`  [jsonrpc] getVersion error: ${e.message}`);
                return { version: rootPkg.version || "dev", theme: "system", storage: {} };
            }
        }
        // Autocomplete
        case "autocomplete":
            return svc.autocomplete(p);
        case "getAutocompleteSettings":
            return svc.getAutocompleteSettings();
        case "saveAutocompleteSettings":
            svc.saveAutocompleteSettings(p);
            return { ok: true };
        // AI transform (translate / proofread / summarize) — gated by per-feature
        // toggles in autocomplete settings, both default false.
        case "aiTransform":
            return svc.aiTransform(p);
        // Attachments
        case "getAttachment": {
            const att = await svc.getAttachment(p.accountId, p.uid, p.attachmentId, p.folderId);
            // att.content is USUALLY a Buffer, but for some parts (a 7bit/quoted-
            // printable text/calendar invite — `invite.ics`) mailparser hands back
            // a STRING. `someString.toString("base64")` IGNORES the argument and
            // returns the string verbatim, so the client got raw .ics text and
            // atob() threw "string to be decoded is not correctly encoded" (Bob
            // 2026-06-18, "failing to save invite"). Normalise to a Buffer first so
            // the wire payload is always real base64. Buffer.from() handles string
            // (utf8), Uint8Array, and Buffer.
            const buf = Buffer.isBuffer(att.content) ? att.content : Buffer.from(att.content ?? "");
            return { content: buf.toString("base64"), contentType: att.contentType, filename: att.filename };
        }
        case "openAttachment":
            return await svc.openAttachment(p.accountId, p.uid, p.attachmentId, p.folderId, p.filename);
        case "getMessageSource":
            return await svc.getMessageSource(p.accountId, p.uid, p.folderId);
        case "openExternal":
            return await svc.openExternal(p.url);
        case "popoutWindow":
            return await svc.popoutWindow(p.accountId, p.uid, p.folderId, p.subject);
        case "registerMailto":
            return await svc.registerMailto();
        case "grammarLint":
            return await svc.grammarLint(p.blocks);
        case "popoutCompose":
            return await svc.popoutCompose(p.init);
        case "consumePopoutComposeInit":
            return svc.consumePopoutComposeInit(p.id);
        case "closeComposePopout":
            return svc.closeComposePopout(p.id);
        case "fetchImageAsDataUri":
            return await svc.fetchImageAsDataUri(p.url);
        case "popoutMainWindow":
            return await svc.popoutMainWindow(p.accountId ? { accountId: p.accountId, folderId: p.folderId, name: p.name } : undefined);
        case "getReminderSound":
            return await svc.getReminderSound();
        // Setup & Repair
        case "setupAccount":
            return svc.setupAccount(p.name, p.email, p.password);
        case "repairAccounts":
            return svc.repairAccounts();
        default:
            throw new Error(`Unknown action: ${action}`);
    }
}
//# sourceMappingURL=jsonrpc.js.map