import * as fs from "fs";
import * as path from "path";
import { parseAgency } from "./parser.js";
import { walkNodes } from "./utils/node.js";
import { resolveAgencyImportPath, isAgencyImport, getStdlibDir, } from "./importPaths.js";
/** Type-alias names that resolve to built-in types. */
const RESERVED_TYPE_NAMES = new Set(["Result"]);
/**
 * Cross-file index of every declaration reachable from an entrypoint.
 * Built eagerly: parses every reachable .agency file, classifies its
 * top-level (and nested type-alias) declarations, and follows imports.
 */
export class SymbolTable {
    files;
    constructor(files = {}) {
        this.files = files;
    }
    static build(entrypoint, config = {}) {
        const parsed = {};
        const visited = new Set();
        function visit(filePath) {
            const absPath = path.resolve(filePath);
            if (visited.has(absPath))
                return;
            visited.add(absPath);
            if (!fs.existsSync(absPath))
                return;
            if (config.verbose) {
                console.log(`[SymbolTable] Processing ${absPath}`);
            }
            const contents = fs.readFileSync(absPath, "utf-8");
            const isStdlibIndex = absPath === path.join(getStdlibDir(), "index.agency");
            const parseResult = parseAgency(contents, config, !isStdlibIndex);
            if (!parseResult.success) {
                if (config.verbose) {
                    console.error(`[SymbolTable] Failed to parse ${absPath}: ${JSON.stringify(parseResult, null, 2)}`);
                }
                return;
            }
            const program = parseResult.result;
            parsed[absPath] = { symbols: classifySymbols(program), program };
            for (const { node } of walkNodes(program.nodes)) {
                if (node.type === "importNodeStatement") {
                    visit(resolveAgencyImportPath(node.agencyFile, absPath));
                }
                else if (node.type === "importStatement" &&
                    isAgencyImport(node.modulePath)) {
                    visit(resolveAgencyImportPath(node.modulePath, absPath));
                }
                else if (node.type === "exportFromStatement" &&
                    isAgencyImport(node.modulePath)) {
                    visit(resolveAgencyImportPath(node.modulePath, absPath));
                }
            }
        }
        visit(entrypoint);
        const files = {};
        for (const [filePath, { symbols }] of Object.entries(parsed)) {
            files[filePath] = symbols;
        }
        // Merge re-exports (exportFromStatement) in dependency order with cycle detection.
        const reExportResolved = new Set();
        function resolveReExports(filePath, visiting) {
            if (reExportResolved.has(filePath))
                return;
            if (visiting.includes(filePath)) {
                const chain = [...visiting, filePath].join(" → ");
                throw new Error(`Re-export cycle detected: ${chain}`);
            }
            const entry = parsed[filePath];
            if (!entry) {
                reExportResolved.add(filePath);
                return;
            }
            const newVisiting = [...visiting, filePath];
            for (const node of entry.program.nodes) {
                if (node.type !== "exportFromStatement")
                    continue;
                if (!isAgencyImport(node.modulePath)) {
                    throw new Error(`Re-export source must be an Agency module (std::, pkg::, or .agency path): '${node.modulePath}'`);
                }
                const sourcePath = resolveAgencyImportPath(node.modulePath, filePath);
                resolveReExports(sourcePath, newVisiting);
                mergeExportsFrom(files, filePath, sourcePath, node);
            }
            reExportResolved.add(filePath);
        }
        for (const filePath of Object.keys(parsed)) {
            resolveReExports(filePath, []);
        }
        return new SymbolTable(files);
    }
    has(absPath) {
        return absPath in this.files;
    }
    getFile(absPath) {
        return this.files[absPath];
    }
    filePaths() {
        return Object.keys(this.files);
    }
    /**
     * Walk every file looking for a type alias with the given name. Returns
     * the first match in iteration order. Used to surface imported type
     * definitions for Zod schema generation.
     */
    findTypeAcrossFiles(name) {
        for (const fileSymbols of Object.values(this.files)) {
            const sym = fileSymbols[name];
            if (sym?.kind === "type")
                return sym;
        }
        return undefined;
    }
    /**
     * Resolve every named symbol in an import statement to its source file
     * + SymbolInfo. Skips namespace and default imports. Returns [] for
     * non-Agency imports.
     */
    resolveImport(stmt, fromFile) {
        if (!isAgencyImport(stmt.modulePath))
            return [];
        const file = resolveAgencyImportPath(stmt.modulePath, fromFile);
        const out = [];
        for (const nameType of stmt.importedNames) {
            if (nameType.type !== "namedImport")
                continue;
            for (const originalName of nameType.importedNames) {
                const symbol = this.files[file]?.[originalName];
                if (!symbol)
                    continue;
                out.push({
                    file,
                    originalName,
                    localName: nameType.aliases[originalName] ?? originalName,
                    symbol,
                });
            }
        }
        return out;
    }
    resolveImportedNodes(stmt, fromFile) {
        const file = resolveAgencyImportPath(stmt.agencyFile, fromFile);
        const out = [];
        for (const name of stmt.importedNodes) {
            const symbol = this.files[file]?.[name];
            if (!symbol)
                continue;
            out.push({ file, originalName: name, localName: name, symbol });
        }
        return out;
    }
}
/**
 * Walk the top-level program node list to find `@tag(...)` nodes
 * sitting directly above type aliases, returning a map from alias name
 * to its pending tag list. This mirrors what the TypescriptPreprocessor's
 * `attachTags` does later in the pipeline, but happens early so symbol
 * table consumers (other modules that import this alias) see annotations
 * attached to the TypeSymbol.
 *
 * Pre-pass only, no mutation: keeps SymbolTable.build idempotent and
 * decoupled from the preprocessor.
 */
export function collectTypeAliasTags(program) {
    const byName = {};
    function walk(nodes) {
        let pending = [];
        for (const node of nodes) {
            if (node.type === "tag") {
                pending.push(node);
                continue;
            }
            if (node.type === "typeAlias" && pending.length > 0) {
                byName[node.aliasName] = [...(byName[node.aliasName] ?? []), ...pending];
            }
            pending = [];
            // Recurse into nested bodies that may contain typeAlias + tag siblings.
            if (node.body && Array.isArray(node.body)) {
                walk(node.body);
            }
        }
    }
    walk(program.nodes);
    return byName;
}
/**
 * Classify symbols in a parsed Agency program.
 * Uses walkNodes to find symbols at all nesting levels (e.g. type aliases inside functions).
 */
export function classifySymbols(program) {
    const symbols = {};
    const typeAliasTags = collectTypeAliasTags(program);
    for (const { node } of walkNodes(program.nodes)) {
        switch (node.type) {
            case "graphNode":
                symbols[node.nodeName] = {
                    kind: "node",
                    name: node.nodeName,
                    loc: node.loc,
                    parameters: node.parameters,
                    returnType: node.returnType ?? null,
                    returnTypeValidated: node.returnTypeValidated,
                    exported: !!node.exported,
                    interruptKinds: collectDirectInterruptKinds(node.body),
                };
                break;
            case "function":
                symbols[node.functionName] = {
                    kind: "function",
                    name: node.functionName,
                    loc: node.loc,
                    safe: !!node.safe,
                    exported: !!node.exported,
                    parameters: node.parameters,
                    returnType: node.returnType ?? null,
                    returnTypeValidated: node.returnTypeValidated,
                    interruptKinds: collectDirectInterruptKinds(node.body),
                };
                break;
            case "typeAlias":
                if (RESERVED_TYPE_NAMES.has(node.aliasName)) {
                    throw new Error(`'${node.aliasName}' is a reserved built-in type; cannot be redefined.`);
                }
                symbols[node.aliasName] = {
                    kind: "type",
                    name: node.aliasName,
                    loc: node.loc,
                    exported: !!node.exported,
                    aliasedType: node.aliasedType,
                    ...(node.typeParams ? { typeParams: node.typeParams } : {}),
                    ...(node.valueParams ? { valueParams: node.valueParams } : {}),
                    ...(typeAliasTags[node.aliasName]?.length
                        ? { tags: typeAliasTags[node.aliasName] }
                        : {}),
                };
                break;
            case "assignment":
                if (node.exported && node.static && node.declKind === "const") {
                    symbols[node.variableName] = {
                        kind: "constant",
                        name: node.variableName,
                        loc: node.loc,
                        exported: true,
                    };
                }
                break;
        }
    }
    return symbols;
}
function collectDirectInterruptKinds(body) {
    const kinds = [];
    for (const { node } of walkNodes(body)) {
        if (node.type === "interruptStatement" && !kinds.includes(node.kind)) {
            kinds.push(node.kind);
        }
    }
    return kinds.map((k) => ({ kind: k }));
}
function isExportedSymbol(sym) {
    // Nodes are importable without an explicit `export` keyword (see importResolver
    // — node imports skip the assertExported check). Treat them as exported for
    // re-export purposes so `export { main } from "..."` and `export * from "..."`
    // pick them up regardless of whether the source wrote `export node`.
    if (sym.kind === "node")
        return true;
    return !!sym.exported;
}
function symbolKindLabel(sym) {
    switch (sym.kind) {
        case "function":
            return "Function";
        case "node":
            return "Node";
        case "type":
            return "Type";
        // The "constant" branch below is defensive — the only current caller
        // (the namedExport "not exported" error path) cannot reach it:
        // ConstantSymbol is only ever added when `exported && static && const`.
        case "constant":
            return "Constant";
    }
}
/**
 * Merge symbols flowing through one `exportFromStatement` from `sourcePath`
 * into the re-exporter's `FileSymbols`. Hard errors on missing symbols,
 * non-exported sources, and collisions.
 */
export function mergeExportsFrom(files, reExporterPath, sourcePath, stmt) {
    const sourceSymbols = files[sourcePath];
    if (!sourceSymbols) {
        throw new Error(`Re-export source '${stmt.modulePath}' could not be resolved`);
    }
    const targetSymbols = files[reExporterPath] ?? {};
    files[reExporterPath] = targetSymbols;
    if (stmt.body.kind === "starExport") {
        for (const [name, sym] of Object.entries(sourceSymbols)) {
            if (!isExportedSymbol(sym))
                continue;
            mergeOne(targetSymbols, name, name, sym, false, sourcePath, stmt);
        }
        return;
    }
    // namedExport
    for (const originalName of stmt.body.names) {
        const sym = sourceSymbols[originalName];
        if (!sym) {
            throw new Error(`Symbol '${originalName}' is not defined in '${stmt.modulePath}'`);
        }
        if (!isExportedSymbol(sym)) {
            throw new Error(`${symbolKindLabel(sym)} '${originalName}' in '${stmt.modulePath}' is not exported. Add the 'export' keyword to its definition.`);
        }
        const localName = stmt.body.aliases[originalName] ?? originalName;
        if (sym.kind === "node" && localName !== originalName) {
            throw new Error(`Node '${originalName}' from '${stmt.modulePath}' cannot be re-exported under a different name. ` +
                `Re-exported nodes preserve their original name because the source graph is merged wholesale.`);
        }
        const isSafe = stmt.body.safeNames.includes(originalName);
        if (sym.kind === "node" && isSafe) {
            throw new Error(`The 'safe' modifier cannot be applied to node '${originalName}' from '${stmt.modulePath}'. ` +
                `'safe' is only meaningful for functions; nodes do not carry a safe flag.`);
        }
        mergeOne(targetSymbols, localName, originalName, sym, isSafe, sourcePath, stmt);
    }
}
function mergeOne(targetSymbols, localName, originalName, sourceSym, forceSafe, sourcePath, stmt) {
    const existing = targetSymbols[localName];
    if (existing) {
        if (!("reExportedFrom" in existing) || !existing.reExportedFrom) {
            const at = existing.loc ? ` at line ${existing.loc.line + 1}` : "";
            throw new Error(`Re-exported name '${localName}' collides with local declaration${at}`);
        }
        const sameSource = existing.reExportedFrom.sourceFile === sourcePath &&
            existing.reExportedFrom.originalName === originalName;
        if (!sameSource) {
            throw new Error(`Name '${localName}' is re-exported from both '${existing.reExportedFrom.sourceFile}' and '${sourcePath}'. Disambiguate with explicit 'export { ${localName} as ... } from ...'.`);
        }
        return; // idempotent re-merge
    }
    // Build the merged entry. We copy the source's SymbolInfo fields and
    // override name, loc, exported, and (for functions) safe.
    const base = {
        name: localName,
        loc: stmt.loc,
        reExportedFrom: { sourceFile: sourcePath, originalName },
    };
    let copied;
    switch (sourceSym.kind) {
        case "function":
            copied = {
                ...sourceSym,
                ...base,
                exported: true,
                safe: forceSafe ? true : sourceSym.safe,
            };
            break;
        case "node":
            copied = { ...sourceSym, ...base, exported: true };
            break;
        case "type":
            copied = { ...sourceSym, ...base, exported: true };
            break;
        case "constant":
            copied = { ...sourceSym, ...base, exported: true };
            break;
    }
    targetSymbols[localName] = copied;
}
