import { createHash, randomBytes, randomUUID } from "node:crypto";
import { createDocumentStore } from "./state-backend.js";
const DEFAULT_MAX_RUNS = 500;
export class WorkflowStore {
    maxRuns;
    store;
    constructor(workspace, backend = "json", maxRuns = DEFAULT_MAX_RUNS) {
        this.maxRuns = maxRuns;
        this.store = createDocumentStore({
            workspace,
            backend,
            fileName: "workflows.json",
            sqliteKey: "workflows",
        });
    }
    listTemplates() {
        return this.payload().templates.sort(sortByUpdated);
    }
    getTemplate(id) {
        return this.payload().templates.find((template) => template.id === id) ?? null;
    }
    saveTemplate(input) {
        const now = new Date().toISOString();
        const id = normalizeId(input.id) || randomId();
        let template;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const existing = payload.templates.find((candidate) => candidate.id === id);
            template = normalizeTemplate({
                ...existing,
                ...input,
                id,
                createdAt: existing?.createdAt ?? input.createdAt ?? now,
                updatedAt: now,
            });
            payload.templates = upsertById(payload.templates, template);
            payload.versions = appendVersion(payload.versions ?? [], "template", template, input.ownerUserId);
            return payload;
        });
        return template;
    }
    deleteTemplate(id) {
        let removed = false;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const next = payload.templates.filter((template) => template.id !== id);
            removed = next.length !== payload.templates.length;
            payload.templates = next;
            if (removed) {
                payload.versions = (payload.versions ?? []).filter((version) => !(version.kind === "template" && version.entityId === id));
            }
            return payload;
        });
        return removed;
    }
    listWorkflows() {
        return this.payload().workflows.sort(sortByUpdated);
    }
    getWorkflow(id) {
        return this.payload().workflows.find((workflow) => workflow.id === id) ?? null;
    }
    saveWorkflow(input) {
        const now = new Date().toISOString();
        const id = normalizeId(input.id) || randomId();
        let workflow;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const existing = payload.workflows.find((candidate) => candidate.id === id);
            workflow = normalizeWorkflow({
                ...existing,
                ...input,
                id,
                createdAt: existing?.createdAt ?? input.createdAt ?? now,
                updatedAt: now,
            });
            payload.workflows = upsertById(payload.workflows, workflow);
            payload.versions = appendVersion(payload.versions ?? [], "workflow", workflow, input.ownerUserId);
            return payload;
        });
        return workflow;
    }
    deleteWorkflow(id) {
        let removed = false;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const next = payload.workflows.filter((workflow) => workflow.id !== id);
            removed = next.length !== payload.workflows.length;
            payload.workflows = next;
            if (removed) {
                payload.versions = (payload.versions ?? []).filter((version) => !(version.kind === "workflow" && version.entityId === id));
            }
            return payload;
        });
        return removed;
    }
    listWorkflowTriggers(workflowId) {
        return [...(this.getWorkflow(workflowId)?.triggers ?? [])].sort(sortByUpdated);
    }
    createWorkflowTrigger(workflowId, input = {}) {
        const now = new Date().toISOString();
        const token = `nrt_${randomBytes(24).toString("base64url")}`;
        const trigger = normalizeWorkflowTrigger({
            id: randomId(),
            kind: input.kind,
            name: input.name || `${input.kind === "webhook" ? "Webhook" : "API"} trigger`,
            enabled: input.enabled !== false,
            tokenHash: hashWorkflowTriggerToken(token),
            createdAt: now,
            updatedAt: now,
        });
        let updated;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const workflow = payload.workflows.find((candidate) => candidate.id === workflowId);
            if (!workflow) {
                throw new Error(`Workflow not found: ${workflowId}`);
            }
            updated = normalizeWorkflow({
                ...workflow,
                triggers: [...(workflow.triggers ?? []), trigger],
                updatedAt: now,
            });
            payload.workflows = upsertById(payload.workflows, updated);
            payload.versions = appendVersion(payload.versions ?? [], "workflow", updated, workflow.ownerUserId);
            return payload;
        });
        const updatedWorkflow = updated;
        const saved = updatedWorkflow.triggers?.find((candidate) => candidate.id === trigger.id) ?? trigger;
        return { workflow: updatedWorkflow, trigger: saved, token };
    }
    deleteWorkflowTrigger(workflowId, triggerId) {
        let updated = null;
        let removed = false;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const workflow = payload.workflows.find((candidate) => candidate.id === workflowId);
            if (!workflow) {
                throw new Error(`Workflow not found: ${workflowId}`);
            }
            const nextTriggers = (workflow.triggers ?? []).filter((trigger) => trigger.id !== triggerId);
            removed = nextTriggers.length !== (workflow.triggers ?? []).length;
            if (!removed) {
                updated = workflow;
                return payload;
            }
            updated = normalizeWorkflow({ ...workflow, triggers: nextTriggers, updatedAt: new Date().toISOString() });
            payload.workflows = upsertById(payload.workflows, updated);
            payload.versions = appendVersion(payload.versions ?? [], "workflow", updated, workflow.ownerUserId);
            return payload;
        });
        return { workflow: updated, removed };
    }
    findWorkflowTriggerByToken(token) {
        const tokenHash = hashWorkflowTriggerToken(token);
        for (const workflow of this.listWorkflows()) {
            const trigger = (workflow.triggers ?? []).find((candidate) => candidate.tokenHash === tokenHash);
            if (trigger) {
                return { workflow, trigger };
            }
        }
        return null;
    }
    markWorkflowTriggerUsed(workflowId, triggerId) {
        const now = new Date().toISOString();
        let updated = null;
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            const workflow = payload.workflows.find((candidate) => candidate.id === workflowId);
            if (!workflow)
                return payload;
            const triggers = (workflow.triggers ?? []).map((trigger) => trigger.id === triggerId ? { ...trigger, lastTriggeredAt: now, updatedAt: now } : trigger);
            if (!triggers.some((trigger) => trigger.id === triggerId)) {
                updated = workflow;
                return payload;
            }
            updated = normalizeWorkflow({ ...workflow, triggers, updatedAt: now });
            payload.workflows = upsertById(payload.workflows, updated);
            return payload;
        });
        return updated;
    }
    listVersions(kind, entityId) {
        return this.payload().versions
            .filter((version) => version.kind === kind && version.entityId === entityId)
            .sort((left, right) => right.version - left.version);
    }
    getVersion(kind, entityId, version) {
        return this.payload().versions.find((item) => item.kind === kind && item.entityId === entityId && item.version === version) ?? null;
    }
    latestVersion(kind, entityId) {
        return this.listVersions(kind, entityId)[0] ?? null;
    }
    restoreVersion(kind, entityId, version, ownerUserId) {
        const record = this.getVersion(kind, entityId, version);
        if (!record)
            return null;
        if (kind === "template") {
            return this.saveTemplate({ ...record.snapshot, id: entityId, ownerUserId });
        }
        return this.saveWorkflow({ ...record.snapshot, id: entityId, ownerUserId });
    }
    diffVersions(kind, entityId, fromVersion, toVersion) {
        const versions = this.listVersions(kind, entityId).slice().reverse();
        const to = toVersion ? this.getVersion(kind, entityId, toVersion) : versions.at(-1) ?? null;
        const from = fromVersion ? this.getVersion(kind, entityId, fromVersion) : versions.at(-2) ?? null;
        return {
            kind,
            entityId,
            fromVersion: from?.version,
            toVersion: to?.version,
            changes: diffObjects(from?.snapshot, to?.snapshot),
        };
    }
    exportTemplate(id, version) {
        const versionRecord = version ? this.getVersion("template", id, version) : this.latestVersion("template", id);
        const template = versionRecord?.snapshot ?? this.getTemplate(id) ?? undefined;
        return template ? { kind: "template", exportedAt: new Date().toISOString(), template, version: versionRecord ?? undefined } : null;
    }
    exportWorkflow(id, version) {
        const versionRecord = version ? this.getVersion("workflow", id, version) : this.latestVersion("workflow", id);
        const workflow = versionRecord?.snapshot ?? this.getWorkflow(id) ?? undefined;
        return workflow ? { kind: "workflow", exportedAt: new Date().toISOString(), workflow, version: versionRecord ?? undefined } : null;
    }
    importTemplate(input, ownerUserId) {
        const raw = importedRecord(input, "template");
        return this.saveTemplate({
            ...raw,
            id: randomId(),
            ownerUserId,
            createdAt: undefined,
            updatedAt: undefined,
            name: raw.name ? String(raw.name) : "Imported template",
            prompt: raw.prompt ?? "",
        });
    }
    importWorkflow(input, ownerUserId) {
        const raw = importedRecord(input, "workflow");
        return this.saveWorkflow({
            ...raw,
            id: randomId(),
            ownerUserId,
            createdAt: undefined,
            updatedAt: undefined,
            name: raw.name ? String(raw.name) : "Imported workflow",
            steps: raw.steps ?? [],
            triggers: undefined,
        });
    }
    listRuns(limit = 100) {
        return this.payload().runs
            .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))
            .slice(0, Math.max(1, Math.min(this.maxRuns, limit)));
    }
    getRun(id) {
        return this.payload().runs.find((run) => run.id === id) ?? null;
    }
    saveRun(input) {
        const run = normalizeRun(input);
        this.store.update((current) => {
            const payload = this.normalizePayload(current);
            payload.runs = upsertById(payload.runs, run)
                .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))
                .slice(0, this.maxRuns);
            return payload;
        });
        return run;
    }
    patchRun(id, patch) {
        const existing = this.getRun(id);
        if (!existing)
            return null;
        return this.saveRun({ ...existing, ...patch, id, updatedAt: patch.updatedAt ?? new Date().toISOString() });
    }
    appendRunLog(id, input) {
        const existing = this.getRun(id);
        if (!existing)
            return null;
        const entry = normalizeRunLog({
            id: input.id ?? randomId(),
            at: input.at ?? new Date().toISOString(),
            level: input.level,
            scope: input.scope,
            stepId: input.stepId,
            message: input.message,
            detail: input.detail,
        });
        return this.saveRun({
            ...existing,
            logs: [...(existing.logs ?? []), entry].slice(-500),
            updatedAt: new Date().toISOString(),
        });
    }
    payload() {
        return this.normalizePayload(this.store.read());
    }
    normalizePayload(payload) {
        if (!payload || payload.version !== 1) {
            return { version: 1, templates: [], workflows: [], runs: [], versions: [] };
        }
        return {
            version: 1,
            templates: Array.isArray(payload.templates) ? payload.templates.map(normalizeTemplate) : [],
            workflows: Array.isArray(payload.workflows) ? payload.workflows.map(normalizeWorkflow) : [],
            runs: Array.isArray(payload.runs) ? payload.runs.map(normalizeRun).slice(0, this.maxRuns) : [],
            versions: normalizeVersions(payload.versions),
        };
    }
}
export function extractTemplateVariables(prompt) {
    const names = new Set();
    for (const match of prompt.matchAll(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*\}\}/g)) {
        if (match[1])
            names.add(match[1]);
    }
    return [...names].map((name) => ({ name, required: true }));
}
export function renderTemplateText(text, variables) {
    return text.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*\}\}/g, (_match, name) => variables[name] ?? "");
}
export function missingRequiredTemplateVariables(definitions, variables) {
    return definitions
        .filter((variable) => variable.required !== false &&
        variable.defaultValue === undefined &&
        !Object.prototype.hasOwnProperty.call(variables, variable.name))
        .map((variable) => variable.name);
}
export function assertRequiredTemplateVariables(definitions, variables, label = "Workflow") {
    const missing = missingRequiredTemplateVariables(definitions, variables);
    if (missing.length > 0) {
        throw new Error(`${label} is missing required variable(s): ${missing.join(", ")}`);
    }
}
function normalizeTemplate(input) {
    const now = new Date().toISOString();
    const variables = input.variables?.length ? input.variables : extractTemplateVariables(input.prompt);
    return {
        id: normalizeId(input.id) || randomId(),
        name: String(input.name ?? "Untitled template").trim() || "Untitled template",
        description: cleanOptional(input.description),
        tags: normalizeTags(input.tags),
        prompt: String(input.prompt ?? "").trim(),
        variables: variables.map(normalizeVariable),
        defaultAgentId: input.defaultAgentId,
        defaultWorkspace: cleanOptional(input.defaultWorkspace),
        defaultModel: cleanOptional(input.defaultModel),
        defaultReasoning: cleanOptional(input.defaultReasoning),
        defaultLaunchProfile: cleanOptional(input.defaultLaunchProfile),
        scope: input.scope === "shared" ? "shared" : "private",
        ownerUserId: cleanOptional(input.ownerUserId),
        createdAt: validDate(input.createdAt) ?? now,
        updatedAt: validDate(input.updatedAt) ?? now,
    };
}
function normalizeWorkflow(input) {
    const now = new Date().toISOString();
    return {
        id: normalizeId(input.id) || randomId(),
        name: String(input.name ?? "Untitled workflow").trim() || "Untitled workflow",
        description: cleanOptional(input.description),
        tags: normalizeTags(input.tags),
        steps: (input.steps ?? []).map(normalizeStep),
        schedule: normalizeSchedule(input.schedule),
        triggers: normalizeWorkflowTriggers(input.triggers),
        scope: input.scope === "shared" ? "shared" : "private",
        ownerUserId: cleanOptional(input.ownerUserId),
        createdAt: validDate(input.createdAt) ?? now,
        updatedAt: validDate(input.updatedAt) ?? now,
    };
}
function normalizeStep(input) {
    const id = normalizeId(input.id) || randomId();
    return {
        id,
        name: String(input.name ?? `Step ${id.slice(0, 4)}`).trim() || "Step",
        type: input.type === "workflow" ? "workflow" : input.type === "plugin" ? "plugin" : "prompt",
        prompt: cleanOptional(input.prompt),
        templateId: cleanOptional(input.templateId),
        workflowId: cleanOptional(input.workflowId),
        pluginId: cleanOptional(input.pluginId),
        pluginActionId: cleanOptional(input.pluginActionId),
        pluginInput: normalizeRecord(input.pluginInput),
        pluginOutputVariables: normalizeStringRecord(input.pluginOutputVariables),
        condition: normalizeCondition(input.condition),
        retryPolicy: normalizeRetryPolicy(input.retryPolicy),
        agentId: input.agentId,
        workspace: cleanOptional(input.workspace),
        workspaceMode: normalizeWorkspaceMode(input.workspaceMode),
        model: cleanOptional(input.model),
        reasoningEffort: cleanOptional(input.reasoningEffort),
        launchProfileId: cleanOptional(input.launchProfileId),
        sessionMode: input.sessionMode === "new" || input.sessionMode === "attach" ? input.sessionMode : "current",
        threadId: cleanOptional(input.threadId),
        target: normalizeTarget(input.target),
        requiresApproval: Boolean(input.requiresApproval),
        continueOnError: Boolean(input.continueOnError),
    };
}
function normalizeRun(input) {
    const now = new Date().toISOString();
    const workflowSnapshot = input.workflowSnapshot ? normalizeWorkflow(input.workflowSnapshot) : undefined;
    const templateSnapshot = input.templateSnapshot ? normalizeTemplate(input.templateSnapshot) : undefined;
    return {
        ...input,
        id: normalizeId(input.id) || randomId(),
        workflowVersion: normalizePositiveNumber(input.workflowVersion),
        templateVersion: normalizePositiveNumber(input.templateVersion),
        workflowSnapshot,
        templateSnapshot,
        name: String(input.name ?? "Workflow run").trim() || "Workflow run",
        status: normalizeRunStatus(input.status),
        variables: input.variables && typeof input.variables === "object" ? input.variables : {},
        steps: Array.isArray(input.steps) ? input.steps.map(normalizeStepRun) : [],
        currentStepIndex: Math.max(0, Number.isFinite(input.currentStepIndex) ? input.currentStepIndex : 0),
        createdAt: validDate(input.createdAt) ?? now,
        updatedAt: validDate(input.updatedAt) ?? now,
        startedAt: validDate(input.startedAt),
        finishedAt: validDate(input.finishedAt),
        error: cleanOptional(input.error),
        logs: normalizeRunLogs(input.logs),
    };
}
function normalizeStepRun(input) {
    return {
        stepId: normalizeId(input.stepId) || randomId(),
        name: String(input.name ?? "Step").trim() || "Step",
        status: normalizeStepRunStatus(input.status),
        prompt: cleanOptional(input.prompt),
        pluginId: cleanOptional(input.pluginId),
        pluginActionId: cleanOptional(input.pluginActionId),
        correlationId: cleanOptional(input.correlationId),
        target: normalizeTarget(input.target),
        sessionMode: input.sessionMode === "new" || input.sessionMode === "attach" ? input.sessionMode : input.sessionMode === "current" ? "current" : undefined,
        agentId: input.agentId,
        workspace: cleanOptional(input.workspace),
        workspaceMode: normalizeWorkspaceMode(input.workspaceMode),
        model: cleanOptional(input.model),
        reasoningEffort: cleanOptional(input.reasoningEffort),
        launchProfileId: cleanOptional(input.launchProfileId),
        requiresApproval: input.requiresApproval === undefined ? undefined : Boolean(input.requiresApproval),
        continueOnError: input.continueOnError === undefined ? undefined : Boolean(input.continueOnError),
        retryPolicy: normalizeRetryPolicy(input.retryPolicy),
        startedAt: validDate(input.startedAt),
        finishedAt: validDate(input.finishedAt),
        error: cleanOptional(input.error),
        attempts: Math.max(0, Number.isFinite(input.attempts) ? input.attempts : 0) || undefined,
        attemptHistory: normalizeAttemptHistory(input.attemptHistory),
        approvedAt: validDate(input.approvedAt),
        skippedReason: cleanOptional(input.skippedReason),
        pauseReason: cleanOptional(input.pauseReason),
        inputPreview: cleanOptional(input.inputPreview),
        outputSummary: cleanOptional(input.outputSummary),
        pluginOutput: input.pluginOutput,
        variables: normalizeStringRecord(input.variables),
    };
}
function normalizeRunLogs(input) {
    if (!Array.isArray(input))
        return undefined;
    const logs = input
        .map((item) => {
        if (!item || typeof item !== "object")
            return null;
        return normalizeRunLog(item);
    })
        .filter((item) => Boolean(item));
    return logs.length ? logs.slice(-500) : undefined;
}
function normalizeRunLog(input) {
    return {
        id: normalizeId(input.id) || randomId(),
        at: validDate(input.at) ?? new Date().toISOString(),
        level: input.level === "warn" || input.level === "error" ? input.level : "info",
        scope: input.scope === "step" ? "step" : "run",
        stepId: cleanOptional(input.stepId),
        message: String(input.message ?? "").trim() || "Workflow event",
        detail: cleanOptional(input.detail),
    };
}
function normalizeVersions(input) {
    if (!Array.isArray(input))
        return [];
    return input
        .map(normalizeVersionRecord)
        .filter((version) => Boolean(version))
        .sort((left, right) => {
        if (left.kind !== right.kind)
            return left.kind.localeCompare(right.kind);
        if (left.entityId !== right.entityId)
            return left.entityId.localeCompare(right.entityId);
        return left.version - right.version;
    })
        .slice(-2_000);
}
function normalizeVersionRecord(input) {
    if (!input || typeof input !== "object" || Array.isArray(input))
        return null;
    const record = input;
    const kind = normalizeVersionKind(record.kind);
    const entityId = normalizeId(record.entityId);
    const version = normalizePositiveNumber(record.version);
    const snapshot = normalizeVersionSnapshot(kind, record.snapshot);
    if (!kind || !entityId || !version || !snapshot)
        return null;
    return {
        id: normalizeId(record.id) || `${kind}_${entityId}_v${version}`,
        kind,
        entityId,
        version,
        name: String(record.name ?? snapshot.name ?? `${kind} ${version}`).trim(),
        createdAt: validDate(record.createdAt) ?? new Date().toISOString(),
        createdByUserId: cleanOptional(record.createdByUserId),
        snapshot,
    };
}
function normalizeVersionKind(value) {
    return value === "template" || value === "workflow" ? value : null;
}
function normalizeVersionSnapshot(kind, snapshot) {
    if (!kind || !snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
        return null;
    const record = snapshot;
    if (kind === "template") {
        return normalizeTemplate({
            id: normalizeId(record.id),
            name: String(record.name ?? "Untitled template"),
            prompt: String(record.prompt ?? ""),
            description: cleanOptional(record.description),
            tags: Array.isArray(record.tags) ? record.tags.map(String) : [],
            variables: Array.isArray(record.variables) ? record.variables : undefined,
            defaultAgentId: cleanOptional(record.defaultAgentId),
            defaultWorkspace: cleanOptional(record.defaultWorkspace),
            defaultModel: cleanOptional(record.defaultModel),
            defaultReasoning: cleanOptional(record.defaultReasoning),
            defaultLaunchProfile: cleanOptional(record.defaultLaunchProfile),
            scope: record.scope === "shared" ? "shared" : "private",
            ownerUserId: cleanOptional(record.ownerUserId),
            createdAt: validDate(record.createdAt),
            updatedAt: validDate(record.updatedAt),
        });
    }
    return normalizeWorkflow({
        id: normalizeId(record.id),
        name: String(record.name ?? "Untitled workflow"),
        description: cleanOptional(record.description),
        tags: Array.isArray(record.tags) ? record.tags.map(String) : [],
        steps: Array.isArray(record.steps) ? record.steps : [],
        schedule: record.schedule,
        scope: record.scope === "shared" ? "shared" : "private",
        ownerUserId: cleanOptional(record.ownerUserId),
        createdAt: validDate(record.createdAt),
        updatedAt: validDate(record.updatedAt),
    });
}
function appendVersion(versions, kind, snapshot, userId) {
    const normalized = normalizeVersionSnapshot(kind, snapshot);
    if (!normalized)
        return versions;
    if (kind === "workflow") {
        delete normalized.triggers;
    }
    const entityVersions = versions
        .filter((version) => version.kind === kind && version.entityId === normalized.id)
        .sort((left, right) => right.version - left.version);
    const latest = entityVersions[0];
    if (latest && snapshotFingerprint(latest.snapshot) === snapshotFingerprint(normalized)) {
        return versions;
    }
    const nextVersion = (latest?.version ?? 0) + 1;
    const record = {
        id: `${kind}_${normalized.id}_v${nextVersion}`,
        kind,
        entityId: normalized.id,
        version: nextVersion,
        name: normalized.name,
        createdAt: new Date().toISOString(),
        createdByUserId: cleanOptional(userId),
        snapshot: cloneJson(normalized),
    };
    return [...versions, record]
        .filter((item) => item.kind !== kind || item.entityId !== normalized.id || item.version > nextVersion - 50)
        .slice(-2_000);
}
function snapshotFingerprint(snapshot) {
    const copy = cloneJson(snapshot);
    delete copy.updatedAt;
    return JSON.stringify(copy);
}
function cloneJson(value) {
    return JSON.parse(JSON.stringify(value));
}
function diffObjects(before, after, basePath = "") {
    if (JSON.stringify(before) === JSON.stringify(after))
        return [];
    const beforeRecord = objectDiffRecord(before);
    const afterRecord = objectDiffRecord(after);
    if (!beforeRecord || !afterRecord) {
        return [{ path: basePath || "$", type: before === undefined ? "added" : after === undefined ? "removed" : "changed", before, after }];
    }
    const keys = [...new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])].sort();
    return keys.flatMap((key) => {
        const path = basePath ? `${basePath}.${key}` : key;
        if (!(key in beforeRecord))
            return [{ path, type: "added", after: afterRecord[key] }];
        if (!(key in afterRecord))
            return [{ path, type: "removed", before: beforeRecord[key] }];
        return diffObjects(beforeRecord[key], afterRecord[key], path);
    }).slice(0, 200);
}
function objectDiffRecord(value) {
    return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function importedRecord(input, kind) {
    const record = input && typeof input === "object" ? input : {};
    const bundleKind = normalizeVersionKind(record.kind);
    const bundled = kind === "template" ? record.template : record.workflow;
    const version = record.version && typeof record.version === "object" ? record.version : null;
    const snapshot = version?.snapshot ?? bundled ?? input;
    if (bundleKind && bundleKind !== kind) {
        throw new Error(`Import bundle is for ${bundleKind}, not ${kind}.`);
    }
    if (!snapshot || typeof snapshot !== "object") {
        throw new Error(`Invalid ${kind} import bundle.`);
    }
    return cloneJson(snapshot);
}
function normalizeAttemptHistory(input) {
    if (!Array.isArray(input))
        return undefined;
    const attempts = input.map((item) => {
        if (!item || typeof item !== "object")
            return null;
        const record = item;
        const attempt = Math.max(1, Math.floor(Number(record.attempt) || 1));
        const startedAt = validDate(record.startedAt);
        if (!startedAt)
            return null;
        const status = record.status === "completed" || record.status === "failed" ? record.status : "running";
        return {
            attempt,
            status,
            startedAt,
            finishedAt: validDate(record.finishedAt),
            correlationId: cleanOptional(record.correlationId),
            error: cleanOptional(record.error),
        };
    }).filter((item) => Boolean(item));
    return attempts.length ? attempts.slice(-20) : undefined;
}
function normalizeVariable(input) {
    return {
        name: String(input.name ?? "").trim(),
        label: cleanOptional(input.label),
        required: input.required !== false,
        defaultValue: cleanOptional(input.defaultValue),
    };
}
function normalizeTags(tags) {
    return Array.isArray(tags)
        ? [...new Set(tags.map((tag) => String(tag).trim()).filter(Boolean))]
        : [];
}
function normalizeWorkflowTriggers(input) {
    if (!Array.isArray(input))
        return undefined;
    const triggers = input
        .map((item) => {
        if (!item || typeof item !== "object")
            return null;
        const trigger = normalizeWorkflowTrigger(item);
        return trigger.tokenHash ? trigger : null;
    })
        .filter((item) => Boolean(item));
    return triggers.length ? triggers.slice(-50) : undefined;
}
function normalizeWorkflowTrigger(input) {
    const now = new Date().toISOString();
    return {
        id: normalizeId(input.id) || randomId(),
        kind: normalizeTriggerKind(input.kind),
        name: String(input.name ?? "Workflow trigger").trim() || "Workflow trigger",
        enabled: input.enabled !== false,
        tokenHash: cleanOptional(input.tokenHash) ?? "",
        createdAt: validDate(input.createdAt) ?? now,
        updatedAt: validDate(input.updatedAt) ?? now,
        lastTriggeredAt: validDate(input.lastTriggeredAt),
    };
}
function normalizeTriggerKind(value) {
    return value === "webhook" ? "webhook" : "api";
}
export function hashWorkflowTriggerToken(token) {
    return createHash("sha256").update(String(token ?? ""), "utf8").digest("hex");
}
function normalizeTarget(target) {
    const value = String(target ?? "local").trim();
    return value.startsWith("peer:") ? value : "local";
}
function normalizeWorkspaceMode(value) {
    return value === "shared" || value === "worktree" || value === "attached" ? value : undefined;
}
function normalizeRunStatus(status) {
    return status === "queued" || status === "running" || status === "completed" || status === "failed" || status === "aborted" || status === "paused"
        ? status
        : "queued";
}
function normalizeStepRunStatus(status) {
    return status === "pending" || status === "skipped" ? status : normalizeRunStatus(status);
}
function normalizeCondition(input) {
    if (!input || typeof input !== "object" || Array.isArray(input))
        return undefined;
    const record = input;
    const variable = cleanOptional(record.variable);
    if (!variable)
        return undefined;
    const operator = ["exists", "equals", "not_equals", "contains", "not_contains"].includes(String(record.operator))
        ? String(record.operator)
        : "exists";
    return { variable, operator, value: cleanOptional(record.value) };
}
function normalizeRecord(input) {
    return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
}
function normalizeStringRecord(input) {
    if (!input || typeof input !== "object" || Array.isArray(input))
        return undefined;
    const output = {};
    for (const [key, value] of Object.entries(input)) {
        output[key] = String(value ?? "");
    }
    return Object.keys(output).length ? output : undefined;
}
function normalizeRetryPolicy(input) {
    if (!input || typeof input !== "object" || Array.isArray(input))
        return undefined;
    const record = input;
    const maxAttempts = Math.max(1, Math.min(10, Math.floor(Number(record.maxAttempts) || 1)));
    const delayMs = Math.max(0, Math.min(60 * 60 * 1000, Math.floor(Number(record.delayMs) || 0)));
    return maxAttempts > 1 || delayMs > 0 ? { maxAttempts, delayMs } : undefined;
}
function normalizeSchedule(input) {
    if (!input || typeof input !== "object" || Array.isArray(input))
        return undefined;
    const record = input;
    const intervalMinutes = Math.max(0, Math.floor(Number(record.intervalMinutes) || 0)) || undefined;
    const cron = cleanOptional(record.cron);
    const timezone = cleanOptional(record.timezone);
    const runAt = validDate(record.runAt);
    const nextRunAt = validDate(record.nextRunAt) ?? runAt ?? (Boolean(record.enabled) && intervalMinutes ? new Date(Date.now() + intervalMinutes * 60 * 1000).toISOString() : undefined);
    const schedule = {
        enabled: Boolean(record.enabled),
        runAt,
        intervalMinutes,
        cron,
        timezone,
        nextRunAt,
        lastRunAt: validDate(record.lastRunAt),
    };
    return schedule.enabled || runAt || intervalMinutes || cron ? schedule : undefined;
}
function upsertById(items, item) {
    const index = items.findIndex((candidate) => candidate.id === item.id);
    if (index < 0)
        return [...items, item];
    const next = [...items];
    next[index] = item;
    return next;
}
function sortByUpdated(left, right) {
    return Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
}
function normalizeId(value) {
    return String(value ?? "").trim().replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 80);
}
function cleanOptional(value) {
    const text = String(value ?? "").trim();
    return text || undefined;
}
function validDate(value) {
    return typeof value === "string" && Number.isFinite(Date.parse(value)) ? value : undefined;
}
function normalizePositiveNumber(value) {
    const number = Math.floor(Number(value));
    return Number.isFinite(number) && number > 0 ? number : undefined;
}
function randomId() {
    return randomUUID().replace(/-/g, "").slice(0, 12);
}
