import { createCorrelationId, toPromptEnvelope } from "../state/prompt-store.js";
import { extractTemplateVariables, assertRequiredTemplateVariables, missingRequiredTemplateVariables, renderTemplateText, } from "../state/workflow-store.js";
const WORKFLOW_WAIT_MS = 1_000;
const WORKFLOW_SCHEDULE_POLL_MS = 30_000;
const MAX_SUBFLOW_DEPTH = 5;
export class RelayWorkflowService {
    options;
    activeRuns = new Set();
    scheduleTimer;
    constructor(options) {
        this.options = options;
        if (options.schedulerEnabled === false) {
            this.scheduleTimer = null;
            return;
        }
        this.scheduleTimer = setInterval(() => this.runDueSchedules(), WORKFLOW_SCHEDULE_POLL_MS);
        this.scheduleTimer.unref?.();
        const recoveryTimer = setTimeout(() => this.recoverInterruptedRuns(), 250);
        recoveryTimer.unref?.();
    }
    dispose() {
        if (this.scheduleTimer) {
            clearInterval(this.scheduleTimer);
        }
    }
    list() {
        return {
            templates: this.options.store.listTemplates(),
            workflows: this.options.store.listWorkflows(),
            runs: this.options.store.listRuns(100),
        };
    }
    saveTemplate(input, actor) {
        const template = this.options.store.saveTemplate({ ...input, ownerUserId: input.ownerUserId ?? actor?.id });
        this.record("workflow_template_saved", "info", template.name, actor);
        return template;
    }
    deleteTemplate(id, actor) {
        const removed = this.options.store.deleteTemplate(id);
        this.record("workflow_template_deleted", removed ? "info" : "failed", id, actor);
        return { removed };
    }
    saveWorkflow(input, actor) {
        const workflow = this.options.store.saveWorkflow({ ...input, schedule: prepareWorkflowSchedule(input.schedule), ownerUserId: input.ownerUserId ?? actor?.id });
        this.record("workflow_saved", "info", workflow.name, actor);
        return workflow;
    }
    deleteWorkflow(id, actor) {
        const removed = this.options.store.deleteWorkflow(id);
        this.record("workflow_deleted", removed ? "info" : "failed", id, actor);
        return { removed };
    }
    listTemplateVersions(id) {
        return this.options.store.listVersions("template", id);
    }
    listWorkflowVersions(id) {
        return this.options.store.listVersions("workflow", id);
    }
    diffTemplateVersions(id, fromVersion, toVersion) {
        return this.options.store.diffVersions("template", id, fromVersion, toVersion);
    }
    diffWorkflowVersions(id, fromVersion, toVersion) {
        return this.options.store.diffVersions("workflow", id, fromVersion, toVersion);
    }
    restoreTemplateVersion(id, version, actor) {
        const restored = this.options.store.restoreVersion("template", id, version, actor?.id);
        if (!restored)
            throw new Error(`Template version not found: ${id} v${version}`);
        this.record("workflow_template_version_restored", "info", `${restored.name} v${version}`, actor);
        return restored;
    }
    restoreWorkflowVersion(id, version, actor) {
        const restored = this.options.store.restoreVersion("workflow", id, version, actor?.id);
        if (!restored)
            throw new Error(`Workflow version not found: ${id} v${version}`);
        this.record("workflow_version_restored", "info", `${restored.name} v${version}`, actor);
        return restored;
    }
    exportTemplate(id, version) {
        const bundle = this.options.store.exportTemplate(id, version);
        if (!bundle)
            throw new Error(`Template not found: ${id}`);
        return bundle;
    }
    exportWorkflow(id, version) {
        const bundle = this.options.store.exportWorkflow(id, version);
        if (!bundle)
            throw new Error(`Workflow not found: ${id}`);
        return bundle;
    }
    importTemplate(input, actor) {
        const template = this.options.store.importTemplate(input, actor?.id);
        this.record("workflow_template_imported", "info", template.name, actor);
        return template;
    }
    importWorkflow(input, actor) {
        const workflow = this.options.store.importWorkflow(input, actor?.id);
        this.record("workflow_imported", "info", workflow.name, actor);
        return workflow;
    }
    previewTemplateVersion(id, version, variables = {}) {
        const record = this.requireVersion("template", id, version);
        const template = record.snapshot;
        return {
            templateId: template.id,
            name: `${template.name} v${record.version}`,
            prompts: [{
                    stepId: template.id,
                    name: template.name,
                    prompt: renderPromptTemplate(template, variables),
                }],
        };
    }
    previewWorkflowVersion(id, version, variables = {}) {
        const record = this.requireVersion("workflow", id, version);
        const workflow = record.snapshot;
        return {
            workflowId: workflow.id,
            name: `${workflow.name} v${record.version}`,
            prompts: workflow.steps.map((step) => ({
                stepId: step.id,
                name: step.name,
                prompt: this.renderWorkflowStepPrompt(step, variables),
            })),
        };
    }
    previewTemplate(id, variables = {}) {
        const template = this.requireTemplate(id);
        return {
            templateId: template.id,
            name: template.name,
            prompts: [{
                    stepId: template.id,
                    name: template.name,
                    prompt: renderPromptTemplate(template, variables),
                }],
        };
    }
    previewWorkflow(id, variables = {}) {
        const workflow = this.requireWorkflow(id);
        return {
            workflowId: workflow.id,
            name: workflow.name,
            prompts: workflow.steps.map((step) => ({
                stepId: step.id,
                name: step.name,
                prompt: this.renderWorkflowStepPrompt(step, variables),
            })),
        };
    }
    dryRunWorkflow(id, variables = {}, versionNumber) {
        const workflow = versionNumber
            ? this.requireVersion("workflow", id, versionNumber).snapshot
            : this.requireWorkflow(id);
        const preview = versionNumber
            ? this.previewWorkflowVersion(id, versionNumber, variables)
            : this.previewWorkflow(id, variables);
        const defs = this.workflowVariableDefinitions(workflow);
        const missingVariables = missingRequiredTemplateVariables(defs, variables);
        const warnings = [];
        if (!workflow.steps.length)
            warnings.push("Workflow has no steps.");
        const approvalSteps = workflow.steps.filter((step) => step.requiresApproval).length;
        if (approvalSteps)
            warnings.push(`${approvalSteps} step(s) require manual approval.`);
        const peerSteps = workflow.steps.filter((step) => step.target && step.target !== "local").length;
        if (peerSteps)
            warnings.push(`${peerSteps} step(s) run on peers.`);
        return {
            ...preview,
            valid: missingVariables.length === 0 && workflow.steps.length > 0,
            variables: defs.map((variable) => ({
                name: variable.name,
                label: variable.label,
                required: variable.required !== false,
                provided: Object.prototype.hasOwnProperty.call(variables, variable.name),
                defaultValue: variable.defaultValue,
            })),
            missingVariables,
            warnings,
        };
    }
    listWorkflowTriggers(workflowId) {
        this.requireWorkflow(workflowId);
        return this.options.store.listWorkflowTriggers(workflowId);
    }
    createWorkflowTrigger(workflowId, input = {}, actor) {
        const result = this.options.store.createWorkflowTrigger(workflowId, input);
        this.record("workflow_trigger_created", "info", `${result.workflow.name}: ${result.trigger.name}`, actor);
        return result;
    }
    deleteWorkflowTrigger(workflowId, triggerId, actor) {
        const result = this.options.store.deleteWorkflowTrigger(workflowId, triggerId);
        this.record("workflow_trigger_deleted", result.removed ? "info" : "failed", `${workflowId}: ${triggerId}`, actor);
        return result;
    }
    async runWorkflowTriggerToken(token, variables = {}) {
        const match = this.options.store.findWorkflowTriggerByToken(token);
        if (!match) {
            throw new Error("Workflow trigger token is invalid.");
        }
        if (!match.trigger.enabled) {
            throw new Error("Workflow trigger is disabled.");
        }
        const version = this.options.store.latestVersion("workflow", match.workflow.id);
        const workflowForRun = version?.snapshot ?? match.workflow;
        this.assertWorkflowRequiredVariables(workflowForRun, variables);
        this.options.store.markWorkflowTriggerUsed(match.workflow.id, match.trigger.id);
        const actor = {
            channel: "system",
            id: `workflow-trigger:${match.trigger.id}`,
            label: `Workflow trigger: ${match.trigger.name}`,
        };
        this.record("workflow_trigger_run", "queued", `${match.workflow.name}: ${match.trigger.name}`, actor);
        return this.queueWorkflowRun(workflowForRun, variables, actor, version?.version, version?.snapshot);
    }
    async runTemplate(id, variables = {}, actor) {
        const template = this.requireTemplate(id);
        const version = this.options.store.latestVersion("template", id);
        return this.queueTemplateRun(template, variables, actor, version?.version, version?.snapshot);
    }
    async runTemplateVersion(id, versionNumber, variables = {}, actor) {
        const version = this.requireVersion("template", id, versionNumber);
        return this.queueTemplateRun(version.snapshot, variables, actor, version.version, version.snapshot);
    }
    async queueTemplateRun(template, variables, actor, templateVersion, templateSnapshot) {
        assertRequiredTemplateVariables(template.variables, variables, template.name);
        const prompt = renderPromptTemplate(template, variables);
        const now = new Date().toISOString();
        const run = this.options.store.saveRun({
            id: createRunId(),
            templateId: template.id,
            templateVersion,
            templateSnapshot,
            name: template.name,
            status: "queued",
            ownerUserId: actor?.id,
            variables,
            steps: [{
                    stepId: template.id,
                    name: template.name,
                    status: "pending",
                    prompt,
                    target: "local",
                    sessionMode: "current",
                }],
            currentStepIndex: 0,
            createdAt: now,
            updatedAt: now,
        });
        this.logRun(run.id, "info", "run", "Template run queued.", template.name);
        this.upsertRunJob(run, actor);
        this.options.appendActivity({
            source: "web",
            status: "queued",
            type: "workflow_template_run_queued",
            threadId: null,
            actor,
            prompt,
            detail: template.name,
        });
        void this.executeTemplateRun(run.id, actor).catch((error) => {
            const latest = this.options.store.getRun(run.id) ?? run;
            this.failRun(latest, error, actor);
        });
        return run;
    }
    async executeTemplateRun(id, actor) {
        const initial = this.options.store.getRun(id);
        if (!initial?.templateId)
            return;
        const template = initial.templateSnapshot ?? this.requireTemplate(initial.templateId);
        const correlationId = createCorrelationId();
        const prompt = renderPromptTemplate(template, initial.variables);
        const startedAt = new Date().toISOString();
        this.activeRuns.add(id);
        let run = this.options.store.patchRun(id, {
            status: "running",
            startedAt,
            steps: patchStep(initial.steps, template.id, {
                status: "running",
                prompt,
                inputPreview: prompt.slice(0, 2_000),
                correlationId,
                startedAt,
            }),
        }) ?? initial;
        this.logRun(id, "info", "run", "Template run started.", template.name);
        this.upsertRunJob(run, actor);
        this.options.appendActivity({
            source: "web",
            status: "running",
            type: "workflow_template_run_started",
            threadId: null,
            actor,
            correlationId,
            prompt,
            detail: template.name,
        });
        try {
            await this.waitForIdle(id);
            const session = await this.options.getSession(false);
            await this.options.runPrompt(session, { ...toPromptEnvelope(prompt), correlationId, activityActor: actor });
            const finishedAt = new Date().toISOString();
            const latest = this.options.store.getRun(id) ?? run;
            const completed = this.options.store.patchRun(id, {
                status: "completed",
                finishedAt,
                currentStepIndex: 1,
                steps: patchStep(latest.steps, template.id, { status: "completed", finishedAt }),
            }) ?? latest;
            this.logRun(id, "info", "run", "Template run completed.", template.name);
            this.upsertRunJob(completed, actor);
        }
        catch (error) {
            const latest = this.options.store.getRun(id) ?? run;
            const errorText = error instanceof Error ? error.message : String(error);
            const failedAt = new Date().toISOString();
            const withFailedStep = this.options.store.patchRun(id, {
                steps: patchStep(latest.steps, template.id, { status: "failed", finishedAt: failedAt, error: errorText }),
            }) ?? latest;
            this.logRun(id, "error", "run", "Template run failed.", errorText);
            this.failRun(withFailedStep, error, actor);
        }
        finally {
            this.activeRuns.delete(id);
        }
    }
    runWorkflow(id, variables = {}, actor) {
        const workflow = this.requireWorkflow(id);
        const version = this.options.store.latestVersion("workflow", id);
        return this.queueWorkflowRun(workflow, variables, actor, version?.version, version?.snapshot);
    }
    runWorkflowVersion(id, versionNumber, variables = {}, actor) {
        const version = this.requireVersion("workflow", id, versionNumber);
        return this.queueWorkflowRun(version.snapshot, variables, actor, version.version, version.snapshot);
    }
    queueWorkflowRun(workflow, variables, actor, workflowVersion, workflowSnapshot) {
        if (workflow.steps.length === 0) {
            throw new Error("Workflow has no steps.");
        }
        this.assertWorkflowRequiredVariables(workflow, variables);
        const now = new Date().toISOString();
        const run = this.options.store.saveRun({
            id: createRunId(),
            workflowId: workflow.id,
            workflowVersion,
            workflowSnapshot,
            name: workflow.name,
            status: "queued",
            ownerUserId: actor?.id,
            variables,
            steps: workflow.steps.map((step) => this.initialStepRun(step, variables)),
            currentStepIndex: 0,
            createdAt: now,
            updatedAt: now,
        });
        this.logRun(run.id, "info", "run", "Workflow run queued.", workflow.name);
        this.upsertRunJob(run, actor);
        this.record("workflow_run_queued", "queued", workflow.name, actor);
        void this.executeWorkflowRun(run.id, actor).catch((error) => {
            const latest = this.options.store.getRun(run.id) ?? run;
            this.failRun(latest, error, actor);
        });
        return run;
    }
    async cancelRun(id, actor) {
        const run = this.options.store.patchRun(id, {
            status: "aborted",
            finishedAt: new Date().toISOString(),
            error: "Cancelled by user.",
        });
        if (!run)
            return null;
        this.logRun(id, "warn", "run", "Workflow run cancelled by user.");
        if (this.activeRuns.has(id)) {
            await this.options.abort(actor).catch(() => { });
        }
        this.upsertRunJob(run, actor);
        this.record("workflow_run_aborted", "aborted", run.name, actor);
        return run;
    }
    resumeRun(id, actor) {
        const run = this.options.store.getRun(id);
        if (!run?.workflowId)
            return null;
        if (run.status !== "paused")
            return run;
        const currentStep = run.steps[run.currentStepIndex];
        const resumed = this.options.store.patchRun(id, {
            status: "queued",
            error: undefined,
            finishedAt: undefined,
            steps: currentStep ? patchStep(run.steps, currentStep.stepId, { approvedAt: new Date().toISOString() }) : run.steps,
        }) ?? run;
        this.logRun(id, "info", "run", "Workflow run resumed.");
        this.upsertRunJob(resumed, actor);
        this.record("workflow_run_resumed", "queued", resumed.name, actor);
        void this.executeWorkflowRun(id, actor).catch((error) => {
            const latest = this.options.store.getRun(id) ?? resumed;
            this.failRun(latest, error, actor);
        });
        return resumed;
    }
    rerunFromFailedStep(id, actor) {
        const run = this.options.store.getRun(id);
        if (!run)
            return null;
        const failedIndex = run.steps.findIndex((step) => step.status === "failed");
        const startIndex = failedIndex >= 0 ? failedIndex : Math.min(run.currentStepIndex, Math.max(0, run.steps.length - 1));
        const resetSteps = run.steps.map((step, index) => index < startIndex ? step : {
            ...step,
            status: "pending",
            startedAt: undefined,
            finishedAt: undefined,
            error: undefined,
            correlationId: undefined,
            approvedAt: step.requiresApproval ? step.approvedAt : undefined,
        });
        const patched = this.options.store.patchRun(id, {
            status: "queued",
            error: undefined,
            finishedAt: undefined,
            currentStepIndex: startIndex,
            steps: resetSteps,
        }) ?? run;
        this.logRun(id, "info", "run", "Workflow run queued from failed step.", `Starting at step ${startIndex + 1}.`);
        this.upsertRunJob(patched, actor);
        this.record("workflow_run_rerun_from_failed_step", "queued", `${patched.name} from step ${startIndex + 1}`, actor);
        if (patched.workflowId) {
            void this.executeWorkflowRun(id, actor).catch((error) => {
                const latest = this.options.store.getRun(id) ?? patched;
                this.failRun(latest, error, actor);
            });
        }
        else if (patched.templateId) {
            void this.executeTemplateRun(id, actor).catch((error) => {
                const latest = this.options.store.getRun(id) ?? patched;
                this.failRun(latest, error, actor);
            });
        }
        return patched;
    }
    runReport(id) {
        const run = this.options.store.getRun(id);
        if (!run) {
            throw new Error(`Workflow run not found: ${id}`);
        }
        const started = run.startedAt ? Date.parse(run.startedAt) : Date.parse(run.createdAt);
        const finished = run.finishedAt ? Date.parse(run.finishedAt) : undefined;
        const durationMs = Number.isFinite(started) && Number.isFinite(finished) ? Math.max(0, finished - started) : null;
        return {
            generatedAt: new Date().toISOString(),
            run,
            summary: {
                status: run.status,
                totalSteps: run.steps.length,
                completedSteps: run.steps.filter((step) => step.status === "completed").length,
                failedSteps: run.steps.filter((step) => step.status === "failed").length,
                skippedSteps: run.steps.filter((step) => step.status === "skipped").length,
                durationMs,
            },
            steps: run.steps,
            logs: run.logs ?? [],
        };
    }
    async executeWorkflowRun(id, actor, depth = 0) {
        const initial = this.options.store.getRun(id);
        if (!initial?.workflowId)
            return;
        const workflow = initial.workflowSnapshot ?? this.requireWorkflow(initial.workflowId);
        this.activeRuns.add(id);
        let run = this.options.store.patchRun(id, {
            status: "running",
            startedAt: new Date().toISOString(),
        }) ?? initial;
        this.logRun(id, "info", "run", "Workflow run started.", run.name);
        this.upsertRunJob(run, actor);
        this.record("workflow_run_started", "running", run.name, actor);
        try {
            for (let index = run.currentStepIndex; index < workflow.steps.length; index += 1) {
                run = this.options.store.getRun(id) ?? run;
                if (run.status === "aborted")
                    return;
                const step = workflow.steps[index];
                const stepRun = run.steps.find((candidate) => candidate.stepId === step.id);
                if (!conditionMatches(step, run.variables)) {
                    const finishedAt = new Date().toISOString();
                    run = this.options.store.patchRun(id, {
                        currentStepIndex: index + 1,
                        steps: patchStep(run.steps, step.id, { status: "skipped", skippedReason: conditionDetail(step), finishedAt }),
                    }) ?? run;
                    this.logRun(id, "info", "step", "Step skipped.", conditionDetail(step), step.id);
                    this.upsertRunJob(run, actor);
                    continue;
                }
                if (step.requiresApproval && !stepRun?.approvedAt) {
                    const pauseReason = `Approval required before ${step.name}.`;
                    run = this.options.store.patchRun(id, {
                        status: "paused",
                        currentStepIndex: index,
                        steps: patchStep(run.steps, step.id, { pauseReason }),
                    }) ?? run;
                    this.logRun(id, "warn", "step", "Step paused for approval.", pauseReason, step.id);
                    this.upsertRunJob(run, actor);
                    this.record("workflow_run_paused_for_approval", "queued", `${run.name} before ${step.name}`, actor);
                    this.options.broadcastStatus(`Workflow ${run.name} paused before ${step.name}. Approval is required.`, "warn");
                    return;
                }
                await this.runStepWithRetry(id, workflow, step, index, actor, depth);
            }
            const completed = this.options.store.patchRun(id, {
                status: "completed",
                finishedAt: new Date().toISOString(),
                currentStepIndex: workflow.steps.length,
            });
            if (completed) {
                this.logRun(id, "info", "run", "Workflow run completed.", completed.name);
                this.upsertRunJob(completed, actor);
                this.record("workflow_run_completed", "completed", completed.name, actor);
            }
        }
        finally {
            this.activeRuns.delete(id);
        }
    }
    async runStepWithRetry(id, workflow, step, index, actor, depth) {
        const policy = step.retryPolicy ?? { maxAttempts: 1, delayMs: 0 };
        let lastError;
        for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
            try {
                await this.runStep(id, workflow, step, index, actor, attempt, depth);
                return;
            }
            catch (error) {
                lastError = error;
                if (attempt >= policy.maxAttempts || step.continueOnError)
                    throw error;
                this.logRun(id, "warn", "step", "Step attempt failed; retry scheduled.", error instanceof Error ? error.message : String(error), step.id);
                await new Promise((resolve) => setTimeout(resolve, policy.delayMs));
            }
        }
        throw lastError instanceof Error ? lastError : new Error(String(lastError));
    }
    async runStep(id, workflow, step, index, actor, attempt, depth) {
        let run = this.options.store.getRun(id);
        if (!run)
            return;
        const correlationId = createCorrelationId();
        const existingStep = run.steps.find((candidate) => candidate.stepId === step.id);
        const prompt = existingStep?.prompt || this.renderWorkflowStepPrompt(step, run.variables);
        const startedAt = new Date().toISOString();
        run = this.options.store.patchRun(id, {
            currentStepIndex: index,
            steps: patchStep(run.steps, step.id, {
                status: "running",
                prompt,
                correlationId,
                startedAt,
                error: undefined,
                attempts: attempt,
                ...stepRunMetadata(step),
                attemptHistory: appendAttemptHistory(run.steps.find((candidate) => candidate.stepId === step.id)?.attemptHistory, {
                    attempt,
                    status: "running",
                    startedAt,
                    correlationId,
                }),
            }),
        }) ?? run;
        this.logRun(id, "info", "step", `Step ${attempt > 1 ? `attempt ${attempt} ` : ""}started.`, step.name, step.id);
        this.upsertRunJob(run, actor);
        try {
            let pluginResult;
            if (step.type === "workflow") {
                await this.runSubflow(id, step, run.variables, actor, depth);
            }
            else if (step.type === "plugin") {
                pluginResult = await this.runPluginStep(step, run.variables);
            }
            else if (step.target !== "local") {
                await this.runPeerStep(step, prompt, correlationId, actor);
            }
            else {
                await this.prepareStepSession(step, actor);
                await this.waitForIdle(id);
                const session = await this.options.getSession(false);
                await this.options.runPrompt(session, { ...toPromptEnvelope(prompt), correlationId, activityActor: actor });
            }
            const latest = this.options.store.getRun(id) ?? run;
            const finishedAt = new Date().toISOString();
            const nextVariables = pluginResult?.variables ? { ...latest.variables, ...pluginResult.variables } : latest.variables;
            const next = this.options.store.patchRun(id, {
                variables: nextVariables,
                steps: patchStep(latest.steps, step.id, {
                    status: "completed",
                    finishedAt,
                    outputSummary: step.type === "workflow" ? "Subflow completed." : step.type === "plugin" ? "Plugin action completed." : step.target !== "local" ? "Peer step completed." : "Prompt completed.",
                    pluginOutput: pluginResult?.output,
                    variables: pluginResult?.variables,
                    attemptHistory: finishAttemptHistory(latest.steps.find((candidate) => candidate.stepId === step.id)?.attemptHistory, attempt, "completed", finishedAt),
                }),
            });
            this.logRun(id, "info", "step", "Step completed.", step.name, step.id);
            if (next)
                this.upsertRunJob(next, actor);
        }
        catch (error) {
            const latest = this.options.store.getRun(id) ?? run;
            const errorText = error instanceof Error ? error.message : String(error);
            const finishedAt = new Date().toISOString();
            const next = this.options.store.patchRun(id, {
                status: step.continueOnError ? "running" : "failed",
                error: step.continueOnError ? undefined : errorText,
                finishedAt: step.continueOnError ? undefined : finishedAt,
                steps: patchStep(latest.steps, step.id, {
                    status: "failed",
                    finishedAt,
                    error: errorText,
                    attemptHistory: finishAttemptHistory(latest.steps.find((candidate) => candidate.stepId === step.id)?.attemptHistory, attempt, "failed", finishedAt, errorText),
                }),
            });
            this.logRun(id, "error", "step", "Step failed.", errorText, step.id);
            if (next)
                this.upsertRunJob(next, actor);
            this.options.appendActivity({
                source: "web",
                status: "failed",
                type: "workflow_step_failed",
                threadId: null,
                actor,
                correlationId,
                prompt,
                detail: `${workflow.name} / ${step.name}: ${errorText}`,
            });
            if (!step.continueOnError)
                throw error;
        }
    }
    async prepareStepSession(step, actor) {
        if (step.sessionMode === "attach") {
            if (!step.threadId)
                throw new Error(`Workflow step ${step.name} needs a thread id.`);
            if (step.agentId)
                await this.options.setAgent(step.agentId, actor);
            await this.options.attachSession(step.threadId, actor);
            return;
        }
        if (step.sessionMode === "new") {
            await this.options.newSession({
                agentId: step.agentId,
                workspace: step.workspace,
                workspaceMode: step.workspaceMode,
                model: step.model,
                reasoningEffort: step.reasoningEffort,
                launchProfileId: step.launchProfileId,
            }, actor);
            return;
        }
        if (step.agentId) {
            await this.options.setAgent(step.agentId, actor);
        }
    }
    async runPeerStep(step, prompt, correlationId, actor) {
        const peerId = peerIdFromWorkflowTarget(step.target);
        if (!peerId) {
            throw new Error(`Unsupported workflow target: ${step.target}`);
        }
        if (!this.options.runPeerPromptStep) {
            throw new Error("Peer workflow execution is not available in this runtime.");
        }
        const result = await this.options.runPeerPromptStep(peerId, step, prompt, correlationId, actor);
        this.options.appendActivity({
            source: "web",
            status: result.status === "completed" ? "completed" : "info",
            type: "workflow_peer_step_completed",
            threadId: null,
            actor,
            correlationId,
            prompt,
            detail: result.detail ?? `Peer ${peerId} finished workflow step ${step.name}.`,
        });
    }
    async waitForIdle(runId) {
        for (;;) {
            const run = this.options.store.getRun(runId);
            if (run?.status === "aborted") {
                throw new Error("Workflow run was cancelled.");
            }
            const session = await this.options.getSession(false);
            if (!this.options.isSessionBusy(session))
                return;
            await new Promise((resolve) => setTimeout(resolve, WORKFLOW_WAIT_MS));
        }
    }
    renderWorkflowStepPrompt(step, variables) {
        if (step.type === "workflow") {
            const workflow = step.workflowId ? this.requireWorkflow(step.workflowId) : null;
            if (!workflow)
                throw new Error(`Workflow step ${step.name} has no subflow.`);
            return `Run subflow: ${workflow.name}`;
        }
        if (step.type === "plugin") {
            if (!step.pluginId || !step.pluginActionId) {
                throw new Error(`Workflow step ${step.name} has no plugin action.`);
            }
            return `Run plugin action: ${step.pluginId}/${step.pluginActionId}`;
        }
        if (step.templateId) {
            return renderPromptTemplate(this.requireTemplate(step.templateId), variables);
        }
        const prompt = renderTemplateText(step.prompt ?? "", variables).trim();
        if (!prompt) {
            throw new Error(`Workflow step ${step.name} has no prompt.`);
        }
        return prompt;
    }
    async runSubflow(parentRunId, step, variables, actor, depth) {
        if (depth >= MAX_SUBFLOW_DEPTH)
            throw new Error("Workflow subflow depth limit reached.");
        if (!step.workflowId)
            throw new Error(`Workflow step ${step.name} has no subflow.`);
        const workflow = this.requireWorkflow(step.workflowId);
        this.assertWorkflowRequiredVariables(workflow, variables);
        const version = this.options.store.latestVersion("workflow", workflow.id);
        const now = new Date().toISOString();
        const run = this.options.store.saveRun({
            id: createRunId(),
            workflowId: workflow.id,
            workflowVersion: version?.version,
            workflowSnapshot: version?.snapshot,
            name: `${workflow.name} (subflow)`,
            status: "queued",
            ownerUserId: actor?.id,
            variables,
            steps: workflow.steps.map((subStep) => ({
                stepId: subStep.id,
                name: subStep.name,
                status: "pending",
                prompt: this.renderWorkflowStepPrompt(subStep, variables),
                ...stepRunMetadata(subStep),
            })),
            currentStepIndex: 0,
            createdAt: now,
            updatedAt: now,
        });
        this.logRun(parentRunId, "info", "step", "Subflow queued.", workflow.name, step.id);
        this.logRun(run.id, "info", "run", "Subflow run queued.", `Parent run: ${parentRunId}`);
        this.record("workflow_subflow_queued", "queued", `${parentRunId}: ${workflow.name}`, actor);
        await this.executeWorkflowRun(run.id, actor, depth + 1);
        const finished = this.options.store.getRun(run.id);
        if (!finished || finished.status !== "completed") {
            throw new Error(`Subflow ${workflow.name} finished with status ${finished?.status ?? "unknown"}.`);
        }
    }
    requireTemplate(id) {
        const template = this.options.store.getTemplate(id);
        if (!template)
            throw new Error(`Template not found: ${id}`);
        return template;
    }
    requireWorkflow(id) {
        const workflow = this.options.store.getWorkflow(id);
        if (!workflow)
            throw new Error(`Workflow not found: ${id}`);
        return workflow;
    }
    requireVersion(kind, id, version) {
        const record = this.options.store.getVersion(kind, id, version);
        if (!record)
            throw new Error(`${kind === "template" ? "Template" : "Workflow"} version not found: ${id} v${version}`);
        return record;
    }
    initialStepRun(step, variables) {
        return {
            stepId: step.id,
            name: step.name,
            status: "pending",
            prompt: this.renderWorkflowStepPrompt(step, variables),
            ...stepRunMetadata(step),
        };
    }
    workflowVariableDefinitions(workflow, seen = new Set()) {
        if (seen.has(workflow.id))
            return [];
        seen.add(workflow.id);
        const variables = new Map();
        const add = (variable) => {
            const name = String(variable.name ?? "").trim();
            if (!name || variables.has(name))
                return;
            variables.set(name, {
                name,
                label: variable.label,
                required: variable.required !== false,
                defaultValue: variable.defaultValue,
            });
        };
        for (const step of workflow.steps) {
            if (step.type === "workflow" && step.workflowId) {
                const subflow = this.options.store.getWorkflow(step.workflowId);
                if (subflow) {
                    this.workflowVariableDefinitions(subflow, seen).forEach(add);
                }
                continue;
            }
            if (step.type === "plugin") {
                collectVariablesFromUnknown(step.pluginInput).forEach(add);
                continue;
            }
            if (step.templateId) {
                this.options.store.getTemplate(step.templateId)?.variables.forEach(add);
            }
            else {
                extractTemplateVariables(step.prompt ?? "").forEach(add);
            }
        }
        return [...variables.values()];
    }
    async runPluginStep(step, variables) {
        if (!this.options.pluginService) {
            throw new Error("Plugin workflow actions are not available in this runtime.");
        }
        if (!step.pluginId || !step.pluginActionId) {
            throw new Error(`Workflow step ${step.name} has no plugin action.`);
        }
        const input = renderPluginInput(step.pluginInput ?? {}, variables);
        const result = await this.options.pluginService.invokeWorkflowAction(step.pluginId, step.pluginActionId, input);
        if (!result.ok) {
            throw new Error(result.stderr || "Plugin workflow action failed.");
        }
        if (step.pluginOutputVariables && Object.keys(step.pluginOutputVariables).length > 0) {
            const mapped = { ...(result.variables ?? {}) };
            for (const [variable, pathExpression] of Object.entries(step.pluginOutputVariables)) {
                const value = valueAtPath(result.output, pathExpression);
                if (value !== undefined)
                    mapped[variable] = String(value);
            }
            result.variables = Object.keys(mapped).length ? mapped : undefined;
        }
        return result;
    }
    assertWorkflowRequiredVariables(workflow, variables) {
        assertRequiredTemplateVariables(this.workflowVariableDefinitions(workflow), variables, workflow.name);
    }
    failRun(run, error, actor) {
        const errorText = error instanceof Error ? error.message : String(error);
        const failed = this.options.store.patchRun(run.id, {
            status: "failed",
            error: errorText,
            finishedAt: new Date().toISOString(),
        }) ?? run;
        this.logRun(run.id, "error", "run", "Workflow run failed.", errorText);
        this.upsertRunJob(failed, actor);
        this.record("workflow_run_failed", "failed", `${run.name}: ${errorText}`, actor);
        return failed;
    }
    upsertRunJob(run, actor) {
        this.options.upsertJob({
            id: `workflow-run:${run.id}`,
            kind: "workflow-run",
            title: run.name,
            status: run.status === "paused" ? "queued" : run.status,
            source: "web",
            threadId: null,
            owner: actor,
            startedAt: run.startedAt ?? run.createdAt,
            updatedAt: run.updatedAt,
            finishedAt: run.finishedAt,
            summary: run.error ?? `${run.steps.filter((step) => step.status === "completed").length}/${run.steps.length} steps completed`,
            correlationId: run.steps.find((step) => step.correlationId)?.correlationId,
            canCancel: run.status === "queued" || run.status === "running" || run.status === "paused",
            canRetry: run.status === "paused" || run.status === "failed" || run.status === "aborted",
            canReadLog: true,
        });
    }
    record(type, status, detail, actor) {
        this.options.appendActivity({
            source: "web",
            status,
            type,
            threadId: null,
            actor,
            detail,
        });
        this.options.appendAudit({
            action: "command",
            status: status === "failed" ? "failed" : "ok",
            contextKey: "web:dashboard",
            actor,
            description: `${type}: ${detail}`,
        });
    }
    logRun(runId, level, scope, message, detail, stepId) {
        this.options.store.appendRunLog(runId, {
            level,
            scope,
            stepId,
            message,
            detail,
        });
    }
    runDueSchedules() {
        const now = Date.now();
        for (const workflow of this.options.store.listWorkflows()) {
            const schedule = workflow.schedule;
            if (schedule?.enabled && !schedule.nextRunAt && schedule.cron) {
                this.options.store.saveWorkflow({ ...workflow, schedule: prepareWorkflowSchedule(schedule) });
                continue;
            }
            if (!schedule?.enabled || !schedule.nextRunAt || Date.parse(schedule.nextRunAt) > now)
                continue;
            if (this.activeRuns.has(`scheduled:${workflow.id}`))
                continue;
            const nextSchedule = nextWorkflowSchedule(schedule, now);
            this.options.store.saveWorkflow({ ...workflow, schedule: nextSchedule });
            const scheduledActor = { channel: "system", id: "workflow-scheduler", label: "Workflow scheduler" };
            this.runWorkflow(workflow.id, {}, scheduledActor);
        }
    }
    recoverInterruptedRuns() {
        const actor = { channel: "system", id: "workflow-recovery", label: "Workflow recovery" };
        for (const run of this.options.store.listRuns(500)) {
            if (this.activeRuns.has(run.id))
                continue;
            if (run.status !== "queued" && run.status !== "running")
                continue;
            const patched = this.options.store.patchRun(run.id, {
                status: "queued",
                error: undefined,
                finishedAt: undefined,
            }) ?? run;
            this.logRun(patched.id, "warn", "run", "Interrupted workflow run recovered.", patched.name);
            this.upsertRunJob(patched, actor);
            this.record("workflow_run_recovered", "queued", patched.name, actor);
            if (patched.workflowId) {
                void this.executeWorkflowRun(patched.id, actor).catch((error) => {
                    const latest = this.options.store.getRun(patched.id) ?? patched;
                    this.failRun(latest, error, actor);
                });
            }
            else if (patched.templateId) {
                void this.executeTemplateRun(patched.id, actor).catch((error) => {
                    const latest = this.options.store.getRun(patched.id) ?? patched;
                    this.failRun(latest, error, actor);
                });
            }
        }
    }
}
export function renderPromptTemplate(template, variables) {
    const merged = Object.fromEntries(template.variables.map((variable) => [variable.name, variable.defaultValue ?? ""]));
    return renderTemplateText(template.prompt, { ...merged, ...variables }).trim();
}
function patchStep(steps, stepId, patch) {
    return steps.map((step) => step.stepId === stepId ? { ...step, ...patch } : step);
}
function stepRunMetadata(step) {
    return {
        target: step.target,
        sessionMode: step.sessionMode,
        agentId: step.agentId,
        workspace: step.workspace,
        workspaceMode: step.workspaceMode,
        model: step.model,
        reasoningEffort: step.reasoningEffort,
        launchProfileId: step.launchProfileId,
        pluginId: step.pluginId,
        pluginActionId: step.pluginActionId,
        requiresApproval: step.requiresApproval,
        continueOnError: step.continueOnError,
        retryPolicy: step.retryPolicy,
    };
}
function appendAttemptHistory(existing, attempt) {
    return [...(existing ?? []).filter((item) => item.attempt !== attempt.attempt), attempt].slice(-20);
}
function finishAttemptHistory(existing, attempt, status, finishedAt, error) {
    const history = existing?.length ? existing : [{ attempt, status: "running", startedAt: finishedAt }];
    return history.map((item) => item.attempt === attempt ? { ...item, status, finishedAt, error } : item).slice(-20);
}
function conditionDetail(step) {
    const condition = step.condition;
    if (!condition)
        return "Condition did not match.";
    return `Condition did not match: ${condition.variable} ${condition.operator}${condition.value ? ` ${condition.value}` : ""}.`;
}
function createRunId() {
    return `run_${createCorrelationId()}`;
}
function conditionMatches(step, variables) {
    const condition = step.condition;
    if (!condition)
        return true;
    const value = variables[condition.variable] ?? "";
    const expected = condition.value ?? "";
    if (condition.operator === "exists")
        return value.trim().length > 0;
    if (condition.operator === "equals")
        return value === expected;
    if (condition.operator === "not_equals")
        return value !== expected;
    if (condition.operator === "contains")
        return value.includes(expected);
    if (condition.operator === "not_contains")
        return !value.includes(expected);
    return true;
}
function nextWorkflowSchedule(schedule, now) {
    const lastRunAt = new Date(now).toISOString();
    if (schedule.cron) {
        const next = nextCronDate(schedule.cron, schedule.timezone, new Date(now + 60_000));
        return {
            ...schedule,
            lastRunAt,
            nextRunAt: next?.toISOString(),
            enabled: Boolean(next),
        };
    }
    const intervalMs = (schedule.intervalMinutes ?? 0) * 60 * 1000;
    return {
        ...schedule,
        lastRunAt,
        nextRunAt: intervalMs > 0 ? new Date(now + intervalMs).toISOString() : undefined,
        enabled: intervalMs > 0 ? schedule.enabled : false,
    };
}
function prepareWorkflowSchedule(schedule) {
    if (!schedule?.enabled || schedule.nextRunAt || !schedule.cron)
        return schedule;
    const next = nextCronDate(schedule.cron, schedule.timezone, new Date(Date.now() + 60_000));
    return {
        ...schedule,
        nextRunAt: next?.toISOString(),
        enabled: Boolean(next),
    };
}
function nextCronDate(expression, timezone, from) {
    const parts = expression.trim().split(/\s+/);
    if (parts.length !== 5)
        return null;
    const fields = [
        parseCronField(parts[0], 0, 59),
        parseCronField(parts[1], 0, 23),
        parseCronField(parts[2], 1, 31),
        parseCronField(parts[3], 1, 12),
        parseCronField(parts[4], 0, 6),
    ];
    if (fields.some((field) => !field))
        return null;
    const tz = timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone;
    const cursor = new Date(from);
    cursor.setSeconds(0, 0);
    const deadline = cursor.getTime() + 366 * 24 * 60 * 60 * 1000;
    while (cursor.getTime() <= deadline) {
        const parts = datePartsInTimezone(cursor, tz);
        if (fields[0].has(parts.minute) &&
            fields[1].has(parts.hour) &&
            fields[2].has(parts.day) &&
            fields[3].has(parts.month) &&
            fields[4].has(parts.weekday)) {
            return cursor;
        }
        cursor.setMinutes(cursor.getMinutes() + 1);
    }
    return null;
}
function parseCronField(field, min, max) {
    const values = new Set();
    for (const part of field.split(",")) {
        const [rangePart = "", stepPart] = part.split("/");
        const step = stepPart ? Math.max(1, Number(stepPart)) : 1;
        if (!Number.isFinite(step))
            return null;
        const range = rangePart === "*" ? [min, max] : rangePart.includes("-") ? rangePart.split("-").map(Number) : [Number(rangePart), Number(rangePart)];
        const [start, end] = range;
        if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || start > end)
            return null;
        for (let value = start; value <= end; value += step)
            values.add(value);
    }
    return values;
}
function datePartsInTimezone(date, timezone) {
    const parts = new Intl.DateTimeFormat("en-US", {
        timeZone: timezone,
        minute: "numeric",
        hour: "numeric",
        day: "numeric",
        month: "numeric",
        weekday: "short",
        hour12: false,
    }).formatToParts(date);
    const value = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
    const weekdayText = parts.find((part) => part.type === "weekday")?.value ?? "Sun";
    return {
        minute: value("minute"),
        hour: value("hour") % 24,
        day: value("day"),
        month: value("month"),
        weekday: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(weekdayText.slice(0, 3)),
    };
}
function collectVariablesFromUnknown(input) {
    try {
        return extractTemplateVariables(JSON.stringify(input ?? {}));
    }
    catch {
        return [];
    }
}
function renderPluginInput(input, variables) {
    return renderUnknownTemplate(input, variables);
}
function valueAtPath(value, expression) {
    let current = value;
    for (const part of expression.split(".").map((item) => item.trim()).filter(Boolean)) {
        if (!current || typeof current !== "object" || Array.isArray(current))
            return undefined;
        current = current[part];
    }
    return current;
}
function renderUnknownTemplate(value, variables) {
    if (typeof value === "string") {
        return renderTemplateText(value, variables);
    }
    if (Array.isArray(value)) {
        return value.map((item) => renderUnknownTemplate(item, variables));
    }
    if (value && typeof value === "object") {
        return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, renderUnknownTemplate(item, variables)]));
    }
    return value;
}
function peerIdFromWorkflowTarget(target) {
    return target.startsWith("peer:") ? target.slice("peer:".length).trim() || null : null;
}
