import { getSession } from "../lib/neo4j.js";
import { notTrashed } from "../../../../../lib/graph-trash/dist/index.js";
import { writeNodeWithEdges, injectConversationProvenance, } from "../../../../../lib/graph-write/dist/index.js";
const OLLAMA_URL = process.env.OLLAMA_URL ?? "http://localhost:11434";
const EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
async function computeEmbedding(text) {
    try {
        const res = await fetch(`${OLLAMA_URL}/api/embed`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ model: EMBED_MODEL, input: text }),
        });
        if (!res.ok)
            return [];
        const data = (await res.json());
        return data.embeddings[0] ?? [];
    }
    catch {
        // Non-fatal: contact is created without embedding, reindex can fix later
        return [];
    }
}
export async function contactCreate(params) {
    const { givenName, familyName, email, telephone, jobTitle, source, status = "prospect", accountId, relationships, createdBy, } = params;
    const normalizedEmail = email?.toLowerCase().trim();
    const normalizedPhone = telephone?.trim();
    const session = getSession();
    try {
        // Dedup only fires when a stable identifier (email/telephone) is present.
        // With no identifier the agent has explicitly chosen to write a sparse
        // Person; dedup/conflict resolution moves downstream
        // "agent and user manage dedup and conflicts").
        const dedupConditions = [];
        const dedupParams = { accountId };
        if (normalizedEmail) {
            dedupConditions.push("p.email = $email");
            dedupParams.email = normalizedEmail;
        }
        if (normalizedPhone) {
            dedupConditions.push("p.telephone = $telephone");
            dedupParams.telephone = normalizedPhone;
        }
        if (dedupConditions.length > 0) {
            // Trashed persons already have null email/telephone
            // (UNIQUE_KEYS_BY_LABEL snapshot), so OR-matching by those keys would
            // miss them — the notTrashed() filter is belt-and-braces against the
            // legacy deletedAt soft-delete state.
            const existing = await session.run(`MATCH (p:Person {accountId: $accountId})
         WHERE (${dedupConditions.join(" OR ")}) AND ${notTrashed("p")}
         RETURN elementId(p) AS nodeId, p.email AS email, p.telephone AS telephone`, dedupParams);
            if (existing.records.length > 0) {
                const matchedEmail = existing.records[0].get("email");
                const matchedPhone = existing.records[0].get("telephone");
                const matchedIdentifier = matchedEmail ?? matchedPhone ?? "unknown";
                return {
                    nodeId: existing.records[0].get("nodeId"),
                    identifier: matchedIdentifier,
                    created: false,
                    alreadyExists: true,
                };
            }
        }
        const props = {
            source,
            status,
            accountId,
            createdOn: new Date().toISOString(),
        };
        if (givenName)
            props.givenName = givenName;
        if (normalizedEmail)
            props.email = normalizedEmail;
        if (normalizedPhone)
            props.telephone = normalizedPhone;
        if (familyName)
            props.familyName = familyName;
        if (jobTitle)
            props.jobTitle = jobTitle;
        const identifierText = normalizedEmail
            ? `<${normalizedEmail}>`
            : normalizedPhone ?? "";
        const nameText = [givenName, familyName].filter(Boolean).join(" ");
        const textForEmbedding = `Person: ${nameText || "(unnamed)"} ${identifierText} source: ${source} status: ${status}`.trim();
        const embedding = await computeEmbedding(textForEmbedding);
        if (embedding.length > 0) {
            props.embedding = embedding;
        }
        // direct admin asks ("add Anneke as person") need a PRODUCED
        // edge from the active :AdminConversation rather than a synthetic :Task.
        // The admin server stamps `SESSION_NODE_ID` in the spawn env; the
        // helper resolves the node, verifies accountId parity, and prepends the
        // synthetic edge. No-op when env is unset or `relationships` already
        // carries an inbound `:PRODUCED`.
        const resolvedRelationships = await injectConversationProvenance({
            session,
            relationships,
            accountId: accountId ?? "",
            writeLabels: ["Person"],
            conversationNodeId: process.env.SESSION_NODE_ID,
            logNamespace: "mcp:contacts",
            tool: "contact-create",
        });
        const result = await writeNodeWithEdges({
            session,
            labels: ["Person"],
            props,
            relationships: resolvedRelationships,
            createdBy,
        });
        const nameIdentifier = [givenName, familyName].filter(Boolean).join(" ");
        const primaryIdentifier = normalizedEmail ?? normalizedPhone ?? (nameIdentifier || "unknown");
        return {
            nodeId: result.nodeId,
            identifier: primaryIdentifier,
            created: true,
            alreadyExists: false,
        };
    }
    finally {
        await session.close();
    }
}
//# sourceMappingURL=contact-create.js.map