/**
 * Sync manager — extracted from android-bootstrap.ts for use in both
 * main-thread and Worker contexts.
 *
 * Platform-specific dependencies are injected:
 *   - emitEvent: posts events to the UI
 *   - vlog: verbose remote logging
 *   - createTcpTransport: factory for TCP transport (BridgeTcpTransport or WorkerTcpTransport)
 */

import type { WebMailxDB } from "./db.js";
import type { WebMessageStore } from "./web-message-store.js";
import type { WebSyncManager } from "./web-service.js";
import { GmailApiWebProvider } from "./gmail-api-web.js";
import { ImapWebProvider } from "./imap-web-provider.js";
import { SmtpClient, type SmtpAuth } from "@bobfrankston/smtp-direct";
import { storeBus } from "@bobfrankston/mailx-bus";
import type { MailProvider, ProviderMessage } from "./provider-types.js";
import type { Folder, EmailAddress, AccountConfig } from "@bobfrankston/mailx-types";

function toEmailAddress(addr: { name?: string; address?: string } | undefined): EmailAddress {
    return { name: addr?.name || "", address: addr?.address || "" };
}

export interface SyncManagerDeps {
    emitEvent: (event: any) => void;
    vlog: (msg: string) => void;
    createTcpTransport: () => any;  // TcpTransport — BridgeTcpTransport or WorkerTcpTransport
}

export class SyncManager implements WebSyncManager {
    private providers = new Map<string, MailProvider>();
    private tokenProviders = new Map<string, () => Promise<string>>();
    private deps: SyncManagerDeps;

    constructor(
        private db: WebMailxDB,
        private bodyStore: WebMessageStore,
        deps: SyncManagerDeps,
    ) {
        this.deps = deps;
    }

    on(_event: string, _handler: (...args: any[]) => void): void { /* stub */ }
    emit(event: string, ...args: any[]): void { this.deps.emitEvent({ type: event, ...args[0] }); }

    async addAccount(account: AccountConfig): Promise<void> {
        this.deps.vlog(`addAccount id=${account.id} email=${account.email} host=${account.imap?.host} auth=${account.imap?.auth}`);
        this.db.upsertAccount(account.id, account.name, account.email, JSON.stringify(account));
        if (this.isGmailAccount(account)) {
            const tokenProvider = this.tokenProviders.get(account.id);
            if (tokenProvider) {
                this.providers.set(account.id, new GmailApiWebProvider(tokenProvider));
                console.log(`[sync] ${account.id}: Gmail API provider registered`);
            } else {
                console.warn(`[sync] ${account.id}: no token provider`);
            }
        } else if (account.imap?.host && account.imap?.user) {
            try {
                const provider = new ImapWebProvider({
                    server: account.imap.host,
                    port: account.imap.port || 993,
                    username: account.imap.user,
                    password: account.imap.password,
                    inactivityTimeout: 300000,
                    fetchChunkSize: 10,
                    fetchChunkSizeMax: 100,
                }, this.deps.createTcpTransport);
                this.providers.set(account.id, provider);
                this.deps.vlog(`addAccount ${account.id}: IMAP provider registered (${account.imap.host}:${account.imap.port})`);
                console.log(`[sync] ${account.id}: IMAP provider registered (${account.imap.host})`);
            } catch (e: any) {
                this.deps.vlog(`addAccount ${account.id}: IMAP provider FAILED: ${e.message}`);
                console.error(`[sync] ${account.id}: IMAP provider failed: ${e.message}`);
            }
        } else {
            this.deps.vlog(`addAccount ${account.id}: no imap config, skipping`);
        }
    }

    setTokenProvider(accountId: string, provider: () => Promise<string>): void {
        this.tokenProviders.set(accountId, provider);
    }

    private isGmailAccount(account: AccountConfig): boolean {
        return account.imap?.host?.includes("gmail") || account.email?.endsWith("@gmail.com") || false;
    }

    private getProvider(accountId: string): MailProvider | null {
        return this.providers.get(accountId) || null;
    }

    async syncAll(): Promise<void> {
        const accounts = this.db.getAccounts();
        this.deps.vlog(`syncAll: ${accounts.length} accounts in DB: ${accounts.map((a: any) => a.id).join(",")}`);

        // Phase 1: Sync INBOX for every account first — user sees new mail fast.
        for (const account of accounts) {
            if (!this.providers.has(account.id)) continue;
            try {
                const folders = await this.syncFolders(account.id);
                const inbox = folders.find((f: any) => f.specialUse === "inbox");
                if (inbox) {
                    await this.syncFolder(account.id, inbox.id);
                    this.deps.emitEvent({ type: "syncComplete", accountId: account.id });
                }
            } catch (e: any) {
                console.error(`[sync] ${account.id} inbox: ${e.message}`);
            }
        }

        // Phase 2: Remaining folders.
        for (const account of accounts) {
            if (!this.providers.has(account.id)) continue;
            try {
                const folders = this.db.getFolders(account.id);
                const remaining = folders.filter((f: any) => f.specialUse !== "inbox");
                for (const folder of remaining) {
                    try { await this.syncFolder(account.id, folder.id); }
                    catch (e: any) { console.error(`[sync] Skip ${folder.path}: ${e.message}`); }
                }
                this.db.updateLastSync(account.id, Date.now());
                this.deps.emitEvent({ type: "syncComplete", accountId: account.id });
            } catch (e: any) {
                console.error(`[sync] ${account.id}: ${e.message}`);
                this.deps.vlog(`syncAll: ${account.id} ERROR: ${e.message}`);
                this.deps.emitEvent({ type: "syncError", accountId: account.id, error: e.message });
            }
        }
    }

    async syncAccount(accountId: string): Promise<void> {
        const folders = await this.syncFolders(accountId);
        for (const folder of folders) {
            try { await this.syncFolder(accountId, folder.id); }
            catch (e: any) { console.error(`[sync] Skip ${folder.path}: ${e.message}`); }
        }
        this.db.updateLastSync(accountId, Date.now());
        this.deps.emitEvent({ type: "syncComplete", accountId });
    }

    async syncFolders(accountId: string): Promise<Folder[]> {
        const provider = this.getProvider(accountId);
        if (!provider) {
            const existing = this.db.getFolders(accountId);
            this.deps.vlog(`syncFolders: ${accountId} no provider, returning ${existing.length} cached folders`);
            return existing;
        }
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: "folders", progress: 0 });
        console.log(`[sync] ${accountId}: listing folders from provider`);
        let providerFolders: any[] = [];
        try {
            providerFolders = await provider.listFolders();
        } catch (e: any) {
            console.error(`[sync] ${accountId}: listFolders threw: ${e?.message || e}`);
            throw e;
        }
        console.log(`[sync] ${accountId}: provider returned ${providerFolders.length} folders` +
            (providerFolders.length > 0 ? ` (sample: ${providerFolders.slice(0, 3).map((f: any) => f.path || f.name).join(", ")})` : ""));
        for (const folder of providerFolders) {
            const flags = folder.flags || [];
            if (flags.some((f: string) => f.toLowerCase() === "\\noselect")) continue;
            this.db.upsertFolder(accountId, folder.path, folder.name, folder.specialUse, folder.delimiter);
        }
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: "folders", progress: 100 });
        const dbFolders = this.db.getFolders(accountId);
        console.log(`[sync] ${accountId}: ${dbFolders.length} folders in db after upsert` +
            `, inbox=${dbFolders.some((f: any) => f.specialUse === "inbox") ? "yes" : "no"}`);
        this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
        return dbFolders;
    }

    async syncFolder(accountId: string, folderId: number): Promise<void> {
        const provider = this.getProvider(accountId);
        if (!provider) return;
        const folders = this.db.getFolders(accountId);
        const folder = folders.find((f: any) => f.id === folderId);
        if (!folder) return;

        this.deps.emitEvent({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 0 });
        const highestUid = this.db.getHighestUid(accountId, folderId);
        const startDate = new Date(Date.now() - 30 * 86400000);

        let messages: ProviderMessage[];
        if (highestUid > 0) {
            messages = await provider.fetchSince(folder.path, highestUid, { source: false });
            messages = messages.filter((m: any) => m.uid > highestUid);
        } else {
            const tomorrow = new Date(Date.now() + 86400000);
            messages = await provider.fetchByDate(folder.path, startDate, tomorrow, { source: false });
        }

        if (messages.length > 0) {
            console.log(`[sync] ${folder.path}: ${messages.length} messages`);
            this.storeProviderMessages(accountId, folderId, messages);
            this.db.recalcFolderCounts(folderId);
            this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
        }

        // Reconcile deletions
        try {
            const serverUidsArr = await provider.getUids(folder.path);
            const serverUids = new Set(serverUidsArr);
            const localUids = this.db.getUidsForFolder(accountId, folderId);
            if (serverUidsArr.length === 0 && localUids.length > 0) {
                console.log(`[sync] ${folder.path}: reconcile skipped — server returned empty but local has ${localUids.length}`);
            } else {
                const toDelete = localUids.filter((uid: number) => !serverUids.has(uid));
                const RECONCILE_DELETE_THRESHOLD = 0.5;
                if (localUids.length > 0 && toDelete.length / localUids.length > RECONCILE_DELETE_THRESHOLD) {
                    console.log(`[sync] ${folder.path}: reconcile refused — would delete ${toDelete.length}/${localUids.length}`);
                } else {
                    for (const uid of toDelete) {
                        this.db.deleteMessage(accountId, folderId, uid);
                        this.bodyStore.deleteMessage(accountId, folderId, uid).catch(() => {});
                    }
                    if (toDelete.length > 0) {
                        console.log(`[sync] ${folder.path}: reconciled ${toDelete.length} deletions`);
                        this.db.recalcFolderCounts(folderId);
                        this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
                    }
                }
            }
        } catch (e: any) {
            console.error(`[sync] ${folder.path}: reconcile error: ${e.message}`);
        }

        this.deps.emitEvent({ type: "folderSynced", accountId, entries: [{ folderId, syncedAt: Date.now() }] });
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 100 });
    }

    private storeProviderMessages(accountId: string, folderId: number, messages: ProviderMessage[]): void {
        this.db.beginTransaction();
        try {
            for (const msg of messages) {
                const flags: string[] = [];
                if (msg.seen) flags.push("\\Seen");
                if (msg.flagged) flags.push("\\Flagged");
                if (msg.answered) flags.push("\\Answered");
                if (msg.draft) flags.push("\\Draft");
                const bodyPath = msg.providerId ? `gmail:${msg.providerId}` : "";
                // `messages.date` is NOT NULL. An unparseable envelope date
                // yields an *Invalid Date* — truthy, `instanceof Date`, but
                // `.getTime()` is NaN, which binds as NULL and fails the
                // constraint, aborting the whole batch. Default any non-finite
                // value to now.
                const dateRaw = msg.date instanceof Date ? msg.date.getTime()
                    : typeof msg.date === "number" ? msg.date
                    : NaN;
                const dateMs = Number.isFinite(dateRaw) ? dateRaw : Date.now();
                const sentRaw = (msg as any).sentDate instanceof Date ? (msg as any).sentDate.getTime()
                    : typeof (msg as any).sentDate === "number" ? (msg as any).sentDate
                    : NaN;
                this.db.upsertMessage({
                    accountId, folderId, uid: msg.uid,
                    messageId: msg.messageId || "", inReplyTo: "", references: [],
                    date: dateMs,
                    sentDate: Number.isFinite(sentRaw) ? sentRaw : undefined,
                    subject: msg.subject || "",
                    from: toEmailAddress(msg.from?.[0]),
                    to: msg.to.map((a: any) => toEmailAddress(a)),
                    cc: msg.cc.map((a: any) => toEmailAddress(a)),
                    flags, size: msg.size || 0, hasAttachments: false, preview: "", bodyPath,
                });
            }
            this.db.commitTransaction();
        } catch (e: any) {
            this.db.rollbackTransaction();
            console.error(`[sync] storeMessages error: ${e.message}`);
        }
    }

    async fetchMessageBody(accountId: string, folderId: number, uid: number): Promise<Uint8Array | null> {
        const t0 = Date.now();
        // Cache first — IndexedDB lookup is O(1) and should always win on a
        // previously-fetched body. If this path misses on something the user
        // clearly fetched before, the cache is broken (wrong key shape, wiped
        // IndexedDB, account-id change) and needs investigation — log so it's
        // visible.
        if (await this.bodyStore.hasMessage(accountId, folderId, uid)) {
            // Cache hit is the common case on every body open — not worth a
            // log line each time. Only an actual fetch (below) is logged.
            return await this.bodyStore.getMessage(accountId, folderId, uid);
        }
        const provider = this.getProvider(accountId);
        if (!provider) { console.warn(`[fetchBody] no provider for ${accountId}`); return null; }
        const envelope = this.db.getMessageByUid(accountId, uid, folderId);
        const bp = (envelope as any)?.bodyPath || "";

        // Wall-clock timeout — without this, an IMAP provider that dangles
        // (Dovecot silently dropped the socket, BridgeTransport stalled) hung
        // the viewer forever. 60 s is generous for a single-message BODY[]
        // fetch on a phone connection; legit large bodies finish in seconds,
        // and anything longer means the socket is dead and retrying on a fresh
        // one is faster than waiting.
        const FETCH_TIMEOUT_MS = 60_000;
        const fetchPromise = (async (): Promise<any> => {
            if (bp.startsWith("gmail:") && (provider as any).fetchById) {
                const providerId = bp.substring(6);
                return (provider as any).fetchById(providerId, { source: true });
            }
            const folders = this.db.getFolders(accountId);
            const folder = folders.find((f: any) => f.id === folderId);
            if (!folder) return null;
            return provider.fetchOne(folder.path, uid, { source: true });
        })();
        let msg: any = null;
        try {
            msg = await Promise.race([
                fetchPromise,
                new Promise((_, reject) => setTimeout(
                    () => reject(new Error(`body-fetch timeout ${FETCH_TIMEOUT_MS / 1000}s (${accountId}/${folderId}/${uid})`)),
                    FETCH_TIMEOUT_MS
                )),
            ]);
        } catch (e: any) {
            console.error(`[fetchBody] failed ${accountId}/${folderId}/${uid} after ${Date.now() - t0}ms: ${e?.message || e}`);
            throw e;
        }

        if (!msg?.source) {
            console.warn(`[fetchBody] No source returned for ${accountId}/${folderId}/${uid} (bp=${bp}, ${Date.now() - t0}ms)`);
            return null;
        }
        const raw = new TextEncoder().encode(msg.source);
        await this.bodyStore.putMessage(accountId, folderId, uid, raw);
        this.db.updateBodyPath(accountId, folderId, uid, `idb:${accountId}/${folderId}/${uid}`);
        console.log(`[fetchBody] fetched + cached ${accountId}/${folderId}/${uid} (${raw.byteLength} bytes, ${Date.now() - t0}ms)`);
        return raw;
    }

    /** Publish a Store event AND the legacy emitEvent() shape until every
     *  consumer subscribes via storeBus. Same shape as the desktop side —
     *  that's the load-bearing property for cross-platform code sharing. */
    private publishStore(event: { topic: string; kind: string; [k: string]: any }): void {
        storeBus.publish(event as any);
        // Bridge to existing emitEvent listeners. Drop this when all WebView
        // consumers switch to subscribeStore (mailx-bus → bridge → WebView).
        if (event.kind === "folderCountsChanged") {
            this.deps.emitEvent({ type: "folderCountsChanged", accountId: event.accountId, counts: {} });
        } else if (event.kind === "messageMoved") {
            this.deps.emitEvent({ type: "messageMoved", accountId: event.accountId, fromFolderId: event.folderId, toFolderId: event.targetFolderId, uid: event.uid });
        } else if (event.kind === "messageRemoved") {
            this.deps.emitEvent({ type: "messageDeleted", accountId: event.accountId, folderId: event.folderId, uid: event.uid });
        }
    }

    async updateFlagsLocal(accountId: string, uid: number, folderId: number, flags: string[]): Promise<void> {
        this.db.updateMessageFlags(accountId, folderId, uid, flags);
        this.db.recalcFolderCounts(folderId);
        this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
        const env: any = this.db.getMessageByUid(accountId, uid, folderId);
        const msgUuid: string | undefined = env?.uuid;
        if (msgUuid) this.publishStore({ topic: `message:${msgUuid}`, kind: "flagsChanged", accountId, folderId, uid, msgUuid, flags });
        this.publishStore({ topic: `account:${accountId}`, kind: "folderCountsChanged", accountId, folderId });
    }

    async trashMessage(accountId: string, folderId: number, uid: number): Promise<void> {
        const env: any = this.db.getMessageByUid(accountId, uid, folderId);
        const msgUuid: string | undefined = env?.uuid;
        this.db.deleteMessage(accountId, folderId, uid);
        this.db.queueSyncAction(accountId, "trash", uid, folderId);
        if (msgUuid) this.publishStore({ topic: `message:${msgUuid}`, kind: "messageRemoved", accountId, folderId, uid, msgUuid });
        this.publishStore({ topic: `account:${accountId}`, kind: "folderCountsChanged", accountId, folderId });
    }

    async trashMessages(accountId: string, messages: { uid: number; folderId: number }[]): Promise<void> {
        for (const m of messages) await this.trashMessage(accountId, m.folderId, m.uid);
    }

    async moveMessage(accountId: string, uid: number, folderId: number, targetFolderId: number): Promise<void> {
        const env: any = this.db.getMessageByUid(accountId, uid, folderId);
        const msgUuid: string | undefined = env?.uuid;
        this.db.queueSyncAction(accountId, "move", uid, folderId, { targetFolderId });
        if (msgUuid) this.publishStore({ topic: `message:${msgUuid}`, kind: "messageMoved", accountId, folderId, targetFolderId, uid, msgUuid });
    }

    async moveMessages(accountId: string, messages: { uid: number; folderId: number }[], targetFolderId: number): Promise<void> {
        for (const m of messages) await this.moveMessage(accountId, m.uid, m.folderId, targetFolderId);
    }

    async moveMessageCrossAccount(): Promise<void> {
        throw new Error("Cross-account move not supported on mobile");
    }

    async undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void> {
        this.db.queueSyncAction(accountId, "undelete", uid, folderId);
    }

    /** Q112: drain queued move/flag/trash actions to the provider. Android is
     *  standalone — it pushes state changes to Gmail (or other provider) the
     *  same way desktop does, so local actions propagate without needing a
     *  desktop to relay them. Called from android-bootstrap on startup and
     *  every 2-min sync tick. `send` actions are drained separately by
     *  processSendQueue. */
    async processSyncActions(accountId: string): Promise<void> {
        const provider: any = this.getProvider(accountId);
        if (!provider) return;
        const pending = this.db.getPendingSyncActions(accountId)
            .filter((a: any) => a.action !== "send");
        if (pending.length === 0) return;
        const folders = this.db.getFolders(accountId);
        const folderPath = (id: number): string | null => {
            const f = folders.find((x: any) => x.id === id);
            return f?.path || null;
        };
        for (const p of pending) {
            const path = folderPath(p.folderId);
            if (!path) { this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown folder ${p.folderId}`); continue; }
            try {
                if (p.action === "flags" && typeof provider.setFlags === "function") {
                    await provider.setFlags(path, p.uid, Array.isArray(p.flags) ? p.flags : (p.flags ? [p.flags] : []));
                } else if (p.action === "trash" && typeof provider.trashMessage === "function") {
                    await provider.trashMessage(path, p.uid);
                } else if (p.action === "move" && typeof provider.moveMessage === "function") {
                    const toId = p.targetFolderId as number;
                    const toPath = folderPath(toId);
                    if (!toPath) { this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown target folder ${toId}`); continue; }
                    await provider.moveMessage(path, p.uid, toPath);
                } else {
                    // Unsupported action for this provider — don't loop forever.
                    this.db.failSyncActionByUid(accountId, p.action, p.uid, `provider does not support ${p.action}`);
                    continue;
                }
                this.db.completeSyncActionByUid(accountId, p.action, p.uid);
            } catch (e: any) {
                const msg = e?.message || String(e);
                console.error(`[sync-action] ${accountId} ${p.action} uid=${p.uid}: ${msg}`);
                this.db.failSyncActionByUid(accountId, p.action, p.uid, msg);
            }
        }
    }

    async markFolderRead(folderId: number): Promise<void> {
        this.db.markFolderRead(folderId);
    }

    async emptyFolder(accountId: string, folderId: number): Promise<void> {
        const uids = this.db.getUidsForFolder(accountId, folderId);
        for (const uid of uids) {
            this.db.deleteMessage(accountId, folderId, uid);
            this.bodyStore.deleteMessage(accountId, folderId, uid).catch(() => {});
        }
        this.db.recalcFolderCounts(folderId);
        this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
    }

    queueOutgoingLocal(accountId: string, rawMessage: string): void {
        const provider = this.getProvider(accountId);
        if (provider && typeof (provider as any).sendRaw === "function") {
            (provider as any).sendRaw(rawMessage)
                .then((result: { id: string; threadId: string }) => {
                    console.log(`[send] ${accountId}: sent via Gmail API (id=${result.id})`);
                    this.deps.emitEvent({ type: "sendComplete", accountId, messageId: result.id });
                })
                .catch((e: any) => {
                    console.error(`[send] ${accountId}: Gmail send failed: ${e.message}`);
                    this.deps.emitEvent({ type: "sendError", accountId, error: e.message });
                });
            return;
        }

        const accounts = this.db.getAccountConfigs();
        const row = accounts.find((a: any) => a.id === accountId);
        if (!row) {
            const e = "Unknown account";
            this.deps.emitEvent({ type: "sendError", accountId, error: e });
            throw new Error(e);
        }
        let account: AccountConfig;
        try { account = JSON.parse(row.configJson); }
        catch {
            const e = "Account config malformed";
            this.deps.emitEvent({ type: "sendError", accountId, error: e });
            throw new Error(e);
        }
        if (!account.smtp) {
            const e = "No SMTP config for this account";
            this.deps.emitEvent({ type: "sendError", accountId, error: e });
            throw new Error(e);
        }

        this.sendViaSmtpDirect(accountId, account, rawMessage)
            .then((result) => {
                console.log(`[send] ${accountId}: sent via SMTP (${result.accepted.length} accepted, ${result.rejected.length} rejected)`);
                this.deps.emitEvent({ type: "sendComplete", accountId });
                // File a Sent copy. SMTP delivers but doesn't store anything —
                // without this APPEND a phone-sent message on an IMAP account
                // never appeared in Sent (Bob 2026-07-01, the 19:36 Screenshot).
                // Gmail's API path self-files, so only this branch needs it.
                // Best-effort: the message was delivered either way; failure is
                // logged, and the desktop's sent-sweep can repair later.
                const p = this.getProvider(accountId) as any;
                if (p && typeof p.appendToSent === "function") {
                    p.appendToSent(rawMessage)
                        .then((uid: number | null) => console.log(`[send] ${accountId}: Sent copy filed${uid ? ` (uid ${uid})` : ""}`))
                        .catch((e: any) => console.error(`[send] ${accountId}: Sent copy APPEND failed: ${e.message}`));
                }
            })
            .catch((e: any) => {
                console.error(`[send] ${accountId}: SMTP send failed: ${e.message}`);
                this.deps.emitEvent({ type: "sendError", accountId, error: e.message });
            });
    }

    private async sendViaSmtpDirect(
        accountId: string, account: AccountConfig, raw: string,
    ): Promise<{ accepted: string[]; rejected: { address: string; code: number; message: string }[] }> {
        const smtp = account.smtp!;
        const smtpPort = smtp.port || 587;
        const smtpHost = smtp.host || account.imap?.host;
        if (!smtpHost) throw new Error("No SMTP host");

        const smtpUser = smtp.user || account.imap?.user || account.email;
        const authType = smtp.auth || (account.imap?.password ? "password" : undefined);
        let auth: SmtpAuth | undefined;
        if (authType === "password") {
            const pass = smtp.password || account.imap?.password;
            if (!pass) throw new Error("SMTP password not configured");
            auth = { method: "PLAIN", user: smtpUser, pass };
        } else if (authType === "oauth2") {
            const tp = this.tokenProviders.get(accountId);
            if (!tp) throw new Error("OAuth token provider not registered");
            const token = await tp();
            auth = { method: "XOAUTH2", user: smtpUser, token };
        }

        const parseAddrs = (s: string) => s.match(/[\w.+-]+@[\w.-]+/g) || [];
        // HEADER SECTION only, unfolded — matching over the whole raw file
        // scraped Cc/Bcc lines out of the quoted reply chain in the body and
        // silently added those addresses to RCPT TO (desktop hit this
        // 2026-07-21; same code shape here). See mailx-imap sendRaw.
        const headerEnd = raw.search(/\r?\n\r?\n/);
        const headerSec = (headerEnd === -1 ? raw : raw.slice(0, headerEnd))
            .replace(/\r?\n[ \t]+/g, " ");
        const toMatch = headerSec.match(/^To:\s*(.+)$/mi);
        const ccMatch = headerSec.match(/^Cc:\s*(.+)$/mi);
        const bccMatch = headerSec.match(/^Bcc:\s*(.+)$/mi);
        const fromMatch = headerSec.match(/^From:\s*(.+)$/mi);
        const recipients = [
            ...(toMatch ? parseAddrs(toMatch[1]) : []),
            ...(ccMatch ? parseAddrs(ccMatch[1]) : []),
            ...(bccMatch ? parseAddrs(bccMatch[1]) : []),
        ];
        const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
        if (recipients.length === 0) throw new Error("No recipients");

        // Strip Bcc from the transmitted copy — headers only (a body line
        // that merely LOOKS like "Bcc:" in quoted text must not be eaten).
        // Folded continuation lines go with it. The `(\r?\n|$)` tail
        // consumes the line ending so no blank line is left mid-headers;
        // the final trim handles a Bcc sitting as the last header line.
        const stripBcc = (h: string): string =>
            h.replace(/^Bcc:[^\n]*(\r?\n[ \t][^\n]*)*(\r?\n|$)/mi, "").replace(/\r?\n$/, "");
        const rawToSend = headerEnd === -1
            ? stripBcc(raw)
            : stripBcc(raw.slice(0, headerEnd)) + raw.slice(headerEnd);

        const client = new SmtpClient({
            host: smtpHost,
            port: smtpPort,
            secure: smtpPort === 465,
            auth,
            localname: "mailx-android",
        }, this.deps.createTcpTransport);
        try {
            await client.connect();
            return await client.sendMail({ from: sender, to: recipients }, rawToSend);
        } finally {
            try { await client.quit(); } catch { /* ignore */ }
        }
    }

    async saveDraft(_accountId: string, _raw: string, _prevUid?: number, _draftId?: string): Promise<number | null> {
        return null;
    }

    async deleteDraft(_accountId: string, _uid: number): Promise<void> {}

    async reauthenticate(_accountId: string): Promise<boolean> { return false; }
    async searchOnServer(_accountId: string, _query: string): Promise<any[]> { return []; }
    async syncAllContacts(): Promise<void> {}
}
