/**
 * Step resolver — dependency ordering and template variable resolution.
 *
 * Pure functions: steps + context in → ordered execution plan out.
 * No I/O, no side effects, no Neo4j.
 *
 *   STEPS (unordered)
 *     │
 *     v
 *   resolveOrder()  ──→  Kahn's algorithm (topological sort)
 *     │                    ├─ cycle detected → CyclicDependencyError
 *     │                    └─ position tiebreak for independent steps
 *     v
 *   ORDERED STEPS
 *     │
 *     v
 *   resolveTemplates()  ──→  {{key.path}} substitution
 *     │                        ├─ JSON parse: object → pass-through, array → {_items}, else {_raw}
 *     │                        └─ unresolved → UnresolvedTemplateError
 *     v
 *   RESOLVED PARAMS
 */
// ─────────────────────────────────────────────────────────────
// Dependency resolution — Kahn's algorithm
// ─────────────────────────────────────────────────────────────
/**
 * Topological sort with position-based tiebreaking.
 * Returns steps in execution order. Throws on cycles.
 */
export function resolveOrder(steps) {
    const byId = new Map();
    const inDegree = new Map();
    const dependents = new Map();
    for (const step of steps) {
        byId.set(step.stepId, step);
        inDegree.set(step.stepId, 0);
        dependents.set(step.stepId, []);
    }
    for (const step of steps) {
        if (!step.dependsOn)
            continue;
        for (const depId of step.dependsOn) {
            if (!byId.has(depId)) {
                throw new Error(`Step "${step.label}" (${step.stepId}) depends on unknown step "${depId}"`);
            }
            inDegree.set(step.stepId, (inDegree.get(step.stepId) ?? 0) + 1);
            dependents.get(depId).push(step.stepId);
        }
    }
    // Seed queue with steps that have no dependencies, sorted by position
    const queue = steps
        .filter((s) => (inDegree.get(s.stepId) ?? 0) === 0)
        .sort((a, b) => a.position - b.position);
    const ordered = [];
    while (queue.length > 0) {
        const current = queue.shift();
        ordered.push(current);
        for (const depId of dependents.get(current.stepId) ?? []) {
            const newDegree = (inDegree.get(depId) ?? 1) - 1;
            inDegree.set(depId, newDegree);
            if (newDegree === 0) {
                queue.push(byId.get(depId));
                // Re-sort by position for deterministic ordering among freed steps
                queue.sort((a, b) => a.position - b.position);
            }
        }
    }
    if (ordered.length !== steps.length) {
        const stuck = steps
            .filter((s) => !ordered.includes(s))
            .map((s) => `"${s.label}" (${s.stepId})`);
        throw new Error(`Cycle detected in step dependencies — stuck steps: ${stuck.join(", ")}`);
    }
    return ordered;
}
// ─────────────────────────────────────────────────────────────
// Template resolution
// ─────────────────────────────────────────────────────────────
/**
 * Parse a step's MCP tool response text into a structured output.
 * If the text is valid JSON: objects are returned directly, arrays are
 * wrapped as { _items: array }. Non-object/array JSON and unparseable
 * text are wrapped as { _raw: text }.
 */
export function parseStepOutput(text) {
    // Strip markdown code fences — LLMs frequently wrap JSON in ```json blocks
    // despite instructions not to. Try the cleaned version first, fall back to raw.
    const cleaned = text
        .replace(/^```json?\s*/i, "")
        .replace(/\s*```\s*$/i, "")
        .trim();
    const toParse = cleaned || text;
    try {
        const parsed = JSON.parse(toParse);
        if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
            return parsed;
        }
        if (Array.isArray(parsed)) {
            return { _items: parsed };
        }
        return { _raw: toParse };
    }
    catch {
        return { _raw: toParse };
    }
}
/**
 * Resolve dot-notation path into a nested object.
 * "a.b.c" on { a: { b: { c: 42 } } } → 42
 * Returns undefined if any segment is missing.
 */
function resolvePath(obj, path) {
    let current = obj;
    for (const segment of path.split(".")) {
        if (current === null || current === undefined || typeof current !== "object") {
            return undefined;
        }
        current = current[segment];
    }
    return current;
}
/**
 * Resolve all {{template.variables}} in a params object.
 * Context is a map of outputKey → parsed step output.
 *
 * Throws UnresolvedTemplateError if any variable cannot be resolved.
 */
export function resolveTemplates(params, context) {
    const resolved = {};
    for (const [key, value] of Object.entries(params)) {
        if (typeof value === "string") {
            resolved[key] = resolveStringTemplate(value, context);
        }
        else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
            resolved[key] = resolveTemplates(value, context);
        }
        else {
            resolved[key] = value;
        }
    }
    return resolved;
}
/**
 * Resolve template variables in a single string value.
 * Supports full replacement: "{{a.b}}" → value of a.b
 * Supports interpolation: "Hello {{name}}" → "Hello John"
 */
export function resolveStringTemplate(template, context) {
    const TEMPLATE_RE = /\{\{([^}]+)\}\}/g;
    const matches = [...template.matchAll(TEMPLATE_RE)];
    if (matches.length === 0)
        return template;
    // If the entire string is a single template variable, return the raw value
    // (preserving type — number, boolean, object, etc.)
    if (matches.length === 1 && matches[0][0] === template) {
        const path = matches[0][1].trim();
        const value = resolveContextPath(path, context);
        if (value === undefined) {
            throw new Error(`Unresolved template variable '{{${path}}}' — not found in context or prior step outputs`);
        }
        return value;
    }
    // Multiple variables or mixed text+variable — string interpolation
    return template.replace(TEMPLATE_RE, (_match, rawPath) => {
        const path = rawPath.trim();
        const value = resolveContextPath(path, context);
        if (value === undefined) {
            throw new Error(`Unresolved template variable '{{${path}}}' — not found in context or prior step outputs`);
        }
        return typeof value === "object" ? JSON.stringify(value) : String(value);
    });
}
/**
 * Resolve a dotted path against the context.
 * First segment is the outputKey, remaining segments traverse the output object.
 * e.g. "newContact.contactId" → context["newContact"]["contactId"]
 */
function resolveContextPath(path, context) {
    const [rootKey, ...rest] = path.split(".");
    const root = context[rootKey];
    if (root === undefined)
        return undefined;
    if (rest.length === 0)
        return root;
    return resolvePath(root, rest.join("."));
}
// ─────────────────────────────────────────────────────────────
// Fallback template rendering (degradation path)
// ─────────────────────────────────────────────────────────────
const FALLBACK_DEFAULT_TEMPLATE = "⚠️ AI reasoning is temporarily unavailable ({{_error}}).\n\n" +
    "The workflow has fallen back to raw data.\n\n" +
    "{{_priorOutputs}}";
/**
 * Max characters (JS string length, UTF-16 code units) for the
 * `{{_priorOutputs}}` substitution. Keeps banner + error line intact
 * under the workflow-execute MAX_OUTPUT_SIZE (10 KB) storage cap.
 * Uses char count to match the existing MAX_OUTPUT_SIZE truncation
 * pattern in workflow-execute.ts; multi-byte characters may exceed
 * 8 KB on the wire but stay within the storage cap.
 */
const PRIOR_OUTPUTS_MAX_CHARS = 8 * 1024;
/**
 * Serialise prior step outputs into a human-readable dump for inclusion
 * in a fallback delivery message. Caps at 8 KB of characters.
 *
 * Excluded keys (typically the seeded inputContext keys from the workflow
 * invocation) are skipped entirely — seeded inputs can contain credentials
 * or trigger metadata that must not leak into delivered output.
 */
function serialisePriorOutputs(outputContext, excludeKeys) {
    const keys = Object.keys(outputContext).filter((k) => !excludeKeys.has(k));
    if (keys.length === 0)
        return "";
    const parts = [];
    for (const key of keys) {
        const val = outputContext[key];
        parts.push(`### ${key}`);
        if (val && typeof val === "object" && typeof val._raw === "string") {
            parts.push(val._raw);
        }
        else {
            try {
                parts.push(JSON.stringify(val, null, 2));
            }
            catch {
                parts.push(String(val));
            }
        }
    }
    const joined = parts.join("\n\n");
    if (joined.length <= PRIOR_OUTPUTS_MAX_CHARS)
        return joined;
    return joined.slice(0, PRIOR_OUTPUTS_MAX_CHARS) + "\n...[truncated]";
}
/**
 * Render a fallback output string when an LLM step's call cannot succeed.
 *
 * Lenient by design — unresolved `{{ref}}` variables become
 * `<unresolved: ref>` rather than throwing. A fallback that itself throws
 * would defeat the degradation mechanism.
 *
 *   DEFAULT TEMPLATE → banner + {{_error}} + {{_priorOutputs}}
 *   CUSTOM TEMPLATE  → author-supplied string with the same special
 *                      variables plus any {{stepKey.field}} from outputContext
 *
 * This is a dedicated helper, not a `lenient: true` flag on
 * resolveStringTemplate(), because only fallback rendering needs the
 * lenient semantics — normal step execution must still fail loudly on
 * unresolved refs.
 */
export function renderFallbackTemplate(params) {
    const { template, outputContext, error, excludeFromPriorOutputs } = params;
    const effective = template ?? FALLBACK_DEFAULT_TEMPLATE;
    const excludeKeys = excludeFromPriorOutputs ?? new Set();
    const priorOutputs = serialisePriorOutputs(outputContext, excludeKeys);
    const TEMPLATE_RE = /\{\{([^}]+)\}\}/g;
    return effective.replace(TEMPLATE_RE, (_match, rawPath) => {
        const path = rawPath.trim();
        if (path === "_error")
            return error;
        if (path === "_priorOutputs")
            return priorOutputs;
        const value = resolveContextPath(path, outputContext);
        if (value === undefined) {
            console.error(`[workflow-execute] fallback template has unresolved ref: ${path}`);
            return `<unresolved: ${path}>`;
        }
        if (typeof value === "object")
            return JSON.stringify(value);
        return String(value);
    });
}
//# sourceMappingURL=step-resolver.js.map