/**
 * Standalone folder populator.
 *
 * Bypasses mailx's broken sync loop and directly fetches the latest N
 * messages from every folder of every IMAP account, writing rows into the
 * mailx.db. Designed to run while rmfmail.exe is killed (overnight job).
 *
 * Why: mailx's sync has been silently failing on bobma — bobma/Sent has
 * never had real server-fetched rows in the local DB. Rather than fight
 * the sync loop's connection-state corruption, this tool uses a single
 * fresh IMAP connection (which we proved completes SELECT Sent in 26ms)
 * and pumps messages into the DB directly via upsertMessage.
 *
 * Run:
 *   1. Kill rmfmail.exe
 *   2. cd Y:/dev/email/mailx/app
 *   3. npx tsx Y:/dev/email/mailx/dev/populate-folders.ts [--n 500] [--account bobma] [--folder Sent]
 *
 * Defaults: N=500 latest messages per folder, all IMAP accounts, all folders.
 *
 * Notes:
 *   - Bodies are NOT fetched here — only envelope rows. Body fetch is
 *     mailx's prefetch loop's job; this tool just unblocks the metadata.
 *   - One connection, sequential folders. No parallelism that could
 *     trigger the same connection-state issues mailx hits.
 *   - Progress logged per folder. Failures logged + skipped, never bail.
 *   - Idempotent: re-running re-upserts. UNIQUE(account, folder, uid)
 *     constraint means existing rows are updated, not duplicated.
 */

import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { NativeImapClient } from "@bobfrankston/iflow-direct";
import { NodeTcpTransport } from "@bobfrankston/node-tcp-transport";
import { MailxDB } from "@bobfrankston/mailx-store";

// ── Args ──
const args = process.argv.slice(2);
const argVal = (name: string, def?: string): string | undefined => {
    const i = args.indexOf(`--${name}`);
    if (i < 0 || i === args.length - 1) return def;
    return args[i + 1];
};
const FETCH_N = Number(argVal("n", "500"));
const ONLY_ACCOUNT = argVal("account");
const ONLY_FOLDER = argVal("folder");
const CONNECT_TIMEOUT_MS = Number(argVal("timeout", "60000"));

// ── Config ──
const home = process.env.USERPROFILE || os.homedir();
const dbDir = path.join(home, ".rmfmail");
const accountsPath = path.join(dbDir, "accounts.jsonc");
if (!fs.existsSync(accountsPath)) {
    console.error(`accounts.jsonc not found at ${accountsPath}`);
    process.exit(1);
}

// JSONC: strip // and /* */ before parsing
function parseJsonc(raw: string): any {
    const stripped = raw
        .replace(/\/\*[\s\S]*?\*\//g, "")
        .replace(/^\s*\/\/.*$/gm, "");
    return JSON.parse(stripped);
}

const accountsCfg = parseJsonc(fs.readFileSync(accountsPath, "utf-8"));
const accounts = accountsCfg.accounts || [];

// ── Helpers ──
function ts(): string { return new Date().toISOString().replace("T", " ").slice(0, 19); }
// log() writes synchronously to stdout AND to the log file via direct fs.write
// — console.log when redirected to a file via shell `>` gets line-buffered,
// which delays heartbeats and timeout messages until a chunk fills. Direct
// fs.writeSync flushes per call.
const logPath = process.env.POPULATE_LOG_FILE || path.join(home, ".rmfmail", "logs", "populate-overnight.log");
let logFd: number | null = null;
try { logFd = fs.openSync(logPath, "a"); } catch { /* fall through to console only */ }
function log(msg: string): void {
    const line = `[${ts()}] ${msg}\n`;
    if (logFd !== null) {
        try { fs.writeSync(logFd, line); } catch { /* */ }
    }
    process.stdout.write(line);
}

// Skip Gmail / API-backed accounts — they don't speak IMAP through this
// tool's path, and Gmail's sync route is separate (REST). Same for any
// account without an `imap` block.
function shouldUseImap(acct: any): boolean {
    if (!acct?.imap?.host) return false;
    const host = String(acct.imap.host).toLowerCase();
    if (host.includes("gmail.com") || host.includes("googlemail.com")) return false;
    return true;
}

// Skip noselect / [Gmail] virtual folders / huge label folders (All Mail).
// Folders we DO want: every selectable real folder, including Sent / Drafts /
// custom user folders. The skip list is conservative.
//
// Optional: pass --skip <pattern>,<pattern2> to also skip folders whose
// names match any of the given prefixes/regexes. Useful for skipping slow
// archive folders like Added2.* on bobma where Dovecot serves envelopes at
// ~9s/msg — they'd block the run for hours.
const SKIP_PATTERNS = (argVal("skip", "") || "").split(",").map(s => s.trim()).filter(Boolean);
function matchesSkipPattern(name: string): string | null {
    for (const pat of SKIP_PATTERNS) {
        try {
            if (new RegExp(pat).test(name)) return pat;
        } catch { /* not a regex — try prefix */ }
        if (name.startsWith(pat)) return pat;
    }
    return null;
}
function shouldSkipFolder(folder: { id: number; name: string; path?: string; specialUse: string | null; flags: string[] }): string | null {
    const flags = folder.flags || [];
    if (flags.some(f => /noselect/i.test(f))) return "noselect";
    if (folder.name === "[Gmail]") return "Gmail virtual";
    // Match against the FULL PATH (e.g. "Added2.Dewayne") not just the leaf
    // name ("Dewayne"). Skip patterns like ^Added need to see the parent.
    const target = folder.path || folder.name;
    const m = matchesSkipPattern(target);
    if (m) return `--skip ${m}`;
    return null;
}

/** If --skip-populated is given, skip any folder that already has at least
 *  this many local rows. Lets a re-run focus on the un-populated ones. */
const SKIP_POPULATED_THRESHOLD = Number(argVal("skip-populated", "0"));

// Folder priority — process special-use first (Sent, Drafts, INBOX, Trash),
// then everything else alphabetically. Slow archive folders thus wait until
// the user-visible folders are done.
const SPECIAL_PRIORITY = ["sent", "drafts", "inbox", "trash", "junk"];
function folderPriority(f: { name: string; specialUse: string | null }): number {
    const su = (f.specialUse || "").toLowerCase();
    const i = SPECIAL_PRIORITY.indexOf(su);
    if (i >= 0) return i;
    if (f.name.toLowerCase() === "inbox") return SPECIAL_PRIORITY.indexOf("inbox");
    return SPECIAL_PRIORITY.length + 100;
}

/** Per-folder timeout — the populate is best-effort. If any single folder
 *  hasn't finished SELECT+FETCH within this window, skip it and continue.
 *  Avoids one slow folder wedging the entire run. Set generously — we'd
 *  rather give a slow folder a chance than skip it; reconnect-on-error
 *  recovers the connection state for the next folder either way. */
const PER_FOLDER_TIMEOUT_MS = Number(argVal("foldertimeout", "180000"));

function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
    return Promise.race([
        p,
        new Promise<T>((_, reject) =>
            setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
        ),
    ]);
}

// ── Main ──
async function populateAccount(db: MailxDB, account: any): Promise<{ folders: number; messages: number; failed: number }> {
    const folders = db.getFolders(account.id);
    if (folders.length === 0) {
        log(`  [${account.id}] no folders in DB — run mailx sync at least once first to populate the folder list`);
        return { folders: 0, messages: 0, failed: 0 };
    }

    const port = account.imap.port || 993;
    const useTls = account.imap.tls !== false;
    log(`  [${account.id}] connecting to ${account.imap.host}:${port} ${useTls ? "(TLS)" : "(plain)"}...`);

    const newClient = (): NativeImapClient => new NativeImapClient(
        {
            server: account.imap.host,
            port,
            username: account.imap.user || account.imap.username,
            password: account.imap.password,
            verbose: false,
            inactivityTimeout: CONNECT_TIMEOUT_MS,
            greetingTimeout: 30_000,
        },
        () => new NodeTcpTransport(),
    );
    let client: NativeImapClient = newClient();

    let counters = { folders: 0, messages: 0, failed: 0 };
    try {
        await client.connect();
        log(`  [${account.id}] connected.`);

        // Order: special-use folders first (Sent, Drafts, INBOX, Trash),
        // then everything else alphabetically. So if the run gets killed
        // partway, the user-facing folders are already populated.
        const targets = (ONLY_FOLDER
            ? folders.filter(f => f.name === ONLY_FOLDER || f.path === ONLY_FOLDER)
            : [...folders].sort((a, b) => {
                const pa = folderPriority(a as any);
                const pb = folderPriority(b as any);
                if (pa !== pb) return pa - pb;
                return a.name.localeCompare(b.name);
            }));
        log(`  [${account.id}] ${targets.length} folder(s) to process (special-use first)`);

        for (const folder of targets) {
            const skip = shouldSkipFolder(folder as any);
            if (skip) {
                log(`    skip ${folder.path || folder.name} (${skip})`);
                continue;
            }
            if (SKIP_POPULATED_THRESHOLD > 0) {
                const localCount = db.getMessageCount(account.id, folder.id);
                if (localCount >= SKIP_POPULATED_THRESHOLD) {
                    log(`    skip ${folder.path || folder.name} (already has ${localCount} local rows)`);
                    continue;
                }
            }
            const t0 = Date.now();
            try {
                const info = await withTimeout(
                    client.select(folder.path || folder.name),
                    PER_FOLDER_TIMEOUT_MS,
                    `SELECT ${folder.name}`,
                );
                const startUid = db.getHighestUid(account.id, folder.id);
                if (info.exists === 0) {
                    log(`    ${folder.name}: empty server-side`);
                    counters.folders++;
                    continue;
                }
                // Fetch the latest N by SEQUENCE number — sequence ranges
                // are dense (1..exists, no gaps from deletions), so the
                // server returns exactly N envelopes. fetchLatestN uses
                // sequence FETCH internally, with BODY.PEEK[HEADER] for
                // mailx's own field needs — that's fine for our purposes
                // and we get progress via the chunk callback.
                const before = Date.now();
                let chunkN = 0;
                const msgs = await withTimeout(
                    client.fetchLatestN(FETCH_N, { source: false }, (chunk) => {
                        chunkN += chunk.length;
                        if (chunkN > 0 && chunkN % 50 === 0) log(`      ${folder.name}: streamed ${chunkN}, ${Date.now() - before}ms in`);
                    }),
                    PER_FOLDER_TIMEOUT_MS,
                    `FETCH ${folder.name}`,
                );
                let stored = 0;
                for (const m of msgs) {
                    if (typeof m.uid !== "number" || m.uid <= 0) continue;
                    db.upsertMessage({
                        accountId: account.id,
                        folderId: folder.id,
                        uid: m.uid,
                        messageId: m.messageId || "",
                        inReplyTo: m.inReplyTo || "",
                        references: [],
                        date: m.date ? m.date.getTime() : 0,
                        subject: m.subject || "",
                        from: (m.from && m.from[0]) ? { name: m.from[0].name || "", address: m.from[0].address || "" } : { name: "", address: "" },
                        to: (m.to || []).map(a => ({ name: a.name || "", address: a.address || "" })),
                        cc: (m.cc || []).map(a => ({ name: a.name || "", address: a.address || "" })),
                        flags: Array.from(m.flags || []),
                        size: m.size || 0,
                        hasAttachments: false,        // populated by body-prefetch later
                        preview: "",                  // populated by body-prefetch later
                        bodyPath: "",                 // body not fetched here
                    });
                    stored++;
                }
                db.recalcFolderCounts(folder.id);
                counters.folders++;
                counters.messages += stored;
                const took = Date.now() - t0;
                log(`    ${folder.name}: server=${info.exists}, fetched=${msgs.length}, stored=${stored}, prevHighUid=${startUid} (${took}ms)`);
            } catch (e: any) {
                counters.failed++;
                log(`    ${folder.name}: ERROR ${e?.message || e}`);
                // Connection state is suspect after a timeout/error. Tear
                // it down and open a fresh one so the next folder doesn't
                // inherit the wedge. Skip-on-failure path; never bail.
                try { await client.logout(); } catch { /* */ }
                try {
                    client = newClient();
                    await client.connect();
                    log(`    [reconnect] ${account.id}: fresh connection after error`);
                } catch (reErr: any) {
                    log(`    [reconnect] FAILED: ${reErr?.message || reErr} — bailing on this account`);
                    break;
                }
            }
        }
    } finally {
        try { await client.logout(); } catch { /* */ }
    }
    return counters;
}

async function main(): Promise<void> {
    log(`mailx populate-folders starting`);
    log(`  config: dbDir=${dbDir} fetchN=${FETCH_N}${ONLY_ACCOUNT ? " account=" + ONLY_ACCOUNT : ""}${ONLY_FOLDER ? " folder=" + ONLY_FOLDER : ""}`);

    const db = new MailxDB(dbDir);
    let totalFolders = 0, totalMessages = 0, totalFailed = 0;

    for (const acct of accounts) {
        if (!acct.enabled && acct.enabled !== undefined) continue;
        if (ONLY_ACCOUNT && acct.id !== ONLY_ACCOUNT) continue;
        if (!shouldUseImap(acct)) {
            log(`[${acct.id}] skipped (not IMAP / Gmail-API account)`);
            continue;
        }
        log(`[${acct.id}] starting...`);
        try {
            const c = await populateAccount(db, acct);
            log(`[${acct.id}] done: ${c.folders} folders, ${c.messages} messages, ${c.failed} failed`);
            totalFolders += c.folders; totalMessages += c.messages; totalFailed += c.failed;
        } catch (e: any) {
            log(`[${acct.id}] FATAL ${e?.message || e}`);
        }
    }

    log(`---`);
    log(`Total: ${totalFolders} folders processed, ${totalMessages} messages stored, ${totalFailed} failures`);
    log(`Done. Restart rmfmail to see populated folders.`);
}

main().catch(e => {
    console.error("fatal:", e);
    process.exit(1);
});
