import { randomUUID } from "node:crypto";
import { getDriver } from "../lib/neo4j.js";
import { computeEmbedding } from "../lib/embeddings.js";
import { stampCreatedBy, } from "../../../../../lib/graph-write/dist/index.js";
/**
 * Validates that blocks references form a DAG (no cycles).
 * Uses Kahn's algorithm (topological sort) on the workItems array.
 * Throws ValidationError if cycles detected or indices out of range.
 */
function validateBlocks(workItems) {
    const n = workItems.length;
    // Build adjacency list and in-degree array
    const adj = Array.from({ length: n }, () => []);
    const inDegree = new Array(n).fill(0);
    for (let i = 0; i < n; i++) {
        const blocks = workItems[i].blocks;
        if (!blocks)
            continue;
        for (const target of blocks) {
            if (target < 0 || target >= n || !Number.isInteger(target)) {
                throw new Error(`ValidationError: blocks index ${target} out of range (0-${n - 1})`);
            }
            if (target === i) {
                throw new Error(`ValidationError: work item ${i} cannot block itself`);
            }
            // i blocks target → edge from i to target
            adj[i].push(target);
            inDegree[target]++;
        }
    }
    // Kahn's algorithm — process nodes with in-degree 0
    const queue = [];
    for (let i = 0; i < n; i++) {
        if (inDegree[i] === 0)
            queue.push(i);
    }
    let processed = 0;
    while (queue.length > 0) {
        const node = queue.shift();
        processed++;
        for (const neighbor of adj[node]) {
            inDegree[neighbor]--;
            if (inDegree[neighbor] === 0)
                queue.push(neighbor);
        }
    }
    if (processed < n) {
        throw new Error("ValidationError: circular dependency detected in work item blocks");
    }
}
export async function projectCreate(params) {
    const { accountId, name, description, tier, clientRef, targetDate, dueDate, raisedBy, affects, workItems = [], sessionKey, createdBy, } = params;
    // --- Validation ---
    if (workItems.length > 50) {
        throw new Error("ValidationError: workItems exceeds maximum of 50");
    }
    if (workItems.length > 0) {
        validateBlocks(workItems);
    }
    // --- Pre-compute embeddings (outside transaction) ---
    const parentText = `Project: ${name} — ${description} tier: ${tier}`;
    const parentEmbedding = await computeEmbedding(parentText);
    const childEmbeddings = [];
    for (const item of workItems) {
        const text = `Task: ${item.name} — ${item.description} status: pending priority: ${item.priority ?? "normal"}`;
        childEmbeddings.push(await computeEmbedding(text));
    }
    // --- Build node properties ---
    const projectId = randomUUID();
    const now = new Date().toISOString();
    const timestamp = now.slice(0, 16).replace("T", " ");
    const startNote = `[${timestamp}] [PROJECT:START] ${name} — ${tier} tier, ${workItems.length} tasks planned`;
    const rawProjectProps = {
        taskId: projectId,
        accountId,
        name,
        description,
        status: "pending",
        priority: "normal",
        tier,
        phase: "planning",
        notes: startNote,
        createdAt: now,
        updatedAt: now,
    };
    if (clientRef)
        rawProjectProps.clientRef = clientRef;
    if (targetDate)
        rawProjectProps.targetDate = targetDate;
    if (dueDate)
        rawProjectProps.dueDate = dueDate;
    if (parentEmbedding.length > 0)
        rawProjectProps.embedding = parentEmbedding;
    // Write doctrine: flatten provenance onto the node
    // so forensic queries (WHERE n.createdByTool = 'project-create') work.
    // project-create inlines the doctrine instead of routing through
    // writeNodeWithEdges because the outer transaction must encompass
    // child-Task and BLOCKS/RAISED_BY/AFFECTS creation — nested
    // executeWrite is not permitted on a session that already has a
    // managed tx open.
    const projectProps = stampCreatedBy(rawProjectProps, createdBy);
    const childIds = [];
    const childPropsList = [];
    for (let i = 0; i < workItems.length; i++) {
        const item = workItems[i];
        const childId = randomUUID();
        childIds.push(childId);
        const rawChildProps = {
            taskId: childId,
            accountId,
            name: item.name,
            description: item.description,
            status: "pending",
            priority: item.priority ?? "normal",
            createdAt: now,
            updatedAt: now,
        };
        if (item.dueDate)
            rawChildProps.dueDate = item.dueDate;
        if (childEmbeddings[i].length > 0)
            rawChildProps.embedding = childEmbeddings[i];
        // Stamp the same provenance on children. Each child has its own
        // adjacency (HAS_TASK back to the Project) so no per-child
        // pre-resolve is needed — the edge is co-created below.
        childPropsList.push(stampCreatedBy(rawChildProps, createdBy));
    }
    // --- Single transaction: create all nodes and relationships ---
    const driver = getDriver();
    const session = driver.session();
    const tx = session.beginTransaction();
    try {
        // Write doctrine pre-validation: resolve every
        // adjacency source in one UNWIND round-trip BEFORE the Project is
        // created. A Project that has no workItems, no raisedBy, no affects,
        // and no admin-session Conversation is noise — reject loudly per
        // doctrine. This mirrors writeNodeWithEdges' pre-check idiom but
        // stays inside the outer tx so child-Task failures still roll the
        // Project back atomically.
        const candidateIds = [];
        if (raisedBy)
            candidateIds.push(raisedBy);
        if (affects && affects.length > 0)
            candidateIds.push(...affects);
        let resolvedSessionId = null;
        if (sessionKey) {
            const convRes = await tx.run(`MATCH (c:Conversation {sessionKey: $sessionKey, accountId: $accountId})
         RETURN elementId(c) AS id LIMIT 1`, { sessionKey, accountId });
            if (convRes.records.length > 0) {
                resolvedSessionId = convRes.records[0].get("id");
            }
        }
        if (candidateIds.length > 0) {
            const check = await tx.run(`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN collect(id) AS found`, { ids: candidateIds });
            const found = check.records[0].get("found");
            const missing = candidateIds.filter((id) => !found.includes(id));
            if (missing.length > 0) {
                throw new Error(`TargetNotFound: ${missing.length} of ${candidateIds.length} relationship target(s) did not resolve (elementId mismatch): ${missing.join(", ")}`);
            }
        }
        const hasAdjacency = workItems.length > 0 ||
            !!raisedBy ||
            (affects && affects.length > 0) ||
            !!resolvedSessionId;
        if (!hasAdjacency) {
            throw new Error("Write doctrine violated: project-create requires at least one adjacency — pass workItems (≥1), raisedBy, affects, or invoke inside an admin session with an existing Conversation.");
        }
        // Doctrine-satisfying CREATE: adjacency is guaranteed by the
        // pre-validation above. The Project is linked either via HAS_TASK
        // (children below), RAISED_BY/AFFECTS (explicit params), or PART_OF
        // Conversation (session). Within this single tx, intermediate
        // state is not externally observable, so creating the Project
        // first and edges in subsequent statements is safe — the
        // [graph-health] sweep only sees committed state.
        // See .docs/neo4j.md § Write doctrine.
        await tx.run(`CREATE (p:Project:Task $props)`, { props: projectProps });
        // Link to session if the Conversation was resolved above. MATCH not
        // MERGE — a missing Conversation must not spawn an orphan
        // Conversation just to satisfy PART_OF.
        if (resolvedSessionId) {
            await tx.run(`MATCH (p:Project {taskId: $projectId, accountId: $accountId})
         MATCH (c) WHERE elementId(c) = $sessionId
         CREATE (p)-[:PART_OF]->(c)`, { projectId, accountId, sessionId: resolvedSessionId });
        }
        // Create child :Task nodes and HAS_TASK relationships
        for (const childProps of childPropsList) {
            await tx.run(`MATCH (p:Project {taskId: $projectId, accountId: $accountId})
         CREATE (t:Task $childProps)
         CREATE (p)-[:HAS_TASK]->(t)`, { projectId, accountId, childProps });
        }
        // Create BLOCKS relationships between children
        for (let i = 0; i < workItems.length; i++) {
            const blocks = workItems[i].blocks;
            if (!blocks)
                continue;
            for (const target of blocks) {
                await tx.run(`MATCH (blocker:Task {taskId: $blockerId, accountId: $accountId})
           MATCH (blocked:Task {taskId: $blockedId, accountId: $accountId})
           CREATE (blocker)-[:BLOCKS]->(blocked)`, { blockerId: childIds[i], blockedId: childIds[target], accountId });
            }
        }
        // Create RAISED_BY relationship
        if (raisedBy) {
            const result = await tx.run(`MATCH (p:Project {taskId: $projectId, accountId: $accountId})
         MATCH (person) WHERE elementId(person) = $raisedBy
         CREATE (p)-[:RAISED_BY]->(person)
         RETURN person`, { projectId, accountId, raisedBy });
            if (result.records.length === 0) {
                throw new Error(`TargetNotFound: raisedBy person not found (${raisedBy})`);
            }
        }
        // Create AFFECTS relationships
        if (affects && affects.length > 0) {
            for (const entityId of affects) {
                const result = await tx.run(`MATCH (p:Project {taskId: $projectId, accountId: $accountId})
           MATCH (e) WHERE elementId(e) = $entityId
           CREATE (p)-[:AFFECTS]->(e)
           RETURN e`, { projectId, accountId, entityId });
                if (result.records.length === 0) {
                    throw new Error(`TargetNotFound: affects entity not found (${entityId})`);
                }
            }
        }
        await tx.commit();
    }
    catch (err) {
        await tx.rollback();
        throw err;
    }
    finally {
        await session.close();
    }
    console.error(`project-create: created ${projectId} with ${childIds.length} children, tier=${tier}`);
    return { projectId, childTaskIds: childIds };
}
//# sourceMappingURL=project-create.js.map