/**
 * API client — all operations go through the IPC bridge (mailxapi).
 * mailxapi is injected by the launcher (msger on desktop, MAUI on Android).
 */

declare const mailxapi: any;

function getIpc(): any {
    if (typeof mailxapi !== "undefined" && mailxapi?.isApp) return mailxapi;
    if ((window as any).opener?.mailxapi?.isApp) return (window as any).opener.mailxapi;
    // Compose iframe — check parent
    if ((window as any).parent?.mailxapi?.isApp) return (window as any).parent.mailxapi;
    return null;
}

/** Build a proxy bridge that forwards every method call to the parent window
 *  via postMessage. Used when the compose iframe's own attempt to reach
 *  msger's IPC silently drops messages — empirically, `sendMessage` and
 *  `saveDraft` both hit this (user-visible: "Sending…" spinner forever;
 *  "Draft save failed: mailxapi timeout"). The main window's bridge is
 *  provably fine, so the iframe routes through it. */
function buildRelayBridge(): any {
    const pending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void; timer: any }>();
    window.addEventListener("message", (ev: MessageEvent) => {
        if (!ev.data || ev.data.type !== "mailx-ipc-result" || !ev.data.id) return;
        const entry = pending.get(ev.data.id);
        if (!entry) return;
        pending.delete(ev.data.id);
        clearTimeout(entry.timer);
        if (ev.data.ok) entry.resolve(ev.data.result);
        else entry.reject(new Error(ev.data.error || "parent-relay ipc error"));
    });
    const call = (method: string, args: any[]): Promise<any> => {
        const id = `ipc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                pending.delete(id);
                reject(new Error(`parent-relay timeout: ${method}`));
            }, 120000);
            pending.set(id, { resolve, reject, timer });
            try {
                (window.parent as any).postMessage({ type: "mailx-ipc", id, method, args }, "*");
            } catch (e) {
                clearTimeout(timer);
                pending.delete(id);
                reject(e);
            }
        });
    };
    // Proxy: any property access returns a function that forwards to parent.
    // `isApp` / `platform` / other non-function reads return sensible defaults
    // so existing getIpc-style checks still work.
    return new Proxy({}, {
        get(_t, prop: string) {
            if (prop === "isApp") return true;
            if (prop === "platform") return (window.parent as any)?.mailxapi?.platform || "webview2";
            if (prop === "onEvent") {
                // Event subscription can't be relayed simply — iframes that need
                // events are rare. Fall back to direct parent bridge for onEvent
                // since the subscription path doesn't hit the broken send path.
                return (handler: any) => (window.parent as any)?.mailxapi?.onEvent?.(handler);
            }
            return (...args: any[]) => call(prop, args);
        }
    });
}

let cachedRelayBridge: any = null;
function ipc(): any {
    // Direct bridge is fine for the top window (main mailx app). The iframe
    // (compose) can't trust its own bridge resolution because msger-routed
    // sendMessage / saveDraft IPCs disappear without trace. So when we're in
    // a child frame with a parent bridge, go through the parent.
    const inIframe = window.parent && window.parent !== window;
    if (inIframe && (window.parent as any)?.mailxapi?.isApp) {
        if (!cachedRelayBridge) cachedRelayBridge = buildRelayBridge();
        return cachedRelayBridge;
    }
    const bridge = getIpc();
    if (!bridge) throw new Error("IPC bridge not available");
    return bridge;
}

// ── Abort controller for message-list requests ──

let messageListAbort: AbortController | null = null;

export function abortMessageListRequests(): void {
    if (messageListAbort) {
        messageListAbort.abort();
        messageListAbort = null;
    }
}

// ── API Methods ──

export function getAccounts() {
    return ipc().getAccounts();
}

export function getFolders(accountId: string) {
    return ipc().getFolders(accountId);
}

export function getMessages(accountId: string, folderId: number, page = 1, pageSize = 50, flaggedOnly = false, sort?: string, sortDir?: string, dateBasis?: "sent" | "received") {
    abortMessageListRequests();
    return ipc().getMessages(accountId, folderId, page, pageSize, sort, sortDir, undefined, flaggedOnly, dateBasis);
}

export function getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis?: "sent" | "received") {
    abortMessageListRequests();
    return ipc().getUnifiedInbox(page, pageSize, flaggedOnly, dateBasis);
}

export function searchMessages(query: string, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0, includeTrashSpam = false) {
    return ipc().searchMessages(query, page, pageSize, scope, accountId, folderId, includeTrashSpam);
}

/** Abort any in-flight server (IMAP) search. Fire-and-forget — the daemon
 *  bumps a generation counter its per-folder loop checks between batches. */
export function cancelServerSearch() {
    return ipc().cancelServerSearch?.();
}

export function getMessage(accountId: string, uid: number, allowRemote = false, folderId?: number) {
    return ipc().getMessage(accountId, uid, allowRemote, folderId);
}

export function updateFlags(accountId: string, uid: number, flags: string[], folderId?: number) {
    return ipc().updateFlags(accountId, uid, flags, folderId);
}

export function triggerSync() {
    return ipc().syncAll();
}

export function syncAccount(accountId: string) {
    return ipc().syncAccount(accountId);
}

/** Sync one folder on demand (lazy-folder-sync: fired when the user opens a
 *  folder so it's fresh without the app sweeping all folders on a tight timer). */
export function syncFolderNow(accountId: string, folderId: number) {
    return ipc().syncFolderNow?.(accountId, folderId);
}

export function reauthenticate(accountId: string) {
    return ipc().reauthenticate(accountId);
}

export function reauthGoogleScopes(): Promise<{ cleared: number }> {
    return (ipc() as any).reauthGoogleScopes();
}

export function getSyncPending() {
    return ipc().getSyncPending();
}

export function getDiagnostics(): Promise<any> {
    return ipc().getDiagnostics?.() ?? Promise.resolve([]);
}

/** Account that supplies `feature` data (calendar / tasks / contacts).
 *  Resolution: per-feature primary flag → catch-all `primary` → first account.
 *  Pass e.g. "calendar" to honor `primaryCalendar:true` overrides; omit for
 *  back-compat single-flag behavior. */
export function getPrimaryAccount(feature?: string): Promise<any> {
    return ipc().getPrimaryAccount?.(feature) ?? Promise.resolve(null);
}

// Calendar / Tasks: two-way cache. Reads return local-cached rows; writes
// commit locally and queue a push to Google. Service layer handles drain.
export function getCalendarEvents(fromMs: number, toMs: number): Promise<any[]> {
    return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
}
/** List the user's selected Google calendars (id, name, color, primary) so
 *  the sidebar can render a checkbox + icon per calendar. */
export function getCalendars(): Promise<Array<{ id: string; name: string; color: string; primary: boolean }>> {
    return ipc().getCalendars?.() ?? Promise.resolve([]);
}
export function createCalendarEvent(ev: {
    title: string; startMs: number; endMs: number; allDay?: boolean;
    location?: string; notes?: string;
}): Promise<{ uuid: string }> {
    return ipc().createCalendarEvent?.(ev);
}
export function updateCalendarEvent(uuid: string, patch: any): Promise<{ ok: boolean }> {
    return ipc().updateCalendarEvent?.(uuid, patch);
}
export function deleteCalendarEvent(uuid: string): Promise<{ ok: boolean }> {
    return ipc().deleteCalendarEvent?.(uuid);
}
export function getTasks(includeCompleted = false): Promise<any[]> {
    return ipc().getTasks?.(includeCompleted) ?? Promise.resolve([]);
}
export function createTask(t: { title: string; notes?: string; dueMs?: number }): Promise<{ uuid: string }> {
    return ipc().createTask?.(t);
}
export function updateTask(uuid: string, patch: any): Promise<{ ok: boolean }> {
    return ipc().updateTask?.(uuid, patch);
}
export function deleteTask(uuid: string): Promise<{ ok: boolean }> {
    return ipc().deleteTask?.(uuid);
}
export function drainStoreSync(): Promise<{ ok: boolean }> {
    return ipc().drainStoreSync?.();
}

/** Report the currently-viewed message as spam → appends a row to
 *  `~/.mailx/spam.csv`. Placeholder: no folder move, no flag change, no
 *  auto-delete. Training data for a smarter pass later. */
export function recordSpamReport(accountId: string, uid: number, folderId: number): Promise<{ ok: boolean }> {
    return ipc().recordSpamReport?.(accountId, uid, folderId);
}

export function getOutboxStatus() {
    return (ipc() as any).getOutboxStatus();
}

export function listQueuedOutgoing() {
    return (ipc() as any).listQueuedOutgoing();
}

export function cancelQueuedOutgoing(p: string) {
    return (ipc() as any).cancelQueuedOutgoing(p);
}

export function searchContacts(query: string) {
    return ipc().searchContacts(query);
}

export function hasCcHistoryTo(email: string): Promise<{ hasCc: boolean }> {
    return (ipc() as any).hasCcHistoryTo(email);
}

export function hasBccHistoryTo(email: string): Promise<{ hasBcc: boolean }> {
    return (ipc() as any).hasBccHistoryTo(email);
}

export function listContacts(query: string, page = 1, pageSize = 100) {
    return (ipc() as any).listContacts(query, page, pageSize);
}

export function upsertContact(name: string, email: string, fields?: Record<string, string>) {
    return (ipc() as any).upsertContact(name, email, fields);
}

export function deleteContact(email: string) {
    return (ipc() as any).deleteContact(email);
}

export function addPreferredContact(entry: { name: string; email: string; source?: string; organization?: string }) {
    return (ipc() as any).addPreferredContact(entry.name, entry.email, entry.source, entry.organization);
}

export function getPriorityLists(): Promise<{ senders: string[]; domains: string[] }> {
    return (ipc() as any).getPriorityLists();
}

export function setPrioritySender(email: string, value: boolean, name?: string): Promise<{ ok: boolean }> {
    return (ipc() as any).setPrioritySender(email, value, name);
}

export function setPriorityDomain(domain: string, value: boolean): Promise<{ ok: boolean }> {
    return (ipc() as any).setPriorityDomain(domain, value);
}

export function addToDenylist(email: string) {
    return (ipc() as any).addToDenylist(email);
}

export function openLocalPath(which: "config" | "log") {
    return (ipc() as any).openLocalPath(which);
}
/** Open an absolute file path (under ~/.rmfmail) in the OS default
 *  *text* editor — Notepad on Windows, TextEdit on Mac, $EDITOR or
 *  xdg-open(.txt) on Linux. Distinct from the file's default app,
 *  which for .eml is usually Outlook / a mail client. */
export function openInTextEditor(path: string): Promise<{ ok: boolean; opener: string; reason?: string }> {
    return (ipc() as any).openInTextEditor?.(path) ?? Promise.resolve({ ok: false, opener: "none", reason: "no host" });
}

export function allowRemoteContent(type: string, value: string) {
    return ipc().allowRemoteContent(type, value);
}
export function getUserDict(): Promise<string[]> {
    return (ipc() as any).getUserDict?.() ?? Promise.resolve([]);
}
export function addUserDictWord(word: string): Promise<string[]> {
    return (ipc() as any).addUserDictWord?.(word) ?? Promise.resolve([]);
}
export function addUserDictWords(words: string[]): Promise<string[]> {
    return (ipc() as any).addUserDictWords?.(words) ?? Promise.resolve([]);
}
export function removeUserDictWord(word: string): Promise<string[]> {
    return (ipc() as any).removeUserDictWord?.(word) ?? Promise.resolve([]);
}
export function flagSenderOrDomain(type: "sender" | "domain", value: string): Promise<{ flagged: boolean }> {
    return (ipc() as any).flagSenderOrDomain?.(type, value) ?? Promise.resolve({ flagged: false });
}

export function deleteMessage(accountId: string, uid: number, folderId?: number) {
    return ipc().deleteMessage?.(accountId, uid, folderId);
}

export function deleteMessages(accountId: string, uids: number[], folderIds?: number[]) {
    if (uids.length === 1) return deleteMessage(accountId, uids[0], folderIds?.[0]);
    return ipc().deleteMessages?.(accountId, uids, folderIds);
}

export function moveMessages(accountId: string, uids: number[], targetFolderId: number, folderIds?: number[], targetAccountId?: string) {
    // folderIds[] disambiguates each uid to its source folder — IMAP UIDs are
    // per-folder, so a bare (account, uid) lookup can move the wrong message.
    // The single-uid shortcut is only safe when there is no such
    // disambiguation to throw away: service.moveMessage resolves the row by
    // (account, uid) alone, so taking that path WITH folderIds in hand would
    // discard the one fact that makes the move unambiguous.
    if (uids.length === 1 && (targetAccountId || !folderIds?.length)) {
        return moveMessage(accountId, uids[0], targetFolderId, targetAccountId);
    }
    return ipc().moveMessages?.(accountId, uids, targetFolderId, folderIds);
}

export function markAsSpamMessages(accountId: string, uids: number[], folderIds?: number[]): Promise<{ targetFolderId: number; moved: number }> {
    return ipc().markAsSpamMessages?.(accountId, uids, folderIds);
}

/** Copy messages to a target folder (they stay in the source too). Carries
 *  per-message source folderIds index-aligned with uids, same as
 *  deleteMessages, so the daemon copies from the exact (folder, uid). */
export function copyMessages(accountId: string, uids: number[], folderIds: number[] | undefined, targetFolderId: number) {
    return ipc().copyMessages?.(accountId, uids, folderIds, targetFolderId);
}

export function undeleteMessage(accountId: string, uid: number, folderId: number) {
    return ipc().undeleteMessage?.(accountId, uid, folderId);
}

export function moveMessage(accountId: string, uid: number, targetFolderId: number, targetAccountId?: string) {
    return ipc().moveMessage?.(accountId, uid, targetFolderId, targetAccountId);
}

export function restartServer() {
    return ipc().restart?.();
}

export function markFolderRead(accountId: string, folderId: number) {
    return ipc().markFolderRead?.(accountId, folderId);
}

export function createFolder(accountId: string, parentPath: string, name: string) {
    return ipc().createFolder?.(accountId, parentPath, name);
}

/** Rename a folder in place, or reparent it when `newParentId` is given (drag a
 *  folder onto another). Both are a single server-side mailbox rename. */
export function renameFolder(accountId: string, folderId: number, newName: string, newParentId?: number) {
    return ipc().renameFolder?.(accountId, folderId, newName, newParentId);
}

export function deleteFolder(accountId: string, folderId: number) {
    return ipc().deleteFolder?.(accountId, folderId);
}

export function moveFolderToTrash(accountId: string, folderId: number) {
    return ipc().moveFolderToTrash?.(accountId, folderId);
}

export function emptyFolder(accountId: string, folderId: number) {
    return ipc().emptyFolder?.(accountId, folderId);
}

/** Ship a named event to the Node log as `[client] <tag> <data>`. Fire and
 *  forget — never awaits, never throws, never blocks the caller. Tries two
 *  paths so a broken primary channel can't swallow the trace:
 *    1. Direct bridge call (self / opener / parent mailxapi).
 *    2. parent.postMessage fallback — the main window listens and relays.
 *  The fallback matters because the whole point of tracing is to diagnose
 *  a broken iframe bridge; a single-path tracer that goes through that same
 *  bridge is useless in exactly the case we need it. */
/** Capture every console.log / console.warn / console.error / console.info
 *  / console.debug call AND every uncaught error + unhandledrejection, and
 *  route them through logClientEvent to the daemon log.
 *
 *  Why: debugging "the list is empty" or "preview but no summary" required
 *  asking the user to open devtools and read the console. That's slow and
 *  often the user has already navigated away by the time they look. With
 *  this hook everything the WebView console would show is also in
 *  rmfmail-YYYY-MM-DD.log — searchable, persistent, no devtools required.
 *
 *  Idempotent. Original console methods preserved on _origConsole so any
 *  internal logger (including logClientEvent's own bridge fallback) can
 *  still write to the real console without recursing through the hook. */
export function installConsoleCapture(): void {
    const g: any = globalThis as any;
    if (g.__mailxConsoleCaptureInstalled) return;
    g.__mailxConsoleCaptureInstalled = true;

    const orig = {
        log: console.log.bind(console),
        info: console.info.bind(console),
        warn: console.warn.bind(console),
        error: console.error.bind(console),
        debug: console.debug.bind(console),
    };
    g._origConsole = orig;

    const serialize = (v: any): any => {
        if (v == null) return v;
        const t = typeof v;
        if (t === "string" || t === "number" || t === "boolean") return v;
        if (v instanceof Error) return { __err: true, message: v.message, stack: v.stack };
        try {
            // Cap large objects so a circular ref or a giant DOM dump
            // doesn't OOM the IPC channel.
            const s = JSON.stringify(v);
            return s.length > 4096 ? s.slice(0, 4096) + "…[truncated]" : JSON.parse(s);
        } catch {
            try { return String(v); } catch { return "[unserializable]"; }
        }
    };

    const forward = (level: "warn" | "error", args: any[]): void => {
        try {
            // First arg often a format string; rest are values. Pass as array.
            logClientEvent(`console.${level}`, { args: args.map(serialize) });
        } catch { /* never throw from log capture */ }
    };

    // CRITICAL: only forward warn + error to the daemon log. The first cut
    // of this (1.1.178) forwarded console.log/info/debug too — but mailx logs
    // console.log on a hot path (per-message preview timing, sync ticks),
    // turning EVERY one into an IPC round-trip. That ~100x'd IPC traffic,
    // saturated the channel, and the getUnifiedInbox RESPONSE (sent by the
    // daemon in ~180ms) couldn't get processed client-side — the list hung on
    // "Loading…" forever (Bob 2026-05-28, "gave up"). warn/error are rare and
    // high-value; log/info/debug stay console-only. Explicit logClientEvent()
    // calls elsewhere are unaffected.
    console.log = (...args: any[]) => { orig.log(...args); };
    console.info = (...args: any[]) => { orig.info(...args); };
    console.debug = (...args: any[]) => { orig.debug(...args); };
    console.warn = (...args: any[]) => { forward("warn", args); orig.warn(...args); };
    console.error = (...args: any[]) => { forward("error", args); orig.error(...args); };

    // Uncaught errors — what stops renderMessages mid-execution and leaves
    // a blank list. Without this we'd see nothing in the daemon log.
    try {
        window.addEventListener("error", (e: ErrorEvent) => {
            try {
                logClientEvent("window.error", {
                    message: e.message,
                    filename: e.filename,
                    lineno: e.lineno,
                    colno: e.colno,
                    stack: e.error?.stack || null,
                });
            } catch { /* */ }
        });
        window.addEventListener("unhandledrejection", (e: PromiseRejectionEvent) => {
            try {
                const r: any = e.reason;
                const msg = r?.message || String(r);
                // Break the cascade: a `logClientEvent` IPC timeout is itself a
                // rejection; forwarding it spawns ANOTHER logClientEvent which
                // can also time out → a self-amplifying flood (Bob 2026-05-28
                // saw dozens/sec). Never forward tracing-channel rejections.
                if (/logClientEvent|mailxapi timeout/i.test(msg)) return;
                logClientEvent("window.unhandledrejection", {
                    message: msg,
                    stack: r?.stack || null,
                });
            } catch { /* */ }
        });
    } catch { /* */ }
}

export function logClientEvent(tag: string, data?: any): void {
    let delivered = false;
    try {
        const bridge = typeof (globalThis as any).mailxapi !== "undefined" && (globalThis as any).mailxapi?.isApp ? (globalThis as any).mailxapi
            : (window as any).opener?.mailxapi?.isApp ? (window as any).opener.mailxapi
            : (window as any).parent?.mailxapi?.isApp ? (window as any).parent.mailxapi
            : null;
        if (bridge?.logClientEvent) {
            // bridge.logClientEvent returns a callNode promise that REJECTS on
            // IPC timeout. Swallow it — an unhandled rejection here cascades
            // (the rejection gets logged → spawns another logClientEvent →
            // …). Fire-and-forget with a no-op catch.
            const p: any = bridge.logClientEvent(tag, data);
            if (p && typeof p.catch === "function") p.catch(() => { /* tracing is best-effort */ });
            delivered = true;
        }
    } catch { /* never throw from tracing */ }
    try {
        if (window.parent && window.parent !== window) {
            (window.parent as any).postMessage({ type: "mailx-trace", tag, data, bridged: delivered }, "*");
        }
    } catch { /* */ }
}

export function sendMessage(body: any) {
    return ipc().sendMessage?.(body);
}

/** Answer leg of the --debug-server /api/eval round trip (see the
 *  `debugEval` event handler in app.ts). */
export function debugEvalResult(id: string, result: any, error?: string) {
    return ipc().debugEvalResult?.(id, result, error);
}

export function saveDraft(body: any) {
    return ipc().saveDraft?.(body);
}

// ── Events ──

type EventHandler = (event: any) => void;
const eventHandlers: EventHandler[] = [];

export function onEvent(handler: EventHandler): () => void {
    eventHandlers.push(handler);
    return () => {
        const i = eventHandlers.indexOf(handler);
        if (i >= 0) eventHandlers.splice(i, 1);
    };
}

// ── Store-bus subscriptions (mirrors packages/mailx-store/bus.ts) ──
//
// The service forwards every storeBus event over IPC as `{ _event: "store",
// topic, kind, ... }`. Mirror the topic/wildcard semantics here so consumers
// subscribe by topic instead of filtering inside a single onEvent callback.
// Same shape as the server-side bus — that's the load-bearing property.

type StoreEvent = { topic: string; kind: string; [k: string]: any };
type StoreHandler = (event: StoreEvent) => void;
const storeSubs = new Map<string, Set<StoreHandler>>();

export function subscribeStore(topic: string, handler: StoreHandler): () => void {
    let set = storeSubs.get(topic);
    if (!set) { set = new Set(); storeSubs.set(topic, set); }
    set.add(handler);
    return () => {
        const s = storeSubs.get(topic);
        if (s) { s.delete(handler); if (s.size === 0) storeSubs.delete(topic); }
    };
}

function deliverStore(event: StoreEvent): void {
    const exact = storeSubs.get(event.topic);
    if (exact) for (const h of exact) { try { h(event); } catch (e) { console.error("[store-bus]", e); } }
    const wild = storeSubs.get("*");
    if (wild) for (const h of wild) { try { h(event); } catch (e) { console.error("[store-bus]", e); } }
}

export function connectEvents(): void {
    ipc().onEvent((event: any) => {
        if (event && event._event === "store") deliverStore(event as StoreEvent);
        for (const h of eventHandlers) h(event);
    });
}

// ── Autocomplete ──

export function autocomplete(body: { subject: string; to: string; bodyText: string; cursorOffset: number }, signal?: AbortSignal) {
    return ipc().autocomplete?.(body);
}

export function getAutocompleteSettings() {
    return ipc().getAutocompleteSettings?.();
}

export function saveAutocompleteSettings(settings: any) {
    return ipc().saveAutocompleteSettings?.(settings);
}

export function getVersion() {
    return ipc().getVersion();
}

export function getSettings() {
    return ipc().getSettings();
}

export function saveSettings(settings: any) {
    return ipc().saveSettingsData?.(settings);
}

export function repairAccounts() {
    return ipc().repairAccounts?.();
}

export function deleteDraft(accountId: string, draftUid: number, draftId?: string) {
    return ipc().deleteDraft?.(accountId, draftUid, draftId);
}

/** Compose-open probe: newer server copy of this draft (edited on another
 *  machine)? `null` when there isn't one, or the service doesn't support
 *  the check (Android/web) — compose then just keeps the local copy. */
export function checkDraftNewer(accountId: string, draftId: string, sinceUid: number): Promise<{ uid: number; folderId: number } | null> {
    return (ipc() as any).checkDraftNewer?.(accountId, draftId, sinceUid) ?? Promise.resolve(null);
}

export function addContact(name: string, email: string) {
    return ipc().addContact?.(name, email);
}

export function getThreadMessages(accountId: string, threadId: string) {
    return ipc().getThreadMessages?.(accountId, threadId);
}

export function readJsoncFile(name: string): Promise<{ content: string | null }> {
    return ipc().readJsoncFile?.(name);
}
export function getReminderSound(): Promise<{ mute?: boolean; dataBase64?: string; mime?: string }> {
    return ipc().getReminderSound?.();
}
export function writeJsoncFile(name: string, content: string): Promise<{ ok: boolean }> {
    return ipc().writeJsoncFile?.(name, content);
}
export function formatJsonc(content: string): Promise<{ content: string }> {
    return (ipc() as any).formatJsonc?.(content);
}
export function readConfigHelp(name: string): Promise<{ content: string }> {
    return ipc().readConfigHelp?.(name) ?? Promise.resolve({ content: "" });
}
export function unsubscribeOneClick(url: string): Promise<{ ok: boolean; status: number; statusText: string }> {
    return ipc().unsubscribeOneClick?.(url);
}
export function openInWord(editId: string, html: string, fromPopout = false): Promise<{ ok: boolean; path: string; opener: string; reused?: boolean; superseded?: boolean }> {
    return (ipc() as any).openInWord?.(editId, html, fromPopout) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
}
export function closeWordEdit(editId: string): Promise<void> {
    return (ipc() as any).closeWordEdit?.(editId) ?? Promise.resolve();
}

/** Show an OS-level always-on-top reminder popup via msger. Returns the
 *  label of the button the user clicked. Empty string when the host
 *  isn't available (browser fallback) — caller should fall back to an
 *  in-WebView popup in that case. */
export function showReminderPopup(opts: {
    title: string;
    html: string;
    buttons: string[];
    size?: { width: number; height: number };
    pos?: { x: number; y: number };
    /** Occurrence key registered daemon-side so closeReminderPopup can retract. */
    popupKey?: string;
}): Promise<{ button: string; form?: any; reason?: string }> {
    return (ipc() as any).showReminderPopup?.(opts) ?? Promise.resolve({ button: "", reason: "no host" });
}

/** Retract an open OS reminder popup (occurrence key) — no-op if it isn't
 *  open or the host doesn't support it. */
export function closeReminderPopup(key: string): Promise<{ ok: boolean }> {
    return (ipc() as any).closeReminderPopup?.(key) ?? Promise.resolve({ ok: false });
}

/** Cross-device reminder dismissed/snoozed state. `null` when the running
 *  service doesn't implement it (Android/web before parity) — the alarm
 *  poller then just stays per-device, exactly the old behavior. */
export function mergeReminderState(patch: {
    dismissed: Record<string, number>; snoozed: Record<string, number>;
}): Promise<{ dismissed: Record<string, number>; snoozed: Record<string, number> } | null> {
    return (ipc() as any).mergeReminderState?.(patch) ?? Promise.resolve(null);
}

/** Read + delete a pending mailto: drop file, if any (P115). Used at app
 *  startup so a `mailx --mailto <url>` that just spawned us doesn't lose
 *  its compose payload to the daemon-fires-before-app-registers race
 *  window. Subsequent live clicks arrive via the `openMailto` event. */
export function consumePendingMailto(): Promise<{
    to: string[]; cc: string[]; bcc: string[];
    subject: string; body: string; inReplyTo: string;
} | null> {
    return ipc().consumePendingMailto?.() ?? Promise.resolve(null);
}

/** Read + delete a pending Windows-share drop file, if any (C46). Startup
 *  twin of the `openShare` event — covers a share that spawned the daemon
 *  before its fs.watch armed. Attachments arrive base64-encoded, staged
 *  files already reaped by the service. */
export function consumePendingShare(): Promise<{
    title: string; text: string; url: string;
    attachments: { filename: string; mimeType: string; dataBase64: string }[];
    skipped: { name: string; reason: string }[];
} | null> {
    return (ipc() as any).consumePendingShare?.() ?? Promise.resolve(null);
}

/** Run an AI text transform (translate / proofread / summarize). Returns
 *  empty `text` with a `reason` when the feature is disabled or the provider
 *  errors — caller should surface `reason` in a status bar, not throw. */
export function aiTransform(req: {
    action: "translate" | "proofread" | "summarize" | "extractEvent";
    text: string;
    targetLang?: string;
    nowISO?: string;
}): Promise<{ text: string; reason?: string; event?: any; events?: any[] }> {
    return ipc().aiTransform?.(req) ?? Promise.resolve({ text: "", reason: "AI not available in this host" });
}

export function setupAccount(name: string, email: string, password: string) {
    return ipc().setupAccount?.(name, email, password);
}

export async function getAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number): Promise<{ content: string; contentType: string; filename: string }> {
    return ipc().getAttachment(accountId, uid, attachmentId, folderId);
}

/** Desktop: have the Node service save the attachment and open it with the
 *  OS default app. Returns undefined when the host has no such method (real
 *  browser / --server mode) so the caller can fall back to a blob download. */
export async function openAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number, filename?: string): Promise<{ ok: boolean; path: string } | undefined> {
    const fn = (ipc() as any).openAttachment;
    return fn ? fn(accountId, uid, attachmentId, folderId, filename) : undefined;
}

/** Raw RFC 822 source (base64) of a message — for "Save message as .eml". */
export async function getMessageSource(accountId: string, uid: number, folderId?: number): Promise<{ dataBase64: string; filename: string }> {
    return (ipc() as any).getMessageSource(accountId, uid, folderId);
}

/** Open a message in its own native OS window (msger WebView spawned by the
 *  daemon). Resolves { ok:false, reason } when no popout host is available —
 *  caller falls back to the in-window overlay. */
/** Delete attachment(s) from a received message. Local-first strip + queued
 *  server replace. Throws with a user-readable message on unsupported hosts
 *  (Gmail accounts, Android today). */
export async function deleteAttachments(accountId: string, uid: number, folderId: number | undefined, ids: number[] | "all"): Promise<{ ok: boolean; removed: number; remaining: number }> {
    const fn = (ipc() as any).deleteAttachments;
    if (!fn) throw new Error("Attachment delete not available in this host");
    return fn(accountId, uid, folderId, ids);
}

/** Offline grammar lint via the daemon's Harper engine. Throws when the
 *  host has no grammarLint (Android web-service today) — caller disables. */
export async function grammarLint(blocks: string[]): Promise<{ blocks: { start: number; end: number; kind: string; message: string; suggestions: string[] }[][] }> {
    const fn = (ipc() as any).grammarLint;
    if (!fn) throw new Error("no grammarLint bridge");
    return fn(blocks);
}

/** Register rmfmail as the OS-level mailto: handler (Settings menu). */
export async function registerMailto(): Promise<{ ok: boolean; message: string }> {
    const fn = (ipc() as any).registerMailto;
    if (!fn) return { ok: false, message: "Not available in this host — run `rmfmail -register-mailto` from a terminal." };
    return fn();
}

export async function popoutWindow(accountId: string, uid: number, folderId?: number, subject?: string): Promise<{ ok: boolean; reason?: string }> {
    const fn = (ipc() as any).popoutWindow;
    if (!fn) return { ok: false, reason: "no popoutWindow bridge" };
    return fn(accountId, uid, folderId, subject);
}

/** Take a live compose into its own native OS window. init is the full
 *  compose state; the daemon stashes it and spawns a popout-server-backed
 *  msger window that consumes it on boot. */
export async function popoutCompose(init: any): Promise<{ ok: boolean; reason?: string }> {
    const fn = (ipc() as any).popoutCompose;
    if (!fn) return { ok: false, reason: "no popoutCompose bridge" };
    return fn(init);
}

/** Popout compose page boot: one-shot fetch of the init stashed by popoutCompose. */
export async function consumePopoutComposeInit(id: string): Promise<{ init: any | null }> {
    const fn = (ipc() as any).consumePopoutComposeInit;
    if (!fn) return { init: null };
    return fn(id);
}

/** Ask the daemon to close this compose-popout window (after send/discard). */
export function closeComposePopout(id: string): Promise<{ ok: boolean }> {
    const fn = (ipc() as any).closeComposePopout;
    return fn ? fn(id) : Promise.resolve({ ok: false });
}

/** Compose paste: daemon-side fetch of a remote image, returned as a data:
 *  URI (or { error }) — beats hotlink protection and editor CSP. */
export function fetchImageAsDataUri(url: string): Promise<{ dataUri?: string; error?: string }> {
    const fn = (ipc() as any).fetchImageAsDataUri;
    return fn ? fn(url) : Promise.resolve({ error: "no bridge" });
}

/** View → New window: open another full mailx window as its own OS window
 *  (e.g. Drafts in one, Sent in another, side by side). */
export async function popoutMainWindow(accountId?: string, folderId?: number, name?: string): Promise<{ ok: boolean; reason?: string }> {
    const fn = (ipc() as any).popoutMainWindow;
    if (!fn) return { ok: false, reason: "no popoutMainWindow bridge" };
    return fn(accountId, folderId, name);
}

export async function getDeviceAccounts(): Promise<{ email: string; name: string }[]> {
    return ipc().getDeviceAccounts?.() ?? [];
}

// Legacy exports for backward compatibility
export const connectWebSocket = connectEvents;
export const onWsEvent = onEvent;
