import { getSession } from "../lib/neo4j.js";
import { embedBatch } from "../lib/embeddings.js";
import { extractCache } from "./memory-ingest-extract.js";
import { deleteDocumentChildren } from "../lib/document-hierarchy.js";
export async function memoryIngest(params) {
    const { accountId, attachmentId, documentSummary, sections, scope, entities, sourceUrl, sourceType, keywords: rawKeywords, userKeywords: rawUserKeywords, } = params;
    if (!scope) {
        throw new Error("scope is required — valid values: 'public', 'shared', 'admin', 'user:{identifier}'");
    }
    // Normalize and merge keywords: user-supplied ∪ LLM-extracted, deduplicated.
    // User keywords appear first (cosmetic — both are equal after merge).
    const normalizeArray = (arr) => arr.map((k) => k.toLowerCase().trim()).filter(Boolean);
    const hasUserKeywords = rawUserKeywords && rawUserKeywords.length > 0;
    const hasKeywords = rawKeywords && rawKeywords.length > 0;
    const keywords = hasUserKeywords || hasKeywords
        ? [...new Set([
                ...normalizeArray(rawUserKeywords ?? []),
                ...normalizeArray(rawKeywords ?? []),
            ])]
        : undefined;
    const t0 = Date.now();
    const log = (stage, detail) => console.error(`[memory-ingest] [${attachmentId.slice(0, 8)}] ${stage}${detail ? ` — ${detail}` : ""} (${Date.now() - t0}ms)`);
    log("start", `${sections.length} sections, scope=${scope}`);
    // 1. Retrieve cached content from memory-ingest-extract
    const cached = extractCache.get(attachmentId);
    if (!cached) {
        throw new Error(`No cached extract found for attachment "${attachmentId}". ` +
            `Call memory-ingest-extract first to extract and chunk the document.`);
    }
    log("cache-hit", cached.filename);
    // Validate section count matches
    if (sections.length !== cached.sections.length) {
        throw new Error(`Section count mismatch: model provided ${sections.length} sections ` +
            `but the extracted document has ${cached.sections.length} sections. ` +
            `Provide exactly one summary per section returned by memory-ingest-extract.`);
    }
    // Validate chunk summary counts per section
    for (let i = 0; i < sections.length; i++) {
        const expectedChunks = cached.sections[i].chunks.length;
        const providedSummaries = sections[i].chunkSummaries.length;
        if (providedSummaries !== expectedChunks) {
            throw new Error(`Chunk count mismatch in section "${sections[i].title}": ` +
                `model provided ${providedSummaries} chunk summaries ` +
                `but the section has ${expectedChunks} chunks.`);
        }
    }
    const { filename, mimeType } = cached;
    const now = new Date().toISOString();
    // 2. Collect all texts that need embedding in a flat array.
    const textsToEmbed = [];
    // Document-level summary
    textsToEmbed.push(documentSummary);
    const docEmbedIdx = 0;
    // Section-level summaries
    const sectionEmbedIndices = [];
    for (const section of sections) {
        sectionEmbedIndices.push(textsToEmbed.length);
        textsToEmbed.push(section.summary);
    }
    // Chunk-level summaries
    const chunkEmbedIndices = [];
    for (let si = 0; si < sections.length; si++) {
        for (let ci = 0; ci < sections[si].chunkSummaries.length; ci++) {
            chunkEmbedIndices.push({
                sectionIdx: si,
                chunkIdx: ci,
                embedIdx: textsToEmbed.length,
            });
            textsToEmbed.push(sections[si].chunkSummaries[ci]);
        }
    }
    // 3. Batch embed all summaries in a single Ollama call.
    log("embedding", `${textsToEmbed.length} texts`);
    const embeddings = await embedBatch(textsToEmbed);
    log("embedded", `${embeddings.length} vectors`);
    // 4. Write nodes to Neo4j.
    log("neo4j-write", "starting");
    const session = getSession();
    let documentNodeId = "";
    let totalChunks = 0;
    let entityLinks = 0;
    try {
        // 4a. Create KnowledgeDocument node
        // Build optional SET clauses for web-sourced properties.
        // When sourceUrl/sourceType/keywords are undefined (file uploads),
        // the corresponding SET lines are omitted — existing values preserved on re-ingest.
        const optionalSets = [];
        const optionalParams = {};
        if (sourceUrl !== undefined) {
            optionalSets.push("d.sourceUrl = $sourceUrl");
            optionalParams.sourceUrl = sourceUrl;
        }
        if (sourceType !== undefined) {
            optionalSets.push("d.sourceType = $sourceType");
            optionalParams.sourceType = sourceType;
        }
        if (keywords !== undefined) {
            optionalSets.push("d.keywords = $keywords");
            optionalParams.keywords = keywords;
        }
        const optionalSetClause = optionalSets.length > 0
            ? ", " + optionalSets.join(", ")
            : "";
        const docResult = await session.run(`MERGE (d:KnowledgeDocument { attachmentId: $attachmentId })
       SET d.accountId      = $accountId,
           d.name           = $filename,
           d.encodingFormat  = $mimeType,
           d.summary         = $documentSummary,
           d.sectionCount    = $sectionCount,
           d.scope           = $scope,
           d.embedding       = $embedding,
           d.createdAt       = $createdAt,
           d.updatedAt       = $updatedAt
           ${optionalSetClause}
       RETURN elementId(d) AS nodeId`, {
            attachmentId,
            accountId,
            filename,
            mimeType,
            documentSummary,
            sectionCount: sections.length,
            scope,
            embedding: embeddings[docEmbedIdx],
            createdAt: now,
            updatedAt: now,
            ...optionalParams,
        });
        documentNodeId = docResult.records[0].get("nodeId");
        // 4a-cleanup. Remove stale children before re-creating.
        // On first ingestion the queries return nothing and the DELETEs are no-ops.
        // Also clear any soft-delete marker on the document itself (re-ingestion revives).
        const cleanup = await deleteDocumentChildren(attachmentId, session);
        if (cleanup.sections > 0 || cleanup.chunks > 0 || cleanup.references > 0) {
            log("cleanup", `deleted ${cleanup.sections} sections, ${cleanup.chunks} chunks, ${cleanup.references} references`);
        }
        // Clear deletedAt if the document was previously soft-deleted
        await session.run(`MATCH (d:KnowledgeDocument { attachmentId: $attachmentId })
       WHERE d.deletedAt IS NOT NULL
       REMOVE d.deletedAt`, { attachmentId });
        // 4b. Create Section nodes and link HAS_SECTION
        const sectionNodeIds = [];
        for (let si = 0; si < sections.length; si++) {
            const section = sections[si];
            const sectionResult = await session.run(`CREATE (s:Section {
           accountId:  $accountId,
           title:      $title,
           summary:    $summary,
           position:   $position,
           scope:      $scope,
           embedding:  $embedding,
           createdAt:  $createdAt,
           updatedAt:  $updatedAt
         })
         RETURN elementId(s) AS nodeId`, {
                accountId,
                title: section.title,
                summary: section.summary,
                position: si,
                scope,
                embedding: embeddings[sectionEmbedIndices[si]],
                createdAt: now,
                updatedAt: now,
            });
            const sectionNodeId = sectionResult.records[0].get("nodeId");
            sectionNodeIds.push(sectionNodeId);
            // Link KnowledgeDocument -[HAS_SECTION]-> Section
            await session.run(`MATCH (d), (s)
         WHERE elementId(d) = $docId AND elementId(s) = $sectionId
         CREATE (d)-[:HAS_SECTION]->(s)`, { docId: documentNodeId, sectionId: sectionNodeId });
        }
        // 4c. Create Chunk nodes and link HAS_CHUNK — content from cache
        for (const { sectionIdx, chunkIdx, embedIdx } of chunkEmbedIndices) {
            const cachedChunk = cached.sections[sectionIdx].chunks[chunkIdx];
            const chunkSummary = sections[sectionIdx].chunkSummaries[chunkIdx];
            const chunkResult = await session.run(`CREATE (c:Chunk {
           accountId:  $accountId,
           summary:    $summary,
           content:    $content,
           position:   $position,
           scope:      $scope,
           embedding:  $embedding,
           createdAt:  $createdAt,
           updatedAt:  $updatedAt
         })
         RETURN elementId(c) AS nodeId`, {
                accountId,
                summary: chunkSummary,
                content: cachedChunk.content,
                position: chunkIdx,
                scope,
                embedding: embeddings[embedIdx],
                createdAt: now,
                updatedAt: now,
            });
            const chunkNodeId = chunkResult.records[0].get("nodeId");
            totalChunks++;
            // Link Section -[HAS_CHUNK]-> Chunk
            await session.run(`MATCH (s), (c)
         WHERE elementId(s) = $sectionId AND elementId(c) = $chunkId
         CREATE (s)-[:HAS_CHUNK]->(c)`, { sectionId: sectionNodeIds[sectionIdx], chunkId: chunkNodeId });
        }
        // 4d. Create REFERENCES links to entities
        if (entities && entities.length > 0) {
            for (const entity of entities) {
                try {
                    await session.run(`MATCH (d), (e)
             WHERE elementId(d) = $docId AND elementId(e) = $entityId
             MERGE (d)-[:REFERENCES]->(e)`, { docId: documentNodeId, entityId: entity.nodeId });
                    entityLinks++;
                }
                catch {
                    // Entity node may have been deleted — skip silently, log via caller
                }
            }
        }
        log("neo4j-done", `doc=${documentNodeId.slice(0, 12)} sections=${sections.length} chunks=${totalChunks} entityLinks=${entityLinks}`);
        return {
            documentNodeId,
            sectionCount: sections.length,
            chunkCount: totalChunks,
            entityLinks,
            documentSummary,
            keywords,
        };
    }
    finally {
        // Evict cache entry — content is now persisted in Neo4j
        extractCache.delete(attachmentId);
        await session.close();
        log("complete");
    }
}
//# sourceMappingURL=memory-ingest.js.map