/**
 * NestJS analysis tools — B1-B5 + C (nest_audit meta-orchestrator).
 * Discoverable via discover_tools(query="nestjs").
 */
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { getCodeIndex } from "./index-tools.js";
import { extractNestConventions } from "./project-tools.js";
// ---------------------------------------------------------------------------
// B5: nest_lifecycle_map — types + implementation
// ---------------------------------------------------------------------------
const LIFECYCLE_HOOKS = new Set([
    "onModuleInit",
    "onModuleDestroy",
    "onApplicationBootstrap",
    "onApplicationShutdown",
    "beforeApplicationShutdown",
]);
export async function nestLifecycleMap(repo) {
    const index = await getCodeIndex(repo);
    if (!index) {
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    }
    const hooks = [];
    const errors = [];
    for (const sym of index.symbols) {
        if (!LIFECYCLE_HOOKS.has(sym.name))
            continue;
        if (sym.kind !== "method" && sym.kind !== "function")
            continue;
        // Determine parent class name from source or file context
        let className = "Unknown";
        const source = sym.source ?? "";
        // Try to find the enclosing class via parent_id (if available)
        const symAny = sym;
        if (symAny.parent_id) {
            const parentSym = index.symbols.find((s) => s.id === symAny.parent_id);
            if (parentSym)
                className = parentSym.name;
        }
        // Fallback: look for class name in source
        if (className === "Unknown") {
            // Check if there's a class symbol in the same file that contains this method
            const classSym = index.symbols.find((s) => s.file === sym.file && s.kind === "class" && s.start_line <= sym.start_line && s.end_line >= sym.end_line);
            if (classSym)
                className = classSym.name;
        }
        const isAsync = /async\s/.test(source.slice(0, 50));
        hooks.push({
            class_name: className,
            file: sym.file,
            hook: sym.name,
            is_async: isAsync,
        });
    }
    return { hooks, ...(errors.length > 0 ? { errors } : {}) };
}
export async function nestModuleGraph(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    const maxModules = options?.max_modules ?? 200;
    const moduleFiles = index.files.filter((f) => f.path.endsWith(".module.ts") || f.path.endsWith(".module.js"));
    const modules = [];
    const edges = [];
    const errors = [];
    let truncated = false;
    // Parse each module file
    for (const file of moduleFiles) {
        if (modules.length >= maxModules) {
            truncated = true;
            break;
        }
        let source;
        try {
            source = await readFile(join(index.root, file.path), "utf-8");
        }
        catch (err) {
            errors.push({ file: file.path, reason: `readFile failed: ${err instanceof Error ? err.message : String(err)}` });
            continue;
        }
        const conv = extractNestConventions(source, file.path);
        // Find the module class name from source
        const classMatch = /export\s+class\s+(\w+Module)\b/.exec(source);
        const moduleName = classMatch?.[1] ?? file.path.replace(/.*\//, "").replace(/\.module\.[jt]sx?$/, "Module");
        // Check for @Global()
        const isGlobal = /@Global\s*\(\s*\)/.test(source) || conv.modules.some((m) => m.is_global && m.name === moduleName);
        // Extract exports array
        const exportNames = [];
        const exportsMatch = /exports:\s*\[([^\]]*)\]/s.exec(source);
        if (exportsMatch) {
            const inner = exportsMatch[1];
            for (const m of inner.matchAll(/(\w+)/g)) {
                exportNames.push(m[1]);
            }
        }
        // Extract provider names
        const providerNames = [];
        const providersMatch = /providers:\s*\[([^\]]*)\]/s.exec(source);
        if (providersMatch) {
            const inner = providersMatch[1];
            for (const m of inner.matchAll(/(?:useClass:\s*)?(\w+(?:Service|Provider|Guard|Interceptor|Pipe|Filter|Factory|Strategy))\b/g)) {
                providerNames.push(m[1]);
            }
        }
        modules.push({
            name: moduleName,
            file: file.path,
            is_global: isGlobal,
            imports: conv.modules.map((m) => m.name),
            exports: exportNames,
            providers: providerNames,
            controllers: conv.controllers,
        });
    }
    // Build edges: module → imported module
    const moduleNames = new Set(modules.map((m) => m.name));
    for (const mod of modules) {
        for (const imp of mod.imports) {
            if (moduleNames.has(imp)) {
                edges.push({ from: mod.name, to: imp });
            }
        }
    }
    // Detect circular deps via DFS
    const circular_deps = detectCycles(modules.map((m) => m.name), edges);
    return {
        modules,
        edges,
        circular_deps,
        ...(errors.length > 0 ? { errors } : {}),
        ...(truncated ? { truncated } : {}),
    };
}
/** DFS cycle detection on directed graph. Exported for reuse in nest-ext-tools.ts (G12). */
export function detectCycles(nodes, edges) {
    const adj = new Map();
    for (const n of nodes)
        adj.set(n, []);
    for (const e of edges)
        adj.get(e.from)?.push(e.to);
    const cycles = [];
    const visited = new Set();
    const inStack = new Set();
    const path = [];
    function dfs(node) {
        if (inStack.has(node)) {
            const cycleStart = path.indexOf(node);
            if (cycleStart >= 0)
                cycles.push(path.slice(cycleStart).concat(node));
            return;
        }
        if (visited.has(node))
            return;
        visited.add(node);
        inStack.add(node);
        path.push(node);
        for (const next of adj.get(node) ?? [])
            dfs(next);
        path.pop();
        inStack.delete(node);
    }
    for (const n of nodes)
        dfs(n);
    return cycles;
}
// ---------------------------------------------------------------------------
// Shared helpers: constructor injection parsing (CQ14)
// ---------------------------------------------------------------------------
/** Extract constructor parameter body using paren counting (handles decorated params) */
function extractConstructorBody(source) {
    const ctorIdx = source.indexOf("constructor(");
    if (ctorIdx === -1)
        return null;
    const start = ctorIdx + "constructor(".length;
    let depth = 1;
    let i = start;
    while (i < source.length && depth > 0) {
        if (source[i] === "(")
            depth++;
        else if (source[i] === ")")
            depth--;
        i++;
    }
    return depth === 0 ? source.slice(start, i - 1) : null;
}
/** Extract injected type names from a constructor body string */
function extractInjectedTypes(ctorBody) {
    const types = [];
    // R-6 fix: separate depth counters for () and <> to avoid cross-corruption
    const params = [];
    let parenDepth = 0;
    let angleDepth = 0;
    let current = "";
    for (const ch of ctorBody) {
        if (ch === "(")
            parenDepth++;
        else if (ch === ")")
            parenDepth--;
        else if (ch === "<")
            angleDepth++;
        else if (ch === ">")
            angleDepth--;
        if (ch === "," && parenDepth === 0 && angleDepth === 0) {
            params.push(current.trim());
            current = "";
        }
        else {
            current += ch;
        }
    }
    if (current.trim())
        params.push(current.trim());
    for (const param of params) {
        // Extract type after the last `:` — handles decorators before the param name
        const colonIdx = param.lastIndexOf(":");
        if (colonIdx === -1)
            continue;
        const typeStr = param.slice(colonIdx + 1).trim();
        // G3: Detect container generics like Repository<User>, Model<Comment>, Repo<X>.
        // For container types, unwrap and return the inner type parameter so consumers
        // can distinguish Repository<Article> from Repository<Comment>.
        const genericMatch = typeStr.match(/^(\w+)<\s*(\w+)\s*(?:,[^>]*)?>/);
        if (genericMatch) {
            const outer = genericMatch[1];
            const inner = genericMatch[2];
            if (/^(Repository|Repo|Model|Collection|Array|Set|Map|List|Observable|Promise|Ref|Token|Provider|Class)$/.test(outer)) {
                types.push(inner);
                continue;
            }
        }
        // Non-container type — return the outer name as before
        const typeMatch = typeStr.match(/^(\w+)/);
        if (typeMatch)
            types.push(typeMatch[1]);
    }
    return types;
}
/** Parse @Injectable() classes from source */
function parseInjectableClasses(source) {
    const results = [];
    const re = /@Injectable\s*\(([^)]*)\)\s*(?:export\s+)?class\s+(\w+)/g;
    let m;
    while ((m = re.exec(source)) !== null) {
        const args = m[1] ?? "";
        const name = m[2];
        const scopeMatch = args.match(/scope:\s*Scope\.(\w+)/);
        results.push({ name, ...(scopeMatch ? { scope: scopeMatch[1] } : {}) });
    }
    return results;
}
export async function nestDIGraph(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    const maxNodes = options?.max_nodes ?? 200;
    const focus = options?.focus;
    const nodes = [];
    const edges = [];
    const errors = [];
    let truncated = false;
    // Scan files for @Injectable classes
    const candidateFiles = index.files.filter((f) => {
        if (focus && !f.path.includes(focus))
            return false;
        return f.path.endsWith(".ts") || f.path.endsWith(".js");
    });
    for (const file of candidateFiles) {
        if (nodes.length >= maxNodes) {
            truncated = true;
            break;
        }
        let source;
        try {
            source = await readFile(join(index.root, file.path), "utf-8");
        }
        catch (err) {
            errors.push({ file: file.path, reason: `readFile failed: ${err instanceof Error ? err.message : String(err)}` });
            continue;
        }
        const injectables = parseInjectableClasses(source);
        for (const inj of injectables) {
            if (nodes.length >= maxNodes) {
                truncated = true;
                break;
            }
            nodes.push({
                name: inj.name,
                file: file.path,
                kind: "provider",
                ...(inj.scope ? { scope: inj.scope } : {}),
            });
            // Extract constructor injection
            // Find the class body for this specific injectable
            const classIdx = source.indexOf(`class ${inj.name}`);
            if (classIdx === -1)
                continue;
            const classSource = source.slice(classIdx);
            const ctorBody = extractConstructorBody(classSource);
            if (!ctorBody)
                continue;
            const injectedTypes = extractInjectedTypes(ctorBody);
            for (const type of injectedTypes) {
                edges.push({ from: inj.name, to: type, via: "inject" });
            }
        }
    }
    // Detect cycles
    const nodeNames = nodes.map((n) => n.name);
    const cycles = detectCycles(nodeNames, edges.map((e) => ({ from: e.from, to: e.to })));
    return {
        nodes,
        edges,
        cycles,
        cross_module_warnings: [], // TODO: implement cross-module warnings in future task
        ...(errors.length > 0 ? { errors } : {}),
        ...(truncated ? { truncated } : {}),
    };
}
// ---------------------------------------------------------------------------
// Shared helpers: guard/interceptor/pipe parsing (CQ14)
// ---------------------------------------------------------------------------
/** Parse @UseGuards(...) from source, returns guard class names.
 * R-8 fix: handles both class-ref form @UseGuards(AuthGuard) and
 * instantiation form @UseGuards(new ThrottlerGuard()). */
function parseUseGuards(source) {
    const results = [];
    // Match the full @UseGuards(...) arg including nested parens for `new Guard()`
    const re = /@UseGuards\s*\(\s*([^)]*(?:\([^)]*\)[^)]*)*)\s*\)/g;
    let m;
    while ((m = re.exec(source)) !== null) {
        const args = m[1];
        // Extract class names — both bare refs and `new ClassName(...)` instantiations
        for (const part of args.split(",")) {
            const trimmed = part.trim();
            if (!trimmed)
                continue;
            const newMatch = /new\s+(\w+)/.exec(trimmed);
            if (newMatch) {
                results.push(newMatch[1]);
                continue;
            }
            const bareMatch = /^(\w+)$/.exec(trimmed);
            if (bareMatch)
                results.push(bareMatch[1]);
        }
    }
    return results;
}
/**
 * Factory for @Use*() decorator parsers. parseUseGuards is separate because
 * it needs to handle `new Guard()` instantiation form (R-8 fix).
 */
function makeUseDecoratorParser(decoratorName) {
    const re = new RegExp(`@${decoratorName}\\s*\\(\\s*([\\w\\s,]+)\\s*\\)`, "g");
    return (source) => {
        const results = [];
        let m;
        re.lastIndex = 0;
        while ((m = re.exec(source)) !== null) {
            for (const name of m[1].split(",").map((s) => s.trim()).filter(Boolean)) {
                results.push(name);
            }
        }
        return results;
    };
}
const parseUseInterceptors = makeUseDecoratorParser("UseInterceptors");
const parseUsePipes = makeUseDecoratorParser("UsePipes");
const parseUseFilters = makeUseDecoratorParser("UseFilters");
/** Built-in NestJS decorators that should NOT be reported as custom metadata */
const BUILTIN_DECORATORS = new Set([
    "Get", "Post", "Put", "Delete", "Patch", "Options", "Head", "All",
    "Controller", "Injectable", "Module", "Global",
    "UseGuards", "UseInterceptors", "UsePipes", "UseFilters",
    "Param", "Body", "Query", "Headers", "Req", "Res", "Next", "Ip", "Session", "HostParam",
    "Version", "ApiOperation", "ApiBearerAuth", "ApiTags", "ApiResponse", "ApiProperty", "ApiParam", "ApiBody", "ApiQuery",
    "HealthCheck", "HealthIndicator",
    "Catch", "Optional", "Inject", "InjectRepository", "InjectModel",
    "Resolver", "Query" /*gql*/, "Mutation", "Subscription", "Args", "ResolveField",
    "WebSocketGateway", "SubscribeMessage", "MessageBody", "ConnectedSocket",
    "MessagePattern", "EventPattern", "Payload", "Ctx",
    "Cron", "Interval", "Timeout", "OnEvent",
    "Entity", "Column", "PrimaryGeneratedColumn", "PrimaryColumn", "OneToMany", "ManyToOne", "OneToOne", "ManyToMany",
    "JoinColumn", "JoinTable", "CreateDateColumn", "UpdateDateColumn", "DeleteDateColumn", "Index", "Unique",
]);
/**
 * G4: Parse custom decorators on methods (e.g. @Roles('admin'), @Public(), @CurrentUser()).
 * Returns decorator name + raw argument string (may be empty).
 * Excludes built-in NestJS decorators (BUILTIN_DECORATORS set).
 */
function parseCustomDecorators(source) {
    const results = [];
    // Match @PascalCase(args) — capture name and argument text
    const re = /@([A-Z]\w*)\s*\(([^)]*)\)/g;
    let m;
    while ((m = re.exec(source)) !== null) {
        const name = m[1];
        if (BUILTIN_DECORATORS.has(name))
            continue;
        // Strip quotes from simple string args for readable output
        const args = m[2].trim().replace(/^['"`]|['"`]$/g, "");
        results.push({ name, args });
    }
    return results;
}
/** G1: glob-like matching for NestJS middleware forRoutes against resolved route paths */
function matchMiddlewareRoute(mwRoute, routePath, routeMethod) {
    // Method filter
    if (mwRoute.method && mwRoute.method !== "ALL" && mwRoute.method !== routeMethod)
        return false;
    // Normalise both sides to leading-slash form for comparison
    const norm = (p) => "/" + p.replace(/^\/+|\/+$/g, "");
    const mwNorm = norm(mwRoute.path);
    const routeNorm = norm(routePath);
    // '*' matches everything
    if (mwNorm === "/*" || mwRoute.path === "*")
        return true;
    // Convert NestJS glob (users/*) into anchored regex
    // escape regex metachars except '*', then replace '*' with '.*'
    const escaped = mwNorm.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
    const re = new RegExp(`^${escaped}($|/)`);
    return re.test(routeNorm);
}
export async function nestGuardChain(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    const maxRoutes = options?.max_routes ?? 300;
    const routes = [];
    const errors = [];
    let truncated = false;
    // 1. Collect global guards/interceptors/pipes from module files + G1 middleware chains
    const globalChain = [];
    const middlewareEntries = [];
    for (const file of index.files) {
        if (!file.path.endsWith(".module.ts") && !file.path.endsWith(".module.js"))
            continue;
        let source;
        try {
            source = await readFile(join(index.root, file.path), "utf-8");
        }
        catch (err) {
            errors.push({ file: file.path, reason: `readFile failed: ${err instanceof Error ? err.message : String(err)}` });
            continue;
        }
        const conv = extractNestConventions(source, file.path);
        for (const g of conv.global_guards)
            globalChain.push({ layer: "global", type: "guard", name: g.name, file: g.file });
        for (const f of conv.global_filters)
            globalChain.push({ layer: "global", type: "filter", name: f.name, file: f.file });
        for (const p of conv.global_pipes)
            globalChain.push({ layer: "global", type: "pipe", name: p.name, file: p.file });
        for (const i of conv.global_interceptors)
            globalChain.push({ layer: "global", type: "interceptor", name: i.name, file: i.file });
        // G1: defensive default since middleware_chains is optional on older profiles
        for (const mw of conv.middleware_chains ?? []) {
            middlewareEntries.push({ middleware: mw.middleware, routes: mw.routes, file: mw.file });
        }
    }
    // 2. Scan controller files
    const controllerFiles = index.files.filter((f) => f.path.endsWith(".controller.ts") || f.path.endsWith(".controller.js"));
    const methods = ["Get", "Post", "Put", "Delete", "Patch", "All", "Head", "Options"];
    for (const file of controllerFiles) {
        if (routes.length >= maxRoutes) {
            truncated = true;
            break;
        }
        let source;
        try {
            source = await readFile(join(index.root, file.path), "utf-8");
        }
        catch (err) {
            errors.push({ file: file.path, reason: `readFile failed: ${err instanceof Error ? err.message : String(err)}` });
            continue;
        }
        // Controller-level info
        const ctrlMatch = /@Controller\s*\(\s*['"`]([^'"`]*)['"`]/.exec(source);
        const ctrlPrefix = ctrlMatch?.[1] ?? "";
        const ctrlClassMatch = /class\s+(\w+)/.exec(source);
        const ctrlClass = ctrlClassMatch?.[1] ?? "UnknownController";
        // Controller-level decorators (before class body — find source before first method)
        const classIdx = source.indexOf(`class ${ctrlClass}`);
        const ctrlHeader = classIdx >= 0 ? source.slice(0, classIdx) : "";
        const ctrlGuards = parseUseGuards(ctrlHeader).map((n) => ({ layer: "controller", type: "guard", name: n }));
        const ctrlInterceptors = parseUseInterceptors(ctrlHeader).map((n) => ({ layer: "controller", type: "interceptor", name: n }));
        const ctrlPipes = parseUsePipes(ctrlHeader).map((n) => ({ layer: "controller", type: "pipe", name: n }));
        const ctrlFilters = parseUseFilters(ctrlHeader).map((n) => ({ layer: "controller", type: "filter", name: n }));
        const ctrlLevelChain = [...ctrlGuards, ...ctrlInterceptors, ...ctrlPipes, ...ctrlFilters];
        // Collect ALL method decorator positions first to bound lookback correctly
        const allMethodPositions = [];
        for (const method of methods) {
            const reStr = new RegExp(`@${method}\\s*\\(\\s*['"\`]([^'"\`]*)['"\`]\\s*\\)`, "g");
            const reEmpty = new RegExp(`@${method}\\s*\\(\\s*\\)`, "g");
            let m;
            while ((m = reStr.exec(source)) !== null)
                allMethodPositions.push({ method, path: m[1] ?? "", pos: m.index });
            while ((m = reEmpty.exec(source)) !== null)
                allMethodPositions.push({ method, path: "", pos: m.index });
        }
        allMethodPositions.sort((a, b) => a.pos - b.pos);
        for (let idx = 0; idx < allMethodPositions.length; idx++) {
            if (routes.length >= maxRoutes) {
                truncated = true;
                break;
            }
            const mm = allMethodPositions[idx];
            // Normalise: collapse slashes, trim trailing slash (except for root "/")
            const rawPath = `/${ctrlPrefix}/${mm.path}`.replace(/\/+/g, "/");
            const fullPath = rawPath.length > 1 ? rawPath.replace(/\/$/, "") : rawPath;
            if (options?.path && fullPath !== options.path)
                continue;
            // Lookback window: from previous method decorator (or class start) to current
            const prevEnd = idx > 0 ? allMethodPositions[idx - 1].pos + 10 : (classIdx >= 0 ? classIdx : 0);
            const methodCtx = source.slice(Math.max(prevEnd, 0), mm.pos);
            const methodGuards = parseUseGuards(methodCtx).map((n) => ({ layer: "method", type: "guard", name: n }));
            const methodInterceptors = parseUseInterceptors(methodCtx).map((n) => ({ layer: "method", type: "interceptor", name: n }));
            const methodPipes = parseUsePipes(methodCtx).map((n) => ({ layer: "method", type: "pipe", name: n }));
            const methodFilters = parseUseFilters(methodCtx).map((n) => ({ layer: "method", type: "filter", name: n }));
            // G4: custom decorators (e.g. @Roles('admin'), @Public()) — method-level only
            const methodMetadata = parseCustomDecorators(methodCtx).map((d) => ({
                layer: "method",
                type: "metadata",
                name: d.name,
                ...(d.args ? { args: d.args } : {}),
            }));
            // G1: match middleware entries against this route
            const middlewareChain = [];
            for (const mw of middlewareEntries) {
                for (const mwRoute of mw.routes) {
                    if (matchMiddlewareRoute(mwRoute, fullPath, mm.method.toUpperCase())) {
                        middlewareChain.push({ layer: "middleware", type: "guard", name: mw.middleware, file: mw.file });
                        break;
                    }
                }
            }
            routes.push({
                route: fullPath,
                method: mm.method.toUpperCase(),
                controller: ctrlClass,
                file: file.path,
                chain: [...globalChain, ...middlewareChain, ...ctrlLevelChain, ...methodGuards, ...methodInterceptors, ...methodPipes, ...methodFilters, ...methodMetadata],
            });
        }
    }
    return {
        routes,
        ...(errors.length > 0 ? { errors } : {}),
        ...(truncated ? { truncated } : {}),
    };
}
export async function nestRouteInventory(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    const maxRoutes = options?.max_routes ?? 500;
    const errors = [];
    let truncated = false;
    // Use findNestJSHandlers with wildcard path to get ALL routes
    // We pass "/**" as a wildcard — but findNestJSHandlers uses matchPath which
    // doesn't support wildcards. Instead, we scan controllers ourselves.
    const controllerFiles = index.files.filter((f) => f.path.endsWith(".controller.ts") || f.path.endsWith(".controller.js"));
    const routes = [];
    const methods = ["Get", "Post", "Put", "Delete", "Patch", "All", "Head", "Options"];
    for (const file of controllerFiles) {
        if (routes.length >= maxRoutes) {
            truncated = true;
            break;
        }
        let source;
        try {
            source = await readFile(join(index.root, file.path), "utf-8");
        }
        catch (err) {
            errors.push({ file: file.path, reason: `readFile failed: ${err instanceof Error ? err.message : String(err)}` });
            continue;
        }
        const ctrlMatchStr = /@Controller\s*\(\s*['"`]([^'"`]*)['"`]/.exec(source);
        // G9: also try @Controller({ path: '...', version: '...' }) object form
        const ctrlObjMatch = !ctrlMatchStr ? /@Controller\s*\(\s*\{[^}]*path:\s*['"`]([^'"`]*)['"`]/.exec(source) : null;
        const ctrlMatchEmpty = !ctrlMatchStr && !ctrlObjMatch ? /@Controller\s*\(\s*\)/.exec(source) : null;
        const ctrlPrefix = ctrlMatchStr?.[1] ?? ctrlObjMatch?.[1] ?? (ctrlMatchEmpty ? "" : "");
        const ctrlClassMatch = /class\s+(\w+)/.exec(source);
        const ctrlClass = ctrlClassMatch?.[1] ?? "UnknownController";
        // Guards at controller level
        const classIdx = source.indexOf(`class ${ctrlClass}`);
        const ctrlHeader = classIdx >= 0 ? source.slice(0, classIdx) : "";
        const ctrlGuards = parseUseGuards(ctrlHeader);
        // G9: controller-level version — method can override
        const ctrlVersion = parseControllerVersion(ctrlHeader);
        // Collect all method decorator positions first — enables bounded lookback per method
        const allMethodDecoratorPositions = [];
        for (const method of methods) {
            const reStr = new RegExp(`@${method}\\s*\\(\\s*['"\`]([^'"\`]*)['"\`]\\s*\\)`, "g");
            const reEmpty = new RegExp(`@${method}\\s*\\(\\s*\\)`, "g");
            let m;
            while ((m = reStr.exec(source)) !== null) {
                allMethodDecoratorPositions.push({ method, path: m[1] ?? "", pos: m.index, matchLen: m[0].length });
            }
            while ((m = reEmpty.exec(source)) !== null) {
                allMethodDecoratorPositions.push({ method, path: "", pos: m.index, matchLen: m[0].length });
            }
        }
        allMethodDecoratorPositions.sort((a, b) => a.pos - b.pos);
        for (let idx = 0; idx < allMethodDecoratorPositions.length; idx++) {
            if (routes.length >= maxRoutes) {
                truncated = true;
                break;
            }
            const mm = allMethodDecoratorPositions[idx];
            const handler = resolveHandlerName(source, mm.pos + mm.matchLen);
            if (!handler)
                continue;
            const rawPath = mm.path
                ? `/${ctrlPrefix}/${mm.path}`.replace(/\/+/g, "/")
                : `/${ctrlPrefix}`.replace(/\/+/g, "/") || "/";
            const fullPath = rawPath.length > 1 ? rawPath.replace(/\/$/, "") : rawPath;
            if (routes.some((r) => r.file === file.path && r.handler === handler && r.method === mm.method.toUpperCase()))
                continue;
            // Bounded backward lookback: from previous decorator end to current decorator start
            const prevEnd = idx > 0
                ? allMethodDecoratorPositions[idx - 1].pos + allMethodDecoratorPositions[idx - 1].matchLen + 20
                : (classIdx >= 0 ? classIdx : 0);
            routes.push(buildRouteBounded(mm.method, fullPath, handler, mm.pos, prevEnd));
        }
        // buildRoute is replaced by buildRouteBounded which takes the lookback start
        function buildRouteBounded(method, fullPath, handler, methodIdx, lookbackStart) {
            const before = source.slice(Math.max(lookbackStart, 0), methodIdx);
            let endPos = methodIdx + 800;
            const hNameRe = new RegExp(`\\b${handler}\\s*\\(`);
            const hMatch = hNameRe.exec(source.slice(methodIdx, methodIdx + 800));
            if (hMatch)
                endPos = methodIdx + hMatch.index;
            const after = source.slice(methodIdx, Math.min(source.length, endPos));
            const methodCtx = before + after;
            // Bound paramCtx to the method's own signature via paren counting —
            // prevents leakage from adjacent methods in the same file.
            const paramCtx = extractMethodSignature(source, endPos);
            const methodGuards = parseUseGuards(methodCtx);
            const allGuards = [...ctrlGuards, ...methodGuards];
            const methodVersion = parseVersionFromContext(methodCtx);
            const swagger = parseSwaggerFromContext(methodCtx, ctrlHeader);
            const healthCheck = isHealthCheck(methodCtx);
            const inlinePipes = parseInlinePipes(methodCtx);
            const entry = {
                method: method.toUpperCase(),
                path: fullPath,
                handler,
                controller: ctrlClass,
                file: file.path,
                guards: allGuards,
                params: parseParamDecorators(paramCtx),
            };
            const version = methodVersion ?? ctrlVersion;
            if (version)
                entry.version = version;
            if (swagger)
                entry.swagger = swagger;
            if (healthCheck)
                entry.is_health_check = true;
            if (inlinePipes.length > 0)
                entry.inline_pipes = inlinePipes;
            return entry;
        }
    }
    const protectedCount = routes.filter((r) => r.guards.length > 0).length;
    return {
        routes,
        stats: {
            total_routes: routes.length,
            protected: protectedCount,
            unprotected: routes.length - protectedCount,
        },
        ...(errors.length > 0 ? { errors } : {}),
        ...(truncated ? { truncated } : {}),
    };
}
/**
 * Extract the method parameter signature starting at the handler name position.
 * Paren-counts to find the matching `)` for the parameter list — prevents
 * scanning into adjacent methods which would produce duplicate @Param/@Body.
 */
function extractMethodSignature(source, handlerNamePos) {
    // Find the opening paren of the handler's parameter list
    let i = handlerNamePos;
    // Skip handler name + whitespace
    while (i < source.length && /[\w\s]/.test(source[i]))
        i++;
    if (source[i] !== "(")
        return source.slice(handlerNamePos, Math.min(source.length, handlerNamePos + 400));
    // Paren-count to find matching close
    let depth = 1;
    const start = i + 1;
    i++;
    while (i < source.length && depth > 0) {
        if (source[i] === "(")
            depth++;
        else if (source[i] === ")")
            depth--;
        if (depth === 0)
            break;
        i++;
    }
    return source.slice(start, i);
}
/** Parse @Param/@Body/@Query decorators from source context */
function parseParamDecorators(source) {
    const params = [];
    const re = /@(Param|Body|Query|Headers)\s*\(\s*(?:['"`](\w+)['"`])?\s*\)/g;
    let m;
    while ((m = re.exec(source)) !== null) {
        params.push({ decorator: m[1], name: m[2] ?? "" });
    }
    return params;
}
/**
 * Resolve the method handler name after a @Get/@Post/etc decorator.
 * Skips intermediate stacked decorators (@Version, @ApiOperation, @UseGuards, etc.)
 * using paren counting to handle nested args like @UsePipes(new Pipe({...})).
 */
function resolveHandlerName(source, afterDecoratorPos) {
    let pos = afterDecoratorPos;
    const maxScan = pos + 1000;
    while (pos < Math.min(source.length, maxScan)) {
        // Skip whitespace and newlines
        while (pos < source.length && /\s/.test(source[pos]))
            pos++;
        if (pos >= source.length)
            return undefined;
        // If we hit another decorator, skip the whole decorator including args (paren count)
        if (source[pos] === "@") {
            pos++; // skip @
            // skip decorator name
            while (pos < source.length && /\w/.test(source[pos]))
                pos++;
            // skip args if present (paren-counted)
            while (pos < source.length && /\s/.test(source[pos]))
                pos++;
            if (source[pos] === "(") {
                let depth = 1;
                pos++;
                while (pos < source.length && depth > 0) {
                    if (source[pos] === "(")
                        depth++;
                    else if (source[pos] === ")")
                        depth--;
                    pos++;
                }
            }
            continue;
        }
        // Skip keywords like async, public, private
        const kwMatch = /^(?:async|public|private|protected)\s+/.exec(source.slice(pos));
        if (kwMatch) {
            pos += kwMatch[0].length;
            continue;
        }
        // Should now be at the method name
        const nameMatch = /^(\w+)\s*\(/.exec(source.slice(pos));
        if (nameMatch)
            return nameMatch[1];
        return undefined;
    }
    return undefined;
}
/** G9: Extract @Version('n') or @Controller({ version: 'n' }) — method-level takes precedence */
function parseVersionFromContext(methodCtx) {
    const m = /@Version\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/.exec(methodCtx);
    return m ? m[1] : undefined;
}
function parseControllerVersion(ctrlHeader) {
    const m = /@Controller\s*\(\s*\{[^}]*version:\s*['"`]([^'"`]+)['"`]/.exec(ctrlHeader);
    return m ? m[1] : undefined;
}
/** G10: Extract Swagger annotations */
function parseSwaggerFromContext(methodCtx, ctrlHeader) {
    const result = {};
    const summaryMatch = /@ApiOperation\s*\(\s*\{[^}]*summary:\s*['"`]([^'"`]+)['"`]/.exec(methodCtx);
    if (summaryMatch)
        result.summary = summaryMatch[1];
    if (/@ApiBearerAuth\s*\(/.test(methodCtx) || /@ApiBearerAuth\s*\(/.test(ctrlHeader)) {
        result.bearer = true;
    }
    const tagsMatch = /@ApiTags\s*\(\s*((?:['"`][^'"`]+['"`]\s*,?\s*)+)\)/.exec(ctrlHeader);
    if (tagsMatch) {
        const tags = [...tagsMatch[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map((m) => m[1]);
        if (tags.length > 0)
            result.tags = tags;
    }
    return Object.keys(result).length > 0 ? result : undefined;
}
/** G13: Check for @HealthCheck() decorator on method */
function isHealthCheck(methodCtx) {
    return /@HealthCheck\s*\(\s*\)/.test(methodCtx);
}
/** G11: Extract inline pipe constructions from @UsePipes(new ValidationPipe(...)) */
function parseInlinePipes(methodCtx) {
    const pipes = [];
    const re = /@UsePipes\s*\(\s*new\s+(\w+Pipe)\s*\(/g;
    let m;
    while ((m = re.exec(methodCtx)) !== null)
        pipes.push(m[1]);
    return pipes;
}
/**
 * Wave 3 Feature 1: visualize the full NestJS request pipeline for a single route.
 * Walks middleware.configure() chain, global guards/filters/pipes/interceptors,
 * controller-level decorators, method-level decorators, and the handler itself.
 * Returns ordered steps + optional mermaid diagram.
 */
export async function nestRequestPipeline(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    const targetRoute = options.route;
    const targetMethod = (options.method ?? "GET").toUpperCase();
    // Reuse nestGuardChain for the chain + nestRouteInventory for handler name + file
    const [chainResult, inventoryResult] = await Promise.all([
        nestGuardChain(repo, { path: targetRoute }),
        nestRouteInventory(repo),
    ]);
    const matchingRoute = chainResult.routes.find((r) => r.route === targetRoute && r.method === targetMethod);
    const inventoryEntry = inventoryResult.routes.find((r) => r.path === targetRoute && r.method === targetMethod);
    if (!matchingRoute) {
        return {
            route: targetRoute,
            method: targetMethod,
            controller: "NotFound",
            handler: "NotFound",
            file: "",
            steps: [],
            ...(chainResult.errors ? { errors: chainResult.errors } : {}),
        };
    }
    const resolvedHandler = inventoryEntry?.handler ?? "anonymous";
    // Convert chain items to pipeline steps in canonical execution order:
    //   middleware → global guards → controller guards → method guards
    //   → global pipes → controller pipes → method pipes → inline pipes
    //   → global interceptors (pre) → controller interceptors (pre) → method interceptors (pre)
    //   → handler
    //   → method interceptors (post) → controller interceptors (post) → global interceptors (post)
    //   → global filters → controller filters → method filters
    const steps = [];
    const byLayerAndType = (layer, type) => matchingRoute.chain.filter((c) => c.layer === layer && c.type === type);
    // 1. Middleware layer (runs first in NestJS request pipeline)
    for (const c of byLayerAndType("middleware", "guard")) {
        steps.push({ layer: "middleware", name: c.name, ...(c.file ? { file: c.file } : {}), note: "module.configure()" });
    }
    // 2. Guards (global → controller → method)
    for (const c of byLayerAndType("global", "guard"))
        steps.push({ layer: "global-guard", name: c.name, ...(c.file ? { file: c.file } : {}) });
    for (const c of byLayerAndType("controller", "guard"))
        steps.push({ layer: "controller-guard", name: c.name });
    for (const c of byLayerAndType("method", "guard"))
        steps.push({ layer: "method-guard", name: c.name });
    // 3. Pipes (global → controller → method)
    for (const c of byLayerAndType("global", "pipe"))
        steps.push({ layer: "global-pipe", name: c.name, ...(c.file ? { file: c.file } : {}) });
    for (const c of byLayerAndType("controller", "pipe"))
        steps.push({ layer: "controller-pipe", name: c.name });
    for (const c of byLayerAndType("method", "pipe"))
        steps.push({ layer: "method-pipe", name: c.name });
    // 4. Interceptors (pre-handler; global → controller → method)
    for (const c of byLayerAndType("global", "interceptor"))
        steps.push({ layer: "global-interceptor", name: c.name, ...(c.file ? { file: c.file } : {}), note: "pre-handler" });
    for (const c of byLayerAndType("controller", "interceptor"))
        steps.push({ layer: "controller-interceptor", name: c.name, note: "pre-handler" });
    for (const c of byLayerAndType("method", "interceptor"))
        steps.push({ layer: "method-interceptor", name: c.name, note: "pre-handler" });
    // 5. Custom metadata decorators (@Roles, @Public, etc. — informational, don't execute)
    for (const c of byLayerAndType("method", "metadata")) {
        steps.push({
            layer: "custom-metadata",
            name: c.name,
            note: c.args ? `args: ${c.args}` : "metadata only",
        });
    }
    // 6. Handler (the method itself)
    steps.push({
        layer: "handler",
        name: resolvedHandler,
        file: matchingRoute.file,
        note: `${matchingRoute.method} ${matchingRoute.route}`,
    });
    // 7. Filters (for exception handling — run if handler throws)
    for (const c of byLayerAndType("global", "filter"))
        steps.push({ layer: "global-filter", name: c.name, ...(c.file ? { file: c.file } : {}), note: "exception handler" });
    for (const c of byLayerAndType("controller", "filter"))
        steps.push({ layer: "controller-filter", name: c.name, note: "exception handler" });
    for (const c of byLayerAndType("method", "filter"))
        steps.push({ layer: "method-filter", name: c.name, note: "exception handler" });
    const result = {
        route: matchingRoute.route,
        method: matchingRoute.method,
        controller: matchingRoute.controller,
        handler: resolvedHandler,
        file: matchingRoute.file,
        steps,
        ...(chainResult.errors ? { errors: chainResult.errors } : {}),
    };
    // Optional mermaid output
    if (options.output_format === "mermaid") {
        const lines = ["flowchart TD"];
        lines.push(`    Request["${matchingRoute.method} ${matchingRoute.route}"]`);
        let prev = "Request";
        let idx = 0;
        for (const step of steps) {
            idx++;
            const id = `S${idx}`;
            const label = step.note ? `${step.name}<br/>${step.note}` : step.name;
            const shape = step.layer === "handler" ? `(["${label}"])` : `["${label}"]`;
            lines.push(`    ${id}${shape}`);
            lines.push(`    ${prev} --> ${id}`);
            prev = id;
        }
        result.mermaid = lines.join("\n");
    }
    return result;
}
const ALL_NEST_CHECKS = [
    "modules", "routes", "di", "guards", "lifecycle", "patterns",
    "graphql", "websocket", "schedule", "typeorm", "microservice",
];
export async function nestAudit(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    // Check if this is a NestJS repo
    const { detectFrameworks } = await import("../utils/framework-detect.js");
    const frameworks = detectFrameworks(index);
    if (!frameworks.has("nestjs")) {
        return {
            framework_detected: false,
            summary: { total_routes: 0, cycles: 0, violations: 0, anti_pattern_hits: 0, failed_checks: 0, truncated_checks: [] },
        };
    }
    const enabledChecks = new Set((options?.checks ?? [...ALL_NEST_CHECKS]));
    const tasks = [];
    if (enabledChecks.has("lifecycle")) {
        tasks.push(nestLifecycleMap(repo).then((r) => ({ name: "lifecycle", result: r }))
            .catch((e) => ({ name: "lifecycle", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("modules")) {
        tasks.push(nestModuleGraph(repo).then((r) => ({ name: "modules", result: r }))
            .catch((e) => ({ name: "modules", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("di")) {
        tasks.push(nestDIGraph(repo).then((r) => ({ name: "di", result: r }))
            .catch((e) => ({ name: "di", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("guards")) {
        tasks.push(nestGuardChain(repo).then((r) => ({ name: "guards", result: r }))
            .catch((e) => ({ name: "guards", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("routes")) {
        tasks.push(nestRouteInventory(repo).then((r) => ({ name: "routes", result: r }))
            .catch((e) => ({ name: "routes", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("patterns")) {
        tasks.push((async () => {
            const { searchPatterns, listPatterns } = await import("./pattern-tools.js");
            const nestPatterns = listPatterns().filter((p) => p.name.startsWith("nest-"));
            const results = [];
            for (const p of nestPatterns) {
                const r = await searchPatterns(repo, p.name);
                if (r.matches.length > 0)
                    results.push({ pattern: p.name, count: r.matches.length });
            }
            return { name: "patterns", result: results };
        })().catch((e) => ({ name: "patterns", error: e instanceof Error ? e.message : String(e) })));
    }
    // Wave 2 checks — lazy import to avoid eager loading if nest-ext-tools isn't needed
    if (enabledChecks.has("graphql")) {
        tasks.push((async () => {
            const { nestGraphQLMap } = await import("./nest-ext-tools.js");
            return { name: "graphql", result: await nestGraphQLMap(repo) };
        })().catch((e) => ({ name: "graphql", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("websocket")) {
        tasks.push((async () => {
            const { nestWebSocketMap } = await import("./nest-ext-tools.js");
            return { name: "websocket", result: await nestWebSocketMap(repo) };
        })().catch((e) => ({ name: "websocket", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("schedule")) {
        tasks.push((async () => {
            const { nestScheduleMap } = await import("./nest-ext-tools.js");
            return { name: "schedule", result: await nestScheduleMap(repo) };
        })().catch((e) => ({ name: "schedule", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("typeorm")) {
        tasks.push((async () => {
            const { nestTypeOrmMap } = await import("./nest-ext-tools.js");
            return { name: "typeorm", result: await nestTypeOrmMap(repo) };
        })().catch((e) => ({ name: "typeorm", error: e instanceof Error ? e.message : String(e) })));
    }
    if (enabledChecks.has("microservice")) {
        tasks.push((async () => {
            const { nestMicroserviceMap } = await import("./nest-ext-tools.js");
            return { name: "microservice", result: await nestMicroserviceMap(repo) };
        })().catch((e) => ({ name: "microservice", error: e instanceof Error ? e.message : String(e) })));
    }
    const settled = await Promise.all(tasks);
    // Aggregate
    const auditErrors = [];
    const warnings = [];
    const truncatedChecks = [];
    let lifecycleResult;
    let moduleResult;
    let diResult;
    let guardResult;
    let routeResult;
    let patternResults;
    let graphqlResult;
    let websocketResult;
    let scheduleResult;
    let typeormResult;
    let microserviceResult;
    for (const item of settled) {
        if (item.error) {
            auditErrors.push({ check: item.name, reason: item.error });
            continue;
        }
        switch (item.name) {
            case "lifecycle":
                lifecycleResult = item.result;
                break;
            case "modules": {
                const r = item.result;
                moduleResult = r;
                if (r.truncated)
                    truncatedChecks.push("modules");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "di": {
                const r = item.result;
                diResult = r;
                if (r.truncated)
                    truncatedChecks.push("di");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "guards": {
                const r = item.result;
                guardResult = r;
                if (r.truncated)
                    truncatedChecks.push("guards");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "routes": {
                const r = item.result;
                routeResult = r;
                if (r.truncated)
                    truncatedChecks.push("routes");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "patterns":
                patternResults = item.result;
                break;
            case "graphql": {
                const r = item.result;
                graphqlResult = r;
                if (r.truncated)
                    truncatedChecks.push("graphql");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "websocket": {
                const r = item.result;
                websocketResult = r;
                if (r.truncated)
                    truncatedChecks.push("websocket");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "schedule": {
                const r = item.result;
                scheduleResult = r;
                if (r.truncated)
                    truncatedChecks.push("schedule");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "typeorm": {
                const r = item.result;
                typeormResult = r;
                if (r.truncated)
                    truncatedChecks.push("typeorm");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
            case "microservice": {
                const r = item.result;
                microserviceResult = r;
                if (r.truncated)
                    truncatedChecks.push("microservice");
                if (r.errors)
                    warnings.push(...r.errors);
                break;
            }
        }
    }
    const totalRoutes = routeResult?.stats.total_routes ?? 0;
    const cycles = (moduleResult?.circular_deps.length ?? 0) + (diResult?.cycles.length ?? 0) + (typeormResult?.cycles.length ?? 0);
    const antiPatternHits = patternResults?.reduce((sum, p) => sum + p.count, 0) ?? 0;
    return {
        framework_detected: true,
        ...(lifecycleResult ? { lifecycle_map: lifecycleResult } : {}),
        ...(moduleResult ? { module_graph: moduleResult } : {}),
        ...(diResult ? { di_graph: diResult } : {}),
        ...(guardResult ? { guard_chain: guardResult } : {}),
        ...(routeResult ? { route_inventory: routeResult } : {}),
        ...(graphqlResult ? { graphql_map: graphqlResult } : {}),
        ...(websocketResult ? { websocket_map: websocketResult } : {}),
        ...(scheduleResult ? { schedule_map: scheduleResult } : {}),
        ...(typeormResult ? { typeorm_map: typeormResult } : {}),
        ...(microserviceResult ? { microservice_map: microserviceResult } : {}),
        ...(patternResults ? { anti_patterns: patternResults } : {}),
        summary: {
            total_routes: totalRoutes,
            cycles,
            violations: (moduleResult?.circular_deps.length ?? 0) + (diResult?.cross_module_warnings.length ?? 0),
            anti_pattern_hits: antiPatternHits,
            failed_checks: auditErrors.length,
            truncated_checks: truncatedChecks,
        },
        ...(warnings.length > 0 ? { warnings } : {}),
        ...(auditErrors.length > 0 ? { errors: auditErrors } : {}),
    };
}
//# sourceMappingURL=nest-tools.js.map