/**
 * Workflow execution engine.
 *
 * Loads a workflow's structured steps, resolves their dependency order,
 * executes each step by spawning the target plugin's MCP server, passes
 * outputs between steps via named template variables, and records the run
 * as WorkflowRun + StepResult nodes.
 *
 * After task 127 the engine handles only `type:'tool'` steps. Plugins that
 * want LLM reasoning own their own SDK call inside a tool.
 *
 *   LOAD WORKFLOW + STEPS
 *     │
 *     v
 *   CREATE WorkflowRun (status: running)
 *     │
 *     v
 *   RESOLVE ORDER (topological sort)
 *     │
 *     v
 *   FOR EACH STEP:
 *     ├─ resolve {{templates}} in params → spawn MCP server → call tool
 *     ├─ parse output (JSON or _raw fallback, strips markdown fences)
 *     ├─ store output under step's outputKey
 *     ├─ create StepResult node (status: completed | skipped | failed)
 *     └─ on failure: abort | skip | retry (per step config)
 *     │
 *     v
 *   UPDATE WorkflowRun status:
 *     any failed → "failed"
 *     any skipped → "partial"
 *     else → "completed"
 */
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { getSession } from "../lib/neo4j.js";
import { resolveOrder, resolveTemplates, parseStepOutput, } from "../lib/step-resolver.js";
import { markRunActive, markRunComplete, terminalTransitionFailed } from "../lib/active-runs.js";
import { notTrashed } from "../../../../../lib/graph-trash/dist/index.js";
// ─────────────────────────────────────────────────────────────
// Constants
// ─────────────────────────────────────────────────────────────
const DEFAULT_TOOL_TIMEOUT_S = 30;
const MAX_OUTPUT_SIZE = 10_240; // 10KB cap on stored step output
// ─────────────────────────────────────────────────────────────
// MCP tool dispatch
// ─────────────────────────────────────────────────────────────
async function dispatchStep(plugin, tool, args, stepAccountId, timeoutMs) {
    const platformRoot = process.env.PLATFORM_ROOT;
    const serverPath = resolve(platformRoot, "plugins", plugin, "mcp", "dist", "index.js");
    // Path traversal check at execution time (defense in depth — also checked at creation)
    const pluginsRoot = resolve(platformRoot, "plugins");
    if (!serverPath.startsWith(pluginsRoot + "/")) {
        throw new Error(`Invalid plugin name: "${plugin}" — path traversal blocked`);
    }
    if (!existsSync(serverPath)) {
        throw new Error(`MCP server not found: ${serverPath}`);
    }
    const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
    const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
    const env = {};
    for (const [k, v] of Object.entries(process.env)) {
        if (v !== undefined)
            env[k] = v;
    }
    env.PLATFORM_ROOT = platformRoot;
    env.ACCOUNT_ID = stepAccountId;
    const transport = new StdioClientTransport({
        command: process.execPath,
        args: [serverPath],
        env,
        stderr: "pipe",
    });
    let childStderr = "";
    const stderrStream = transport.stderr;
    if (stderrStream) {
        stderrStream.on("data", (chunk) => {
            if (childStderr.length < 4096) {
                childStderr += chunk.toString("utf-8").slice(0, 4096 - childStderr.length);
            }
        });
    }
    const client = new Client({ name: "workflow-executor", version: "1.0.0" });
    let timedOut = false;
    const timeout = setTimeout(() => {
        timedOut = true;
        client.close().catch(() => { });
    }, timeoutMs);
    try {
        await client.connect(transport);
        const result = await client.callTool({ name: tool, arguments: args });
        const contentArr = Array.isArray(result.content) ? result.content : [];
        const text = contentArr
            .filter((c) => c.type === "text")
            .map((c) => c.text)
            .join("\n");
        return { text, isError: result.isError === true };
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        const stderrSuffix = childStderr ? `\nChild stderr: ${childStderr.trim()}` : "";
        if (timedOut) {
            throw new Error(`Tool "${tool}" on plugin "${plugin}" timed out after ${timeoutMs}ms${stderrSuffix}`);
        }
        throw new Error(`Tool "${tool}" on plugin "${plugin}" failed: ${msg}${stderrSuffix}`);
    }
    finally {
        clearTimeout(timeout);
        try {
            await client.close();
        }
        catch { /* best-effort cleanup */ }
    }
}
// ─────────────────────────────────────────────────────────────
// Main execution
// ─────────────────────────────────────────────────────────────
export async function workflowExecute(params) {
    const { workflowId, accountId, context: inputContext, trigger = "conversational" } = params;
    const platformRoot = process.env.PLATFORM_ROOT;
    if (!platformRoot) {
        throw new Error("PLATFORM_ROOT environment variable is required for execution");
    }
    // ── Load workflow + steps ────────────────────────────────
    const session = getSession();
    let steps;
    let workflowName;
    try {
        const result = await session.run(`MATCH (w:Workflow {workflowId: $workflowId, accountId: $accountId})
       WHERE ${notTrashed("w")}
       OPTIONAL MATCH (w)-[:HAS_STEP]->(s:WorkflowStep) WHERE ${notTrashed("s")}
       RETURN w, collect(s) AS steps`, { workflowId, accountId });
        if (result.records.length === 0) {
            throw new Error(`Workflow not found: ${workflowId}`);
        }
        const record = result.records[0];
        const w = record.get("w").properties;
        workflowName = w.name;
        const status = w.status;
        if (status !== "active") {
            throw new Error(`Workflow "${workflowName}" cannot be executed — status is "${status}" (must be "active")`);
        }
        const stepsRaw = record.get("steps");
        if (stepsRaw.length === 0) {
            throw new Error(`Workflow "${workflowName}" has no steps`);
        }
        steps = stepsRaw.map((s) => {
            const stepType = s.properties.type;
            if (stepType !== "tool") {
                throw new Error(`Workflow "${workflowName}" has a legacy step of type "${stepType}". ` +
                    `As of task 127 only type:"tool" steps are supported. ` +
                    `Run the plugin's heal tool to migrate.`);
            }
            return {
                stepId: s.properties.stepId,
                dependsOn: s.properties.dependsOn
                    ? JSON.parse(s.properties.dependsOn)
                    : undefined,
                onFailure: s.properties.onFailure ?? "abort",
                retryLimit: s.properties.retryLimit ?? undefined,
                outputKey: s.properties.outputKey ?? undefined,
                timeout: s.properties.timeout ?? undefined,
                position: s.properties.position ?? 0,
                label: s.properties.label,
                type: "tool",
                plugin: s.properties.plugin,
                tool: s.properties.tool,
                params: s.properties.params
                    ? JSON.parse(s.properties.params)
                    : undefined,
            };
        });
    }
    finally {
        await session.close();
    }
    const ordered = resolveOrder(steps);
    // ── Create WorkflowRun node ──────────────────────────────
    const runId = randomUUID();
    const startedAt = new Date().toISOString();
    const runSession = getSession();
    try {
        const runCreate = await runSession.run(`CREATE (r:WorkflowRun $props)
       WITH r
       MATCH (w:Workflow {workflowId: $workflowId})
       WHERE ${notTrashed("w")}
       CREATE (r)-[:RUN_OF]->(w)
       RETURN count(w) AS linkedWorkflows`, {
            workflowId,
            props: {
                runId,
                workflowId,
                accountId,
                trigger,
                status: "running",
                startedAt,
                context: inputContext ? JSON.stringify(inputContext) : null,
            },
        });
        const linked = runCreate.records[0]?.get("linkedWorkflows");
        const linkedCount = typeof linked === "object" && linked !== null && "toNumber" in linked
            ? linked.toNumber()
            : Number(linked ?? 0);
        if (linkedCount === 0) {
            throw new Error(`Workflow was deleted while this execution was starting: ${workflowId}`);
        }
    }
    finally {
        await runSession.close();
    }
    markRunActive(runId);
    try {
        return await executeSteps({
            runId,
            startedAt,
            accountId,
            ordered,
            inputContext,
        });
    }
    catch (err) {
        const errMsg = err instanceof Error ? err.message : String(err);
        console.error(`[workflow-execute] unhandled exception for run=${runId}: ${errMsg}`);
        try {
            await terminalTransitionFailed(runId, `unhandled exception: ${errMsg}`);
        }
        catch (transitionErr) {
            const tMsg = transitionErr instanceof Error ? transitionErr.message : String(transitionErr);
            console.error(`[workflow-execute] terminal-transition write FAILED for run=${runId}: ${tMsg}`);
        }
        throw err;
    }
    finally {
        markRunComplete(runId);
    }
}
async function executeSteps(args) {
    const { runId, startedAt, accountId, ordered, inputContext } = args;
    const outputContext = {};
    if (inputContext) {
        for (const [k, v] of Object.entries(inputContext)) {
            if (typeof v === "object" && v !== null && !Array.isArray(v)) {
                outputContext[k] = v;
            }
            else {
                outputContext[k] = { _raw: String(v) };
            }
        }
    }
    let stepsCompleted = 0;
    let stepsSkipped = 0;
    let stepsFailed = 0;
    let lastError;
    let aborted = false;
    for (const step of ordered) {
        if (aborted)
            break;
        const stepStartedAt = new Date().toISOString();
        let attempt = 0;
        const maxAttempts = step.onFailure === "retry" ? 1 + (step.retryLimit ?? 1) : 1;
        let stepSuccess = false;
        let stepOutput = "";
        let stepError = "";
        const stepLabel = `${step.label} → ${step.plugin}/${step.tool}`;
        console.error(`[workflow-execute] step ${step.position + 1}/${ordered.length}: ${stepLabel}`);
        while (attempt < maxAttempts && !stepSuccess) {
            attempt++;
            try {
                const resolvedParams = step.params
                    ? resolveTemplates(step.params, outputContext)
                    : {};
                const stepTimeoutMs = (step.timeout ?? DEFAULT_TOOL_TIMEOUT_S) * 1000;
                const result = await dispatchStep(step.plugin, step.tool, resolvedParams, accountId, stepTimeoutMs);
                if (result.isError) {
                    throw new Error(`Tool returned error: ${result.text}`);
                }
                stepOutput = result.text;
                stepSuccess = true;
                if (step.outputKey) {
                    outputContext[step.outputKey] = parseStepOutput(stepOutput);
                }
                console.error(`[workflow-execute] step ${step.position + 1} completed in attempt ${attempt}`);
            }
            catch (err) {
                stepError = err instanceof Error ? err.message : String(err);
                console.error(`[workflow-execute] step ${step.position + 1} FAILED (attempt ${attempt}/${maxAttempts}): ${stepError}`);
            }
        }
        const stepCompletedAt = new Date().toISOString();
        const stepStatus = stepSuccess
            ? "completed"
            : step.onFailure === "skip"
                ? "skipped"
                : "failed";
        const storedOutput = stepOutput.length > MAX_OUTPUT_SIZE
            ? stepOutput.slice(0, MAX_OUTPUT_SIZE) + "\n[truncated]"
            : stepOutput;
        const resultSession = getSession();
        try {
            const res = await resultSession.run(`MATCH (r:WorkflowRun {runId: $runId})
         CREATE (r)-[:HAS_RESULT]->(sr:StepResult $props)
         SET sr.createdBySource = 'workflow-execute',
             sr.createdBySession = $runId`, {
                runId,
                props: {
                    // Task 636 — stamp the run's account so StepResult nodes carry
                    // isolation like every other write. Their absence produced 739
                    // isolation-less orphans purged on 2026-06-05.
                    accountId,
                    stepId: step.stepId,
                    runId,
                    status: stepStatus,
                    output: storedOutput || null,
                    error: stepError || null,
                    startedAt: stepStartedAt,
                    completedAt: stepCompletedAt,
                    attempt,
                },
            });
            if (res.summary.counters.updates().nodesCreated === 0) {
                console.error(`[workflow-execute] step-result-no-run runId=${runId} stepId=${step.stepId} — StepResult dropped because no WorkflowRun matched.`);
            }
        }
        finally {
            await resultSession.close();
        }
        if (stepSuccess) {
            stepsCompleted++;
        }
        else if (step.onFailure === "skip") {
            stepsSkipped++;
            console.error(`[workflow-execute] step ${step.position + 1} skipped per onFailure policy`);
        }
        else {
            stepsFailed++;
            lastError = stepError;
            aborted = true;
            console.error(`[workflow-execute] step ${step.position + 1} failed — aborting workflow`);
        }
    }
    let runStatus;
    if (stepsFailed > 0) {
        runStatus = "failed";
    }
    else if (stepsSkipped > 0) {
        runStatus = "partial";
    }
    else {
        runStatus = "completed";
    }
    const completedAt = new Date().toISOString();
    const finalSession = getSession();
    try {
        await finalSession.run(`MATCH (r:WorkflowRun {runId: $runId})
       SET r.status = $status,
           r.completedAt = $completedAt,
           r.error = $error`, {
            runId,
            status: runStatus,
            completedAt,
            error: lastError ?? null,
        });
    }
    finally {
        await finalSession.close();
    }
    const duration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
    console.error(`[workflow-execute] run complete: ${runStatus}, ${duration}ms, ` +
        `${stepsCompleted} completed, ${stepsSkipped} skipped, ${stepsFailed} failed`);
    return {
        runId,
        status: runStatus,
        stepsCompleted,
        stepsDegraded: 0,
        stepsSkipped,
        stepsFailed,
        stepsTotal: ordered.length,
        error: lastError,
    };
}
//# sourceMappingURL=workflow-execute.js.map