import { getSession } from "../lib/neo4j.js";
import { embed } from "../lib/embeddings.js";
import { validateWrite } from "../lib/schema-validator.js";
export async function memoryWrite(params) {
    const { labels, properties, accountId, scope, relationships, schema } = params;
    if (!scope) {
        throw new Error("scope is required — valid values: 'public', 'shared', 'admin', 'user:{identifier}'");
    }
    // Schema validation runs before embedding so a rejected write does not
    // waste an Ollama call. Errors are logged to stderr then re-thrown so the
    // MCP tool handler can report a descriptive message to the agent and the
    // operator can see the rejection in server.log.
    try {
        validateWrite({ labels, properties, accountId }, schema);
    }
    catch (err) {
        console.error(`memory-write: schema validation rejected [${labels.join(", ")}] for account ${accountId ?? "<none>"}: ${err instanceof Error ? err.message : String(err)}`);
        throw err;
    }
    const session = getSession();
    const labelStr = labels.map((l) => `\`${l}\``).join(":");
    try {
        // Compute embedding from a text representation of the node
        const textForEmbedding = buildTextRepresentation(labels, properties);
        const embedding = await embed(textForEmbedding);
        const nodeProps = {
            ...properties,
            accountId,
            scope,
            embedding,
            updatedAt: new Date().toISOString(),
        };
        if (!properties.createdOn && !properties.createdAt) {
            nodeProps.createdAt = new Date().toISOString();
        }
        const createQuery = `
      CREATE (n:${labelStr} $props)
      RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels
    `;
        const result = await session.run(createQuery, { props: nodeProps });
        const record = result.records[0];
        const nodeId = record.get("nodeId");
        const nodeLabels = record.get("nodeLabels");
        // Create relationships if specified
        if (relationships && relationships.length > 0) {
            for (const rel of relationships) {
                const relQuery = rel.direction === "outgoing"
                    ? `MATCH (a), (b) WHERE elementId(a) = $fromId AND elementId(b) = $toId CREATE (a)-[:${rel.type}]->(b)`
                    : `MATCH (a), (b) WHERE elementId(a) = $fromId AND elementId(b) = $toId CREATE (b)-[:${rel.type}]->(a)`;
                await session.run(relQuery, {
                    fromId: nodeId,
                    toId: rel.targetNodeId,
                });
            }
        }
        return { nodeId, labels: nodeLabels, created: true };
    }
    catch (error) {
        // Constraint violations include an unusable internal ID (e.g. "Node(219)").
        // Look up the existing node's elementId so the agent can call memory-update.
        if (error instanceof Error &&
            "code" in error &&
            error.code ===
                "Neo.ClientError.Schema.ConstraintValidationFailed") {
            const parsed = parseConstraintProperty(error.message);
            console.error(`memory-write: constraint violation on ${labels.join(":")}` +
                (parsed
                    ? ` — property '${parsed.propName}' = '${parsed.propValue}'`
                    : ` — could not parse property from: ${error.message}`));
            try {
                let lookupQuery;
                let lookupParams;
                if (parsed) {
                    // propName is safe to interpolate — regex constrains it to \w+ characters
                    lookupQuery = `
            MATCH (n:${labelStr})
            WHERE n.accountId = $accountId AND n.\`${parsed.propName}\` = $propValue
            RETURN elementId(n) AS existingId
          `;
                    lookupParams = { accountId, propValue: parsed.propValue };
                }
                else {
                    lookupQuery = `
            MATCH (n:${labelStr})
            WHERE n.accountId = $accountId
            RETURN elementId(n) AS existingId
            LIMIT 1
          `;
                    lookupParams = { accountId };
                }
                const lookupResult = await session.run(lookupQuery, lookupParams);
                if (lookupResult.records.length > 0) {
                    const existingId = lookupResult.records[0].get("existingId");
                    const reason = parsed
                        ? ` — property '${parsed.propName}' = '${parsed.propValue}' is unique.`
                        : " (match may be approximate).";
                    console.error(`memory-write: conflicting node elementId: ${existingId}`);
                    throw new Error(`Node already exists (elementId: ${existingId})${reason} Use memory-update with this nodeId to modify it.`);
                }
            }
            catch (lookupError) {
                // If the lookup itself produced our actionable error, propagate it.
                if (lookupError instanceof Error &&
                    lookupError.message.startsWith("Node already exists")) {
                    throw lookupError;
                }
                console.error("memory-write: lookup for conflicting node failed:", lookupError);
                // Lookup failed — fall through to re-throw the original constraint error.
            }
        }
        throw error;
    }
    finally {
        await session.close();
    }
}
/** Parse the violated property name and value from a Neo4j constraint error message. */
export function parseConstraintProperty(message) {
    // Neo4j format: "... property `name` = 'value'" or "... property 'name' = 'value'"
    const match = message.match(/property [`'](\w+)[`'] = '([^']*)'/);
    if (!match)
        return null;
    return { propName: match[1], propValue: match[2] };
}
export function buildTextRepresentation(labels, properties) {
    const parts = [`[${labels.join(", ")}]`];
    for (const [key, value] of Object.entries(properties)) {
        if (value !== null && value !== undefined && key !== "embedding") {
            parts.push(`${key}: ${String(value)}`);
        }
    }
    return parts.join(" | ");
}
//# sourceMappingURL=memory-write.js.map