import neo4j from "neo4j-driver";
import { getSession } from "../lib/neo4j.js";
import { computeEmbedding } from "../lib/embedding.js";
import { getThreadInfo, stripAngleBrackets } from "../lib/graph.js";
/**
 * Query Email nodes stored in the graph.
 *
 * Two modes:
 *   - List (no query): Cypher MATCH with optional filters, sorted by receivedAt DESC
 *   - Search (query provided): embed via Ollama, vector similarity on email_embedding index
 *
 * Always scoped to accountId. Thread info enriched when available.
 */
export async function emailGraphQuery(params) {
    const limit = Math.min(Math.max(1, params.limit ?? 20), 50);
    const offset = Math.max(0, params.offset ?? 0);
    const query = params.query?.trim();
    let emails;
    if (query && query.length > 0) {
        emails = await searchMode(query, params, limit);
    }
    else {
        emails = await listMode(params, limit, offset);
    }
    if (emails.length === 0) {
        return formatEmptyResult(params, query);
    }
    // Enrich with thread info (best-effort)
    const messageIds = emails
        .map((e) => e.messageId)
        .filter((id) => !!id);
    let threadMap = new Map();
    try {
        threadMap = await getThreadInfo(messageIds, params.accountId);
    }
    catch {
        // Thread enrichment is optional — continue without it
    }
    const lines = emails
        .map((e) => {
        let entry = `From: ${e.fromName ? `${e.fromName} <${e.fromAddress}>` : e.fromAddress}`;
        entry += `\nSubject: ${e.subject}`;
        entry += `\nDate: ${e.receivedAt}`;
        if (e.direction === "outbound")
            entry += `\nDirection: outbound`;
        if (e.messageId)
            entry += `\nMessage-ID: ${e.messageId}`;
        if (e.inReplyTo)
            entry += `\nIn-Reply-To: ${e.inReplyTo}`;
        if (e.score !== undefined)
            entry += `\nScore: ${e.score.toFixed(3)}`;
        const normalisedMid = stripAngleBrackets(e.messageId);
        if (normalisedMid) {
            const thread = threadMap.get(normalisedMid);
            if (thread) {
                entry += `\nThread-Depth: ${thread.threadDepth}`;
                entry += `\nThread-ID: ${thread.threadId}`;
            }
        }
        if (e.bodyPreview)
            entry += `\n\n${e.bodyPreview}`;
        return entry + "\n---";
    })
        .join("\n\n");
    // Pagination hint for list mode only (search mode is ranked by similarity, not paginated)
    if (!query && emails.length === limit) {
        return `${lines}\n\nPagination: next offset=${offset + limit} (pass as offset to get more results)`;
    }
    return lines;
}
// ---------------------------------------------------------------------------
// List mode — Cypher query with optional filters
// ---------------------------------------------------------------------------
async function listMode(params, limit, offset) {
    // Exclude discarded emails (spam) from list results. Legacy emails without
    // a screening property (IS NULL) are included — only explicitly discarded
    // emails are hidden. Admin can still find them via direct Cypher.
    const conditions = [
        "e.accountId = $accountId",
        "(e.screening <> 'discarded' OR e.screening IS NULL)",
    ];
    const cyParams = {
        accountId: params.accountId,
        limit: neo4j.int(limit),
        offset: neo4j.int(offset),
    };
    if (params.sender) {
        conditions.push("toLower(e.fromAddress) CONTAINS toLower($sender)");
        cyParams.sender = params.sender;
    }
    if (params.dateFrom) {
        conditions.push("e.receivedAt >= $dateFrom");
        cyParams.dateFrom = params.dateFrom;
    }
    if (params.dateTo) {
        conditions.push("e.receivedAt <= $dateTo");
        cyParams.dateTo = params.dateTo;
    }
    if (params.direction === "outbound") {
        conditions.push("e.direction = 'outbound'");
    }
    else if (params.direction === "inbound") {
        conditions.push("e.direction IS NULL");
    }
    const cypher = `
    MATCH (e:Email)
    WHERE ${conditions.join(" AND ")}
    RETURN e
    ORDER BY e.receivedAt DESC
    SKIP $offset
    LIMIT $limit
  `;
    const session = getSession();
    try {
        const result = await session.run(cypher, cyParams);
        return result.records.map((r) => nodeToRecord(r.get("e")));
    }
    finally {
        await session.close();
    }
}
// ---------------------------------------------------------------------------
// Search mode — vector similarity on email_embedding index
// ---------------------------------------------------------------------------
async function searchMode(query, params, limit) {
    const embedding = await computeEmbedding(query);
    if (embedding.length === 0) {
        throw new Error("Semantic search unavailable — the embedding service is not responding. " +
            "Try again without a query to list emails instead.");
    }
    // Over-fetch to account for accountId filtering, then trim
    const topK = Math.min(limit * 3, 150);
    const session = getSession();
    try {
        const result = await session.run(`CALL db.index.vector.queryNodes('email_embedding', $topK, $embedding)
       YIELD node, score
       WHERE node.accountId = $accountId
       RETURN node, score
       ORDER BY score DESC
       LIMIT $limit`, {
            topK: neo4j.int(topK),
            embedding,
            accountId: params.accountId,
            limit: neo4j.int(limit),
        });
        return result.records.map((r) => {
            const record = nodeToRecord(r.get("node"));
            record.score = r.get("score");
            return record;
        });
    }
    finally {
        await session.close();
    }
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function nodeToRecord(node) {
    const p = node.properties;
    return {
        emailId: p.emailId,
        messageId: p.messageId ?? null,
        inReplyTo: p.inReplyTo ?? null,
        fromAddress: p.fromAddress,
        fromName: p.fromName ?? null,
        toAddresses: p.toAddresses ?? [],
        subject: p.subject,
        bodyPreview: p.bodyPreview ?? "",
        receivedAt: p.receivedAt,
        direction: p.direction ?? null,
    };
}
function formatEmptyResult(params, query) {
    const filters = [];
    if (query)
        filters.push(`query: "${query}"`);
    if (params.sender)
        filters.push(`sender: ${params.sender}`);
    if (params.dateFrom)
        filters.push(`from: ${params.dateFrom}`);
    if (params.dateTo)
        filters.push(`to: ${params.dateTo}`);
    if (params.direction)
        filters.push(`direction: ${params.direction}`);
    return `No emails found in the graph${filters.length > 0 ? ` matching ${filters.join(", ")}` : ""}.`;
}
//# sourceMappingURL=email-graph-query.js.map