/**
 * 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";

declare const mailxapi: {
    isApp: boolean;
    platform: string;
    tcp: {
        connect(host: string, port: number, tls: boolean): Promise<number>;
        write(streamId: number, data: string): Promise<void>;
        onData(streamId: number, callback: (data: string) => void): void;
        onClose(streamId: number, callback: (hadError: boolean) => void): void;
        onError(streamId: number, callback: (message: string) => void): void;
        upgradeTLS(streamId: number, servername: string): Promise<void>;
        close(streamId: number): void;
    };
    fs: {
        read(path: string, options?: any): Promise<string>;
        write(path: string, content: string): Promise<void>;
        exists(path: string): Promise<boolean>;
        list(path: string): Promise<string[]>;
        delete(path: string): Promise<void>;
    };
    http: {
        fetch(url: string, init?: any): Promise<string>;
    };
    info: { platform: string; version: string };
};

// ── IMAP Parsing Helpers ──

/** Extract a balanced parenthesized group starting at position */
function extractParenGroup(s: string, start: number): string {
    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: string): string[] {
    const tokens: string[] = [];
    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: string): string {
    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: string): { name: string; address: string }[] {
    if (!token || token === "NIL") return [];
    const addrs: { name: string; address: string }[] = [];
    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: string): string {
    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, (_: string, hex: string) =>
                String.fromCharCode(parseInt(hex, 16))).replace(/_/g, " ");
        } catch { return text; }
    });
}

// ── Settings ──

interface AccountConfig {
    id: string;
    name: string;
    email: string;
    imap: { host: string; port: number; user: string; password?: string; auth?: string };
    smtp: { host: string; port: number; user: string; password?: string; auth?: string };
    enabled?: boolean;
    defaultSend?: boolean;
    label?: string;
}

interface Settings {
    name?: string;
    accounts: AccountConfig[];
    ui?: { theme?: string; editor?: string };
    sync?: { intervalMinutes?: number; historyDays?: number };
}

let settings: Settings | null = null;
const eventHandlers: ((event: any) => void)[] = [];

function emit(event: any): void {
    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 {
    private streamId: number | null = null;
    private buffer = "";
    private dataResolve: ((data: string) => void) | null = null;
    private tagCounter = 0;

    async connect(host: string, port: number, tls: boolean): Promise<string> {
        this.streamId = await mailxapi.tcp.connect(host, port, tls);
        mailxapi.tcp.onData(this.streamId, (data: string) => {
            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();
    }

    private nextTag(): string {
        return `A${++this.tagCounter}`;
    }

    private async readLine(): Promise<string> {
        // 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 */
    private async readUntilTag(tag: string): Promise<string[]> {
        const lines: string[] = [];
        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: string): Promise<string[]> {
        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: string, pass: string): Promise<boolean> {
        const resp = await this.command(`LOGIN "${user}" "${pass}"`);
        return resp.some(l => l.includes(" OK "));
    }

    async xoauth2(user: string, token: string): Promise<boolean> {
        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(): Promise<{ path: string; delimiter: string; flags: string[] }[]> {
        const resp = await this.command('LIST "" "*"');
        const folders: { path: string; delimiter: string; flags: string[] }[] = [];
        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: string): Promise<{ exists: number }> {
        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: string): Promise<{ messages: number }> {
        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: string): Promise<any[]> {
        const resp = await this.command(`UID FETCH ${range} (UID FLAGS ENVELOPE RFC822.SIZE INTERNALDATE)`);
        const messages: any[] = [];
        // 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: { name: string; address: string }[] = [];
            let ccAddrs: { name: string; address: string }[] = [];
            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: number): Promise<string> {
        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(): Promise<void> {
        try { await this.command("LOGOUT"); } catch { /* ignore */ }
        if (this.streamId != null) {
            mailxapi.tcp.close(this.streamId);
            this.streamId = null;
        }
    }

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

    async storeFlags(uid: number, action: string, flags: string[]): Promise<void> {
        await this.command(`UID STORE ${uid} ${action} (${flags.join(" ")})`);
    }

    async copy(uid: number, dest: string): Promise<void> {
        await this.command(`UID COPY ${uid} "${dest}"`);
    }

    async expunge(): Promise<void> {
        await this.command("EXPUNGE");
    }

    async search(criteria: string): Promise<number[]> {
        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(): Promise<Settings> {
    // Try IndexedDB meta first
    const saved = await store.getMeta("settings");
    if (saved) return saved;
    // Default empty
    return { accounts: [] };
}

async function saveSettingsToStorage(s: Settings): Promise<void> {
    settings = s;
    await store.setMeta("settings", s);
}

async function syncAccount(account: AccountConfig): Promise<void> {
    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: boolean;
        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: Record<number, { total: number; unread: number }> = {};
        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: any) {
        console.error(`[local-service] Sync error: ${e.message}`);
        emit({ type: "error", message: `${account.id}: ${e.message}` });
    }
}

async function syncFolder(client: BridgeImapClient, accountId: string, folder: { id: number; path: string }): Promise<void> {
    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: any[];
    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: any) => !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();
}

// ── Transport Implementation ──

export interface LocalTransportCall {
    method: string;
    params: any;
}

export function onEvent(handler: (event: any) => void): void {
    eventHandlers.push(handler);
}

/** Initialize the local service — load settings, start sync */
export async function initialize(initialSettings?: Settings): Promise<void> {
    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(): Settings | null {
    return settings;
}
