import process from "process";
import { formatUnitLiteral, } from "../types.js";
import { variableTypeToString } from "./typescriptGenerator/typeToString.js";
import { mergeDeep } from "../utils.js";
import { PRECEDENCE, } from "../types/binop.js";
export class AgencyGenerator {
    graphNodes = [];
    generatedStatements = [];
    generatedTypeAliases = [];
    typeAliases = {};
    functionsUsed = new Set();
    importNodes = [];
    importedNodes = [];
    /** Comments / blank lines at the very top of the file that must stay
     *  above the (sorted) imports block — e.g. file-level `// @tc-nocheck`,
     *  shebangs, module docstrings. Populated by `partitionImports()`. */
    importHeaderNodes = [];
    /** Comments attached to each import statement (no blank line between
     *  them and the import). Travel with the import when imports get
     *  sorted, so `// @tc-ignore` above an import keeps suppressing it. */
    importAttachedComments = new Map();
    functionDefinitions = {};
    currentScope = [{ type: "global" }];
    program = null;
    agencyConfig = {};
    indentLevel = 0;
    indentSize = 2;
    debug = !!process.env.AGENCY_DEBUG;
    /**
     * When true, imports render in-place (in their original source position)
     * rather than being hoisted to the top of the file and sorted. Used by
     * `agency literate weave`, which needs to render the file in source
     * order so the prose / code alternation matches what the author wrote.
     */
    preserveOrder = false;
    constructor(args = {}) {
        this.agencyConfig = mergeDeep(this.configDefaults(), args.config || {});
        this.preserveOrder = args.preserveOrder ?? false;
        if (this.agencyConfig.verbose) {
            console.log("Generator config:", this.agencyConfig);
        }
    }
    trace(methodName, result) {
        if (!this.debug || result === "")
            return result;
        return `[${methodName}]${result}[/${methodName}]`;
    }
    configDefaults() {
        return {};
    }
    generate(program) {
        this.program = program;
        // Pass 1: Collect all type aliases
        for (const node of program.nodes) {
            if (node.type === "typeAlias") {
                this.processTypeAlias(node);
            }
        }
        // Pass 2: Collect all node names
        for (const node of program.nodes) {
            if (node.type === "graphNode") {
                this.processGraphNodeName(node);
            }
        }
        // Partition the node stream: pull out the header (top-of-file
        // comments/blanks) and each import statement (with any comments
        // directly attached to it). This lets the imports block be sorted
        // while preserving `// @tc-ignore` / `// @tc-nocheck` placement.
        // Skipped in preserveOrder mode (literate weave) because the whole
        // point there is to leave nodes exactly where the author put them.
        if (!this.preserveOrder) {
            program.nodes = this.partitionImports(program.nodes);
        }
        // Pass 3: Collect all node imports
        for (const node of program.nodes) {
            if (node.type === "importNodeStatement") {
                this.importedNodes.push(node);
            }
        }
        // Pass 4: Generate code for tools
        for (const node of program.nodes) {
            if (node.type === "function") {
                this.generatedStatements.push(this.processTool(node));
                this.collectFunctionSignature(node);
            }
        }
        this.preprocessAST();
        // Types that should have a blank line before/after them at the top level
        const BLOCK_TYPES = new Set(["graphNode", "function", "typeAlias"]);
        const NO_SPACE_TYPES = new Set([
            "comment",
            "multiLineComment",
            "tag",
            "newLine",
        ]);
        // Pass 5: Process all nodes and generate code
        const stmtPairs = [];
        for (const node of program.nodes) {
            const result = this.processNode(node);
            if (result !== "" || node.type === "newLine") {
                stmtPairs.push({ type: node.type, code: result });
            }
        }
        // Join top-level statements: blank line between block declarations,
        // single newline between simple statements
        const stmtLines = [];
        for (let i = 0; i < stmtPairs.length; i++) {
            if (i > 0) {
                const prev = stmtPairs[i - 1];
                const curr = stmtPairs[i];
                if ((BLOCK_TYPES.has(prev.type) && !NO_SPACE_TYPES.has(curr.type)) ||
                    (BLOCK_TYPES.has(curr.type) && !NO_SPACE_TYPES.has(prev.type))) {
                    stmtLines.push(""); // blank line
                }
            }
            stmtLines.push(stmtPairs[i].code);
        }
        const output = [];
        // In preserveOrder mode, imports already rendered in-place via
        // processNodeInner, so we skip both the top-of-file header reflow
        // and the sorted import block.
        if (!this.preserveOrder) {
            this.addIfNonEmpty(this.renderImportHeader(), output);
            this.addIfNonEmpty(this.sortAndRenderImports(), output);
        }
        this.addIfNonEmpty(this.generatedTypeAliases.join("\n"), output);
        this.addIfNonEmpty(stmtLines.join("\n"), output);
        return {
            output: output.join("\n"),
        };
    }
    /**
     * Split the node stream into three pieces:
     *
     *   1. `importHeaderNodes` — contiguous comments/blank lines at the
     *      very top of the file (before any code or any import-attached
     *      comment). These render unchanged above the sorted imports block.
     *   2. Imports + comments attached to each one — tracked in
     *      `importAttachedComments`. A comment is "attached" iff it sits
     *      directly above its import with no blank line between them. This
     *      keeps `// @tc-ignore` above an import even when the import is
     *      moved by sorting.
     *   3. Everything else — returned as the new node stream for the
     *      remaining passes to process normally.
     *
     * Imports themselves are kept in `program.nodes` (returned here) so the
     * existing collection pass for `importNodes` / `importedNodes` still
     * sees them. The render path skips them because `processNode` for an
     * import returns "".
     */
    partitionImports(nodes) {
        const isImport = (n) => n.type === "importStatement" || n.type === "importNodeStatement";
        const isComment = (n) => n.type === "comment" || n.type === "multiLineComment";
        // 1) Eat top-of-file header. The header is the contiguous run of
        //    comment lines at the very top, terminated by either:
        //      - a blank line (newLine) — included in the header so the
        //        visual separation between header and imports is preserved,
        //      - any other node (code, import) — header ends just before it.
        //
        //    A blank line *terminates* header eating. Comments below the
        //    blank line are NOT header; they participate in import-comment
        //    attachment instead. This is what lets a user park `// @tc-ignore`
        //    above an import: separate it from the header region with a
        //    blank line, and it travels with its import when imports are
        //    sorted.
        let i = 0;
        const header = [];
        while (i < nodes.length) {
            const n = nodes[i];
            if (isComment(n)) {
                header.push(n);
                i++;
                continue;
            }
            if (n.type === "newLine") {
                header.push(n);
                i++;
                break; // blank line ends the header
            }
            break; // any other node — header ends here
        }
        // 2) Everything eaten as header stays at the top. We do NOT pop the
        //    trailing comments back to attach them to the first import: the
        //    whole contiguous top-of-file region is sacred (per spec: comments
        //    at the top of the file stay at the top). For `@tc-ignore` to
        //    travel with an import, the user separates it from the header
        //    region with a blank line.
        this.importHeaderNodes = header;
        // 3) Process the rest: for any imports, pull off their directly-
        //    preceding comments (no blank line between). Comments separated
        //    by a blank line stay in the stream.
        const rest = [];
        let commentBuf = [];
        while (i < nodes.length) {
            const n = nodes[i];
            if (isComment(n)) {
                commentBuf.push(n);
                i++;
                continue;
            }
            if (n.type === "newLine") {
                // Blank line — any buffered comments are not "attached" to a
                // following import; flush them back into the stream.
                rest.push(...commentBuf, n);
                commentBuf = [];
                i++;
                continue;
            }
            if (isImport(n)) {
                this.importAttachedComments.set(n, commentBuf);
                commentBuf = [];
                rest.push(n);
                i++;
                continue;
            }
            // Other code — comment buffer wasn't attached to an import.
            rest.push(...commentBuf, n);
            commentBuf = [];
            i++;
        }
        rest.push(...commentBuf);
        return rest;
    }
    /**
     * Render the top-of-file header (comments + blank lines that sit above
     * the imports block). Returns an empty string when there are none.
     */
    renderImportHeader() {
        if (this.importHeaderNodes.length === 0)
            return "";
        const lines = [];
        for (const n of this.importHeaderNodes) {
            const code = this.processNode(n);
            if (code !== "" || n.type === "newLine")
                lines.push(code);
        }
        return lines.join("\n");
    }
    addIfNonEmpty(str, lines) {
        if (str.trim() !== "") {
            lines.push(str);
        }
    }
    preprocessAST() { }
    generateBuiltins() {
        return "";
    }
    generateImports() {
        return "";
    }
    preprocess() {
        return "";
    }
    postprocess() {
        return "";
    }
    collectFunctionSignature(node) {
        this.functionDefinitions[node.functionName] = node;
    }
    processGraphNodeName(node) { }
    processNode(node) {
        const result = this.processNodeInner(node);
        return this.trace(node.type, result);
    }
    processNodeInner(node) {
        switch (node.type) {
            case "typeAlias":
                return this.processTypeAlias(node);
            case "assignment":
                return this.processAssignment(node);
            case "function":
                return this.processFunctionDefinition(node);
            case "functionCall":
                return this.processFunctionCall(node);
            case "valueAccess":
                return this.processValueAccess(node);
            case "comment":
                return this.processComment(node);
            case "multiLineComment":
                return this.processMultiLineComment(node);
            case "matchBlock":
                return this.processMatchBlock(node);
            case "number":
            case "unitLiteral":
            case "multiLineString":
            case "string":
            case "variableName":
            case "boolean":
            case "null":
                return this.generateLiteral(node);
            case "returnStatement":
                return this.processReturnStatement(node);
            case "gotoStatement":
                return this.processGotoStatement(node);
            case "debuggerStatement":
                return this.processDebuggerStatement(node);
            case "agencyArray":
                return this.processAgencyArray(node);
            case "agencyObject":
                return this.processAgencyObject(node);
            case "graphNode":
                return this.processGraphNode(node);
            case "importStatement":
                if (this.preserveOrder)
                    return this.processImportStatement(node);
                this.importNodes.push(node);
                return "";
            case "importNodeStatement":
                if (this.preserveOrder)
                    return this.processImportNodeStatement(node);
                this.importedNodes.push(node);
                return "";
            case "exportFromStatement":
                return this.processExportFromStatement(node);
            case "forLoop":
                return this.processForLoop(node);
            case "whileLoop":
                return this.processWhileLoop(node);
            case "ifElse":
                return this.processIfElse(node);
            case "newLine":
                return this.processNewLine(node);
            case "rawCode":
                return node.value;
            case "messageThread":
                return this.processMessageThread(node);
            case "handleBlock":
                return this.processHandleBlock(node);
            case "withModifier":
                return `${this.processNode(node.statement)} with ${node.handlerName}`;
            case "staticStatement":
                return `static ${this.processNode(node.statement)}`;
            case "skill":
                return this.processSkill(node);
            case "binOpExpression":
                return this.processBinOpExpression(node);
            case "keyword":
                return this.processKeyword(node);
            case "tag":
                return this.formatTag(node);
            case "tryExpression":
                // remove extra indentation
                return `try ${this.processNode(node.call).trim()}`;
            case "newExpression":
                return this.processNewExpression(node);
            case "schemaExpression":
                return `schema(${variableTypeToString(node.typeArg, this.typeAliases, true)})`;
            case "regex":
                return `re/${node.pattern}/${node.flags}`;
            case "interruptStatement":
                return this.processInterruptStatement(node);
            case "objectPattern":
            case "arrayPattern":
            case "restPattern":
            case "wildcardPattern":
            case "resultPattern":
                return this.formatPattern(node);
            case "isExpression":
                return `${this.processNode(node.expression).trim()} is ${this.formatPattern(node.pattern)}`;
            case "parallelBlock":
                return this.processParallelBlock(node);
            case "seqBlock":
                return this.processSeqBlock(node);
            default:
                throw new Error(`Unhandled Agency node type: ${node.type}`);
        }
    }
    processInterruptStatement(node) {
        const args = this.renderArgList(node.arguments);
        if (node.kind === "unknown") {
            return this.indentStr(`interrupt${args}`);
        }
        return this.indentStr(`interrupt ${node.kind}${args}`);
    }
    needsParensLeft(child, parentOp) {
        if (child.type !== "binOpExpression")
            return false;
        // For right-associative ops like **, (2 ** 3) ** 4 needs parens on the left
        if (parentOp === "**")
            return PRECEDENCE[child.operator] <= PRECEDENCE[parentOp];
        return PRECEDENCE[child.operator] < PRECEDENCE[parentOp];
    }
    needsParensRight(child, parentOp) {
        if (child.type !== "binOpExpression")
            return false;
        return PRECEDENCE[child.operator] <= PRECEDENCE[parentOp];
    }
    startScope(scope) {
        this.currentScope.push(scope);
    }
    endScope() {
        this.currentScope.pop();
    }
    getCurrentScope() {
        return this.currentScope[this.currentScope.length - 1];
    }
    isAgencyFunction(functionName, context) {
        if (context === "valueAccess") {
            return false;
        }
        return !!this.functionDefinitions[functionName];
    }
    // Indent helpers
    indent(level = this.indentLevel) {
        return " ".repeat(level * this.indentSize);
    }
    increaseIndent() {
        this.indentLevel++;
    }
    decreaseIndent() {
        this.indentLevel--;
    }
    indentStr(str) {
        return `${this.indent()}${str}`;
    }
    // Wrapping helpers
    wrapList(items, prefix, open, close, suffix = "") {
        const inline = `${prefix}${open}${items.join(", ")}${close}${suffix}`;
        if (this.indentStr(inline).length <= 80)
            return inline;
        this.increaseIndent();
        const lines = items.map((item) => this.indentStr(`${item},`));
        this.decreaseIndent();
        return `${prefix}${open}\n${lines.join("\n")}\n${this.indent()}${close}${suffix}`;
    }
    renderParams(parameters) {
        return parameters.map((p) => {
            const prefix = p.variadic ? "..." : "";
            const defaultSuffix = p.defaultValue
                ? ` = ${this.processNode(p.defaultValue).trim()}`
                : "";
            if (p.typeHint) {
                const typeStr = variableTypeToString(p.typeHint, this.typeAliases, true);
                const bang = p.validated ? "!" : "";
                return `${prefix}${p.name}: ${typeStr}${bang}${defaultSuffix}`;
            }
            else {
                return `${prefix}${p.name}${defaultSuffix}`;
            }
        });
    }
    // Type system methods
    stringifyProp(prop) {
        const isUnionWithUndefined = prop.value.type === "unionType" &&
            prop.value.types.some((t) => t.type === "primitiveType" && t.value === "undefined");
        if (isUnionWithUndefined) {
            const nonUndefinedTypes = prop.value.types.filter((t) => !(t.type === "primitiveType" && t.value === "undefined"));
            const unionWithoutUndefined = nonUndefinedTypes.length === 1
                ? nonUndefinedTypes[0]
                : { type: "unionType", types: nonUndefinedTypes };
            let str = `${prop.key}?: ${variableTypeToString(unionWithoutUndefined, this.typeAliases, true)}`;
            if (prop.description) {
                str += ` # ${prop.description}`;
            }
            return str;
        }
        let str = `${prop.key}: ${variableTypeToString(prop.value, this.typeAliases, true)}`;
        if (prop.description) {
            str += ` # ${prop.description}`;
        }
        return str;
    }
    aliasedTypeToString(aliasedType) {
        if (aliasedType.type === "objectType") {
            this.increaseIndent();
            let result = "{\n" +
                aliasedType.properties
                    .map((prop) => {
                    return this.indentStr(this.stringifyProp(prop));
                })
                    .join(";\n") +
                "\n";
            this.decreaseIndent();
            result += this.indentStr("}");
            return result;
        }
        return variableTypeToString(aliasedType, this.typeAliases, true);
    }
    processTypeAlias(node) {
        this.typeAliases[node.aliasName] = node.aliasedType;
        const aliasedTypeStr = this.aliasedTypeToString(node.aliasedType);
        const exportPrefix = node.exported ? "export " : "";
        const typeParamsStr = this.formatTypeParams(node.typeParams);
        const valueParamsStr = this.formatValueParams(node.valueParams);
        const tags = this.formatAttachedTags(node);
        return (this.formatDocComment(node) +
            tags +
            this.indentStr(`${exportPrefix}type ${node.aliasName}${typeParamsStr}${valueParamsStr} = ${aliasedTypeStr}`));
    }
    /**
     * Format the `<T, U = string, ...>` chunk of a generic alias declaration.
     * Returns an empty string when there are no type params.
     */
    formatTypeParams(params) {
        if (!params || params.length === 0)
            return "";
        const parts = params.map((p) => {
            if (!p.default)
                return p.name;
            const def = variableTypeToString(p.default, this.typeAliases, true);
            return `${p.name} = ${def}`;
        });
        return `<${parts.join(", ")}>`;
    }
    /**
     * Format the `(low: number, high: number = 10)` chunk of a
     * value-parameterized alias declaration. Returns an empty string when
     * there are no value params.
     */
    formatValueParams(params) {
        if (!params || params.length === 0)
            return "";
        const parts = params.map((p) => {
            const typeStr = variableTypeToString(p.type, this.typeAliases, true);
            const base = `${p.name}: ${typeStr}`;
            if (p.default === undefined)
                return base;
            return `${base} = ${this.processNode(p.default).trim()}`;
        });
        return `(${parts.join(", ")})`;
    }
    // Assignment and literals
    processAssignment(node) {
        const tags = this.formatAttachedTags(node);
        const chainStr = node.accessChain
            ?.map((ce) => this.processAccessChainElement(ce))
            .join("") ?? "";
        const bangSuffix = node.validated ? "!" : "";
        // Destructuring pattern takes precedence over the bare variableName.
        const lhs = node.pattern
            ? this.formatPattern(node.pattern)
            : node.typeHint
                ? `${node.variableName}${chainStr}: ${variableTypeToString(node.typeHint, this.typeAliases, true)}${bangSuffix}`
                : `${node.variableName}${chainStr}`;
        const exportPrefix = node.exported ? "export " : "";
        const staticPrefix = node.static ? "static " : "";
        const declPrefix = node.declKind ? `${node.declKind} ` : "";
        let valueCode = "";
        if (node.value.type === "binOpExpression") {
            valueCode = this.processBinOpExpression(node.value, true).trim();
        }
        else {
            valueCode = this.processNode(node.value).trim();
        }
        return (tags +
            this.indentStr(`${exportPrefix}${staticPrefix}${declPrefix}${lhs} = ${valueCode}`));
    }
    /**
     * Format a pattern AST node back into Agency pattern syntax. Used by the
     * formatter (it sees the un-lowered AST). Handles binding patterns and
     * match patterns; for match patterns, literal sub-nodes are formatted via
     * the existing literal/expression code paths.
     */
    formatPattern(pattern) {
        switch (pattern.type) {
            case "objectPattern":
                return this.formatObjectPattern(pattern);
            case "arrayPattern":
                return this.formatArrayPattern(pattern);
            case "restPattern":
                return `...${pattern.identifier}`;
            case "wildcardPattern":
                return "_";
            case "resultPattern": {
                const rp = pattern;
                return rp.binding === null ? rp.kind : `${rp.kind}(${rp.binding})`;
            }
            default:
                // variableName / literals — defer to existing rendering
                return this.processNode(pattern).trim();
        }
    }
    formatObjectPattern(node) {
        const parts = node.properties.map((p) => {
            if (p.type === "objectPatternShorthand") {
                return p.name;
            }
            if (p.type === "restPattern") {
                return `...${p.identifier}`;
            }
            const prop = p;
            // If the value is just `variableName` matching the key, emit shorthand.
            if (prop.value.type === "variableName" && prop.value.value === prop.key) {
                return prop.key;
            }
            return `${prop.key}: ${this.formatPattern(prop.value)}`;
        });
        return `{ ${parts.join(", ")} }`;
    }
    formatArrayPattern(node) {
        const parts = node.elements.map((el) => this.formatPattern(el));
        return `[${parts.join(", ")}]`;
    }
    generateLiteral(literal) {
        switch (literal.type) {
            case "number":
                return literal.value;
            case "unitLiteral":
                return formatUnitLiteral(literal);
            case "string":
                return this.generateStringLiteral(literal);
            case "variableName":
                return literal.value;
            case "multiLineString":
                return this.generateMultiLineStringLiteral(literal);
            case "boolean":
                return literal.value ? "true" : "false";
            case "null":
                return "null";
            default:
                return "";
        }
    }
    generateStringLiteral(node) {
        let result = '"';
        for (const segment of node.segments) {
            if (segment.type === "text") {
                result += segment.value;
            }
            else if (segment.type === "interpolation") {
                // processNode (not expressionToString) so nested function calls with
                // block arguments and quoted string literals round-trip correctly.
                result += `\${${this.processNode(segment.expression).trim()}}`;
            }
        }
        result += '"';
        return result;
    }
    generateMultiLineStringLiteral(node) {
        let result = '"""';
        for (const segment of node.segments) {
            if (segment.type === "text") {
                result += segment.value;
            }
            else if (segment.type === "interpolation") {
                result += `\${${this.processNode(segment.expression).trim()}}`;
            }
        }
        result += '"""';
        return result;
        /* .split("\n")
          .map((line) => this.indentStr(line.trim()))
          .join("\n"); */
    }
    // Function methods
    processFunctionDefinition(node) {
        const tags = this.formatAttachedTags(node);
        const { functionName, body, parameters } = node;
        const returnTypeBang = node.returnTypeValidated ? "!" : "";
        const returnTypeStr = node.returnType
            ? ": " +
                variableTypeToString(node.returnType, this.typeAliases, true) +
                returnTypeBang
            : "";
        const prefixes = [];
        if (node.exported)
            prefixes.push("export");
        if (node.safe)
            prefixes.push("safe");
        prefixes.push("def");
        const prefix = `${prefixes.join(" ")} ${functionName}`;
        const renderedParams = this.renderParams(parameters);
        const signature = this.wrapList(renderedParams, prefix, "(", ")", `${returnTypeStr} {`);
        let result = this.indentStr(`${signature}\n`);
        this.increaseIndent();
        if (node.docString) {
            let content = "";
            for (const seg of node.docString.segments) {
                if (seg.type === "text") {
                    content += seg.value;
                }
                else {
                    content += `\${${this.processNode(seg.expression).trim()}}`;
                }
            }
            // Strip outer whitespace so docstrings format consistently —
            // the parser preserves raw source verbatim, including the
            // leading newline + indentation typical of multi-line docstrings.
            const lines = content
                .trim()
                .split("\n")
                .map((l) => l.trim());
            const docLines = [`"""`, ...lines, `"""`];
            const docStr = docLines.map((line) => this.indentStr(line)).join("\n");
            result += `${docStr}\n`;
        }
        const bodyStr = this.renderBody(body);
        if (bodyStr.trim() !== "") {
            result += bodyStr;
        }
        this.decreaseIndent();
        result += this.indentStr(`}`);
        return this.formatDocComment(node) + tags + result;
    }
    processFunctionCall(node) {
        const tags = this.formatAttachedTags(node);
        const expr = this.generateFunctionCallExpression(node, "topLevelStatement");
        return tags + this.indentStr(`${expr}`);
    }
    // Render each argument to a string array
    renderArgs(args, block) {
        const rendered = args.map((arg) => {
            if (arg.type === "namedArgument") {
                return `${arg.name}: ${this.processNode(arg.value).trim()}`;
            }
            if (arg.type === "splat") {
                return `...${this.processNode(arg.value).trim()}`;
            }
            return this.processNode(arg).trim();
        });
        if (block?.inline) {
            const returnStmt = block.body[0];
            const exprStr = this.processNode(returnStmt.value).trim();
            let params = "";
            if (block.params.length === 1) {
                params = block.params[0].name;
            }
            else if (block.params.length > 1) {
                params = `(${block.params.map((p) => p.name).join(", ")})`;
            }
            rendered.push(`\\${params} -> ${exprStr}`);
        }
        return rendered;
    }
    // Format args as inline parenthesized list (no wrapping — used by access chain callers)
    renderArgList(args, block) {
        const rendered = this.renderArgs(args, block);
        return `(${rendered.join(", ")})`;
    }
    generateFunctionCallExpression(node, context) {
        let asyncPrefix = "";
        if (node.async === true) {
            asyncPrefix = "async ";
        }
        else if (node.async === false) {
            asyncPrefix = "await ";
        }
        const block = node.block;
        const inlineBlock = block?.inline ? block : undefined;
        const rendered = this.renderArgs(node.arguments, inlineBlock);
        let result = this.wrapList(rendered, `${asyncPrefix}${node.functionName}`, "(", ")", "");
        if (block && !block.inline) {
            let asClause = "as ";
            if (block.params.length === 1) {
                asClause = `as ${block.params[0].name} `;
            }
            else if (block.params.length > 1) {
                asClause = `as (${block.params.map((p) => p.name).join(", ")}) `;
            }
            this.increaseIndent();
            const bodyStr = this.renderBody(block.body);
            this.decreaseIndent();
            result += ` ${asClause}{\n${bodyStr}${this.indentStr("}")}`;
        }
        return result;
    }
    // Data structures
    processAgencyArray(node) {
        const items = node.items.map((item) => {
            if (item.type === "splat") {
                return `...${this.processNode(item.value).trim()}`;
            }
            return this.processNode(item).trim();
        });
        return this.wrapList(items, "", "[", "]");
    }
    processAgencyObject(node) {
        this.increaseIndent();
        const entries = node.entries.map((entry) => {
            if ("type" in entry && entry.type === "splat") {
                return this.indentStr(`...${this.processNode(entry.value).trim()}`);
            }
            const kv = entry;
            const valueCode = this.processNode(kv.value).trim();
            if (kv.computedKey) {
                const keyCode = this.processNode(kv.computedKey).trim();
                return this.indentStr(`[${keyCode}]: ${valueCode}`);
            }
            return this.indentStr(`${this.addQuotesToKey(kv.key)}: ${valueCode}`);
        });
        this.decreaseIndent();
        if (entries.length === 0) {
            return `{}`;
        }
        const entriesStr = "\n" + entries.join(",\n") + "\n";
        return `{${entriesStr}` + this.indentStr("}");
    }
    addQuotesToKey(key) {
        if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
            return key;
        }
        return `"${key}"`;
    }
    // Access expressions
    processValueAccess(node) {
        let code = this.processNode(node.base).trim();
        for (const element of node.chain) {
            code += this.processAccessChainElement(element);
        }
        return this.indentStr(this.asyncAwaitPrefix(code, node.async));
    }
    asyncAwaitPrefix(code, async) {
        if (async === true) {
            return `async ${code}`;
        }
        else if (async === false) {
            return `await ${code}`;
        }
        return code;
    }
    // Control flow
    processMatchBlock(node) {
        const exprCode = this.processNode(node.expression).trim();
        let result = this.indentStr(`match(${exprCode}) {\n`);
        this.increaseIndent();
        for (const caseNode of node.cases) {
            if (caseNode.type === "comment") {
                result += this.processComment(caseNode);
                continue;
            }
            if (caseNode.type === "newLine") {
                result += this.processNewLine(caseNode);
                continue;
            }
            const pattern = caseNode.caseValue === "_"
                ? "_"
                : this.processNode(caseNode.caseValue).trim();
            const guardCode = caseNode.guard
                ? ` if (${this.processNode(caseNode.guard).trim()})`
                : "";
            const bodyCode = this.processNode(caseNode.body).trim();
            result += this.indentStr(`${pattern}${guardCode} => ${bodyCode}\n`);
        }
        this.decreaseIndent();
        result += this.indentStr(`}`);
        return result;
    }
    processForLoop(node) {
        const iterableCode = this.processNode(node.iterable).trim();
        const itemVarStr = typeof node.itemVar === "string"
            ? node.itemVar
            : this.formatPattern(node.itemVar);
        const vars = node.indexVar ? `${itemVarStr}, ${node.indexVar}` : itemVarStr;
        let result = this.indentStr(`for (${vars} in ${iterableCode}) {\n`);
        this.increaseIndent();
        result += this.renderBody(node.body);
        this.decreaseIndent();
        result += this.indentStr(`}`);
        return result;
    }
    processWhileLoop(node) {
        const conditionCode = this.processNode(node.condition).trim();
        let result = this.indentStr(`while (${conditionCode}) {\n`);
        this.increaseIndent();
        result += this.renderBody(node.body);
        this.decreaseIndent();
        result += this.indentStr(`}`);
        return result;
    }
    processIfElse(node) {
        const conditionCode = this.processNode(node.condition).trim();
        const lines = [];
        lines.push(this.indentStr(`if (${conditionCode}) {\n`));
        this.increaseIndent();
        lines.push(this.renderBody(node.thenBody));
        this.decreaseIndent();
        if (node.elseBody && node.elseBody.length > 0) {
            if (node.elseBody.length === 1 && node.elseBody[0].type === "ifElse") {
                const elseIfCode = this.processIfElse(node.elseBody[0]);
                lines.push(this.indentStr(`} else ${elseIfCode.trimStart()}`));
                return lines.join("");
            }
            else {
                lines.push(this.indentStr(`} else {\n`));
                this.increaseIndent();
                lines.push(this.renderBody(node.elseBody));
                this.decreaseIndent();
            }
        }
        lines.push(this.indentStr(`}`));
        return lines.join("");
    }
    processReturnStatement(node) {
        if (!node.value)
            return this.indentStr("return");
        const valueCode = this.processNode(node.value).trim();
        return this.indentStr(`return ${valueCode}`);
    }
    processGotoStatement(node) {
        const callCode = this.processNode(node.nodeCall).trim();
        return this.indentStr(`goto ${callCode}`);
    }
    processDebuggerStatement(node) {
        return this.indentStr(node.label ? `debugger(${JSON.stringify(node.label)})` : "debugger()");
    }
    // Utility methods
    processComment(node) {
        return this.indentStr(`//${node.content}`);
    }
    processMultiLineComment(node) {
        if (node.isDoc) {
            if (node.isModuleDoc) {
                return this.indentStr(`/** @module\n${node.content}***/`);
            }
            return this.indentStr(`/**${node.content}*/`);
        }
        return this.indentStr(`/*${node.content}*/`);
    }
    formatDocComment(node) {
        if (!node.docComment)
            return "";
        return this.processMultiLineComment(node.docComment) + "\n";
    }
    processImportStatement(node) {
        const modulePath = node.modulePath.startsWith("std::")
            ? node.modulePath.replace(/\.agency$/, "")
            : node.modulePath;
        const suffix = ` from "${modulePath}"`;
        // For single named import, use wrapList
        if (node.importedNames.length === 1 &&
            node.importedNames[0].type === "namedImport") {
            const namedImport = node.importedNames[0];
            const names = namedImport.importedNames.map((name) => {
                const alias = namedImport.aliases[name];
                const base = alias ? `${name} as ${alias}` : name;
                return namedImport.safeNames?.includes(name) ? `safe ${base}` : base;
            });
            return this.indentStr(this.wrapList(names, "import ", "{ ", " }", suffix));
        }
        // Default/namespace/mixed imports — always inline
        const importedNames = node.importedNames.map((name) => this.processImportNameType(name));
        return this.indentStr(`import ${importedNames.join(", ")}${suffix}`);
    }
    processImportNameType(node) {
        switch (node.type) {
            case "namedImport": {
                const names = node.importedNames.map((name) => {
                    const alias = node.aliases[name];
                    const base = alias ? `${name} as ${alias}` : name;
                    return node.safeNames?.includes(name) ? `safe ${base}` : base;
                });
                return `{ ${names.join(", ")} }`;
            }
            case "namespaceImport":
                return `* as ${node.importedNames}`;
            case "defaultImport":
                return `${node.importedNames}`;
        }
    }
    processImportNodeStatement(node) {
        return `import node { ${node.importedNodes.join(", ")} } from "${node.agencyFile}"`;
    }
    processExportFromStatement(node) {
        if (node.body.kind === "starExport") {
            return this.indentStr(`export * from "${node.modulePath}"`);
        }
        const items = node.body.names.map((name) => {
            const alias = node.body.kind === "namedExport" ? node.body.aliases[name] : undefined;
            const safe = node.body.kind === "namedExport" && node.body.safeNames.includes(name);
            const base = alias ? `${name} as ${alias}` : name;
            return safe ? `safe ${base}` : base;
        });
        return this.indentStr(`export { ${items.join(", ")} } from "${node.modulePath}"`);
    }
    sortAndRenderImports() {
        const renderWithAttached = (node, body) => {
            const attached = this.importAttachedComments.get(node) ?? [];
            if (attached.length === 0)
                return body;
            const commentLines = attached.map((c) => this.processNode(c));
            return [...commentLines, body].join("\n");
        };
        const stdlib = [];
        const packages = [];
        const relative = [];
        for (const node of this.importNodes) {
            const entry = {
                modulePath: node.modulePath,
                render: () => renderWithAttached(node, this.processImportStatement(node)),
            };
            if (node.modulePath.startsWith("std::")) {
                stdlib.push(entry);
            }
            else if (node.modulePath.startsWith("pkg::")) {
                packages.push(entry);
            }
            else {
                relative.push(entry);
            }
        }
        for (const node of this.importedNodes) {
            relative.push({
                modulePath: node.agencyFile,
                render: () => renderWithAttached(node, this.processImportNodeStatement(node)),
            });
        }
        const sort = (arr) => arr.sort((a, b) => a.modulePath.localeCompare(b.modulePath));
        sort(stdlib);
        sort(packages);
        sort(relative);
        const groups = [stdlib, packages, relative]
            .filter((g) => g.length > 0)
            .map((g) => g.map((e) => e.render()).join("\n"));
        return groups.join("\n\n");
    }
    processGraphNode(node) {
        const tags = this.formatAttachedTags(node);
        const { nodeName, body, parameters } = node;
        const returnTypeBang = node.returnTypeValidated ? "!" : "";
        const returnTypeStr = node.returnType
            ? ": " +
                variableTypeToString(node.returnType, this.typeAliases, true) +
                returnTypeBang
            : "";
        const exportPrefix = node.exported ? "export " : "";
        const prefix = `${exportPrefix}node ${nodeName}`;
        const renderedParams = this.renderParams(parameters);
        const signature = this.wrapList(renderedParams, prefix, "(", ")", `${returnTypeStr} {`);
        let result = this.indentStr(`${signature}\n`);
        this.increaseIndent();
        if (node.docString) {
            let content = "";
            for (const seg of node.docString.segments) {
                if (seg.type === "text") {
                    content += seg.value;
                }
                else {
                    content += `\${${this.processNode(seg.expression).trim()}}`;
                }
            }
            // Strip outer whitespace so docstrings format consistently —
            // the parser preserves raw source verbatim, including the
            // leading newline + indentation typical of multi-line docstrings.
            const lines = content
                .trim()
                .split("\n")
                .map((l) => l.trim());
            const docLines = [`"""`, ...lines, `"""`];
            const docStr = docLines.map((line) => this.indentStr(line)).join("\n");
            result += `${docStr}\n`;
        }
        result += this.renderBody(body);
        this.decreaseIndent();
        result += this.indentStr(`}`);
        return this.formatDocComment(node) + tags + result;
    }
    processNewExpression(node) {
        const args = node.arguments.map((a) => this.processNode(a)).join(", ");
        return `new ${node.className}(${args})`;
    }
    processTool(node) {
        return "";
    }
    processNewLine(_node) {
        return "";
    }
    renderBody(body) {
        const lines = [];
        for (const stmt of body) {
            const line = this.processNode(stmt);
            if (line !== "" || stmt.type === "newLine") {
                lines.push(line);
            }
        }
        return lines.join("\n").trimEnd() + "\n";
    }
    processMessageThread(node) {
        this.increaseIndent();
        const bodyCodeStr = this.renderBody(node.body);
        this.decreaseIndent();
        const threadType = node.threadType;
        return this.indentStr(`${threadType} {\n${bodyCodeStr}${this.indentStr("}")}`);
    }
    processParallelBlock(node) {
        this.increaseIndent();
        const bodyCodeStr = this.renderBody(node.body);
        this.decreaseIndent();
        return this.indentStr(`parallel {\n${bodyCodeStr}${this.indentStr("}")}`);
    }
    processSeqBlock(node) {
        this.increaseIndent();
        const bodyCodeStr = this.renderBody(node.body);
        this.decreaseIndent();
        return this.indentStr(`seq {\n${bodyCodeStr}${this.indentStr("}")}`);
    }
    processHandleBlock(node) {
        this.increaseIndent();
        const bodyCodeStr = this.renderBody(node.body);
        this.decreaseIndent();
        let handlerStr;
        if (node.handler.kind === "inline") {
            const handlerBang = node.handler.param.validated ? "!" : "";
            const paramStr = node.handler.param.typeHint
                ? `${node.handler.param.name}: ${variableTypeToString(node.handler.param.typeHint, this.typeAliases, true)}${handlerBang}`
                : node.handler.param.name;
            this.increaseIndent();
            const handlerBodyStr = this.renderBody(node.handler.body);
            this.decreaseIndent();
            handlerStr = `(${paramStr}) {\n${handlerBodyStr}${this.indentStr("}")}`;
        }
        else {
            handlerStr = node.handler.functionName;
        }
        return this.indentStr(`handle {\n${bodyCodeStr}${this.indentStr("}")} with ${handlerStr}`);
    }
    processSkill(node) {
        return this.indentStr(`skill "${node.filepath}"`);
    }
    processBinOpExpression(node, assigned = false) {
        const op = node.operator;
        // Unary prefix operators: !x, typeof x, void x
        if (op === "!" || op === "typeof" || op === "void") {
            const operand = this.processNode(node.right).trim();
            const sep = op === "!" ? "" : " ";
            const result = `${op}${sep}${operand}`;
            return assigned ? result : this.indentStr(result);
        }
        // Postfix operators: x++, x--
        if (op === "++" || op === "--") {
            const operand = this.processNode(node.left).trim();
            const result = `${operand}${op}`;
            return assigned ? result : this.indentStr(result);
        }
        const leftStr = this.processNode(node.left).trim();
        const rightStr = this.processNode(node.right).trim();
        const left = this.needsParensLeft(node.left, op) ? `(${leftStr})` : leftStr;
        const right = this.needsParensRight(node.right, op)
            ? `(${rightStr})`
            : rightStr;
        const inline = `${left} ${op} ${right}`;
        // For pipe chains, break into multiple lines if the inline version is too long
        if (op === "|>" && inline.length > 80) {
            const segments = this.flattenPipeChain(node);
            const multiline = segments.join(`\n${this.indent()}|> `);
            return assigned ? multiline : this.indentStr(multiline);
        }
        return assigned ? inline : this.indentStr(inline);
    }
    flattenPipeChain(node) {
        const segments = [];
        let current = node;
        while (current.type === "binOpExpression" && current.operator === "|>") {
            segments.unshift(this.processNode(current.right).trim());
            current = current.left;
        }
        segments.unshift(this.processNode(current).trim());
        return segments;
    }
    processAccessChainElement(node) {
        const dot = node.optional ? "?." : ".";
        switch (node.kind) {
            case "property":
                return `${dot}${node.name}`;
            case "index": {
                const inner = this.processNode(node.index).trim();
                return node.optional ? `?.[${inner}]` : `[${inner}]`;
            }
            case "slice": {
                const start = node.start ? this.processNode(node.start).trim() : "";
                const end = node.end ? this.processNode(node.end).trim() : "";
                const inner = `${start}:${end}`;
                return node.optional ? `?.[${inner}]` : `[${inner}]`;
            }
            case "methodCall":
                return `${dot}${this.generateFunctionCallExpression(node.functionCall, "valueAccess")}`;
            case "call":
                return `${node.optional ? "?." : ""}${this.renderArgList(node.arguments, node.block)}`;
            default:
                throw new Error(`Unknown access chain element kind: ${node.kind}`);
        }
    }
    formatTag(tag) {
        if (tag.arguments.length === 0) {
            return this.indentStr(`@${tag.name}`);
        }
        const args = tag.arguments.map((arg) => this.processNode(arg).trim());
        return this.indentStr(`@${tag.name}(${args.join(", ")})`);
    }
    formatAttachedTags(node) {
        if (!node.tags?.length)
            return "";
        return node.tags.map((tag) => this.formatTag(tag)).join("\n") + "\n";
    }
    processKeyword(node) {
        return this.indentStr(`${node.value}`);
    }
}
export function generateAgency(program, opts = {}) {
    const generator = new AgencyGenerator({ preserveOrder: opts.preserveOrder });
    return (generator
        .generate(program)
        .output.trim()
        .replace(/[ \t]+$/gm, "") + "\n");
}
