import { functionScope, getImportedNames, globalScope, nodeScope, formatUnitLiteral, } from "../types.js";
import { variableTypeToString } from "../backends/typescriptGenerator/typeToString.js";
import { color } from "../utils/termcolors.js";
export function isInsideBlock(ancestors) {
    return ancestors.some((a) => a.type === "blockArgument");
}
/** Unwrap a function call argument to its inner expression. */
function unwrapCallArg(arg) {
    if (arg.type === "splat") {
        return arg.value;
    }
    if (arg.type === "namedArgument") {
        return arg.value;
    }
    return arg;
}
/** Convert a function call argument to string. */
function callArgToString(arg) {
    if (arg.type === "splat") {
        return `...${expressionToString(arg.value)}`;
    }
    if (arg.type === "namedArgument") {
        return `${arg.name}: ${expressionToString(arg.value)}`;
    }
    return expressionToString(arg);
}
/**
 * Extract the base variable name from an interpolation segment's expression.
 * Returns the variable name if the expression is a simple variable or the base
 * of a value access chain. For other expression types, returns an empty string.
 */
export function getBaseVarName(seg) {
    if (seg.expression.type === "variableName") {
        return seg.expression.value;
    }
    if (seg.expression.type === "valueAccess") {
        return seg.expression.base.value;
    }
    return "";
}
/**
 * Render an expression as a string WITHOUT scope prefix.
 * Used for prompt function bodies (where variables are function parameters)
 * and for re-emitting agency source code.
 */
export function expressionToString(expr) {
    switch (expr.type) {
        case "variableName":
            return expr.value;
        case "number":
            return expr.value;
        case "boolean":
            return String(expr.value);
        case "null":
            return "null";
        case "string":
        case "multiLineString":
            return expr.segments.map(seg => seg.type === "text" ? seg.value : `\${${expressionToString(seg.expression)}}`).join("");
        case "functionCall": {
            const args = expr.arguments.map(callArgToString).join(", ");
            return `${expr.functionName}(${args})`;
        }
        case "valueAccess": {
            let code = expressionToString(expr.base);
            for (const element of expr.chain) {
                switch (element.kind) {
                    case "property":
                        code += `.${element.name}`;
                        break;
                    case "index":
                        code += `[${expressionToString(element.index)}]`;
                        break;
                    case "methodCall": {
                        const fc = element.functionCall;
                        const args = fc.arguments.map(callArgToString).join(", ");
                        code += `.${fc.functionName}(${args})`;
                        break;
                    }
                }
            }
            return code;
        }
        case "binOpExpression":
            return `${expressionToString(expr.left)} ${expr.operator} ${expressionToString(expr.right)}`;
        case "agencyArray":
            return `[${expr.items.map(item => item.type === "splat" ? `...${expressionToString(item.value)}` : expressionToString(item)).join(", ")}]`;
        case "agencyObject":
            return `{${expr.entries.map(entry => "type" in entry && entry.type === "splat"
                ? `...${expressionToString(entry.value)}`
                : `${entry.key}: ${expressionToString(entry.value)}`).join(", ")}}`;
        case "tryExpression":
            return `try ${expressionToString(expr.call)}`;
        case "newExpression": {
            const args = expr.arguments.map(a => expressionToString(a)).join(", ");
            return `new ${expr.className}(${args})`;
        }
        case "regex":
            return `/${expr.pattern}/${expr.flags}`;
        case "schemaExpression":
            return `schema(${variableTypeToString(expr.typeArg, {})})`;
        case "interruptStatement": {
            const args = expr.arguments.map(callArgToString).join(", ");
            return `interrupt ${expr.kind}(${args})`;
        }
        case "blockArgument": {
            const params = expr.params.map(p => p.typeHint ? `${p.name}: ${variableTypeToString(p.typeHint, {})}` : p.name).join(", ");
            const paramStr = expr.params.length === 1 && !expr.params[0].typeHint ? params : `(${params})`;
            return `\\${paramStr} -> ...`;
        }
        case "unitLiteral":
            return formatUnitLiteral(expr);
        default:
            return String(expr.value ?? "");
    }
}
export function* getAllVariablesInBody(body) {
    for (const { node } of walkNodes(body)) {
        if (node.type === "assignment") {
            yield { name: node.variableName, node };
            yield* getAllVariablesInBody([node.value]);
        }
        else if (node.type === "function") {
            yield { name: node.functionName, node };
            for (const param of node.parameters) {
                yield { name: param.name, node };
            }
            yield* getAllVariablesInBody(node.body);
        }
        else if (node.type === "graphNode") {
            yield { name: node.nodeName, node };
            for (const param of node.parameters) {
                yield { name: param.name, node };
            }
            yield* getAllVariablesInBody(node.body);
        }
        else if (node.type === "ifElse") {
            yield* getAllVariablesInBody([node.condition]);
            yield* getAllVariablesInBody(node.thenBody);
            if (node.elseBody) {
                yield* getAllVariablesInBody(node.elseBody);
            }
        }
        else if (node.type === "functionCall") {
            for (const arg of node.arguments) {
                yield* getAllVariablesInBody([unwrapCallArg(arg)]);
            }
            if (node.block) {
                for (const param of node.block.params) {
                    yield { name: param.name, node };
                }
                yield* getAllVariablesInBody(node.block.body);
            }
            yield { name: node.functionName, node };
        }
        else if (node.type === "importStatement") {
            for (const nameObj of node.importedNames) {
                for (const name of getImportedNames(nameObj)) {
                    yield { name, node };
                }
            }
        }
        else if (node.type === "importNodeStatement") {
            for (const name of node.importedNodes) {
                yield { name, node };
            }
        }
        else if (node.type === "matchBlock") {
            for (const caseItem of node.cases) {
                if (caseItem.type === "comment")
                    continue;
                if (caseItem.type === "newLine")
                    continue;
                if (caseItem.caseValue === "_")
                    continue;
                yield* getAllVariablesInBody([caseItem.caseValue]);
            }
        }
        else if (node.type === "variableName") {
            yield { name: node.value, node };
        }
        else if (node.type === "valueAccess") {
            yield* getAllVariablesInBody([node.base]);
            for (const element of node.chain) {
                if (element.kind === "index") {
                    yield* getAllVariablesInBody([element.index]);
                }
                else if (element.kind === "slice") {
                    if (element.start)
                        yield* getAllVariablesInBody([element.start]);
                    if (element.end)
                        yield* getAllVariablesInBody([element.end]);
                }
                else if (element.kind === "methodCall") {
                    for (const arg of element.functionCall.arguments) {
                        yield* getAllVariablesInBody([unwrapCallArg(arg)]);
                    }
                }
            }
        }
        else if (node.type === "agencyArray") {
            for (const item of node.items) {
                if (item.type === "splat") {
                    yield* getAllVariablesInBody([item.value]);
                }
                else {
                    yield* getAllVariablesInBody([item]);
                }
            }
        }
        else if (node.type === "agencyObject") {
            for (const entry of node.entries) {
                if ("type" in entry && entry.type === "splat") {
                    yield* getAllVariablesInBody([entry.value]);
                }
                else {
                    const kv = entry;
                    if (kv.computedKey) {
                        yield* getAllVariablesInBody([kv.computedKey]);
                    }
                    yield* getAllVariablesInBody([kv.value]);
                }
            }
        }
        else if (node.type === "string" ||
            node.type === "multiLineString") {
            for (const seg of node.segments) {
                if (seg.type === "interpolation") {
                    yield* getAllVariablesInBody([seg.expression]);
                }
            }
        }
        else if (node.type === "returnStatement") {
            if (node.value)
                yield* getAllVariablesInBody([node.value]);
        }
        else if (node.type === "gotoStatement") {
            yield* getAllVariablesInBody([node.nodeCall]);
        }
        else if (node.type === "forLoop") {
            yield* getAllVariablesInBody([node.iterable]);
            yield* getAllVariablesInBody(node.body);
        }
        else if (node.type === "whileLoop") {
            yield* getAllVariablesInBody(node.body);
        }
        else if (node.type === "messageThread") {
            yield* getAllVariablesInBody(node.body);
        }
        else if (node.type === "handleBlock") {
            yield* getAllVariablesInBody(node.body);
            if (node.handler.kind === "inline") {
                yield* getAllVariablesInBody(node.handler.body);
            }
        }
        else if (node.type === "withModifier") {
            yield* getAllVariablesInBody([node.statement]);
        }
        else if (node.type === "tryExpression") {
            yield* getAllVariablesInBody([node.call]);
        }
    }
}
export function getNodesOfType(nodes, type) {
    const result = [];
    for (const { node } of walkNodes(nodes)) {
        if (node.type === type) {
            result.push(node);
        }
    }
    return result;
}
export let walkNodeDebug = false;
export const setWalkNodeDebug = (value) => (walkNodeDebug = value);
// eslint-disable-next-line max-lines-per-function -- generator walks every AST node kind; refactor tracked separately
export function* walkNodes(nodes, ancestors = [], scopes = []) {
    if (scopes.length === 0) {
        scopes.push(globalScope());
    }
    for (const node of nodes) {
        if (walkNodeDebug)
            console.log(color.magenta("walkNodes:"), { node, ancestors });
        yield { node, ancestors, scopes };
        if (node.type === "function") {
            yield* walkNodes(node.body, [...ancestors, node], [...scopes, functionScope(node.functionName)]);
        }
        else if (node.type === "graphNode") {
            yield* walkNodes(node.body, [...ancestors, node], [...scopes, nodeScope(node.nodeName)]);
        }
        else if (node.type === "newExpression") {
            yield* walkNodes(node.arguments, [...ancestors, node], scopes);
        }
        else if (node.type === "ifElse") {
            yield* walkNodes([node.condition], [...ancestors, node], scopes);
            yield* walkNodes(node.thenBody, [...ancestors, node], scopes);
            if (node.elseBody) {
                yield* walkNodes(node.elseBody, [...ancestors, node], scopes);
            }
        }
        else if (node.type === "forLoop") {
            yield* walkNodes([node.iterable], [...ancestors, node], scopes);
            yield* walkNodes(node.body, [...ancestors, node], scopes);
        }
        else if (node.type === "whileLoop") {
            yield* walkNodes([node.condition], [...ancestors, node], scopes);
            yield* walkNodes(node.body, [...ancestors, node], scopes);
        }
        else if (node.type === "messageThread" ||
            node.type === "parallelBlock" ||
            node.type === "seqBlock") {
            // messageThread carries optional named-arg expressions
            // (`label`, `summarize`, `continue`, `session`, `hidden`).
            // They must be walked so the symbol-table resolver populates
            // the `scope` field on any variable references inside them —
            // otherwise codegen emits bare identifiers instead of
            // `__stack.locals.foo`.
            if (node.type === "messageThread") {
                if (node.label)
                    yield* walkNodes([node.label], [...ancestors, node], scopes);
                if (node.summarize)
                    yield* walkNodes([node.summarize], [...ancestors, node], scopes);
                if (node.continueExpr)
                    yield* walkNodes([node.continueExpr], [...ancestors, node], scopes);
                if (node.sessionExpr)
                    yield* walkNodes([node.sessionExpr], [...ancestors, node], scopes);
                if (node.hidden)
                    yield* walkNodes([node.hidden], [...ancestors, node], scopes);
            }
            yield* walkNodes(node.body, [...ancestors, node], scopes);
        }
        else if (node.type === "handleBlock") {
            yield* walkNodes(node.body, [...ancestors, node], scopes);
            if (node.handler.kind === "inline") {
                yield* walkNodes(node.handler.body, [...ancestors, node], scopes);
            }
        }
        else if (node.type === "withModifier") {
            yield* walkNodes([node.statement], [...ancestors, node], scopes);
        }
        else if (node.type === "returnStatement") {
            if (node.value)
                yield* walkNodes([node.value], [...ancestors, node], scopes);
        }
        else if (node.type === "gotoStatement") {
            yield* walkNodes([node.nodeCall], [...ancestors, node], scopes);
        }
        else if (node.type === "assignment") {
            /* console.log(
              color.red(
                "HI IM IN A FUCKING MESSAGE ARGUMENT!",
                JSON.stringify([node.value]),
              ),
            ); */
            yield* walkNodes([node.value], [...ancestors, node], scopes);
            if (node.accessChain) {
                for (const accessElement of node.accessChain) {
                    if (accessElement.kind === "index") {
                        yield* walkNodes([accessElement.index], [...ancestors, node], scopes);
                    }
                    else if (accessElement.kind === "methodCall") {
                        yield* walkNodes([accessElement.functionCall], [...ancestors, node], scopes);
                    }
                }
            }
        }
        else if (node.type === "functionCall") {
            for (const arg of node.arguments) {
                yield* walkNodes([unwrapCallArg(arg)], [...ancestors, node], scopes);
            }
            if (node.block) {
                yield* walkNodes(node.block.body, [...ancestors, node, node.block], scopes);
            }
        }
        else if (node.type === "matchBlock") {
            yield* walkNodes([node.expression], [...ancestors, node], scopes);
            for (const caseItem of node.cases) {
                if (caseItem.type === "comment")
                    continue;
                if (caseItem.type === "newLine")
                    continue;
                if (caseItem.caseValue !== "_") {
                    yield* walkNodes([caseItem.caseValue], [...ancestors, node], scopes);
                }
                yield* walkNodes([caseItem.body], [...ancestors, node], scopes);
            }
        }
        else if (node.type === "valueAccess") {
            yield* walkNodes([node.base], [...ancestors, node], scopes);
            for (const element of node.chain) {
                if (element.kind === "index") {
                    yield* walkNodes([element.index], [...ancestors, node], scopes);
                }
                else if (element.kind === "slice") {
                    if (element.start)
                        yield* walkNodes([element.start], [...ancestors, node], scopes);
                    if (element.end)
                        yield* walkNodes([element.end], [...ancestors, node], scopes);
                }
                else if (element.kind === "methodCall") {
                    yield* walkNodes([element.functionCall], [...ancestors, node], scopes);
                }
            }
        }
        else if (node.type === "agencyArray") {
            const arrayItems = node.items.map((item) => item.type === "splat" ? item.value : item);
            yield* walkNodes(arrayItems, [...ancestors, node], scopes);
        }
        else if (node.type === "agencyObject") {
            const objChildren = [];
            for (const e of node.entries) {
                if ("type" in e && e.type === "splat") {
                    objChildren.push(e.value);
                }
                else {
                    const kv = e;
                    if (kv.computedKey)
                        objChildren.push(kv.computedKey);
                    objChildren.push(kv.value);
                }
            }
            yield* walkNodes(objChildren, [...ancestors, node], scopes);
        }
        else if (node.type === "string" ||
            node.type === "multiLineString") {
            for (const seg of node.segments) {
                if (seg.type === "interpolation") {
                    yield* walkNodes([seg.expression], [...ancestors, node], scopes);
                }
            }
        }
        else if (node.type === "binOpExpression") {
            yield* walkNodes([node.left], [...ancestors, node], scopes);
            yield* walkNodes([node.right], [...ancestors, node], scopes);
        }
        else if (node.type === "interruptStatement") {
            for (const arg of node.arguments) {
                yield* walkNodes([unwrapCallArg(arg)], [...ancestors, node], scopes);
            }
        }
    }
}
export function walkNodesArray(nodes, ancestors = [], scopes = []) {
    const results = [];
    for (const result of walkNodes(nodes, ancestors, scopes)) {
        results.push(result);
    }
    return results;
}
export function getAllVariablesInBodyArray(body) {
    const results = [];
    for (const result of getAllVariablesInBody(body)) {
        results.push(result);
    }
    return results;
}
