/**
 * IMAP web provider — implements MailProvider using CompatImapClient + BridgeTransport.
 * Used for non-Gmail accounts on Android/WebView where Node.js isn't available.
 *
 * The native shell (MAUI) exposes TCP via window._nativeBridge.tcp.*; we alias it
 * to window.msgapi in initAndroid() because iflow-direct's BridgeTransport expects
 * that global name.
 *
 * Includes automatic retry on broken pipe / connection errors: if an operation fails
 * with a connection-related error, we create a fresh client and retry once.
 */

import { CompatImapClient, BridgeTransport, type ImapClientConfig, type TransportFactory } from "@bobfrankston/iflow-direct";
import type { MailProvider, ProviderFolder, ProviderMessage, FetchOptions } from "./provider-types.js";

/**
 * Convert a NativeFolder (from iflow-direct) into a ProviderFolder,
 * detecting special-use from the IMAP flags.
 */
function toProviderFolder(f: { path: string; delimiter: string; flags: string[] }, special: Record<string, string | undefined>): ProviderFolder {
    const flagsLower = (f.flags || []).map(x => x.toLowerCase());
    let specialUse = "";
    if (f.path === special.inbox || flagsLower.includes("\\inbox") || f.path.toUpperCase() === "INBOX") specialUse = "inbox";
    else if (f.path === special.sent || flagsLower.includes("\\sent")) specialUse = "sent";
    else if (f.path === special.trash || flagsLower.includes("\\trash")) specialUse = "trash";
    else if (f.path === special.drafts || flagsLower.includes("\\drafts")) specialUse = "drafts";
    else if (f.path === special.spam || f.path === special.junk || flagsLower.includes("\\junk")) specialUse = "junk";
    else if (f.path === special.archive || flagsLower.includes("\\archive")) specialUse = "archive";

    // Leaf name = last path segment after delimiter
    const leaf = f.delimiter ? f.path.split(f.delimiter).pop() || f.path : f.path;

    return {
        path: f.path,
        name: leaf,
        delimiter: f.delimiter || "/",
        specialUse,
        flags: f.flags || [],
    };
}

function toProviderMessage(m: any): ProviderMessage {
    return {
        uid: m.uid,
        messageId: m.messageId || "",
        providerId: "",
        date: m.date || null,
        sentDate: m.sentDate || undefined,
        subject: m.subject || "",
        from: m.from || [],
        to: m.to || [],
        cc: m.cc || [],
        seen: !!m.seen,
        flagged: !!m.flagged,
        answered: !!m.answered,
        draft: !!m.draft,
        size: m.size || 0,
        source: m.source || "",
    };
}

/** Check if an error is a connection/broken-pipe error worth retrying */
function isConnectionError(e: any): boolean {
    const msg = (e?.message || "").toLowerCase();
    return msg.includes("broken pipe") || msg.includes("not connected") ||
           msg.includes("connection") || msg.includes("socket") ||
           msg.includes("timeout") || msg.includes("econnreset") ||
           msg.includes("epipe") || msg.includes("closed");
}

export class ImapWebProvider implements MailProvider {
    private client: CompatImapClient;
    private config: ImapClientConfig;
    private transportFactory: TransportFactory;
    private specialFolders: Record<string, string | undefined> = {};
    private folderListCache: ProviderFolder[] | null = null;

    constructor(config: ImapClientConfig, transportFactory?: () => any) {
        this.config = config;
        this.transportFactory = transportFactory || (() => new BridgeTransport());
        this.client = new CompatImapClient(config, this.transportFactory);
    }

    /** Create a fresh client (after broken pipe / connection error) */
    private reconnect(): void {
        console.log("[imap-web] reconnecting after connection error");
        try { this.client.logout(); } catch { /* ignore */ }
        this.client = new CompatImapClient(this.config, this.transportFactory);
    }

    /** Run an operation with one retry on connection error */
    private async withRetry<T>(op: () => Promise<T>, label: string): Promise<T> {
        try {
            return await op();
        } catch (e: any) {
            if (isConnectionError(e)) {
                console.warn(`[imap-web] ${label}: ${e.message} — reconnecting and retrying`);
                this.reconnect();
                return await op();
            }
            throw e;
        }
    }

    async listFolders(): Promise<ProviderFolder[]> {
        const native = await this.withRetry(() => this.client.getFolderList(), "listFolders");
        const special = this.client.getSpecialFolders(native);
        this.specialFolders = special as any;
        const result = native.map(f => toProviderFolder(f, this.specialFolders));
        this.folderListCache = result;
        return result;
    }

    async fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]> {
        const msgs = await this.withRetry(
            () => this.client.fetchMessagesSinceUid(folder, sinceUid, { source: !!options?.source }),
            `fetchSince(${folder})`
        );
        return msgs.map(toProviderMessage);
    }

    async fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions,
        onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]> {
        const wrappedChunk = onChunk ? (raw: any[]) => onChunk(raw.map(toProviderMessage)) : undefined;
        const msgs = await this.withRetry(
            () => this.client.fetchMessageByDate(folder, since, before, { source: !!options?.source }, wrappedChunk),
            `fetchByDate(${folder})`
        );
        return msgs.map(toProviderMessage);
    }

    async fetchByUids(folder: string, uids: number[], options?: FetchOptions): Promise<ProviderMessage[]> {
        if (!uids.length) return [];
        const range = uids.join(",");
        const msgs = await this.withRetry(
            () => this.client.fetchMessages(folder, range, { source: !!options?.source }),
            `fetchByUids(${folder})`
        );
        return msgs.map(toProviderMessage);
    }

    async fetchOne(folder: string, uid: number, options?: FetchOptions): Promise<ProviderMessage | null> {
        const msg = await this.withRetry(
            () => this.client.fetchMessageByUid(folder, uid, { source: !!options?.source }),
            `fetchOne(${folder}/${uid})`
        );
        return msg ? toProviderMessage(msg) : null;
    }

    async getUids(folder: string): Promise<number[]> {
        return this.withRetry(() => this.client.getUids(folder), `getUids(${folder})`);
    }

    /** APPEND a raw message to this account's Sent folder. Used by the
     *  Android send path after a successful SMTP delivery — without it a
     *  phone-sent message never got a Sent copy on IMAP accounts (Gmail's
     *  API self-files; SMTP does not). Resolves the Sent path from the
     *  special-folder map (populated by listFolders; refreshed here if the
     *  cache is cold). Returns the appended UID when the server reports it. */
    async appendToSent(raw: string): Promise<number | null> {
        if (!this.specialFolders.sent) await this.listFolders();
        const sentPath = this.specialFolders.sent || "Sent";
        return this.withRetry(
            () => this.client.appendMessage(sentPath, raw, ["\\Seen"]),
            `appendToSent(${sentPath})`
        );
    }

    async close(): Promise<void> {
        try { await this.client.logout(); } catch { /* ignore */ }
    }
}
