/**
 * Local mail service — runs entirely in the WebView.
 * Uses mailxapi.tcp bridge for IMAP, IndexedDB for storage.
 * Implements the same MailxTransport interface so the client doesn't know the difference.
 *
 * This is the Android equivalent of the Express server + MailxService.
 */
import * as store from "./local-store.js";
// ── IMAP Parsing Helpers ──
/** Extract a balanced parenthesized group starting at position */
function extractParenGroup(s, start) {
    if (s[start] !== "(")
        return "";
    let depth = 0;
    for (let i = start; i < s.length; i++) {
        if (s[i] === "(")
            depth++;
        else if (s[i] === ")") {
            depth--;
            if (depth === 0)
                return s.substring(start, i + 1);
        }
    }
    return s.substring(start);
}
/** Tokenize top-level items in a parenthesized IMAP list */
function tokenizeEnvelope(s) {
    const tokens = [];
    const str = s.trim();
    const start = str.startsWith("(") ? 1 : 0;
    const end = str.endsWith(")") ? str.length - 1 : str.length;
    let i = start;
    while (i < end) {
        while (i < end && str[i] === " ")
            i++;
        if (i >= end)
            break;
        if (str[i] === "(") {
            let depth = 1, j = i + 1;
            while (j < end && depth > 0) {
                if (str[j] === "(")
                    depth++;
                else if (str[j] === ")")
                    depth--;
                j++;
            }
            tokens.push(str.substring(i, j));
            i = j;
        }
        else if (str[i] === '"') {
            let j = i + 1;
            while (j < end) {
                if (str[j] === "\\" && j + 1 < end) {
                    j += 2;
                    continue;
                }
                if (str[j] === '"') {
                    j++;
                    break;
                }
                j++;
            }
            tokens.push(str.substring(i, j));
            i = j;
        }
        else if (str.substring(i, i + 3).toUpperCase() === "NIL") {
            tokens.push("NIL");
            i += 3;
        }
        else {
            let j = i;
            while (j < end && str[j] !== " " && str[j] !== ")" && str[j] !== "(")
                j++;
            tokens.push(str.substring(i, j));
            i = j;
        }
    }
    return tokens;
}
/** Remove quotes from an IMAP string */
function unquoteImap(s) {
    if (!s || s === "NIL")
        return "";
    if (s.startsWith('"') && s.endsWith('"'))
        return s.slice(1, -1).replace(/\\(.)/g, "$1");
    return s;
}
/** Parse IMAP address list: ((name NIL mailbox host)...) */
function parseImapAddressList(token) {
    if (!token || token === "NIL")
        return [];
    const addrs = [];
    const re = /\(([^)]*)\)/g;
    let m;
    while ((m = re.exec(token)) !== null) {
        const parts = tokenizeEnvelope(m[1]);
        if (parts.length >= 4) {
            const name = unquoteImap(parts[0]);
            const mailbox = unquoteImap(parts[2]);
            const host = unquoteImap(parts[3]);
            addrs.push({ name, address: mailbox && host ? `${mailbox}@${host}` : mailbox || "" });
        }
    }
    return addrs;
}
/** Decode =?charset?encoding?text?= encoded words */
function decodeEncodedWords(s) {
    if (!s)
        return "";
    return s.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_match, _charset, encoding, text) => {
        try {
            if (encoding.toUpperCase() === "B")
                return atob(text);
            return text.replace(/=([0-9A-F]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/_/g, " ");
        }
        catch {
            return text;
        }
    });
}
let settings = null;
const eventHandlers = [];
function emit(event) {
    for (const h of eventHandlers) {
        try {
            h(event);
        }
        catch { /* ignore */ }
    }
}
// ── IMAP via Bridge ──
/** Minimal IMAP client that works through the mailxapi.tcp bridge */
class BridgeImapClient {
    streamId = null;
    buffer = "";
    dataResolve = null;
    tagCounter = 0;
    async connect(host, port, tls) {
        this.streamId = await mailxapi.tcp.connect(host, port, tls);
        mailxapi.tcp.onData(this.streamId, (data) => {
            this.buffer += data;
            if (this.dataResolve && this.buffer.includes("\r\n")) {
                const resolve = this.dataResolve;
                this.dataResolve = null;
                resolve(this.buffer);
            }
        });
        // Read greeting
        return this.readLine();
    }
    nextTag() {
        return `A${++this.tagCounter}`;
    }
    async readLine() {
        // Check buffer first
        const idx = this.buffer.indexOf("\r\n");
        if (idx >= 0) {
            const line = this.buffer.substring(0, idx);
            this.buffer = this.buffer.substring(idx + 2);
            return line;
        }
        // Wait for data
        return new Promise(resolve => {
            this.dataResolve = () => {
                const idx = this.buffer.indexOf("\r\n");
                if (idx >= 0) {
                    const line = this.buffer.substring(0, idx);
                    this.buffer = this.buffer.substring(idx + 2);
                    resolve(line);
                }
            };
        });
    }
    /** Read all responses until we get the tagged response */
    async readUntilTag(tag) {
        const lines = [];
        const timeout = setTimeout(() => {
            // Force resolve on timeout
            if (this.dataResolve) {
                this.dataResolve("");
                this.dataResolve = null;
            }
        }, 30000);
        while (true) {
            const line = await this.readLine();
            lines.push(line);
            if (line.startsWith(tag + " ")) {
                clearTimeout(timeout);
                return lines;
            }
        }
    }
    async command(cmd) {
        const tag = this.nextTag();
        const full = `${tag} ${cmd}\r\n`;
        if (this.streamId == null)
            throw new Error("Not connected");
        await mailxapi.tcp.write(this.streamId, full);
        return this.readUntilTag(tag);
    }
    async login(user, pass) {
        const resp = await this.command(`LOGIN "${user}" "${pass}"`);
        return resp.some(l => l.includes(" OK "));
    }
    async xoauth2(user, token) {
        const authStr = btoa(`user=${user}\x01auth=Bearer ${token}\x01\x01`);
        const resp = await this.command(`AUTHENTICATE XOAUTH2 ${authStr}`);
        return resp.some(l => l.includes(" OK "));
    }
    async list() {
        const resp = await this.command('LIST "" "*"');
        const folders = [];
        for (const line of resp) {
            const m = line.match(/^\* LIST \(([^)]*)\) "([^"]*)" (?:"([^"]+)"|(\S+))$/);
            if (m) {
                folders.push({
                    flags: m[1] ? m[1].split(/\s+/).filter(Boolean) : [],
                    delimiter: m[2] || ".",
                    path: m[3] || m[4] || "",
                });
            }
        }
        return folders;
    }
    async select(mailbox) {
        const resp = await this.command(`SELECT "${mailbox}"`);
        let exists = 0;
        for (const line of resp) {
            const m = line.match(/^\* (\d+) EXISTS/);
            if (m)
                exists = parseInt(m[1]);
        }
        return { exists };
    }
    async status(mailbox) {
        const resp = await this.command(`STATUS "${mailbox}" (MESSAGES)`);
        for (const line of resp) {
            const m = line.match(/MESSAGES\s+(\d+)/);
            if (m)
                return { messages: parseInt(m[1]) };
        }
        return { messages: 0 };
    }
    async fetchHeaders(range) {
        const resp = await this.command(`UID FETCH ${range} (UID FLAGS ENVELOPE RFC822.SIZE INTERNALDATE)`);
        const messages = [];
        // Join all response lines to handle multi-line FETCH responses
        const fullResp = resp.join("\r\n");
        // Split on "* N FETCH" boundaries
        const fetchBlocks = fullResp.split(/(?=\* \d+ FETCH)/);
        for (const block of fetchBlocks) {
            if (!block.includes("FETCH"))
                continue;
            const uid = block.match(/UID\s+(\d+)/)?.[1];
            if (!uid)
                continue;
            const flags = block.match(/FLAGS\s+\(([^)]*)\)/)?.[1] || "";
            const size = block.match(/RFC822\.SIZE\s+(\d+)/)?.[1] || "0";
            const dateMatch = block.match(/INTERNALDATE\s+"([^"]+)"/);
            // Parse ENVELOPE: (date subject from sender reply-to to cc bcc in-reply-to message-id)
            let subject = "", fromName = "", fromAddr = "", messageId = "";
            let toAddrs = [];
            let ccAddrs = [];
            const envStart = block.indexOf("ENVELOPE (");
            if (envStart >= 0) {
                const envStr = extractParenGroup(block, envStart + 9);
                const tokens = tokenizeEnvelope(envStr);
                // tokens[0]=date, [1]=subject, [2]=from, [3]=sender, [4]=reply-to, [5]=to, [6]=cc, [7]=bcc, [8]=in-reply-to, [9]=message-id
                if (tokens.length >= 10) {
                    subject = unquoteImap(tokens[1]);
                    messageId = unquoteImap(tokens[9]);
                    const fromList = parseImapAddressList(tokens[2]);
                    if (fromList.length > 0) {
                        fromName = fromList[0].name;
                        fromAddr = fromList[0].address;
                    }
                    toAddrs = parseImapAddressList(tokens[5]);
                    ccAddrs = parseImapAddressList(tokens[6]);
                }
            }
            messages.push({
                uid: parseInt(uid),
                flags: flags.split(/\s+/).filter(Boolean),
                size: parseInt(size),
                date: dateMatch ? new Date(dateMatch[1]).getTime() : Date.now(),
                subject: decodeEncodedWords(subject),
                fromName: decodeEncodedWords(fromName),
                fromAddr,
                messageId,
                to: toAddrs,
                cc: ccAddrs,
            });
        }
        return messages;
    }
    async fetchBody(uid) {
        const tag = this.nextTag();
        const cmd = `${tag} UID FETCH ${uid} (BODY.PEEK[])\r\n`;
        if (this.streamId == null)
            throw new Error("Not connected");
        await mailxapi.tcp.write(this.streamId, cmd);
        // Read response including literal data
        let allData = "";
        const deadline = Date.now() + 30000;
        while (Date.now() < deadline) {
            const chunk = await this.readLine();
            allData += chunk + "\r\n";
            if (chunk.startsWith(tag + " "))
                break;
        }
        return allData;
    }
    async logout() {
        try {
            await this.command("LOGOUT");
        }
        catch { /* ignore */ }
        if (this.streamId != null) {
            mailxapi.tcp.close(this.streamId);
            this.streamId = null;
        }
    }
    async close() {
        try {
            await this.command("CLOSE");
        }
        catch { /* ignore */ }
    }
    async storeFlags(uid, action, flags) {
        await this.command(`UID STORE ${uid} ${action} (${flags.join(" ")})`);
    }
    async copy(uid, dest) {
        await this.command(`UID COPY ${uid} "${dest}"`);
    }
    async expunge() {
        await this.command("EXPUNGE");
    }
    async search(criteria) {
        const resp = await this.command(`UID SEARCH ${criteria}`);
        for (const line of resp) {
            if (line.startsWith("* SEARCH")) {
                return line.substring(9).trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
            }
        }
        return [];
    }
}
// ── Service Methods ──
async function loadSettingsFromStorage() {
    // Try IndexedDB meta first
    const saved = await store.getMeta("settings");
    if (saved)
        return saved;
    // Default empty
    return { accounts: [] };
}
async function saveSettingsToStorage(s) {
    settings = s;
    await store.setMeta("settings", s);
}
async function syncAccount(account) {
    const client = new BridgeImapClient();
    try {
        const greeting = await client.connect(account.imap.host, account.imap.port || 993, (account.imap.port || 993) === 993);
        console.log(`[local-service] Connected to ${account.imap.host}: ${greeting}`);
        // Authenticate
        let authOk;
        if (account.imap.auth === "oauth2") {
            // TODO: OAuth token via bridge — for now skip
            console.log("[local-service] OAuth not yet supported in bridge mode");
            await client.logout();
            return;
        }
        else {
            authOk = await client.login(account.imap.user, account.imap.password || "");
        }
        if (!authOk) {
            console.error("[local-service] Auth failed");
            await client.logout();
            return;
        }
        emit({ type: "syncProgress", accountId: account.id, phase: "folders", progress: 0 });
        // Get folder list
        const folders = await client.list();
        let nextFolderId = (await store.getMeta("nextFolderId")) || 1;
        for (const f of folders) {
            const existing = (await store.getFolders(account.id)).find(ef => ef.path === f.path);
            if (!existing) {
                const flags = f.flags.map(fl => fl.toLowerCase());
                let specialUse = "";
                if (flags.includes("\\inbox") || f.path.toLowerCase() === "inbox")
                    specialUse = "inbox";
                else if (flags.includes("\\sent"))
                    specialUse = "sent";
                else if (flags.includes("\\trash"))
                    specialUse = "trash";
                else if (flags.includes("\\drafts"))
                    specialUse = "drafts";
                else if (flags.includes("\\junk"))
                    specialUse = "junk";
                else if (flags.includes("\\archive"))
                    specialUse = "archive";
                await store.upsertFolder({
                    id: nextFolderId++,
                    accountId: account.id,
                    path: f.path,
                    delimiter: f.delimiter,
                    specialUse,
                    totalCount: 0,
                    unreadCount: 0,
                });
            }
        }
        await store.setMeta("nextFolderId", nextFolderId);
        emit({ type: "syncProgress", accountId: account.id, phase: "folders", progress: 100 });
        // Sync inbox
        const allFolders = await store.getFolders(account.id);
        const inbox = allFolders.find(f => f.specialUse === "inbox");
        if (inbox) {
            await syncFolder(client, account.id, inbox);
        }
        // Emit folder counts
        const updatedFolders = await store.getFolders(account.id);
        const counts = {};
        for (const f of updatedFolders) {
            counts[f.id] = { total: f.totalCount, unread: f.unreadCount };
        }
        emit({ type: "folderCountsChanged", accountId: account.id, counts });
        await client.logout();
    }
    catch (e) {
        console.error(`[local-service] Sync error: ${e.message}`);
        emit({ type: "error", message: `${account.id}: ${e.message}` });
    }
}
async function syncFolder(client, accountId, folder) {
    emit({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 0 });
    const { exists } = await client.select(folder.path);
    const highestUid = await store.getHighestUid(accountId, folder.id);
    let msgs;
    if (highestUid > 0) {
        msgs = await client.fetchHeaders(`${highestUid + 1}:*`);
        msgs = msgs.filter(m => m.uid > highestUid);
    }
    else {
        // First sync — get recent messages
        const uids = await client.search("ALL");
        // Take last 200 UIDs (most recent)
        const recent = uids.slice(-200);
        if (recent.length > 0) {
            msgs = await client.fetchHeaders(recent.join(","));
        }
        else {
            msgs = [];
        }
    }
    // Store messages
    let newCount = 0;
    for (const msg of msgs) {
        await store.upsertMessage({
            key: `${accountId}:${folder.id}:${msg.uid}`,
            accountId,
            folderId: folder.id,
            uid: msg.uid,
            messageId: msg.messageId || "",
            date: msg.date,
            subject: msg.subject || "(no subject)",
            fromName: msg.fromName || "",
            fromAddress: msg.fromAddr || "",
            toJson: JSON.stringify(msg.to || []),
            ccJson: JSON.stringify(msg.cc || []),
            flags: msg.flags.join(","),
            size: msg.size,
            hasAttachments: false,
            preview: "",
            bodyPath: "",
        });
        newCount++;
    }
    // Update folder counts
    const allMsgs = await store.getMessages(accountId, folder.id, 1, 999999);
    const total = allMsgs.total;
    const unread = allMsgs.items.filter((m) => !m.flags.includes("\\Seen")).length;
    await store.updateFolderCounts(folder.id, total, unread);
    emit({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 100 });
    if (newCount > 0) {
        console.log(`[local-service] ${folder.path}: ${newCount} new messages`);
        emit({ type: "folderCountsChanged", accountId, counts: { [folder.id]: { total, unread } } });
    }
    await client.close();
}
export function onEvent(handler) {
    eventHandlers.push(handler);
}
/** Initialize the local service — load settings, start sync */
export async function initialize(initialSettings) {
    await store.init();
    if (initialSettings) {
        await saveSettingsToStorage(initialSettings);
        settings = initialSettings;
    }
    else {
        settings = await loadSettingsFromStorage();
    }
    // Register accounts in IndexedDB
    for (const account of settings.accounts) {
        await store.upsertAccount(account.id, account.name, account.email, JSON.stringify(account));
    }
    emit({ type: "connected" });
    // Start initial sync
    for (const account of settings.accounts) {
        if (account.enabled !== false) {
            syncAccount(account).catch(e => console.error(`[local-service] ${e.message}`));
        }
    }
    // Periodic re-sync every 30 seconds
    setInterval(async () => {
        if (!settings)
            return;
        for (const account of settings.accounts) {
            if (account.enabled !== false) {
                syncAccount(account).catch(e => console.error(`[local-service] periodic: ${e.message}`));
            }
        }
    }, 30000);
}
export function getSettings() {
    return settings;
}
//# sourceMappingURL=local-service.js.map