/**
 * API Endpoint Analyzer
 *
 * Extracts API endpoints from source code for various frameworks.
 *
 * @module scanners/logic/endpoint-analyzer
 */
import { readFile } from "fs/promises";
import { join } from "path";
import { glob } from "glob";
import { logger } from "../../logger.js";
/**
 * Route patterns for different frameworks
 */
const ROUTE_PATTERNS = {
    nextjs: [
        // App Router: export async function GET/POST/etc
        { pattern: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/gi, methodGroup: 1, pathGroup: -1 },
        // Pages Router: handler switch on method
        { pattern: /req\.method\s*===?\s*['"`](GET|POST|PUT|DELETE|PATCH)['"`]/gi, methodGroup: 1, pathGroup: -1 },
    ],
    express: [
        // app.get('/path', handler) or router.get('/path', handler)
        { pattern: /(?:app|router)\.(get|post|put|delete|patch|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
        // app.use('/path', router)
        { pattern: /(?:app|router)\.use\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: -1, pathGroup: 1 },
    ],
    fastify: [
        // fastify.get('/path', handler)
        { pattern: /fastify\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
        // fastify.route({ method, url })
        { pattern: /fastify\.route\s*\(\s*\{[^}]*method:\s*['"`](GET|POST|PUT|DELETE|PATCH)['"`][^}]*url:\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    koa: [
        { pattern: /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    hapi: [
        { pattern: /server\.route\s*\(\s*\{[^}]*method:\s*['"`](GET|POST|PUT|DELETE|PATCH)['"`][^}]*path:\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    nestjs: [
        // @Get('/path'), @Post('/path'), etc.
        { pattern: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]?([^'"`)\s]*)['"`]?\s*\)/gi, methodGroup: 1, pathGroup: 2 },
        // @Controller('/path')
        { pattern: /@Controller\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/gi, methodGroup: -1, pathGroup: 1 },
    ],
    django: [
        // path('route/', view)
        { pattern: /path\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: -1, pathGroup: 1 },
        // @api_view(['GET', 'POST'])
        { pattern: /@api_view\s*\(\s*\[([^\]]+)\]/gi, methodGroup: 1, pathGroup: -1 },
    ],
    flask: [
        // @app.route('/path', methods=['GET', 'POST'])
        { pattern: /@(?:app|blueprint)\.route\s*\(\s*['"`]([^'"`]+)['"`](?:[^)]*methods\s*=\s*\[([^\]]+)\])?/gi, methodGroup: 2, pathGroup: 1 },
    ],
    fastapi: [
        // @app.get('/path')
        { pattern: /@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    rails: [
        // get '/path', to: 'controller#action'
        { pattern: /(get|post|put|patch|delete)\s+['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
        // resources :users
        { pattern: /resources?\s+:(\w+)/gi, methodGroup: -1, pathGroup: 1 },
    ],
    spring: [
        // @GetMapping("/path")
        { pattern: /@(Get|Post|Put|Delete|Patch)Mapping\s*\(\s*(?:value\s*=\s*)?['"`]?([^'"`)\s]*)['"`]?\s*\)/gi, methodGroup: 1, pathGroup: 2 },
        // @RequestMapping(method = RequestMethod.GET, path = "/path")
        { pattern: /@RequestMapping\s*\([^)]*method\s*=\s*RequestMethod\.(GET|POST|PUT|DELETE|PATCH)[^)]*(?:path|value)\s*=\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    laravel: [
        // Route::get('/path', [Controller::class, 'method'])
        { pattern: /Route::(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    gin: [
        // r.GET("/path", handler)
        { pattern: /[rg]\.(GET|POST|PUT|DELETE|PATCH)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    echo: [
        // e.GET("/path", handler)
        { pattern: /e\.(GET|POST|PUT|DELETE|PATCH)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    fiber: [
        // app.Get("/path", handler)
        { pattern: /app\.(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi, methodGroup: 1, pathGroup: 2 },
    ],
    auto: [],
};
/**
 * Patterns to detect authentication middleware
 */
const AUTH_MIDDLEWARE_PATTERNS = [
    /auth(?:enticate|orize)?/i,
    /requireAuth/i,
    /isAuthenticated/i,
    /ensureLoggedIn/i,
    /protect(?:ed)?Route/i,
    /jwt(?:Auth|Verify|Guard)?/i,
    /passport\./,
    /session\./,
    /@UseGuards\s*\([^)]*AuthGuard/i,
    /middleware\s*=\s*\[.*auth/i,
    /IsAuthenticated\(\)/i,
    /Depends\s*\(\s*get_current_user/i,
    /before_action\s+:authenticate/i,
    /auth:api/i,
];
/**
 * Patterns to detect authorization checks
 */
const AUTHZ_CHECK_PATTERNS = [
    { pattern: /user\.id\s*===?\s*(?:req|request|params|ctx)\.[^;]+/i, type: "ownership" },
    { pattern: /\.userId\s*===?\s*(?:req|request|session)\.user/i, type: "ownership" },
    { pattern: /belongsTo|ownedBy|createdBy|authorId/i, type: "ownership" },
    { pattern: /isOwner|canAccess|hasPermission/i, type: "permission" },
    { pattern: /user\.role\s*===?\s*['"`]admin['"`]/i, type: "admin" },
    { pattern: /\.isAdmin|isSuper|isModerator/i, type: "admin" },
    { pattern: /roles?\.(includes|contains|has)\s*\(/i, type: "role" },
    { pattern: /@Roles?\s*\(/i, type: "role" },
    { pattern: /permissions?\.(includes|contains|has)\s*\(/i, type: "permission" },
    { pattern: /can\s*\(\s*['"`]\w+['"`]/i, type: "permission" },
    { pattern: /authorize|checkPermission|requireRole/i, type: "custom" },
];
/**
 * Patterns to detect database access
 */
const DB_ACCESS_PATTERNS = [
    { pattern: /\.findOne\s*\(/i, type: "select" },
    { pattern: /\.findById\s*\(/i, type: "select" },
    { pattern: /\.findUnique\s*\(/i, type: "select" },
    { pattern: /\.findFirst\s*\(/i, type: "select" },
    { pattern: /\.find\s*\(\s*\{/i, type: "select" },
    { pattern: /\.select\s*\(/i, type: "select" },
    { pattern: /SELECT\s+.*\s+FROM/i, type: "select" },
    { pattern: /\.create\s*\(/i, type: "insert" },
    { pattern: /\.insert\s*\(/i, type: "insert" },
    { pattern: /INSERT\s+INTO/i, type: "insert" },
    { pattern: /\.update\s*\(/i, type: "update" },
    { pattern: /\.updateOne\s*\(/i, type: "update" },
    { pattern: /\.updateMany\s*\(/i, type: "update" },
    { pattern: /UPDATE\s+\w+\s+SET/i, type: "update" },
    { pattern: /\.delete\s*\(/i, type: "delete" },
    { pattern: /\.deleteOne\s*\(/i, type: "delete" },
    { pattern: /\.deleteMany\s*\(/i, type: "delete" },
    { pattern: /DELETE\s+FROM/i, type: "delete" },
    { pattern: /\.query\s*\(/i, type: "raw" },
    { pattern: /\.raw\s*\(/i, type: "raw" },
    { pattern: /\$queryRaw/i, type: "raw" },
];
/**
 * Extract path parameters from route path
 */
export function extractPathParams(path) {
    const params = [];
    // Express/Fastify style :param
    const colonParams = path.match(/:(\w+)/g);
    if (colonParams) {
        params.push(...colonParams.map((p) => p.slice(1)));
    }
    // Next.js/bracket style [param]
    const bracketParams = path.match(/\[(\w+)\]/g);
    if (bracketParams) {
        params.push(...bracketParams.map((p) => p.slice(1, -1)));
    }
    // Django/Flask style <param>
    const angleParams = path.match(/<(\w+)(?::\w+)?>/g);
    if (angleParams) {
        params.push(...angleParams.map((p) => p.replace(/<|>|:\w+/g, "")));
    }
    // Spring style {param}
    const curlyParams = path.match(/\{(\w+)\}/g);
    if (curlyParams) {
        params.push(...curlyParams.map((p) => p.slice(1, -1)));
    }
    return params;
}
/**
 * Detect framework from package.json
 */
export async function detectFramework(projectPath) {
    try {
        const pkgPath = join(projectPath, "package.json");
        const pkgContent = await readFile(pkgPath, "utf-8");
        const pkg = JSON.parse(pkgContent);
        const deps = { ...pkg.dependencies, ...pkg.devDependencies };
        // Check in priority order
        if (deps["next"])
            return "nextjs";
        if (deps["@nestjs/core"])
            return "nestjs";
        if (deps["fastify"])
            return "fastify";
        if (deps["koa"])
            return "koa";
        if (deps["@hapi/hapi"])
            return "hapi";
        if (deps["express"])
            return "express";
        // Check for Python frameworks (look for requirements.txt or pyproject.toml)
        // This is a simplified check
        try {
            const reqPath = join(projectPath, "requirements.txt");
            const reqContent = await readFile(reqPath, "utf-8");
            if (reqContent.includes("fastapi"))
                return "fastapi";
            if (reqContent.includes("flask"))
                return "flask";
            if (reqContent.includes("django"))
                return "django";
        }
        catch {
            // No requirements.txt
        }
        return "auto";
    }
    catch {
        return "auto";
    }
}
/**
 * Extract API path from Next.js file path
 */
function getNextjsApiPath(filePath) {
    // app/api/users/[id]/route.ts -> /api/users/[id]
    const appMatch = filePath.match(/app\/(api\/[^/]+(?:\/[^/]+)*?)\/route\.[jt]sx?$/);
    if (appMatch) {
        return "/" + appMatch[1];
    }
    // pages/api/users/[id].ts -> /api/users/[id]
    const pagesMatch = filePath.match(/pages\/(api\/[^/]+(?:\/[^/]+)*?)\.[jt]sx?$/);
    if (pagesMatch) {
        return "/" + pagesMatch[1];
    }
    return "";
}
/**
 * Check if code has authentication middleware
 */
function hasAuthMiddleware(content) {
    return AUTH_MIDDLEWARE_PATTERNS.some((pattern) => pattern.test(content));
}
/**
 * Find authorization checks in code
 */
function findAuthzChecks(content, filePath) {
    const checks = [];
    const lines = content.split("\n");
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        for (const { pattern, type } of AUTHZ_CHECK_PATTERNS) {
            if (pattern.test(line)) {
                // Get context (surrounding lines)
                const contextStart = Math.max(0, i - 2);
                const contextEnd = Math.min(lines.length, i + 3);
                const snippet = lines.slice(contextStart, contextEnd).join("\n");
                checks.push({
                    type,
                    location: { file: filePath, line: i + 1 },
                    snippet,
                    potentiallyBypassable: checkIfBypassable(snippet, type),
                    bypassReason: getBypassReason(snippet, type),
                });
            }
        }
    }
    return checks;
}
/**
 * Check if authorization check might be bypassable
 */
function checkIfBypassable(snippet, type) {
    // Check for common bypass patterns
    const bypassPatterns = [
        /\|\|\s*true/i, // || true
        /if\s*\(\s*false\s*\)/i, // if (false)
        /\/\/.*TODO/i, // TODO comments
        /\/\/.*FIXME/i, // FIXME comments
        /process\.env\.\w+\s*===?\s*['"`]dev/i, // Dev mode bypass
        /NODE_ENV\s*===?\s*['"`]development/i, // Dev env bypass
    ];
    if (type === "ownership" && !/===/.test(snippet)) {
        return true; // Loose equality check
    }
    return bypassPatterns.some((p) => p.test(snippet));
}
/**
 * Get reason why check might be bypassable
 */
function getBypassReason(snippet, type) {
    if (/\|\|\s*true/i.test(snippet)) {
        return "Check always passes (|| true)";
    }
    if (/\/\/.*TODO/i.test(snippet) || /\/\/.*FIXME/i.test(snippet)) {
        return "Check marked as incomplete (TODO/FIXME)";
    }
    if (/process\.env|NODE_ENV/i.test(snippet)) {
        return "Check bypassed in development mode";
    }
    if (type === "ownership" && /==[^=]/.test(snippet)) {
        return "Using loose equality (==) instead of strict (===)";
    }
    return undefined;
}
/**
 * Find database queries in code
 */
function findDbQueries(content, filePath) {
    const queries = [];
    const lines = content.split("\n");
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        for (const { pattern, type } of DB_ACCESS_PATTERNS) {
            if (pattern.test(line)) {
                // Get context
                const contextStart = Math.max(0, i - 2);
                const contextEnd = Math.min(lines.length, i + 3);
                const snippet = lines.slice(contextStart, contextEnd).join("\n");
                // Try to extract table name
                const tableMatch = snippet.match(/(?:from|into|update|delete from|\.)\s*['"` ]?(\w+)['"` ]?/i);
                queries.push({
                    type,
                    table: tableMatch?.[1],
                    hasOwnershipFilter: hasOwnershipFilter(snippet),
                    location: { file: filePath, line: i + 1 },
                    snippet,
                });
            }
        }
    }
    return queries;
}
/**
 * Check if query includes ownership filter
 */
function hasOwnershipFilter(snippet) {
    const ownershipPatterns = [
        /where\s*:?\s*\{[^}]*userId/i,
        /where\s*:?\s*\{[^}]*user[_.]?id/i,
        /where\s*:?\s*\{[^}]*owner/i,
        /where\s*:?\s*\{[^}]*createdBy/i,
        /userId\s*:\s*(?:req|ctx|user)\.user/i,
        /\.\s*filter\s*\([^)]*user/i,
        /AND\s+user_?id\s*=/i,
        /WHERE\s+user_?id\s*=/i,
    ];
    return ownershipPatterns.some((p) => p.test(snippet));
}
/**
 * Extract endpoints from a single file
 */
async function extractEndpointsFromFile(filePath, framework, projectPath) {
    const content = await readFile(filePath, "utf-8");
    const endpoints = [];
    const relativeFilePath = filePath.replace(projectPath + "/", "");
    // Handle Next.js specially since routes are file-based
    if (framework === "nextjs") {
        const apiPath = getNextjsApiPath(relativeFilePath);
        if (apiPath) {
            // Find exported HTTP methods
            const methodPattern = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/gi;
            let match;
            const methods = [];
            while ((match = methodPattern.exec(content)) !== null) {
                methods.push(match[1].toUpperCase());
            }
            // If no explicit methods, it's a Pages API route that handles all methods
            if (methods.length === 0 && /export\s+default\s+(?:async\s+)?function/.test(content)) {
                methods.push("GET", "POST", "PUT", "DELETE", "PATCH");
            }
            if (methods.length > 0) {
                endpoints.push({
                    file: relativeFilePath,
                    line: 1,
                    method: methods.length === 1 ? methods[0] : methods,
                    path: apiPath,
                    framework: "nextjs",
                    pathParams: extractPathParams(apiPath),
                    hasAuth: hasAuthMiddleware(content),
                    authzChecks: findAuthzChecks(content, relativeFilePath),
                    hasDbAccess: DB_ACCESS_PATTERNS.some(({ pattern }) => pattern.test(content)),
                    dbQueries: findDbQueries(content, relativeFilePath),
                });
            }
        }
        return endpoints;
    }
    // Use framework-specific patterns
    const patterns = ROUTE_PATTERNS[framework] || [];
    for (const { pattern, methodGroup, pathGroup } of patterns) {
        let match;
        const regex = new RegExp(pattern.source, pattern.flags);
        while ((match = regex.exec(content)) !== null) {
            const method = methodGroup > 0 ? match[methodGroup]?.toUpperCase() : "GET";
            const path = pathGroup > 0 ? match[pathGroup] : "";
            if (path) {
                // Find the line number
                const linesBefore = content.slice(0, match.index).split("\n");
                const line = linesBefore.length;
                endpoints.push({
                    file: relativeFilePath,
                    line,
                    method,
                    path,
                    framework,
                    pathParams: extractPathParams(path),
                    hasAuth: hasAuthMiddleware(content),
                    authzChecks: findAuthzChecks(content, relativeFilePath),
                    hasDbAccess: DB_ACCESS_PATTERNS.some(({ pattern }) => pattern.test(content)),
                    dbQueries: findDbQueries(content, relativeFilePath),
                });
            }
        }
    }
    return endpoints;
}
/**
 * Get file patterns to search for endpoints
 */
function getSearchPatterns(framework) {
    switch (framework) {
        case "nextjs":
            return [
                "**/app/api/**/route.{ts,js,tsx,jsx}",
                "**/pages/api/**/*.{ts,js,tsx,jsx}",
            ];
        case "express":
        case "fastify":
        case "koa":
        case "hapi":
            return [
                "**/routes/**/*.{ts,js}",
                "**/router/**/*.{ts,js}",
                "**/api/**/*.{ts,js}",
                "**/controllers/**/*.{ts,js}",
            ];
        case "nestjs":
            return ["**/*.controller.{ts,js}"];
        case "django":
            return ["**/views.py", "**/urls.py", "**/api/**/*.py"];
        case "flask":
        case "fastapi":
            return ["**/routes/**/*.py", "**/routers/**/*.py", "**/api/**/*.py", "**/*_router.py"];
        case "rails":
            return ["**/routes.rb", "**/controllers/**/*.rb"];
        case "spring":
            return ["**/*Controller.java", "**/*RestController.java"];
        case "laravel":
            return ["**/routes/*.php", "**/Http/Controllers/**/*.php"];
        case "gin":
        case "echo":
        case "fiber":
            return ["**/routes/**/*.go", "**/handlers/**/*.go", "**/api/**/*.go"];
        default:
            return [
                "**/routes/**/*.{ts,js}",
                "**/api/**/*.{ts,js,py,go,java,php,rb}",
                "**/controllers/**/*.{ts,js,py,go,java,php,rb}",
            ];
    }
}
/**
 * Extract all endpoints from a project
 */
export async function extractEndpoints(projectPath, options) {
    const framework = options?.framework || (await detectFramework(projectPath));
    const searchPatterns = options?.include || getSearchPatterns(framework);
    const exclude = options?.exclude || ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"];
    logger.info("endpoint_analyzer.extract", { projectPath, framework, patterns: searchPatterns });
    const endpoints = [];
    for (const pattern of searchPatterns) {
        try {
            const files = await glob(pattern, {
                cwd: projectPath,
                ignore: exclude,
                absolute: true,
            });
            for (const file of files) {
                try {
                    const fileEndpoints = await extractEndpointsFromFile(file, framework, projectPath);
                    endpoints.push(...fileEndpoints);
                }
                catch (error) {
                    logger.debug("endpoint_analyzer.file_error", { file, error: String(error) });
                }
            }
        }
        catch (error) {
            logger.debug("endpoint_analyzer.glob_error", { pattern, error: String(error) });
        }
    }
    logger.info("endpoint_analyzer.complete", { endpoints: endpoints.length });
    return endpoints;
}
/**
 * Infer resource type from endpoint path
 */
export function inferResourceType(path) {
    // Common RESTful resource patterns
    const resourcePatterns = [
        /\/api\/v?\d*\/?(\w+)(?:\/|$)/i, // /api/v1/users or /api/users
        /\/(\w+)(?:\/|$)/i, // /users
    ];
    for (const pattern of resourcePatterns) {
        const match = path.match(pattern);
        if (match) {
            const resource = match[1].toLowerCase();
            // Singularize common patterns
            if (resource.endsWith("ies")) {
                return resource.slice(0, -3) + "y";
            }
            // Handle words ending in "ses" like "addresses" -> "address"
            if (resource.endsWith("ses") && resource.length > 4) {
                return resource.slice(0, -2);
            }
            // Handle words ending in "es" like "boxes" -> "box", "statuses" -> "status"
            if (resource.endsWith("xes") || resource.endsWith("ches") || resource.endsWith("shes")) {
                return resource.slice(0, -2);
            }
            if (resource.endsWith("s") && !resource.endsWith("ss")) {
                return resource.slice(0, -1);
            }
            return resource;
        }
    }
    return undefined;
}
//# sourceMappingURL=endpoint-analyzer.js.map