/**
 * Dispatcher script: email-fetch
 *
 * Polls IMAP for new emails and persists them as Email nodes in the graph.
 * Standalone dispatcher. Not fired automatically on a running install — the
 * installer-registered cron entry was removed and the replacement
 * surface (Claude Code's desktop scheduled tasks, per maxy-code-prd.md
 * §Scheduled tasks) is not yet wired. Until that lands, this binary runs only
 * when invoked manually or by a future scheduled-task dispatcher. When invoked,
 * checks pollIntervalMinutes before fetching.
 *
 * Flow:
 *   1. Acquire PID lock (skip if another instance is running)
 *   2. Load EmailAccount config + poll state
 *   3. Check if enough time has elapsed since lastPollAt
 *   4. Connect to IMAP, fetch emails since lastFetchedUid
 *   5. Handle UIDVALIDITY changes (reset high-water mark, rely on MERGE dedup)
 *   6. Classify each email via Haiku (clean/suspicious/discard)
 *   7. Store each email as an Email node (MERGE on emailId) with screening verdict
 *      — discarded emails are stored without embedding (no vector pollution)
 *   8. Update high-water mark (lastFetchedUid, lastPollAt, uidValidity)
 *   9. Release lock
 *
 * Failures are logged but never crash the platform — next cycle retries
 * from the same high-water mark.
 *
 *   PLATFORM_ROOT=... ACCOUNT_ID=... node dist/scripts/email-fetch.js
 */
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, appendFileSync } from "node:fs";
import { resolve } from "node:path";
import { homedir } from "node:os";
import { loadCredentials, readPollState, updatePollState } from "../lib/credentials.js";
import { fetchSinceUid } from "../lib/imap.js";
import { storeEmailMessages } from "../lib/graph.js";
import { closeDriver } from "../lib/neo4j.js";
// Task 424 — screening.ts deleted. Task 432 replaces this fetch script with an
// operator-initiated email-ingest flow; until then, every fetched email lands
// with screening='unscreened' so downstream consumers know classification was
// skipped.
// ---------------------------------------------------------------------------
// Brand-aware config directory — reads configDir from brand.json stamped at install time.
// No fallback: if brand.json is missing or incomplete, the platform wasn't properly installed.
// ---------------------------------------------------------------------------
function resolveConfigDir() {
    const platformRoot = process.env.PLATFORM_ROOT;
    if (!platformRoot) {
        throw new Error("PLATFORM_ROOT environment variable is required");
    }
    const brandPath = resolve(platformRoot, "config", "brand.json");
    if (!existsSync(brandPath)) {
        throw new Error(`brand.json not found at ${brandPath} — platform not properly installed`);
    }
    try {
        const brand = JSON.parse(readFileSync(brandPath, "utf-8"));
        if (!brand.configDir) {
            throw new Error(`brand.json at ${brandPath} is missing the configDir field`);
        }
        return brand.configDir;
    }
    catch (err) {
        if (err instanceof SyntaxError) {
            throw new Error(`brand.json at ${brandPath} is not valid JSON: ${err.message}`);
        }
        throw err;
    }
}
const CONFIG_DIR = resolve(homedir(), resolveConfigDir());
// ---------------------------------------------------------------------------
// PID lock — prevents overlapping cron runs
// ---------------------------------------------------------------------------
const LOCK_DIR = CONFIG_DIR;
const LOCK_FILE = resolve(LOCK_DIR, "email-fetch.lock");
function acquireLock() {
    mkdirSync(LOCK_DIR, { recursive: true });
    if (existsSync(LOCK_FILE)) {
        const storedPid = readFileSync(LOCK_FILE, "utf-8").trim();
        try {
            process.kill(Number(storedPid), 0);
            return false; // Process exists — lock is held
        }
        catch {
            unlinkSync(LOCK_FILE); // Stale lock
        }
    }
    writeFileSync(LOCK_FILE, String(process.pid));
    return true;
}
function releaseLock() {
    try {
        if (existsSync(LOCK_FILE)) {
            const storedPid = readFileSync(LOCK_FILE, "utf-8").trim();
            if (storedPid === String(process.pid)) {
                unlinkSync(LOCK_FILE);
            }
        }
    }
    catch {
        // Best-effort cleanup
    }
}
// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------
const LOG_DIR = resolve(CONFIG_DIR, "logs");
const LOG_FILE = resolve(LOG_DIR, "email-fetch.log");
function log(level, msg) {
    const ts = new Date().toISOString();
    const line = `[${ts}] [${level}] ${msg}`;
    console.error(`[email-fetch] ${level}: ${msg} [${ts}]`);
    try {
        mkdirSync(LOG_DIR, { recursive: true });
        appendFileSync(LOG_FILE, line + "\n");
    }
    catch {
        // Best-effort file logging
    }
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
    const accountId = process.env.ACCOUNT_ID;
    if (!accountId) {
        log("ERROR", "ACCOUNT_ID environment variable is required");
        process.exit(1);
    }
    if (!acquireLock()) {
        log("INFO", "another instance running, exiting");
        process.exit(0);
    }
    log("INFO", "lock acquired");
    try {
        // Load credentials
        const credResult = await loadCredentials(accountId);
        if ("error" in credResult) {
            log("INFO", `no credentials configured — ${credResult.error}`);
            return;
        }
        const { config, password } = credResult.credentials;
        // Load poll state
        const pollState = await readPollState(accountId);
        if (!pollState) {
            log("INFO", "no EmailAccount node found, exiting");
            return;
        }
        // Check if enough time has elapsed since the last poll
        if (pollState.lastPollAt) {
            const elapsed = Date.now() - new Date(pollState.lastPollAt).getTime();
            const intervalMs = pollState.pollIntervalMinutes * 60 * 1000;
            if (elapsed < intervalMs) {
                log("INFO", `${Math.round(elapsed / 1000)}s since last poll, interval is ${pollState.pollIntervalMinutes}m — skipping`);
                return;
            }
        }
        // Fetch new emails from IMAP
        let sinceUid = pollState.lastFetchedUid;
        log("INFO", `fetching since UID ${sinceUid} for ${config.agentAddress}`);
        let { uidValidity, emails, maxUid } = await fetchSinceUid(config, password, sinceUid, config.agentAddress);
        // UIDVALIDITY change: old UIDs are meaningless. Re-fetch from 0 (7-day
        // backfill). MERGE on Message-ID prevents duplicate Email nodes.
        if (pollState.uidValidity !== null && pollState.uidValidity !== uidValidity) {
            log("WARN", `UIDVALIDITY changed: ${pollState.uidValidity} → ${uidValidity} — re-fetching from scratch`);
            ({ uidValidity, emails, maxUid } = await fetchSinceUid(config, password, 0, config.agentAddress));
        }
        if (emails.length === 0) {
            log("INFO", "no new emails");
            // Still update poll timestamp so we don't re-check immediately
            await updatePollState(accountId, maxUid || sinceUid, uidValidity);
            return;
        }
        // Task 424 — per-email Haiku screening removed. Operator-initiated
        // triage replaces it under Task 432.
        const emailData = emails.map((e) => ({
            messageId: e.messageId,
            inReplyTo: e.inReplyTo,
            toAddresses: e.toAddresses,
            ccAddresses: e.ccAddresses,
            uid: e.uid,
            uidValidity,
            mailbox: "INBOX",
            recipientAddress: config.agentAddress,
            fromAddress: e.fromAddress,
            fromName: e.fromName,
            subject: e.subject,
            bodyPreview: e.bodyPreview,
            receivedAt: e.date,
            accountId,
            screening: "unscreened",
            screeningReason: "Task 424 — screener removed; awaiting Task 432 operator-initiated triage",
        }));
        log("INFO", `fetched ${emailData.length} email(s); per-email classification skipped (operator-initiated triage pending)`);
        // Sort by receivedAt ascending so thread transcripts read chronologically
        // (storeEmailMessage appends in input order).
        emailData.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
        // Store in graph as :KnowledgeDocument {source:'email'} per thread.
        const { threadsCreated, messagesAppended, duplicates } = await storeEmailMessages(emailData);
        log("INFO", `stored ${threadsCreated} new threads, ${messagesAppended} messages appended to existing threads, ${duplicates} duplicates skipped`);
        // Update high-water mark
        await updatePollState(accountId, maxUid, uidValidity);
        log("INFO", `high-water mark updated to UID ${maxUid}, uidValidity=${uidValidity}`);
    }
    catch (err) {
        log("ERROR", `${err instanceof Error ? err.message : String(err)}`);
    }
    finally {
        await closeDriver();
        releaseLock();
    }
}
main().then(() => process.exit(0)).catch((err) => {
    log("FATAL", `${err instanceof Error ? err.message : String(err)}`);
    releaseLock();
    process.exit(1);
});
//# sourceMappingURL=email-fetch.js.map