import { color } from "../utils/termcolors.js";
import { GLOBAL_SCOPE_KEY, ScopedTypeAliases, buildCompilationUnit, } from "../compilationUnit.js";
import { validateTypeReferences } from "./validate.js";
import { applySuppressions, parseSuppressions } from "./suppression.js";
import { inferReturnTypes } from "./inference.js";
import { buildScopes } from "./scopes.js";
import { checkScopes } from "./checker.js";
import { isAssignable as _isAssignable } from "./assignability.js";
import { inferReturnTypeFor } from "./inference.js";
import { effectiveReturnType } from "./validation.js";
import { analyzeInterruptsFromScopes, checkUnhandledInterruptWarnings, checkCallbackBodyInterrupts, checkHandlerBodyInterrupts, } from "./interruptAnalysis.js";
import { checkUndefinedFunctions } from "./undefinedFunctionDiagnostic.js";
import { checkUndefinedVariables } from "./undefinedVariableDiagnostic.js";
import { RESERVED_FUNCTION_NAMES } from "./resolveCall.js";
import { validateStaticInit } from "./validateStaticInit.js";
import { walkNodes } from "../utils/node.js";
/** Type-alias names that resolve to built-in types. */
const RESERVED_TYPE_NAMES = new Set(["Result", "Success", "Failure"]);
export class TypeChecker {
    program;
    config;
    scopedTypeAliases = new ScopedTypeAliases();
    currentScopeKey = GLOBAL_SCOPE_KEY;
    functionDefs = {};
    nodeDefs = {};
    importedFunctions = {};
    jsImportedNames = {};
    interruptKindsByFunction = {};
    errors = [];
    inferredReturnTypes = {};
    inferringReturnType = new Set();
    sourceText;
    constructor(program, config = {}, info) {
        this.program = program;
        this.config = config;
        const resolved = info ?? buildCompilationUnit(program);
        this.scopedTypeAliases = resolved.typeAliases.clone();
        this.functionDefs = { ...resolved.functionDefinitions };
        this.nodeDefs = Object.fromEntries(resolved.graphNodes.map((n) => [n.nodeName, n]));
        this.importedFunctions = { ...resolved.importedFunctions };
        this.jsImportedNames = { ...resolved.jsImportedNames };
        this.interruptKindsByFunction = resolved.interruptKindsByFunction ?? {};
        this.sourceText = resolved.sourceText;
    }
    get typeAliases() {
        return this.scopedTypeAliases.visibleIn(this.currentScopeKey);
    }
    withScope(key, fn) {
        const prev = this.currentScopeKey;
        this.currentScopeKey = key;
        try {
            return fn();
        }
        finally {
            this.currentScopeKey = prev;
        }
    }
    makeContext() {
        const ctx = {
            programNodes: this.program.nodes,
            scopedTypeAliases: this.scopedTypeAliases,
            currentScopeKey: this.currentScopeKey,
            functionDefs: this.functionDefs,
            nodeDefs: this.nodeDefs,
            importedFunctions: this.importedFunctions,
            jsImportedNames: this.jsImportedNames,
            interruptKindsByFunction: this.interruptKindsByFunction,
            errors: this.errors,
            inferredReturnTypes: this.inferredReturnTypes,
            inferringReturnType: this.inferringReturnType,
            config: this.config,
            getTypeAliases: () => this.typeAliases,
            withScope: (key, fn) => this.withScope(key, fn),
            inferReturnTypeFor: (name, def) => inferReturnTypeFor(name, def, ctx),
        };
        return ctx;
    }
    check() {
        this.errors = [];
        const ctx = this.makeContext();
        // 1. Validate type alias references
        for (const [sk, scopeAliases] of this.scopedTypeAliases.scopes()) {
            this.withScope(sk, () => {
                for (const [name, entry] of Object.entries(scopeAliases)) {
                    // Type parameters declared on a generic alias are in scope inside
                    // its own body. Add them to the visible alias set as opaque entries
                    // so references like `T` don't get flagged as undefined.
                    const localAliases = {
                        ...this.typeAliases,
                    };
                    if (entry.typeParams) {
                        for (const p of entry.typeParams) {
                            localAliases[p.name] = {
                                body: { type: "typeAliasVariable", aliasName: p.name },
                            };
                        }
                        // Defaults must come after all required parameters.
                        let seenDefault = false;
                        for (const p of entry.typeParams) {
                            if (p.default) {
                                seenDefault = true;
                            }
                            else if (seenDefault) {
                                this.errors.push({
                                    message: `Type parameter '${p.name}' (no default) must come before parameters that have defaults in '${name}'.`,
                                });
                            }
                        }
                    }
                    validateTypeReferences(entry.body, name, localAliases, this.errors);
                }
            });
        }
        // 1b. Warn on local definitions that shadow imported functions/nodes.
        // functionDefs and nodeDefs are mutually exclusive by construction
        // (a name is parsed as one or the other, never both).
        const shadowWarning = (name, loc) => this.errors.push({
            message: `'${name}' shadows an imported function.`,
            severity: "warning",
            loc,
        });
        for (const [name, def] of Object.entries(this.functionDefs)) {
            if (this.importedFunctions[name])
                shadowWarning(name, def.loc);
        }
        for (const [name, def] of Object.entries(this.nodeDefs)) {
            if (this.importedFunctions[name])
                shadowWarning(name, def.loc);
        }
        // 1c. Reserve names baked into the language. The synth pipeline relies
        // on `success(x)` and `failure(msg)` parameterizing ResultType, and on
        // `Result` being the built-in type. Allowing user definitions of these
        // names would silently change semantics.
        const reservedFn = (name, loc) => this.errors.push({
            message: `'${name}' is a reserved built-in; cannot be redefined.`,
            loc,
        });
        for (const [name, def] of Object.entries(this.functionDefs)) {
            if (RESERVED_FUNCTION_NAMES.has(name))
                reservedFn(name, def.loc);
        }
        for (const [name, def] of Object.entries(this.nodeDefs)) {
            if (RESERVED_FUNCTION_NAMES.has(name))
                reservedFn(name, def.loc);
        }
        for (const [, aliases] of this.scopedTypeAliases.scopes()) {
            for (const name of Object.keys(aliases)) {
                if (RESERVED_TYPE_NAMES.has(name)) {
                    this.errors.push({
                        message: `'${name}' is a reserved built-in type; cannot be redefined.`,
                    });
                }
            }
        }
        // 1d. Reserved names cannot be `let`/`const` / `static const` declared
        // either. The variable would be unusable as a function (e.g. `schema(X)`
        // always parses as a SchemaExpression regardless of scope), and shadowing
        // primitives like `success`/`failure` silently changes semantics.
        // Walk every assignment in the program — top-level and inside function /
        // graphNode bodies — and check the variable name. Only fires on actual
        // declarations (where `declKind` is set), not reassignments.
        for (const { node } of walkNodes(this.program.nodes)) {
            if (node.type !== "assignment")
                continue;
            if (!node.declKind)
                continue;
            if (RESERVED_FUNCTION_NAMES.has(node.variableName)) {
                this.errors.push({
                    message: `'${node.variableName}' is a reserved built-in; cannot be redefined.`,
                    loc: node.loc,
                });
            }
        }
        // Validated params let a function short-circuit with a failure before
        // the body runs. An explicit non-Result return type contradicts that.
        // (Unannotated returns are auto-wrapped during inference instead.)
        const checkValidatedParamReturn = (name, def) => {
            if (!def.parameters.some((p) => p.validated))
                return;
            if (!def.returnType)
                return;
            const effective = effectiveReturnType(def);
            if (effective && effective.type !== "resultType") {
                const kind = def.type === "function" ? "Function" : "Node";
                this.errors.push({
                    message: `${kind} '${name}' has validated parameters but its return type is not a Result type. Validated parameters can short-circuit with a failure, so the return type must be 'Result<...>'.`,
                    loc: def.loc,
                });
            }
        };
        for (const [name, def] of Object.entries(this.functionDefs)) {
            checkValidatedParamReturn(name, def);
        }
        for (const [name, def] of Object.entries(this.nodeDefs)) {
            checkValidatedParamReturn(name, def);
        }
        // 1f. Doc strings must not interpolate function/node parameters.
        // The description is built when the tool object is constructed at module
        // load time — long before the function is ever called — so parameter
        // values are simply not bound yet. Catch the obvious case here:
        // `${param}` as a top-level interpolation expression.
        const checkDocStringParams = (def) => {
            if (!def.docString)
                return;
            const paramNames = def.parameters.map((p) => p.name);
            for (const seg of def.docString.segments) {
                if (seg.type === "interpolation" &&
                    seg.expression.type === "variableName" &&
                    paramNames.includes(seg.expression.value)) {
                    this.errors.push({
                        message: `Cannot interpolate parameter '${seg.expression.value}' in doc string — parameter values are not known when the tool description is sent to the LLM. Use a global variable instead.`,
                        loc: seg.loc ?? def.docString.loc,
                    });
                }
            }
        };
        for (const def of Object.values(this.functionDefs)) {
            checkDocStringParams(def);
        }
        for (const def of Object.values(this.nodeDefs)) {
            checkDocStringParams(def);
        }
        // 2. Infer return types
        inferReturnTypes(ctx);
        // 3. Build scopes (collects variable types and checks assignments)
        const scopes = buildScopes(ctx);
        // 4. Check function calls, return types, and expressions
        checkScopes(scopes, ctx);
        // 5. Analyze interrupts using functionRefType
        const interruptKindsByFunction = analyzeInterruptsFromScopes(scopes, ctx);
        // 6. Check for unhandled interrupt warnings (uses transitive results)
        checkUnhandledInterruptWarnings(scopes, interruptKindsByFunction, ctx);
        // 6a. Reject `interrupt` inside any callback body. Callbacks fire as
        // side effects; their body cannot pause execution.
        checkCallbackBodyInterrupts(scopes, interruptKindsByFunction, ctx);
        // 6b. Reject handlers whose body may itself raise an interrupt — that
        // re-enters the handler chain and recurses (see HandlerRecursionError).
        checkHandlerBodyInterrupts(scopes, interruptKindsByFunction, ctx);
        // 7. Check for undefined function calls (config-controlled severity).
        checkUndefinedFunctions(scopes, ctx);
        // 8. Check for undefined variable references (config-controlled severity).
        checkUndefinedVariables(scopes, ctx);
        // 9. Validate static initializers + `static <bare>` statements.
        // Direct-only checks against the Phase A surface — per-run-only
        // primitive calls (e.g. `llm()`, `interrupt()`) and obvious
        // post-declaration mutations of statics. Cross-module global
        // reads from statics are caught earlier by `compileClosure`'s
        // `rejectStaticReferencesGlobal`, which has access to the full
        // import closure.
        validateStaticInit(this.program, this.errors);
        return {
            errors: this.applySuppressions(this.deduplicateErrors()),
            scopes,
            interruptKindsByFunction,
        };
    }
    applySuppressions(errors) {
        if (this.sourceText === undefined)
            return errors;
        return applySuppressions(errors, parseSuppressions(this.sourceText));
    }
    deduplicateErrors() {
        const seen = new Set();
        return this.errors.filter((err) => {
            const key = `${err.message}:${err.loc?.start ?? -1}`;
            if (seen.has(key))
                return false;
            seen.add(key);
            return true;
        });
    }
    /** Delegating method preserved for test compatibility. */
    isAssignable(source, target) {
        return _isAssignable(source, target, this.typeAliases);
    }
}
export function typeCheck(program, config = {}, info) {
    const checker = new TypeChecker(program, config, info);
    return checker.check();
}
export function formatErrors(errors, errorType = "error") {
    return errors
        .map((err) => {
        const kind = err.severity ?? errorType;
        const colorFunc = kind === "warning" ? color.yellow : color.red;
        return `${colorFunc(kind)}: ${err.message}`;
    })
        .join("\n");
}
