/**
 * Set up automatic waitlist extraction as a workflow.
 *
 * Creates:
 *   1. A Workflow node with 3 steps (scan → LLM extract → persist)
 *   2. A scheduled Event that dispatches workflow-execute at the
 *      specified interval
 *
 * The extraction prompt is stored in the LLM step and is visible
 * via workflow-get and editable via workflow-update. The user can
 * change what fields are extracted, adjust the model, or modify
 * the prompt — all through conversation.
 *
 * Idempotent: if the workflow already exists, returns its details.
 * Cleans up old-style events that pointed at the removed
 * waitlist-extract tool.
 */
import { randomUUID } from "node:crypto";
import { getSession } from "../lib/neo4j.js";
import { computeEmbedding } from "../lib/embedding.js";
import { healBrokenPrompt, healMissingConversationDate, WORKFLOW_NAME } from "./waitlist-heal.js";
import cronParser from "cron-parser";
const { parseExpression } = cronParser;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DEFAULT_RECURRENCE = "0 */4 * * *"; // Every 4 hours
/**
 * The extraction prompt — migrated from the former anthropic.ts Haiku call.
 * This is the editable payload: the user sees it via workflow-get and
 * modifies it via workflow-update to change extraction behaviour.
 */
const EXTRACTION_PROMPT = "You extract contact details and customer discovery intelligence from visitor chat conversations.\n\n" +
    "Below is a JSON object containing public agent conversations. It has a `conversations` array " +
    "where each element has a `conversationId`, a `createdAt` timestamp, and a `messages` array of user message strings. " +
    "For each conversation that contains an email address, extract the visitor's details.\n\n" +
    "Return ONLY a JSON array of objects. Each object must have these fields:\n" +
    '- "email": the visitor\'s email address (required — skip conversations with no email)\n' +
    '- "givenName": the visitor\'s first/given name, or null if unclear\n' +
    '- "familyName": the visitor\'s last/family name, or null if unclear\n' +
    '- "conversationId": the conversation ID from the input data\n' +
    '- "conversationDate": the createdAt timestamp of the conversation from the input data\n' +
    '- "businessType": what kind of business they run or work in, or null\n' +
    '- "role": their role or position, or null\n' +
    '- "needs": what they need or are looking for, in their own words where possible, or null\n' +
    '- "challenges": pain points or challenges they mentioned, or null\n' +
    '- "interests": specific features, capabilities, or topics they asked about, or null\n' +
    '- "engagementQuality": one of "high" (asked detailed questions, shared specifics), ' +
    '"medium" (some back-and-forth, genuine interest), or "low" (brief, noncommittal), or null\n' +
    '- "otherSignals": any other noteworthy signal (timeline urgency, competitor mentions, referral source), or null\n\n' +
    "Only include fields where the visitor actually revealed information. " +
    "Return valid JSON only. No markdown, no explanation. If no conversations contain an email, return [].\n\n" +
    "Conversations:\n{{conversations}}";
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
export async function waitlistSetup(params) {
    const { accountId, timezone } = params;
    const recurrence = params.recurrence ?? DEFAULT_RECURRENCE;
    const session = getSession();
    try {
        // ------------------------------------------------------------------
        // Clean up old-style events (actionTool: waitlist-extract)
        // These pointed at the removed bespoke extraction tool.
        // ------------------------------------------------------------------
        await session.run(`MATCH (e:Event {
        accountId: $accountId,
        actionPlugin: 'waitlist',
        actionTool: 'waitlist-extract'
      })
       DELETE e`, { accountId });
        // ------------------------------------------------------------------
        // Check for existing workflow
        // ------------------------------------------------------------------
        const existingResult = await session.run(`MATCH (w:Workflow {accountId: $accountId, name: $name})
       WHERE w.status IN ['active', 'draft']
       OPTIONAL MATCH (e:Event {
         accountId: $accountId,
         actionPlugin: 'workflows',
         actionTool: 'workflow-execute'
       })
       WHERE e.eventStatus IN ['scheduled', 'due']
         AND e.actionArgs CONTAINS w.workflowId
       RETURN w.workflowId AS workflowId,
              e.eventId AS eventId,
              e.recurrence AS recurrence,
              e.nextRun AS nextRun
       LIMIT 1`, { accountId, name: WORKFLOW_NAME });
        if (existingResult.records.length > 0) {
            const existing = existingResult.records[0];
            const workflowId = existing.get("workflowId");
            const eventId = existing.get("eventId");
            // Self-healing: detect broken prompt pattern and update surgically.
            // The original prompt referenced {{conversations._raw}} which doesn't
            // exist — waitlist-scan returns structured JSON, not {_raw: text}.
            // Only updates if the broken pattern is present; preserves any other
            // user customizations made via workflow-update.
            await healBrokenPrompt(session, workflowId);
            await healMissingConversationDate(session, workflowId);
            // Workflow exists but no event — create one
            if (!eventId) {
                const newEventResult = await createScheduledEvent(session, accountId, workflowId, recurrence, timezone);
                return {
                    workflowId,
                    eventId: newEventResult.eventId,
                    recurrence,
                    nextRun: newEventResult.nextRun,
                    alreadyExists: true,
                };
            }
            return {
                workflowId,
                eventId,
                recurrence: existing.get("recurrence"),
                nextRun: existing.get("nextRun"),
                alreadyExists: true,
            };
        }
        // ------------------------------------------------------------------
        // Create workflow with 3 steps
        // ------------------------------------------------------------------
        const workflowId = randomUUID();
        const now = new Date().toISOString();
        // Compute embedding for workflow discovery
        const embeddingText = `${WORKFLOW_NAME} — scans public conversations for waitlist sign-ups, extracts contact details and discovery intelligence via LLM`;
        const embedding = await computeEmbedding(embeddingText);
        const workflowProps = {
            workflowId,
            accountId,
            name: WORKFLOW_NAME,
            description: "Scans public agent conversations, extracts contact details and customer discovery intelligence via LLM, " +
                "and creates Person nodes idempotently. Edit the extraction prompt via workflow-update to change what fields are extracted.",
            status: "active",
            createdAt: now,
            updatedAt: now,
        };
        if (embedding.length > 0) {
            workflowProps.embedding = embedding;
        }
        // MERGE on {accountId, name} prevents duplicate workflows when the
        // heartbeat reconciliation and an admin session call setup concurrently.
        // Returns the actual workflowId — may differ from the local one if
        // another caller created the node first.
        const mergeResult = await session.run(`MERGE (w:Workflow {accountId: $accountId, name: $name})
       ON CREATE SET w += $props
       RETURN w.workflowId AS workflowId`, { accountId, name: WORKFLOW_NAME, props: workflowProps });
        const actualWorkflowId = mergeResult.records[0]?.get("workflowId");
        if (actualWorkflowId !== workflowId) {
            // Race: another caller created the workflow concurrently.
            // Return early — next cycle's existence check handles Event creation.
            const eventResult = await createScheduledEvent(session, accountId, actualWorkflowId, recurrence, timezone);
            return {
                workflowId: actualWorkflowId,
                eventId: eventResult.eventId,
                recurrence,
                nextRun: eventResult.nextRun,
                alreadyExists: true,
            };
        }
        // Step 1: Scan public conversations
        const step1Id = randomUUID();
        await session.run(`MATCH (w:Workflow {workflowId: $workflowId})
       CREATE (w)-[:HAS_STEP]->(s:WorkflowStep $props)`, {
            workflowId,
            props: {
                stepId: step1Id,
                workflowId,
                accountId,
                type: "tool",
                plugin: "waitlist",
                tool: "waitlist-scan",
                params: null,
                dependsOn: null,
                onFailure: "abort",
                retryLimit: null,
                outputKey: "conversations",
                timeout: 30,
                position: 0,
                label: "Scan public conversations",
                // LLM-step fields (null for tool steps)
                prompt: null,
                model: null,
                maxTokens: null,
            },
        });
        // Step 2: LLM extraction
        const step2Id = randomUUID();
        await session.run(`MATCH (w:Workflow {workflowId: $workflowId})
       CREATE (w)-[:HAS_STEP]->(s:WorkflowStep $props)`, {
            workflowId,
            props: {
                stepId: step2Id,
                workflowId,
                accountId,
                type: "llm",
                prompt: EXTRACTION_PROMPT,
                model: null, // Uses account's adminModel
                maxTokens: 4096,
                dependsOn: JSON.stringify([step1Id]),
                onFailure: "abort",
                retryLimit: null,
                outputKey: "extractions",
                timeout: 120,
                position: 1,
                label: "Extract contacts and discovery intelligence",
                // Tool-step fields (null for LLM steps)
                plugin: null,
                tool: null,
                params: null,
            },
        });
        // Step 3: Persist extraction results
        await session.run(`MATCH (w:Workflow {workflowId: $workflowId})
       CREATE (w)-[:HAS_STEP]->(s:WorkflowStep $props)`, {
            workflowId,
            props: {
                stepId: randomUUID(),
                workflowId,
                accountId,
                type: "tool",
                plugin: "waitlist",
                tool: "waitlist-persist",
                params: JSON.stringify({
                    extractions: "{{extractions}}",
                    conversations: "{{conversations}}",
                }),
                dependsOn: JSON.stringify([step2Id]),
                onFailure: "abort",
                retryLimit: null,
                outputKey: "results",
                timeout: 60,
                position: 2,
                label: "Create/update Person nodes from extractions",
                // LLM-step fields (null for tool steps)
                prompt: null,
                model: null,
                maxTokens: null,
            },
        });
        // ------------------------------------------------------------------
        // Create scheduled event
        // ------------------------------------------------------------------
        const eventResult = await createScheduledEvent(session, accountId, workflowId, recurrence, timezone);
        return {
            workflowId,
            eventId: eventResult.eventId,
            recurrence,
            nextRun: eventResult.nextRun,
            alreadyExists: false,
        };
    }
    finally {
        await session.close();
    }
}
// ---------------------------------------------------------------------------
// Event creation helper
// ---------------------------------------------------------------------------
async function createScheduledEvent(session, accountId, workflowId, recurrence, timezone) {
    // Validate cron expression and compute nextRun
    let nextRun;
    try {
        const interval = parseExpression(recurrence, {
            currentDate: new Date(),
            tz: timezone,
        });
        nextRun = interval.next().toISOString();
    }
    catch (err) {
        throw new Error(`Invalid recurrence expression: "${recurrence}" — ${err instanceof Error ? err.message : String(err)}`);
    }
    const eventId = randomUUID();
    const now = new Date().toISOString();
    const props = {
        eventId,
        accountId,
        name: "Waitlist lead extraction",
        description: "Executes the waitlist extraction workflow on schedule",
        startDate: now,
        eventStatus: "scheduled",
        recurrence,
        nextRun,
        sourcePlugin: "waitlist",
        actionPlugin: "workflows",
        actionTool: "workflow-execute",
        actionArgs: JSON.stringify({
            workflowId,
            trigger: "schedule",
        }),
        createdAt: now,
        updatedAt: now,
    };
    if (timezone) {
        props.timezone = timezone;
    }
    // Compute embedding for event discoverability
    const embeddingText = "Event: Waitlist lead extraction — runs workflow to scan public conversations for sign-ups";
    const embedding = await computeEmbedding(embeddingText);
    if (embedding.length > 0) {
        props.embedding = embedding;
    }
    await session.run(`CREATE (e:Event $props)`, { props });
    return { eventId, nextRun };
}
//# sourceMappingURL=waitlist-setup.js.map