import { BUILTIN_FUNCTIONS } from "../config.js";
import { getImportedNames, } from "../types.js";
// import { Skill } from "../types/skill.js"; // Unused after llm() refactor
import { expressionToString, getAllVariablesInBodyArray, isInsideBlock, walkNodesArray, } from "../utils/node.js";
import { desugarParallelInBody } from "./parallelDesugar.js";
import { injectSchemaArgsInProgram } from "./injectSchemaArgs.js";
/**
 * Recursively apply a transform function to all body arrays in a node tree.
 * Handles ifElse (thenBody/elseBody), loops, threads, match blocks, handle blocks, etc.
 */
function walkBody(body, fn) {
    const walked = body.map((node) => {
        if (node.type === "ifElse") {
            node.thenBody = walkBody(node.thenBody, fn);
            if (node.elseBody) {
                node.elseBody = walkBody(node.elseBody, fn);
            }
        }
        else if (node.type === "forLoop" ||
            node.type === "whileLoop" ||
            node.type === "messageThread") {
            node.body = walkBody(node.body, fn);
        }
        else if (node.type === "matchBlock") {
            for (const caseItem of node.cases) {
                if (caseItem.type === "comment")
                    continue;
                if (caseItem.type === "newLine")
                    continue;
                caseItem.body = walkBody([caseItem.body], fn)[0];
            }
        }
        else if (node.type === "handleBlock") {
            node.body = walkBody(node.body, fn);
            if (node.handler.kind === "inline") {
                node.handler.body = walkBody(node.handler.body, fn);
            }
        }
        else if (node.type === "withModifier") {
            node.statement = walkBody([node.statement], fn)[0];
        }
        else if (node.type === "functionCall" && node.block) {
            node.block.body = walkBody(node.block.body, fn);
        }
        else if (node.type === "parallelBlock" || node.type === "seqBlock") {
            node.body = walkBody(node.body, fn);
        }
        return node;
    });
    return fn(walked);
}
/** Check if a node is an llm() function call */
function isLlmCall(node) {
    return node.type === "functionCall" && node.functionName === "llm";
}
/** Extract the llm() FunctionCall from a node, if present (handles assignments, returns, etc.) */
function getLlmCall(node) {
    if (isLlmCall(node))
        return node;
    if (node.type === "assignment" && isLlmCall(node.value))
        return node.value;
    if (node.type === "returnStatement" && node.value && isLlmCall(node.value))
        return node.value;
    return null;
}
/** Get a short string representation of an llm() call for comments */
function llmCallToString(call) {
    const firstArg = call.arguments[0];
    if (!firstArg)
        return "llm()";
    if (firstArg.type === "string" || firstArg.type === "multiLineString") {
        const str = firstArg.segments
            .map((seg) => seg.type === "text"
            ? seg.value
            : `{${expressionToString(seg.expression)}}`)
            .join("");
        return str;
    }
    if (firstArg.type === "variableName")
        return `\${${firstArg.value}}`;
    return "llm(...)";
}
function attachTags(nodes) {
    const result = [];
    let pendingTags = [];
    for (const node of nodes) {
        if (node.type === "tag") {
            pendingTags.push(node);
            continue;
        }
        if (pendingTags.length > 0) {
            if (node.type === "graphNode" || node.type === "function" ||
                node.type === "assignment" || node.type === "functionCall" ||
                node.type === "typeAlias") {
                node.tags = [...(node.tags || []), ...pendingTags];
                pendingTags = [];
            }
            else {
                // No valid attach target — preserve tags as standalone nodes
                result.push(...pendingTags);
                pendingTags = [];
            }
        }
        result.push(node);
    }
    // Preserve any trailing tags (e.g., at end of block)
    if (pendingTags.length > 0) {
        result.push(...pendingTags);
    }
    return result;
}
function collectTags(nodes) {
    // walkBody handles control-flow bodies (ifElse, loops, etc.) but not
    // graphNode/function bodies, so recurse into those explicitly.
    const withNestedBodies = nodes.map((node) => {
        if ((node.type === "graphNode" || node.type === "function") && node.body) {
            node.body = collectTags(node.body);
        }
        return node;
    });
    return walkBody(withNestedBodies, attachTags);
}
export class TypescriptPreprocessor {
    program;
    config;
    functionDefinitions = {};
    importedFunctions = {};
    graphNodeDefinitions = {};
    constructor(program, config = {}, info) {
        this.program = program;
        this.config = config;
        if (info) {
            this.functionDefinitions = { ...info.functionDefinitions };
            this.importedFunctions = { ...info.importedFunctions };
            this.graphNodeDefinitions = Object.fromEntries(info.graphNodes.map((n) => [n.nodeName, n]));
        }
    }
    /**
     * Move standalone `tag` nodes onto the next attach-target node
     * (function / graphNode / assignment / functionCall / typeAlias).
     * Public so consumers like the doc generator can run the same
     * tag-attachment that the full `preprocess()` pipeline runs without
     * also running every downstream transform.
     */
    attachTags() {
        this.program.nodes = collectTags(this.program.nodes);
    }
    attachDocComments() {
        const nodes = this.program.nodes;
        const DECLARATION_TYPES = ["function", "graphNode", "typeAlias"];
        const SKIP_TYPES = ["newLine", "tag"];
        const PREAMBLE_TYPES = ["comment", "newLine", "importStatement"];
        // First pass: extract and validate @module doc comment.
        // It must appear before any non-import code, and there can be at most one.
        let seenNonPreamble = false;
        let foundModuleDoc = false;
        for (let i = 0; i < nodes.length; i++) {
            const node = nodes[i];
            if (node.type === "multiLineComment") {
                const mc = node;
                if (mc.isModuleDoc) {
                    const line = mc.loc?.line != null ? mc.loc.line + 1 : "unknown";
                    if (foundModuleDoc) {
                        throw new Error(`Only one @module doc comment is allowed per file (duplicate found at line ${line}).`);
                    }
                    if (seenNonPreamble) {
                        throw new Error(`@module doc comment must appear before any code (found at line ${line}). ` +
                            `Move it to the top of the file or right after the imports.`);
                    }
                    this.program.docComment = mc;
                    nodes.splice(i, 1);
                    i--;
                    foundModuleDoc = true;
                    continue;
                }
                // Non-module multiline comments in the preamble are fine
                if (!mc.isDoc)
                    continue;
            }
            if (!PREAMBLE_TYPES.includes(node.type) && node.type !== "multiLineComment") {
                seenNonPreamble = true;
            }
        }
        // Second pass: attach remaining doc comments to declarations
        const result = [];
        let pendingDocComment = null;
        for (const node of nodes) {
            if (node.type === "multiLineComment" && node.isDoc) {
                pendingDocComment = node;
                continue;
            }
            // Skip nodes that shouldn't break doc comment attachment
            if (pendingDocComment && SKIP_TYPES.includes(node.type)) {
                result.push(node);
                continue;
            }
            if (pendingDocComment && DECLARATION_TYPES.includes(node.type)) {
                const decl = node;
                decl.docComment = pendingDocComment;
                pendingDocComment = null;
            }
            else if (pendingDocComment) {
                // Doc comment wasn't followed by a declaration — keep it as a regular comment
                result.push(pendingDocComment);
                pendingDocComment = null;
            }
            result.push(node);
        }
        // Handle trailing doc comment
        if (pendingDocComment) {
            result.push(pendingDocComment);
        }
        this.program.nodes = result;
    }
    /**
     * Look up the BlockType for a function's block parameter by function name.
     * Checks both local function definitions and imported function signatures.
     */
    findBlockType(functionName) {
        const fnDef = this.functionDefinitions[functionName];
        const params = fnDef?.parameters ?? this.importedFunctions[functionName]?.parameters;
        if (!params)
            return null;
        const blockParam = params.find(p => p.typeHint?.type === "blockType");
        return blockParam?.typeHint ?? null;
    }
    /**
     * Copy type annotations from a BlockType onto block params that lack them.
     */
    applyBlockType(block, blockType) {
        for (let i = 0; i < block.params.length; i++) {
            if (!block.params[i].typeHint && blockType.params[i]) {
                block.params[i].typeHint = blockType.params[i].typeAnnotation;
            }
        }
    }
    /**
     * Walk the AST and propagate block type annotations from function definitions
     * onto direct block arguments that lack type annotations.
     * e.g. map([1,2,3]) as x { ... } — copies param types from map's block type onto x.
     *
     * Note: blocks inside named args (e.g. fn.partial(func: \x -> x)) are not yet
     * handled — the preprocessor would need valueAccess chain resolution to find
     * the receiver function. The builder falls back to 'any' for untyped block params.
     */
    propagateBlockTypes() {
        for (const { node } of walkNodesArray(this.program.nodes)) {
            if (node.type !== "functionCall")
                continue;
            const call = node;
            if (call.block) {
                const blockType = this.findBlockType(call.functionName);
                if (blockType) {
                    this.applyBlockType(call.block, blockType);
                }
            }
        }
    }
    preprocess() {
        this.attachDocComments();
        this.program.nodes = collectTags(this.program.nodes);
        this.desugarParallelBlocks();
        if (Object.keys(this.functionDefinitions).length === 0) {
            this.getFunctionDefinitions();
        }
        if (Object.keys(this.graphNodeDefinitions).length === 0) {
            this.getGraphNodeDefinitions();
        }
        this.propagateBlockTypes();
        injectSchemaArgsInProgram(this.program, this.functionDefinitions, this.importedFunctions);
        this.collectSkills();
        this.addAwaitPendingCalls();
        this.filterExcludedNodeTypes();
        this.filterExcludedBuiltinFunctions();
        this.validateFetchDomains();
        this.validateNoAsyncInLoops();
        this.resolveVariableScopes();
        return this.program;
    }
    /**
     * Walk every function and graph-node body and desugar `parallel { ... }`
     * blocks into the existing `fork` primitive. Also inlines any `seq { ... }`
     * blocks that appear outside a parallel block (where they have no runtime
     * effect). See lib/preprocessors/parallelDesugar.ts.
     */
    desugarParallelBlocks() {
        for (const node of this.program.nodes) {
            if (node.type === "function" || node.type === "graphNode") {
                node.body = desugarParallelInBody(node.body);
            }
        }
    }
    isVarUsedInBody(variableName, nodeToExclude, body) {
        for (const { name, node } of getAllVariablesInBodyArray(body)) {
            if (node === nodeToExclude) {
                continue; // skip the variable declaration/assignment itself
            }
            if (name === variableName) {
                return true;
            }
        }
        return false;
    }
    getFunctionDefinitions() {
        for (const node of this.program.nodes) {
            if (node.type === "function") {
                this.functionDefinitions[node.functionName] = node;
            }
        }
    }
    getGraphNodeDefinitions() {
        for (const node of this.program.nodes) {
            if (node.type === "graphNode") {
                this.graphNodeDefinitions[node.nodeName] = node;
            }
        }
    }
    collectSkills() {
        for (const node of this.program.nodes) {
            if (node.type === "function" || node.type === "graphNode") {
                this.collectSkillsInFunction(node.body);
                node.body = node.body.filter((n) => n.type !== "skill");
            }
        }
    }
    // TODO: Update collectSkillsInFunction to work with llm() as FunctionCall.
    // Skills are now passed in the config object (2nd arg to llm()), but skill
    // statements still need to be collected and merged with config skills.
    collectSkillsInFunction(body) {
        /* Original implementation (used PromptLiteral nodes, needs rewrite for FunctionCall):
        let skillsUsed: Skill[] = [];
    
        const setSkillsForPrompt = (promptNode: PromptLiteral) => {
          promptNode.skills = skillsUsed;
    
          if (skillsUsed.length > 0) {
            const hasReadSkillTool = promptNode.tools?.toolNames.some(
              (t) => t === "readSkill",
            );
            if (!hasReadSkillTool) {
              promptNode.tools = promptNode.tools || {
                type: "usesTool",
                toolNames: [],
              };
              promptNode.tools.toolNames.push("readSkill");
            }
          }
    
          skillsUsed = [];
        };
    
        for (const { node } of walkNodesArray(body)) {
          if (node.type === "skill") {
            skillsUsed.push(node);
          } else if (node.type === "prompt") {
            setSkillsForPrompt(node);
          } else if (node.type === "assignment" && node.value.type === "prompt") {
            setSkillsForPrompt(node.value);
          }
        }
        */
    }
    findChildren(body, type) {
        const children = [];
        for (const { node } of walkNodesArray(body)) {
            if (node.type === type) {
                children.push(node);
            }
        }
        return children;
    }
    isBuiltinFunction(functionName) {
        return BUILTIN_FUNCTIONS[functionName] !== undefined;
    }
    prettifyName(call) {
        if (call.functionName === "llm") {
            return `"llm(${llmCallToString(call).substring(0, 20)})"`;
        }
        return call.functionName;
    }
    renderMermaid() {
        // "graph TD"
        const lines = [];
        const labelLines = [];
        let nodeCounter = 0;
        const nextId = () => `n${nodeCounter++}`;
        const addToolsLabel = (call, callId) => {
            // TODO: Update to extract tools from llm() config object
        };
        for (const node of this.program.nodes) {
            if (node.type !== "function" && node.type !== "graphNode")
                continue;
            const sublines = ["graph LR"];
            const name = node.type === "function" ? node.functionName : node.nodeName;
            const isGraphNode = node.type === "graphNode";
            const functionCalls = this.extractFunctionCalls(node.body);
            // if (functionCalls.length === 0) continue;
            sublines.push(`  subgraph ${name}`);
            // Start node: circles for graph nodes, squares for functions
            const startId = nextId();
            if (isGraphNode) {
                sublines.push(`    ${startId}((${name}))`);
            }
            else {
                sublines.push(`    ${startId}[${name}]`);
            }
            // Group consecutive calls by async/sync
            const groups = this.groupCallsByAsync(functionCalls);
            let previousEnds = [startId];
            for (const group of groups) {
                if (group.type === "sync") {
                    // Sync calls are consecutive: chain one after another
                    for (const call of group.calls) {
                        const callId = nextId();
                        sublines.push(`    ${callId}[${this.prettifyName(call)}]`);
                        for (const prev of previousEnds) {
                            sublines.push(`    ${prev} --> ${callId}`);
                        }
                        addToolsLabel(call, callId);
                        previousEnds = [callId];
                    }
                }
                else {
                    // Async calls are parallel: fork from previous, converge to next
                    const parallelIds = [];
                    for (const call of group.calls) {
                        const callId = nextId();
                        sublines.push(`    ${callId}[${this.prettifyName(call)}]`);
                        for (const prev of previousEnds) {
                            sublines.push(`    ${prev} --> ${callId}`);
                        }
                        addToolsLabel(call, callId);
                        parallelIds.push(callId);
                    }
                    previousEnds = parallelIds;
                }
            }
            sublines.push(`  end`);
            lines.push(sublines.join("\n"));
        }
        const endlines = [];
        endlines.push(`  classDef toolLabel fill:#333333,stroke-dasharray:5 5`);
        endlines.push(...labelLines);
        //lines.push(endlines.join("\n"));
        return lines;
    }
    extractFunctionCalls(body) {
        const calls = [];
        for (const { node } of walkNodesArray(body)) {
            if (node.type === "functionCall") {
                calls.push(node);
            }
        }
        return calls;
    }
    groupCallsByAsync(calls) {
        const groups = [];
        for (const call of calls) {
            const callType = call.async ? "async" : "sync";
            const lastGroup = groups[groups.length - 1];
            if (lastGroup && lastGroup.type === callType) {
                lastGroup.calls.push(call);
            }
            else {
                groups.push({ type: callType, calls: [call] });
            }
        }
        return groups;
    }
    topologicalSortFunctions() {
        const visited = new Set();
        const sorted = [];
        const visit = (funcName) => {
            if (visited.has(funcName)) {
                return;
            }
            visited.add(funcName);
            const funcDef = this.functionDefinitions[funcName];
            if (funcDef) {
                for (const stmt of funcDef.body) {
                    if (stmt.type === "functionCall") {
                        visit(stmt.functionName);
                    }
                }
                sorted.push(funcDef);
            }
        };
        for (const funcName of Object.keys(this.functionDefinitions)) {
            visit(funcName);
        }
        return sorted.reverse();
    }
    addAwaitPendingCalls() {
        for (const node of this.program.nodes) {
            if (node.type === "function" || node.type === "graphNode") {
                node.body = this._addAwaitPendingCalls(node.body);
            }
        }
    }
    _addAwaitPendingCalls(body) {
        /*     // First, recursively process nested function/node bodies
        // (functions and nodes create their own scope, so process them separately)
        for (const node of body) {
          if (node.type === "function" || node.type === "graphNode") {
            node.body = this._addAwaitPendingCalls(node.body);
          } else if (node.type === "ifElse") {
            node.thenBody = this._addAwaitPendingCalls(node.thenBody);
            if (node.elseBody) {
              node.elseBody = this._addAwaitPendingCalls(node.elseBody);
            }
          }
        }
     */
        // Pass 1: Collect all async variables defined in this body and nested non-function bodies
        // Variables in MessageThread, WhileLoop, IfElse are scoped to the containing function/node
        const asyncVarToAssignment = {};
        this._collectAsyncVariablesInScope(body, asyncVarToAssignment);
        const asyncVars = Object.keys(asyncVarToAssignment);
        if (asyncVars.length === 0) {
            return body;
        }
        // Pass 2: Find the first usage of each async variable across all bodies in this scope
        const varToFirstUsageLocation = {};
        for (const varName of asyncVars) {
            const location = this._findFirstUsageInScope(body, varName, asyncVarToAssignment[varName]);
            if (location) {
                varToFirstUsageLocation[varName] = location;
            }
        }
        // Group variables by their first usage location
        const locationToVars = {};
        for (const varName of Object.keys(varToFirstUsageLocation)) {
            const location = varToFirstUsageLocation[varName];
            const locationKey = location.bodyPath.join(",") + ":" + location.indexInBody;
            if (!locationToVars[locationKey]) {
                locationToVars[locationKey] = [];
            }
            locationToVars[locationKey].push(varName);
        }
        // Insert awaitPending calls before first usage
        return this._insertAwaitPendingCalls(body, locationToVars);
    }
    /**
     * Recursively collect all async variables defined in this body and nested non-function bodies.
     * Does not descend into function or graphNode bodies as they have their own scope.
     */
    _collectAsyncVariablesInScope(body, asyncVarToAssignment) {
        for (const node of body) {
            // Don't descend into functions or graph nodes - they have their own scope
            if (node.type === "function" || node.type === "graphNode") {
                continue;
            }
            // Process assignments in this body
            if (node.type === "assignment") {
                const isAsyncCall = node.value.type === "functionCall" && node.value.async;
                if (isAsyncCall) {
                    asyncVarToAssignment[node.variableName] = node;
                }
            }
            // Recursively collect from nested bodies that share the same scope
            if (node.type === "messageThread") {
                this._collectAsyncVariablesInScope(node.body, asyncVarToAssignment);
            }
            else if (node.type === "whileLoop") {
                this._collectAsyncVariablesInScope(node.body, asyncVarToAssignment);
            }
            else if (node.type === "ifElse") {
                this._collectAsyncVariablesInScope(node.thenBody, asyncVarToAssignment);
                if (node.elseBody) {
                    this._collectAsyncVariablesInScope(node.elseBody, asyncVarToAssignment);
                }
            }
            else if (node.type === "handleBlock") {
                this._collectAsyncVariablesInScope(node.body, asyncVarToAssignment);
            }
            else if (node.type === "withModifier") {
                this._collectAsyncVariablesInScope([node.statement], asyncVarToAssignment);
            }
        }
    }
    /**
     * Find the first usage of a variable in this scope (across all bodies).
     * Returns the path to the body and the index within that body.
     */
    _findFirstUsageInScope(body, varName, assignmentNode, bodyPath = []) {
        for (let i = 0; i < body.length; i++) {
            const node = body[i];
            // Don't descend into functions or graph nodes - they have their own scope
            if (node.type === "function" || node.type === "graphNode") {
                continue;
            }
            // Skip the assignment itself
            if (node === assignmentNode) {
                continue;
            }
            // Check if this node uses the variable (excluding nested bodies)
            if (this._nodeUsesVariableDirectly(node, varName)) {
                return { bodyPath, indexInBody: i };
            }
            // Check nested bodies
            if (node.type === "messageThread") {
                const found = this._findFirstUsageInScope(node.body, varName, assignmentNode, [...bodyPath, i]);
                if (found)
                    return found;
            }
            else if (node.type === "whileLoop") {
                const found = this._findFirstUsageInScope(node.body, varName, assignmentNode, [...bodyPath, i]);
                if (found)
                    return found;
            }
            else if (node.type === "handleBlock") {
                const found = this._findFirstUsageInScope(node.body, varName, assignmentNode, [...bodyPath, i]);
                if (found)
                    return found;
            }
            else if (node.type === "withModifier") {
                const found = this._findFirstUsageInScope([node.statement], varName, assignmentNode, [...bodyPath, i]);
                if (found)
                    return found;
            }
            else if (node.type === "ifElse") {
                const foundInThen = this._findFirstUsageInScope(node.thenBody, varName, assignmentNode, [...bodyPath, i, 0]);
                if (foundInThen)
                    return foundInThen;
                if (node.elseBody) {
                    const foundInElse = this._findFirstUsageInScope(node.elseBody, varName, assignmentNode, [...bodyPath, i, 1]);
                    if (foundInElse)
                        return foundInElse;
                }
            }
        }
        return null;
    }
    /**
     * Check if a node uses a variable directly (not in nested bodies).
     */
    _nodeUsesVariableDirectly(node, varName) {
        // For nodes with bodies, we don't check inside the body here
        // (that's done separately in _findFirstUsageInScope)
        if (node.type === "messageThread" ||
            node.type === "whileLoop" ||
            node.type === "handleBlock" ||
            node.type === "function" ||
            node.type === "graphNode") {
            return false;
        }
        // For ifElse, check the condition but not the bodies
        if (node.type === "ifElse") {
            return this._nodeUsesVariable(node.condition, varName);
        }
        // For all other nodes, use the full check
        return this._nodeUsesVariable(node, varName);
    }
    /**
     * Insert awaitPending calls at the appropriate locations in the body.
     */
    _insertAwaitPendingCalls(body, locationToVars, currentPath = []) {
        const newBody = [];
        for (let i = 0; i < body.length; i++) {
            const node = body[i];
            const locationKey = currentPath.join(",") + ":" + i;
            // Check if we need to insert awaitPending before this node
            if (locationToVars[locationKey]) {
                const vars = locationToVars[locationKey];
                const keyArray = vars.map((v) => `__self.__pendingKey_${v}`).join(", ");
                // Strict accessor — emitted inside function/node bodies that
                // run under the withAlsFrame wrap. Bare `__ctx` would still
                // work today (the setupEnv local is in scope), but using the
                // accessor keeps this consistent with the rest of the codegen
                // post-ALS migration and makes the no-frame failure mode
                // actionable.
                const awaitPendingCode = {
                    type: "rawCode",
                    value: `await getRuntimeContext().ctx.pendingPromises.awaitPending([${keyArray}]);`,
                };
                newBody.push(awaitPendingCode);
            }
            // Recursively process nested bodies
            if (node.type === "messageThread") {
                node.body = this._insertAwaitPendingCalls(node.body, locationToVars, [
                    ...currentPath,
                    i,
                ]);
            }
            else if (node.type === "whileLoop") {
                node.body = this._insertAwaitPendingCalls(node.body, locationToVars, [
                    ...currentPath,
                    i,
                ]);
            }
            else if (node.type === "handleBlock") {
                node.body = this._insertAwaitPendingCalls(node.body, locationToVars, [
                    ...currentPath,
                    i,
                ]);
            }
            else if (node.type === "withModifier") {
                node.statement = this._insertAwaitPendingCalls([node.statement], locationToVars, [
                    ...currentPath,
                    i,
                ])[0];
            }
            else if (node.type === "ifElse") {
                node.thenBody = this._insertAwaitPendingCalls(node.thenBody, locationToVars, [...currentPath, i, 0]);
                if (node.elseBody) {
                    node.elseBody = this._insertAwaitPendingCalls(node.elseBody, locationToVars, [...currentPath, i, 1]);
                }
            }
            newBody.push(node);
        }
        return newBody;
    }
    nodeHasBody(node) {
        return (node.type === "function" ||
            node.type === "graphNode" ||
            node.type === "ifElse" ||
            node.type === "whileLoop" ||
            node.type === "messageThread" ||
            node.type === "handleBlock");
    }
    _nodeUsesVariable(node, varName) {
        // Check if the node or any of its children use the variable
        for (const { name } of getAllVariablesInBodyArray([node])) {
            if (name === varName) {
                return true;
            }
        }
        return false;
    }
    /**
     * Filter out nodes based on excludeNodeTypes config
     */
    filterExcludedNodeTypes() {
        if (!this.config.excludeNodeTypes ||
            this.config.excludeNodeTypes.length === 0) {
            return;
        }
        const excludeSet = new Set(this.config.excludeNodeTypes);
        this.program.nodes = this.filterNodesByType(this.program.nodes, excludeSet);
    }
    /**
     * Recursively filter nodes by type, handling all node structures
     */
    filterNodesByType(nodes, excludeSet) {
        const filteredNodes = [];
        for (const node of nodes) {
            // Skip nodes of excluded types
            if (excludeSet.has(node.type)) {
                continue;
            }
            // Recursively filter child nodes
            if (node.type === "function" || node.type === "graphNode") {
                node.body = this.filterNodesByType(node.body, excludeSet);
            }
            else if (node.type === "ifElse") {
                node.thenBody = this.filterNodesByType(node.thenBody, excludeSet);
                if (node.elseBody) {
                    node.elseBody = this.filterNodesByType(node.elseBody, excludeSet);
                }
            }
            else if (node.type === "whileLoop") {
                node.body = this.filterNodesByType(node.body, excludeSet);
            }
            else if (node.type === "messageThread") {
                node.body = this.filterNodesByType(node.body, excludeSet);
            }
            else if (node.type === "handleBlock") {
                node.body = this.filterNodesByType(node.body, excludeSet);
                if (node.handler.kind === "inline") {
                    node.handler.body = this.filterNodesByType(node.handler.body, excludeSet);
                }
            }
            else if (node.type === "withModifier") {
                const filtered = this.filterNodesByType([node.statement], excludeSet)[0];
                if (!filtered)
                    continue;
                node.statement = filtered;
            }
            else if (node.type === "matchBlock") {
                // Filter case bodies - Note: match block bodies are single nodes, not arrays
                // We don't filter them here as they're of a specific type
            }
            else if (node.type === "assignment") {
                node.value = (this.filterNodesByType([node.value], excludeSet)[0] || node.value);
            }
            else if (node.type === "functionCall") {
                node.arguments = this.filterNodesByType(node.arguments, excludeSet);
            }
            else if (node.type === "agencyArray") {
                node.items = this.filterNodesByType(node.items, excludeSet);
            }
            else if (node.type === "agencyObject") {
                node.entries = node.entries.map((entry) => ({
                    ...entry,
                    value: (this.filterNodesByType([entry.value], excludeSet)[0] || entry.value),
                }));
            }
            filteredNodes.push(node);
        }
        return filteredNodes;
    }
    /**
     * Filter out builtin function calls based on excludeBuiltinFunctions config
     */
    filterExcludedBuiltinFunctions() {
        if (!this.config.excludeBuiltinFunctions ||
            this.config.excludeBuiltinFunctions.length === 0) {
            return;
        }
        const excludeSet = new Set(this.config.excludeBuiltinFunctions);
        this.program.nodes = this.filterBuiltinFunctionCalls(this.program.nodes, excludeSet);
    }
    /**
     * Recursively filter builtin function calls
     */
    filterBuiltinFunctionCalls(nodes, excludeSet) {
        const filteredNodes = [];
        for (const node of nodes) {
            // Skip excluded builtin function calls
            if (node.type === "functionCall" && excludeSet.has(node.functionName)) {
                continue;
            }
            // Skip assignments to excluded builtin function calls
            if (node.type === "assignment" &&
                node.value.type === "functionCall" &&
                excludeSet.has(node.value.functionName)) {
                continue;
            }
            // Recursively filter child nodes
            if (node.type === "function" || node.type === "graphNode") {
                node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
            }
            else if (node.type === "ifElse") {
                node.thenBody = this.filterBuiltinFunctionCalls(node.thenBody, excludeSet);
                if (node.elseBody) {
                    node.elseBody = this.filterBuiltinFunctionCalls(node.elseBody, excludeSet);
                }
            }
            else if (node.type === "whileLoop") {
                node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
            }
            else if (node.type === "messageThread") {
                node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
            }
            else if (node.type === "handleBlock") {
                node.body = this.filterBuiltinFunctionCalls(node.body, excludeSet);
                if (node.handler.kind === "inline") {
                    node.handler.body = this.filterBuiltinFunctionCalls(node.handler.body, excludeSet);
                }
            }
            else if (node.type === "withModifier") {
                const filtered = this.filterBuiltinFunctionCalls([node.statement], excludeSet)[0];
                if (!filtered)
                    continue;
                node.statement = filtered;
            }
            else if (node.type === "matchBlock") {
                node.cases = node.cases.map((caseItem) => {
                    if (caseItem.type === "comment") {
                        return caseItem;
                    }
                    if (caseItem.type === "newLine") {
                        return caseItem;
                    }
                    return {
                        ...caseItem,
                        body: this.filterBuiltinFunctionCalls([caseItem.body], excludeSet)[0],
                    };
                });
            }
            else if (node.type === "functionCall") {
                // Filter arguments that are function calls
                node.arguments = this.filterBuiltinFunctionCalls(node.arguments, excludeSet);
            }
            else if (node.type === "agencyArray") {
                node.items = this.filterBuiltinFunctionCalls(node.items, excludeSet);
            }
            else if (node.type === "agencyObject") {
                node.entries = node.entries.map((entry) => {
                    const filteredValues = this.filterBuiltinFunctionCalls([entry.value], excludeSet);
                    return {
                        ...entry,
                        value: filteredValues[0] || entry.value,
                    };
                });
            }
            filteredNodes.push(node);
        }
        return filteredNodes;
    }
    /**
     * Validate fetch calls against allowed/disallowed domains
     */
    validateFetchDomains() {
        const hasAllowed = this.config.allowedFetchDomains &&
            this.config.allowedFetchDomains.length > 0;
        const hasDisallowed = this.config.disallowedFetchDomains &&
            this.config.disallowedFetchDomains.length > 0;
        if (!hasAllowed && !hasDisallowed) {
            return; // No domain restrictions
        }
        // Compute the effective allowed domains
        let effectiveAllowed = null;
        if (hasAllowed) {
            effectiveAllowed = new Set(this.config.allowedFetchDomains);
            // If both allowed and disallowed are set, remove disallowed from allowed
            if (hasDisallowed) {
                for (const domain of this.config.disallowedFetchDomains) {
                    effectiveAllowed.delete(domain);
                }
            }
        }
        const disallowedSet = hasDisallowed
            ? new Set(this.config.disallowedFetchDomains)
            : new Set();
        // Walk through all nodes and validate fetch calls
        for (const { node } of walkNodesArray(this.program.nodes)) {
            if (node.type === "functionCall" &&
                (node.functionName === "fetch" ||
                    node.functionName === "fetchJSON" ||
                    node.functionName === "fetchJson")) {
                this._validateFetchCall(node, effectiveAllowed, disallowedSet);
            }
        }
    }
    /**
     * Validate a single fetch call's domain
     */
    _validateFetchCall(node, effectiveAllowed, disallowedSet) {
        // Get the URL argument (first argument to fetch)
        if (node.arguments.length === 0) {
            return; // No URL provided, let it fail at runtime
        }
        const urlArg = node.arguments[0];
        let url = null;
        // Extract URL if it's a string literal
        if (urlArg.type === "string") {
            // Reconstruct the URL from segments
            url = urlArg.segments
                .map((seg) => (seg.type === "text" ? seg.value : ""))
                .join("");
        }
        if (!url) {
            return; // Can't validate variable URLs at compile time
        }
        // Extract domain from URL
        const domain = this._extractDomain(url);
        if (!domain) {
            return; // Can't extract domain, skip validation
        }
        // Check if domain is allowed
        if (effectiveAllowed !== null && !effectiveAllowed.has(domain)) {
            throw new Error(`Fetch to domain "${domain}" is not allowed. Allowed domains: ${Array.from(effectiveAllowed).join(", ")}`);
        }
        // Check if domain is disallowed
        if (disallowedSet.has(domain)) {
            throw new Error(`Fetch to domain "${domain}" is explicitly disallowed.`);
        }
    }
    /**
     * Extract domain from URL string
     */
    _extractDomain(url) {
        try {
            const urlObj = new URL(url);
            return urlObj.hostname;
        }
        catch {
            return null; // Invalid URL
        }
    }
    /**
     * Validate that no async function calls appear inside loops.
     * Async calls inside loops can't be properly serialized for interrupt
     * resumption because multiple iterations share the same step block,
     * causing branch key collisions.
     */
    validateNoAsyncInLoops() {
        const loopTypes = ["forLoop", "whileLoop"];
        for (const { node, ancestors } of walkNodesArray(this.program.nodes)) {
            if (node.type === "functionCall" && node.async) {
                const insideLoop = ancestors.some((a) => loopTypes.includes(a.type));
                if (insideLoop) {
                    throw new Error(`Async function call "${node.functionName}()" is not allowed inside a loop. ` +
                        `Move the async call into a separate function, or remove the "async" keyword.`);
                }
            }
        }
    }
    /**
     * Get the scope name for a function-like node (function or graphNode).
     */
    getScopeName(node) {
        if (node.type === "function")
            return node.functionName;
        if (node.type === "graphNode")
            return node.nodeName;
        return "unknown";
    }
    /**
     * Resolve variable scopes by annotating AST nodes with their scope.
     * After this pass, every VariableNameLiteral, InterpolationSegment, and Assignment
     * will have a `scope` property indicating whether the variable is global, local, or args.
     */
    // eslint-disable-next-line max-lines-per-function -- multi-pass scope resolution; refactor tracked separately
    resolveVariableScopes() {
        const globalVars = new Set();
        const staticVars = new Set();
        const importedVars = new Set();
        const funcArgs = {};
        const localVarsInFunction = {};
        // First, we collect all global and static variables
        for (const { node, scopes } of walkNodesArray(this.program.nodes)) {
            if (scopes.length === 0) {
                throw new Error(`Top-level nodes should have at least the global scope in their scopes array. Node: ${JSON.stringify({ node })}, scopes: ${JSON.stringify({ scopes })}`);
            }
            if (scopes.at(-1)?.type !== "global")
                continue;
            if (node.type === "assignment") {
                if (node.static) {
                    staticVars.add(node.variableName);
                }
                else {
                    globalVars.add(node.variableName);
                }
                /*       } else if (node.type === "variableName") {
                globalVars.add(node.value);
         */
            }
            else if (node.type === "importStatement") {
                const importedNames = node.importedNames.map(getImportedNames).flat();
                importedNames.forEach((n) => {
                    importedVars.add(n);
                });
            }
            else if (node.type === "importNodeStatement") {
                node.importedNodes.forEach((n) => {
                    importedVars.add(n);
                });
            }
        }
        const lookupScope = (funcName, varName) => {
            // Local scopes (args, locals) take precedence over global scopes,
            // so that `let x` inside a function shadows a global `x`.
            if (funcArgs[funcName] && funcArgs[funcName].includes(varName)) {
                return "args";
            }
            if (localVarsInFunction[funcName] &&
                localVarsInFunction[funcName].has(varName)) {
                return "local";
            }
            // imported takes precedence over global
            if (importedVars.has(varName)) {
                return "imported";
            }
            if (staticVars.has(varName)) {
                return "static";
            }
            if (globalVars.has(varName)) {
                return "global";
            }
            return null;
        };
        // second, make sure all args are scoped correctly,
        // and all vars defined within a function or graph node are scoped to that function or graph node.
        for (const { node, scopes } of walkNodesArray(this.program.nodes)) {
            if (scopes.length === 0) {
                throw new Error(`Top-level nodes should have at least the global scope in their scopes array. Node: ${JSON.stringify({ node })}, scopes: ${JSON.stringify({ scopes })}`);
            }
            const isFunctionLike = node.type === "function" || node.type === "graphNode";
            if (isFunctionLike) {
                const nodeName = this.getScopeName(node);
                // Parameters are in the function's scope
                funcArgs[nodeName] = [...node.parameters.map((p) => p.name)];
                localVarsInFunction[nodeName] = new Set();
                // Pre-register `let`/`const` declarations from the function/node body
                // (but NOT from inside block bodies) so that Phase 1's `lookupScope`
                // calls can correctly resolve outer-scope variables that are captured
                // by closures inside blocks. Without this, a block referencing an
                // outer `let`/`const` would treat the variable as block-local because
                // Phase 2 (which populates localVarsInFunction) hasn't run yet.
                for (const { node: declNode, ancestors } of walkNodesArray(node.body)) {
                    if (declNode.type === "assignment" &&
                        declNode.declKind &&
                        !isInsideBlock(ancestors)) {
                        localVarsInFunction[nodeName].add(declNode.variableName);
                    }
                }
                // Phase 1: Resolve block body variables first.
                // Block params get "blockArgs" scope, new variables inside blocks get "block" scope,
                // and references to outer-scope variables keep their original scope (captured via closure).
                // We do this before the main walk so that the main walk sees these variables
                // already have scopes and skips them.
                for (const { node: bodyNode } of walkNodesArray(node.body)) {
                    if (bodyNode.type === "functionCall" && bodyNode.block) {
                        const blockParamNames = new Set(bodyNode.block.params.map((p) => p.name));
                        const blockLocalNames = new Set();
                        // First pass: identify block-local assignments
                        for (const { node: blockVarNode } of getAllVariablesInBodyArray(bodyNode.block.body)) {
                            if (blockVarNode.type === "assignment") {
                                const name = blockVarNode.variableName;
                                if (!blockParamNames.has(name) && lookupScope(nodeName, name) === null) {
                                    blockLocalNames.add(name);
                                }
                            }
                        }
                        // Second pass: set scopes on all variables in the block body
                        for (const { node: blockVarNode } of getAllVariablesInBodyArray(bodyNode.block.body)) {
                            if (blockVarNode.type === "assignment") {
                                if (blockParamNames.has(blockVarNode.variableName)) {
                                    blockVarNode.scope = "blockArgs";
                                }
                                else if (blockLocalNames.has(blockVarNode.variableName)) {
                                    blockVarNode.scope = "block";
                                }
                                else {
                                    const resolved = lookupScope(nodeName, blockVarNode.variableName);
                                    // eslint-disable-next-line max-depth -- block-scope variable resolution
                                    if (resolved)
                                        blockVarNode.scope = resolved;
                                    // else: leave unscoped — Phase 2 will resolve once locals are registered
                                }
                            }
                            else if (blockVarNode.type === "variableName") {
                                if (blockParamNames.has(blockVarNode.value)) {
                                    blockVarNode.scope = "blockArgs";
                                }
                                else if (blockLocalNames.has(blockVarNode.value)) {
                                    blockVarNode.scope = "block";
                                }
                                else {
                                    const resolved = lookupScope(nodeName, blockVarNode.value);
                                    if (resolved)
                                        blockVarNode.scope = resolved;
                                    // else: leave unscoped — Phase 2 will resolve once locals are registered
                                }
                            }
                        }
                    }
                }
                // Phase 2: Resolve function/node body variables.
                // Variables inside blocks already have scopes from Phase 1, so they are skipped.
                const varsDefinedInFunction = getAllVariablesInBodyArray(node.body);
                for (const { node: varNode } of varsDefinedInFunction) {
                    if (varNode.type === "assignment") {
                        if (varNode.scope)
                            continue; // already resolved in block Phase 1
                        let scope;
                        if (varNode.declKind) {
                            // `let` or `const` declarations always create a new local variable,
                            // even if a global with the same name exists.
                            scope = "local";
                            localVarsInFunction[nodeName].add(varNode.variableName);
                        }
                        else {
                            scope = lookupScope(nodeName, varNode.variableName) ?? "local";
                            if (scope === "static") {
                                throw new Error(`Cannot reassign static variable '${varNode.variableName}'. Static variables are immutable after initialization.`);
                            }
                            if (scope === "local") {
                                localVarsInFunction[nodeName].add(varNode.variableName);
                            }
                        }
                        varNode.scope = scope;
                    }
                    else if (varNode.type === "variableName") {
                        if (varNode.scope)
                            continue; // already resolved in block Phase 1
                        const resolved = lookupScope(nodeName, varNode.value);
                        if (resolved) {
                            varNode.scope = resolved;
                        }
                        else if (this.graphNodeDefinitions[varNode.value]) {
                            throw new Error(`Cannot use node "${varNode.value}" as a value. Nodes are graph transitions, not functions.`);
                        }
                        else if (this.functionDefinitions[varNode.value]) {
                            varNode.scope = "functionRef";
                        }
                        else {
                            varNode.scope = "imported";
                        }
                    }
                }
                // Resolve scope on function call nodes in this function/node body
                for (const { node: callNode } of walkNodesArray(node.body)) {
                    if (callNode.type === "functionCall" && !callNode.scope) {
                        const name = callNode.functionName;
                        if (this.functionDefinitions[name]) {
                            callNode.scope = "functionRef";
                        }
                        else {
                            const resolved = lookupScope(nodeName, name);
                            callNode.scope = resolved ?? "imported";
                        }
                    }
                }
                // Resolve scope on `handle { } with NAME` handler refs in this
                // function/node body. Without this, codegen emits a bare
                // `__call(NAME, ...)` that misses local variables stored in
                // `__stack.locals.NAME`.
                for (const { node: handleNode } of walkNodesArray(node.body)) {
                    if (handleNode.type === "handleBlock" &&
                        handleNode.handler.kind === "functionRef" &&
                        !handleNode.handler.scope) {
                        const name = handleNode.handler.functionName;
                        const resolved = lookupScope(nodeName, name);
                        if (resolved) {
                            handleNode.handler.scope = resolved;
                        }
                        else if (this.graphNodeDefinitions[name]) {
                            throw new Error(`Cannot use node "${name}" as a handler. Nodes are graph transitions, not functions.`);
                        }
                        else if (this.functionDefinitions[name]) {
                            handleNode.handler.scope = "functionRef";
                        }
                        else {
                            handleNode.handler.scope = "imported";
                        }
                    }
                }
            }
        }
        // imported scope for everything else
        for (const { node } of getAllVariablesInBodyArray(this.program.nodes)) {
            if (node.type === "variableName" || node.type === "assignment") {
                if (!node.scope) {
                    const name = node.type === "variableName" ? node.value : node.variableName;
                    const scope = lookupScope("", name);
                    if (scope) {
                        node.scope = scope;
                    }
                    else if (node.type === "variableName" && this.graphNodeDefinitions[name]) {
                        throw new Error(`Cannot use node "${name}" as a value. Nodes are graph transitions, not functions.`);
                    }
                    else if (node.type === "variableName" && this.functionDefinitions[name]) {
                        node.scope = "functionRef";
                    }
                    else {
                        node.scope = "imported";
                    }
                }
            }
        }
        // Resolve scope on top-level function call nodes
        for (const { node } of walkNodesArray(this.program.nodes)) {
            if (node.type === "functionCall" && !node.scope) {
                const name = node.functionName;
                if (this.functionDefinitions[name]) {
                    node.scope = "functionRef";
                }
                else {
                    const scope = lookupScope("", name);
                    node.scope = scope ?? "imported";
                }
            }
        }
        // Resolve scope on expressions inside function/node doc string
        // interpolations. These execute at module load time when the tool
        // definition is constructed, so they only see top-level (global /
        // static / imported) names, never function parameters or locals.
        for (const node of this.program.nodes) {
            if (node.type !== "function" && node.type !== "graphNode")
                continue;
            if (!node.docString)
                continue;
            for (const seg of node.docString.segments) {
                if (seg.type !== "interpolation")
                    continue;
                for (const { node: inner } of walkNodesArray([seg.expression])) {
                    if (inner.type === "variableName" && !inner.scope) {
                        const resolved = lookupScope("", inner.value);
                        if (resolved) {
                            inner.scope = resolved;
                        }
                        else if (this.functionDefinitions[inner.value]) {
                            inner.scope = "functionRef";
                        }
                        else {
                            inner.scope = "imported";
                        }
                    }
                    else if (inner.type === "functionCall" && !inner.scope) {
                        const name = inner.functionName;
                        if (this.functionDefinitions[name]) {
                            inner.scope = "functionRef";
                        }
                        else {
                            inner.scope = lookupScope("", name) ?? "imported";
                        }
                    }
                }
            }
        }
    }
}
