import * as fs from "fs";
import * as path from "path";
import { nextjsFileToApiPath, parseFileEndpoints, } from "./routeParsers.js";
import { logger } from "./logger.js";
function globRecursive(dir, extensions) {
    const results = [];
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch (err) {
        logger.debug("globRecursive: skipping directory", { dir, error: err instanceof Error ? err.message : String(err) });
        return results;
    }
    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === "build")
                continue;
            results.push(...globRecursive(fullPath, extensions));
        }
        else if (extensions.some((ext) => entry.name.endsWith(ext))) {
            results.push(fullPath);
        }
    }
    return results;
}
function safeReadFile(filePath) {
    try {
        return fs.readFileSync(filePath, "utf-8");
    }
    catch {
        return null;
    }
}
/**
 * Returns the relative paths of entry-point files that define the routing
 * hierarchy for this repository. The LLM reads these files in Step 1.5 to
 * trace router/module nesting and build an authoritative path resolution table.
 *
 * Intentionally returns only file paths — no extracted lines. Static regex
 * extraction is fragile and framework-specific; the LLM reads the files directly
 * and understands any framework's routing syntax.
 */
export function grepRouterMountingContext(repositoryPath) {
    // NestJS root module (app.module.ts) is included alongside conventional
    // entry-point names because NestJS routing is implicit through module
    // composition rather than explicit mount calls.
    const entryCandidates = new Set([
        "main.py", "app.py", "server.py", "asgi.py", "wsgi.py",
        "app.ts", "app.js", "server.ts", "server.js", "index.ts", "index.js",
        "urls.py",
        "main.go", "server.go", "router.go", "routes.go",
        "routes.rb", "web.php", "api.php",
        "app.module.ts", "app.module.js",
        "router.ex", // Phoenix/Elixir — defines all scopes and pipelines
        "main.rs", "lib.rs", // Rust (Axum, Actix, Rocket)
    ]);
    // Content signals that indicate a file wires routers/modules together.
    const mountPatterns = [
        /include_router/i,
        /register_blueprint/i,
        /\.use\s*\(\s*["']\//,
        /urlpatterns/,
        /path\s*\(.+include\s*\(/,
        /\.Group\s*\(/,
        /Route::prefix/,
        /namespace\s+(?::[A-Za-z0-9_]+|["'][^"']+["'])/, // Rails: namespace :api or namespace "/api"
        /scope\s+["']\//,
        /@Module\s*\(/,
        /scope\s+["'].*,\s*do/, // Phoenix: scope "/api", MyApp.Router, do
    ];
    const found = [];
    const seenAbsPaths = new Set();
    // Single glob from repo root — globRecursive already recurses so scanning
    // each subdir separately would redundantly re-traverse those subtrees.
    const allFiles = globRecursive(repositoryPath, [".py", ".ts", ".js", ".go", ".rb", ".php", ".rs", ".ex"]);
    for (const f of allFiles) {
        if (/test/i.test(f))
            continue;
        if (seenAbsPaths.has(f))
            continue;
        const basename = path.basename(f);
        const isNamedEntry = entryCandidates.has(basename);
        const isNestModule = /\.module\.(ts|js)$/.test(f);
        if (!isNamedEntry && !isNestModule)
            continue;
        const fc = safeReadFile(f);
        if (fc === null)
            continue;
        if (isNestModule) {
            if (!/@Module\s*\(/.test(fc) || !/imports\s*:\s*\[/.test(fc))
                continue;
        }
        else {
            if (!mountPatterns.some((p) => p.test(fc)))
                continue;
        }
        seenAbsPaths.add(f);
        found.push(f.startsWith(repositoryPath) ? f.slice(repositoryPath.length + 1) : f);
    }
    return found;
}
function addEndpointToMap(endpointMap, apiPath, method, sourceFile, repositoryPath) {
    const relative = sourceFile.startsWith(repositoryPath)
        ? sourceFile.slice(repositoryPath.length + 1) : sourceFile;
    const normalizedPath = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
    const key = `${relative}::${normalizedPath}`;
    const existing = endpointMap.get(key);
    if (existing)
        existing.methods.add(method);
    else
        endpointMap.set(key, { path: normalizedPath, methods: new Set([method]), sourceFile: relative });
}
function scanNextjsFile(file, repositoryPath, endpointMap) {
    const relative = file.startsWith(repositoryPath)
        ? file.slice(repositoryPath.length + 1) : file;
    const apiPath = nextjsFileToApiPath(relative);
    if (!apiPath)
        return false;
    const content = safeReadFile(file);
    if (content === null) {
        addEndpointToMap(endpointMap, apiPath, "MULTI", file, repositoryPath);
        return true;
    }
    const detectedMethods = new Set();
    for (const line of content.split("\n")) {
        const mc = line.match(/req\.method\s*===?\s*["'](GET|POST|PUT|PATCH|DELETE)["']/i);
        if (mc)
            detectedMethods.add(mc[1].toUpperCase());
        const em = line.match(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/i);
        if (em)
            detectedMethods.add(em[1].toUpperCase());
    }
    if (detectedMethods.size > 0) {
        for (const m of detectedMethods)
            addEndpointToMap(endpointMap, apiPath, m, file, repositoryPath);
    }
    else {
        addEndpointToMap(endpointMap, apiPath, "MULTI", file, repositoryPath);
    }
    return true;
}
/** Filename pattern used to identify candidate route/handler files. */
// Extended to cover NestJS (service, gateway, resolver) and other frameworks.
// Tested against the full relative path — terms here are specific enough not to over-match.
const ROUTE_FILE_PATTERN = /route|controller|endpoint|handler|view|urls|api|router|service|gateway|resolver|\bserver\b/i;
// Generic terms like "app" and "main" must only match the basename (filename),
// not directory names — otherwise every file under src/app/ would match by-name
// and fill MAX_CANDIDATE_FILES, potentially skipping the content pass.
const ROUTE_FILE_BASENAME_PATTERN = /\bapp\b|\bmain\b/i;
/**
 * Content-based routing signature: a file is a route file if it contains BOTH
 * a URL-path-like string literal AND an HTTP method registration, regardless of
 * framework or language. This catches any framework whose files don't have
 * route/controller/handler-like names (e.g. Rust `main.rs`, .NET `Startup.cs`,
 * Axum, Ktor, Hono, Elysia, etc.).
 *
 * Two separate patterns are intentional — requiring both suppresses false
 * positives from files that merely reference HTTP methods or paths in other
 * contexts (test fixtures, config, documentation).
 */
const ROUTING_URL_PATTERN = /["'`](\/[a-zA-Z0-9_{}:*-][a-zA-Z0-9_{}/:*-]*)/;
const ROUTING_METHOD_PATTERN = /\.(get|post|put|patch|delete|head|options)\s*\(|@(Get|Post|Put|Patch|Delete|Route|Controller|Handler|RequestMapping)\b|route\s*\(["']|path\s*\(["']|HandleFunc|ServeHTTP|\.(Map|map)(Get|Post|Put|Patch|Delete)\s*\(|\brouting\s*\{/i;
/** Maximum files to return — keeps the LLM prompt size bounded. */
const MAX_CANDIDATE_FILES = 60;
/**
 * Return relative paths of all source files that are likely to define API
 * routes. Two-pass strategy:
 *   1. Fast: filename matches ROUTE_FILE_PATTERN (no file I/O needed).
 *   2. Content: files not matched by name are read and checked for both a
 *      URL-path literal AND an HTTP method registration. This covers any
 *      framework the filename filter misses — Axum, Ktor, .NET, Hono, etc.
 * Result is capped at MAX_CANDIDATE_FILES to keep the LLM prompt bounded.
 */
export function findCandidateRouteFiles(repositoryPath) {
    const sourceFiles = globRecursive(repositoryPath, [
        ".ts", ".tsx", ".js", ".jsx",
        ".py", ".java", ".kt",
        ".go", ".rb", ".php",
        ".rs", ".cs", ".ex", ".exs",
    ]);
    const byName = [];
    const unchecked = [];
    for (const f of sourceFiles) {
        if (/test/i.test(f))
            continue;
        const relative = f.startsWith(repositoryPath) ? f.slice(repositoryPath.length + 1) : f;
        if (ROUTE_FILE_PATTERN.test(relative) || ROUTE_FILE_BASENAME_PATTERN.test(path.basename(relative))) {
            byName.push(relative);
        }
        else {
            unchecked.push(f); // store absolute path for readFileSync
        }
    }
    // Content pass — only run if name pass left budget remaining
    const byContent = [];
    if (byName.length < MAX_CANDIDATE_FILES) {
        const budget = MAX_CANDIDATE_FILES - byName.length;
        let scanned = 0;
        const MAX_SCAN = 2000; // cap file reads; the unchecked array can be much larger
        for (const f of unchecked) {
            if (byContent.length >= budget || scanned++ >= MAX_SCAN)
                break;
            const content = safeReadFile(f);
            if (content === null)
                continue;
            if (ROUTING_URL_PATTERN.test(content) && ROUTING_METHOD_PATTERN.test(content)) {
                const relative = f.startsWith(repositoryPath) ? f.slice(repositoryPath.length + 1) : f;
                byContent.push(relative);
            }
        }
    }
    const combined = [...byName, ...byContent];
    const contentPassSkipped = byName.length >= MAX_CANDIDATE_FILES;
    const result = combined.slice(0, MAX_CANDIDATE_FILES);
    if (combined.length > MAX_CANDIDATE_FILES) {
        logger.warning("findCandidateRouteFiles: hit MAX_CANDIDATE_FILES cap — some route files may be excluded from LLM analysis", {
            cap: MAX_CANDIDATE_FILES,
            byName: byName.length,
            byContent: byContent.length,
            contentPassSkipped,
            totalFound: combined.length,
        });
    }
    return result;
}
export function scanAllRepoEndpoints(repositoryPath) {
    const endpointMap = new Map();
    const sourceFiles = globRecursive(repositoryPath, [".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".kt", ".go", ".rb", ".php", ".rs", ".cs"]);
    for (const file of sourceFiles) {
        if (scanNextjsFile(file, repositoryPath, endpointMap))
            continue;
        const relative = file.startsWith(repositoryPath)
            ? file.slice(repositoryPath.length + 1) : file;
        if (!ROUTE_FILE_PATTERN.test(relative) && !ROUTE_FILE_BASENAME_PATTERN.test(path.basename(relative)))
            continue;
        const content = safeReadFile(file);
        if (content === null)
            continue;
        for (const ep of parseFileEndpoints(content, relative)) {
            addEndpointToMap(endpointMap, ep.path, ep.method, file, repositoryPath);
        }
    }
    return Array.from(endpointMap.values()).map((data) => ({
        path: data.path,
        methods: Array.from(data.methods),
        sourceFile: data.sourceFile,
    }));
}
export function scanRelatedEndpoints(repositoryPath, changedFiles) {
    const relatedDirs = new Set();
    for (const f of changedFiles) {
        const absDir = path.dirname(path.join(repositoryPath, f));
        relatedDirs.add(absDir);
        const parentDir = path.dirname(absDir);
        if (parentDir !== repositoryPath)
            relatedDirs.add(parentDir);
    }
    const endpointMap = new Map();
    for (const dir of relatedDirs) {
        if (!fs.existsSync(dir))
            continue;
        const files = globRecursive(dir, [".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".kt", ".go", ".rb", ".php", ".rs", ".cs"]);
        for (const file of files) {
            if (scanNextjsFile(file, repositoryPath, endpointMap))
                continue;
            const relative = file.startsWith(repositoryPath)
                ? file.slice(repositoryPath.length + 1) : file;
            if (!ROUTE_FILE_PATTERN.test(relative) && !ROUTE_FILE_BASENAME_PATTERN.test(path.basename(relative)))
                continue;
            const fileContent = safeReadFile(file);
            if (fileContent === null)
                continue;
            for (const ep of parseFileEndpoints(fileContent, relative)) {
                addEndpointToMap(endpointMap, ep.path, ep.method, file, repositoryPath);
            }
        }
    }
    return Array.from(endpointMap.values()).map((data) => ({
        path: data.path,
        methods: Array.from(data.methods),
        sourceFile: data.sourceFile,
    }));
}
