import { readFileSync, writeFileSync, mkdirSync, unlinkSync, chmodSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { getSession } from "./neo4j.js";
import neo4j from "neo4j-driver";
const PASSWORD_FILE = join(homedir(), ".maxy", ".email-password");
/**
 * Read the email password from the credential file.
 * Throws if the file does not exist or is empty.
 */
export function readPassword() {
    try {
        const pw = readFileSync(PASSWORD_FILE, "utf-8").trim();
        if (!pw)
            throw new Error("Password file is empty");
        return pw;
    }
    catch (err) {
        if (err.code === "ENOENT") {
            throw new Error(`Email password not configured. Expected at ${PASSWORD_FILE}. Run email-setup first.`);
        }
        throw err;
    }
}
/**
 * Write the email password to the credential file (mode 0600).
 */
export function writePassword(password) {
    mkdirSync(dirname(PASSWORD_FILE), { recursive: true });
    writeFileSync(PASSWORD_FILE, password, { mode: 0o600 });
    chmodSync(PASSWORD_FILE, 0o600);
}
/**
 * Remove the password file (for rollback on partial setup failure).
 */
export function removePassword() {
    try {
        unlinkSync(PASSWORD_FILE);
    }
    catch {
        // File may not exist — that's fine
    }
}
/**
 * Read the EmailAccount config from the graph.
 * Returns null if no EmailAccount node exists for this account.
 */
export async function readConfig(accountId) {
    const session = getSession();
    try {
        const result = await session.run(`MATCH (e:EmailAccount {accountId: $accountId}) RETURN e LIMIT 1`, { accountId });
        if (result.records.length === 0)
            return null;
        const props = result.records[0].get("e").properties;
        return {
            email: props.email,
            agentAddress: props.agentAddress ?? props.email,
            imapHost: props.imapHost,
            imapPort: neo4j.integer.toNumber(props.imapPort),
            imapSecurity: props.imapSecurity,
            smtpHost: props.smtpHost,
            smtpPort: neo4j.integer.toNumber(props.smtpPort),
            smtpSecurity: props.smtpSecurity,
        };
    }
    finally {
        await session.close();
    }
}
/**
 * Write or update the EmailAccount config in the graph.
 * Uses MERGE to be idempotent — calling setup again overwrites the config.
 *
 * When agentAddress changes, updates recipientAddress on all existing Email
 * nodes for this account so stored emails remain consistent with the current
 * config. This prevents stale recipientAddress values from causing confusion
 * in queries (dedup is handled separately by the messageId pre-check).
 */
export async function writeConfig(accountId, config) {
    const oldConfig = await readConfig(accountId);
    const session = getSession();
    try {
        await session.run(`MERGE (e:EmailAccount {accountId: $accountId})
       SET e.email = $email,
           e.agentAddress = $agentAddress,
           e.imapHost = $imapHost,
           e.imapPort = $imapPort,
           e.imapSecurity = $imapSecurity,
           e.smtpHost = $smtpHost,
           e.smtpPort = $smtpPort,
           e.smtpSecurity = $smtpSecurity,
           e.updatedAt = $updatedAt`, {
            accountId,
            email: config.email,
            agentAddress: config.agentAddress,
            imapHost: config.imapHost,
            imapPort: neo4j.int(config.imapPort),
            imapSecurity: config.imapSecurity,
            smtpHost: config.smtpHost,
            smtpPort: neo4j.int(config.smtpPort),
            smtpSecurity: config.smtpSecurity,
            updatedAt: new Date().toISOString(),
        });
        // When agentAddress changes, update recipientAddress on existing emails
        // so stored data stays consistent with the current config.
        if (oldConfig && oldConfig.agentAddress !== config.agentAddress) {
            const updateResult = await session.run(`MATCH (e:Email {accountId: $accountId, recipientAddress: $oldAddress})
         SET e.recipientAddress = $newAddress
         RETURN count(e) AS updated`, {
                accountId,
                oldAddress: oldConfig.agentAddress,
                newAddress: config.agentAddress,
            });
            const updated = neo4j.integer.toNumber(updateResult.records[0]?.get("updated") ?? 0);
            if (updated > 0) {
                console.error(`[email-setup] INFO: updated recipientAddress on ${updated} Email node(s): ${oldConfig.agentAddress} → ${config.agentAddress}`);
            }
        }
    }
    finally {
        await session.close();
    }
}
/**
 * Remove the EmailAccount node from the graph (for rollback).
 */
export async function removeConfig(accountId) {
    const session = getSession();
    try {
        await session.run(`MATCH (e:EmailAccount {accountId: $accountId}) DETACH DELETE e`, { accountId });
    }
    finally {
        await session.close();
    }
}
/**
 * Read poll state from the EmailAccount node.
 * Returns defaults if properties are not yet set (first run).
 */
export async function readPollState(accountId) {
    const session = getSession();
    try {
        const result = await session.run(`MATCH (e:EmailAccount {accountId: $accountId}) RETURN e LIMIT 1`, { accountId });
        if (result.records.length === 0)
            return null;
        const props = result.records[0].get("e").properties;
        return {
            agentSlug: props.agentSlug ?? null,
            pollIntervalMinutes: props.pollIntervalMinutes
                ? neo4j.integer.toNumber(props.pollIntervalMinutes)
                : 5,
            lastFetchedUid: props.lastFetchedUid
                ? neo4j.integer.toNumber(props.lastFetchedUid)
                : 0,
            lastPollAt: props.lastPollAt ?? null,
            uidValidity: props.uidValidity
                ? neo4j.integer.toNumber(props.uidValidity)
                : null,
        };
    }
    finally {
        await session.close();
    }
}
/**
 * Update poll tracking state after a successful fetch cycle.
 */
export async function updatePollState(accountId, lastFetchedUid, uidValidity) {
    const session = getSession();
    try {
        await session.run(`MATCH (e:EmailAccount {accountId: $accountId})
       SET e.lastFetchedUid = $lastFetchedUid,
           e.lastPollAt = $lastPollAt,
           e.uidValidity = $uidValidity`, {
            accountId,
            lastFetchedUid: neo4j.int(lastFetchedUid),
            lastPollAt: new Date().toISOString(),
            uidValidity: neo4j.int(uidValidity),
        });
    }
    finally {
        await session.close();
    }
}
/**
 * Set the agentSlug and poll config on an EmailAccount.
 * Called during email-setup to bind an agent to an email address.
 */
export async function writeBindingConfig(accountId, agentSlug, pollIntervalMinutes) {
    const session = getSession();
    try {
        await session.run(`MATCH (e:EmailAccount {accountId: $accountId})
       SET e.agentSlug = $agentSlug,
           e.pollIntervalMinutes = $pollIntervalMinutes`, {
            accountId,
            agentSlug,
            pollIntervalMinutes: neo4j.int(pollIntervalMinutes),
        });
    }
    finally {
        await session.close();
    }
}
/**
 * Check if an email address is already bound to a different agent.
 * Returns the conflicting agentSlug if claimed, or null if available.
 */
export async function checkAddressBinding(agentAddress, currentSlug) {
    const session = getSession();
    try {
        const result = await session.run(`MATCH (e:EmailAccount)
       WHERE e.agentAddress = $agentAddress
         AND e.agentSlug IS NOT NULL
         AND e.agentSlug <> $currentSlug
       RETURN e.agentSlug AS slug LIMIT 1`, { agentAddress, currentSlug });
        if (result.records.length === 0)
            return null;
        return result.records[0].get("slug");
    }
    finally {
        await session.close();
    }
}
/**
 * Read auto-respond config from the EmailAccount node.
 * Returns defaults if fields are not yet set.
 */
export async function readAutoRespondConfig(accountId) {
    const session = getSession();
    try {
        const result = await session.run(`MATCH (e:EmailAccount {accountId: $accountId}) RETURN e LIMIT 1`, { accountId });
        if (result.records.length === 0)
            return null;
        const props = result.records[0].get("e").properties;
        return {
            autoRespond: props.autoRespond === true,
            respondingAgentSlug: props.respondingAgentSlug ?? null,
            pollIntervalMinutes: props.pollIntervalMinutes ? neo4j.integer.toNumber(props.pollIntervalMinutes) : 5,
            lastRespondedUid: props.lastRespondedUid ? neo4j.integer.toNumber(props.lastRespondedUid) : 0,
            lastPollAt: props.lastPollAt ?? null,
            consecutiveFailures: props.consecutiveFailures ? neo4j.integer.toNumber(props.consecutiveFailures) : 0,
            autoRespondHourlyLimit: props.autoRespondHourlyLimit ? neo4j.integer.toNumber(props.autoRespondHourlyLimit) : 20,
            autoRespondDailyLimit: props.autoRespondDailyLimit ? neo4j.integer.toNumber(props.autoRespondDailyLimit) : 100,
        };
    }
    finally {
        await session.close();
    }
}
/**
 * Write auto-respond config fields on the EmailAccount node.
 * Only updates the specified fields — does not touch email/IMAP/SMTP config.
 */
export async function writeAutoRespondConfig(accountId, config) {
    const session = getSession();
    try {
        const setClauses = [];
        const params = { accountId };
        if (config.autoRespond !== undefined) {
            setClauses.push("e.autoRespond = $autoRespond");
            params.autoRespond = config.autoRespond;
        }
        if (config.respondingAgentSlug !== undefined) {
            setClauses.push("e.respondingAgentSlug = $respondingAgentSlug");
            params.respondingAgentSlug = config.respondingAgentSlug;
        }
        if (config.pollIntervalMinutes !== undefined) {
            setClauses.push("e.pollIntervalMinutes = $pollIntervalMinutes");
            params.pollIntervalMinutes = neo4j.int(config.pollIntervalMinutes);
        }
        if (config.lastRespondedUid !== undefined) {
            setClauses.push("e.lastRespondedUid = $lastRespondedUid");
            params.lastRespondedUid = neo4j.int(config.lastRespondedUid);
        }
        if (config.lastPollAt !== undefined) {
            setClauses.push("e.lastPollAt = $lastPollAt");
            params.lastPollAt = config.lastPollAt;
        }
        if (config.consecutiveFailures !== undefined) {
            setClauses.push("e.consecutiveFailures = $consecutiveFailures");
            params.consecutiveFailures = neo4j.int(config.consecutiveFailures);
        }
        if (config.autoRespondHourlyLimit !== undefined) {
            setClauses.push("e.autoRespondHourlyLimit = $autoRespondHourlyLimit");
            params.autoRespondHourlyLimit = neo4j.int(config.autoRespondHourlyLimit);
        }
        if (config.autoRespondDailyLimit !== undefined) {
            setClauses.push("e.autoRespondDailyLimit = $autoRespondDailyLimit");
            params.autoRespondDailyLimit = neo4j.int(config.autoRespondDailyLimit);
        }
        if (setClauses.length === 0)
            return;
        await session.run(`MATCH (e:EmailAccount {accountId: $accountId}) SET ${setClauses.join(", ")}`, params);
    }
    finally {
        await session.close();
    }
}
/**
 * Read all EmailAccounts that have auto-respond enabled.
 * Returns config + auto-respond settings for each.
 */
export async function readAutoRespondAccounts() {
    const session = getSession();
    try {
        const result = await session.run(`MATCH (e:EmailAccount {autoRespond: true}) RETURN e`);
        return result.records.map((record) => {
            const props = record.get("e").properties;
            return {
                accountId: props.accountId,
                config: {
                    email: props.email,
                    agentAddress: props.agentAddress ?? props.email,
                    imapHost: props.imapHost,
                    imapPort: neo4j.integer.toNumber(props.imapPort),
                    imapSecurity: props.imapSecurity,
                    smtpHost: props.smtpHost,
                    smtpPort: neo4j.integer.toNumber(props.smtpPort),
                    smtpSecurity: props.smtpSecurity,
                },
                autoRespond: {
                    autoRespond: true,
                    respondingAgentSlug: props.respondingAgentSlug ?? null,
                    pollIntervalMinutes: props.pollIntervalMinutes ? neo4j.integer.toNumber(props.pollIntervalMinutes) : 5,
                    lastRespondedUid: props.lastRespondedUid ? neo4j.integer.toNumber(props.lastRespondedUid) : 0,
                    lastPollAt: props.lastPollAt ?? null,
                    consecutiveFailures: props.consecutiveFailures ? neo4j.integer.toNumber(props.consecutiveFailures) : 0,
                    autoRespondHourlyLimit: props.autoRespondHourlyLimit ? neo4j.integer.toNumber(props.autoRespondHourlyLimit) : 20,
                    autoRespondDailyLimit: props.autoRespondDailyLimit ? neo4j.integer.toNumber(props.autoRespondDailyLimit) : 100,
                },
            };
        });
    }
    finally {
        await session.close();
    }
}
/**
 * Load full credentials (config + password).
 * Returns null with a reason if either piece is missing.
 */
export async function loadCredentials(accountId) {
    const config = await readConfig(accountId);
    if (!config) {
        return { error: "Email not configured. Run email-setup to set up the agent's email account." };
    }
    try {
        const password = readPassword();
        return { credentials: { config, password } };
    }
    catch {
        return {
            error: `Email account ${config.email} is configured but the password file is missing. Run email-setup again.`,
        };
    }
}
//# sourceMappingURL=credentials.js.map