import { walkNodes } from "../utils/node.js";
/**
 * Per-run primitives that need an execution context to run. Calling
 * any of these from a `static` initializer (which runs once at
 * process startup, before any agent run has begun) is a logic error
 * — there is no per-run ctx, no thread, no checkpoint store, no LLM
 * client wired up yet.
 *
 * `interrupt` lives on this list as a *function name* even though
 * the parser usually emits an `interruptStatement` node — a bare
 * call like `interrupt("foo")` could parse as either depending on
 * surrounding shape, so we check both. See `checkInterruptStatement`
 * below for the statement form.
 */
export const BANNED_BUILTINS_IN_STATIC_INIT = {
    llm: "`llm()` requires a per-run execution context",
    chat: "`chat()` requires a per-run execution context",
    interrupt: "`interrupt(...)` pauses the per-run execution stack",
    respondToInterrupts: "`respondToInterrupts()` operates on per-run interrupt state",
    rewindFrom: "`rewindFrom()` operates on per-run checkpoint state",
    runBatch: "`runBatch()` schedules per-run agent invocations",
    callback: "`callback(...)` binds a hook into the per-run callback table",
    emit: "`emit(...)` writes to per-run trace state",
    thread: "`thread { ... }` opens a per-run conversation thread",
};
/**
 * Walk a static initializer expression (or bare statement) and emit
 * a diagnostic for each direct call to a banned builtin. Does NOT
 * descend into nested `function` / `graphNode` bodies — those run
 * later, in per-run ctx, so calls there are fine. Matches the
 * ancestor-check pattern used by `collectFreeIdentifiers` in
 * `lib/compiler/initDepGraph.ts`.
 *
 * `contextLabel` is the human-readable subject of the diagnostic
 * (e.g. "static const 'prompt'" or "the `static greet()` bare
 * statement"). Used as the lead of the error message so the user
 * sees what part of their code is being rejected.
 */
export function checkBannedBuiltinCalls(expr, contextLabel) {
    const errors = [];
    for (const { node, ancestors } of walkNodes([expr])) {
        if (ancestors.some((a) => a.type === "function" || a.type === "graphNode")) {
            continue;
        }
        if (node.type === "functionCall") {
            const reason = BANNED_BUILTINS_IN_STATIC_INIT[node.functionName];
            if (reason) {
                errors.push({
                    message: `${contextLabel} cannot call \`${node.functionName}(...)\` — ` +
                        `${reason}, but static initializers run once at process startup ` +
                        `before any per-run state exists. Move this call into a node ` +
                        `or a function called from a node.`,
                    loc: node.loc,
                });
            }
            continue;
        }
        if (node.type === "interruptStatement") {
            errors.push({
                message: `${contextLabel} cannot \`interrupt(...)\` — interrupts pause the ` +
                    `per-run execution stack, but static initializers run once at ` +
                    `process startup before any agent run has begun. Move this ` +
                    `into a node body.`,
                loc: node.loc,
            });
        }
    }
    return errors;
}
/**
 * Names of mutating method calls we recognise on common collection
 * shapes. Used by `checkStaticMutation` to spot `staticArr.push(...)`
 * — the simplest detectable form of post-declaration mutation
 * against a deep-frozen static.
 *
 * Not exhaustive — only the most-likely-to-bite cases. Statics are
 * `__deepFreeze`d at init time, so any actual mutation throws a
 * clean `TypeError` at runtime; this list is a compile-time fast
 * path for the most common user mistake.
 */
const MUTATING_METHODS = {
    push: true,
    pop: true,
    shift: true,
    unshift: true,
    splice: true,
    sort: true,
    reverse: true,
    fill: true,
    copyWithin: true,
    set: true,
    delete: true,
    clear: true,
    add: true,
};
/**
 * Decide whether a top-level node is a post-declaration mutation
 * against one of the known static names, and produce a diagnostic if
 * so. Two shapes are detected:
 *
 *   1. `staticName = ...` — a bare assignment whose target is a
 *      known static, where the assignment is NOT itself the static's
 *      declaration. Mutations inside nested `node` / `function`
 *      bodies are NOT checked here; the type-checker already rejects
 *      reassignment of any `const` via `constReassignment`, so the
 *      mutation case for statics inside per-run code is covered.
 *   2. `staticName.<mutatingMethod>(...)` — a top-level method call
 *      on a known static using one of the names listed in
 *      `MUTATING_METHODS`. Compile-time best-effort; runtime
 *      `__deepFreeze` is still the source of truth.
 *
 * Caller is responsible for filtering to top-level nodes; this
 * helper does not walk recursively. Same shape as PR 2's
 * `nodeFromTopLevel` so the validator can drive both with one loop.
 */
export function checkStaticMutation(node, staticNames) {
    // Assignment shape: `x = ...` where x is a known static and the
    // node is NOT a `const`/`let` declaration (those introduce a new
    // binding, not mutate the static).
    if (node.type === "assignment") {
        if (node.declKind)
            return null;
        if (!staticNames[node.variableName])
            return null;
        return {
            message: `Cannot reassign static \`${node.variableName}\` at module top ` +
                `level — statics are immutable after initialization. Use a global ` +
                `(\`const\`/\`let\` without \`static\`) if you need a mutable value.`,
            loc: node.loc,
        };
    }
    // Method-call shape: `x.push(...)` at top level.
    if (node.type === "valueAccess") {
        if (node.base.type !== "variableName")
            return null;
        const name = node.base.value;
        if (!staticNames[name])
            return null;
        for (const elem of node.chain) {
            if (elem.kind !== "methodCall")
                continue;
            const methodName = elem.functionCall.functionName;
            if (!MUTATING_METHODS[methodName])
                continue;
            return {
                message: `Cannot mutate static \`${name}\` via \`.${methodName}(...)\` at ` +
                    `module top level — statics are deep-frozen after initialization. ` +
                    `Use a global (\`const\`/\`let\` without \`static\`) if you need ` +
                    `a mutable value.`,
                loc: node.loc,
            };
        }
    }
    return null;
}
