import { readFile, access, stat, readdir } from "node:fs/promises";
import { join, resolve, dirname, relative } from "node:path";
import { parse } from "@babel/parser";
import { findStaticImports, parseStaticImport } from "mlly";
import { joinURL } from "ufo";
import { getFullImportPath } from "./resolver.js";
import { traverse } from "./traverse.js";
import { getLogger } from "./util/logger.js";
const CONVENTIONAL_ROUTER_PATHS = [
    "router.tsx",
    "router.ts",
    "src/router.tsx",
    "src/router.ts",
];
let cachedRouterFileLocation = null;
/**
 * Resolve a .js import path to the actual source file (.ts, .tsx, .jsx)
 *
 * In development, imports may reference .js files but the actual source files
 * have TypeScript extensions. This function tries to find the actual source file.
 */
async function resolveSourceFile(jsPath) {
    // If the file exists as-is, return it
    try {
        await access(jsPath);
        return jsPath;
    }
    catch {
        // File doesn't exist, try TypeScript variants
    }
    // Try different extensions
    const extensions = [".ts", ".tsx", ".jsx"];
    const pathWithoutExt = jsPath.replace(/\.js$/, "");
    for (const ext of extensions) {
        const tsPath = pathWithoutExt + ext;
        try {
            await access(tsPath);
            return tsPath;
        }
        catch {
            // Try next extension
        }
    }
    // If none found, return original path (will error later)
    return jsPath;
}
/**
 * Extract local names that refer to createBrowserRouter from react-router imports
 * Handles import aliasing like: import { createBrowserRouter as createRouter } from "react-router"
 */
function extractReactRouterImports(content) {
    const createBrowserRouterAliases = new Set();
    const namespaceImports = new Set();
    try {
        // find the react-router import statement
        const reactRouterImport = findStaticImports(content).find((x) => x.specifier === "react-router");
        if (!reactRouterImport) {
            throw new Error("react-router import not found");
        }
        const parsedReactRouterImport = parseStaticImport(reactRouterImport);
        // loop over named imports
        if (parsedReactRouterImport.namedImports) {
            // for each named import
            for (const [name, alias] of Object.entries(parsedReactRouterImport.namedImports)) {
                // add either the name or the alias (eg. import { createBrowserRouter as createRouter } from 'react-router') to the set
                if (name === "createBrowserRouter") {
                    createBrowserRouterAliases.add(name === alias ? name : alias);
                }
            }
        }
        if (parsedReactRouterImport.namespacedImport) {
            namespaceImports.add(parsedReactRouterImport.namespacedImport);
        }
    }
    catch (e) {
        getLogger().debug(`Error extracting react-router imports: ${e}`);
    }
    return { createBrowserRouterAliases, namespaceImports };
}
/**
 * Check if content contains createBrowserRouter call, accounting for import aliases
 */
function hasCreateBrowserRouter(content) {
    const { createBrowserRouterAliases, namespaceImports } = extractReactRouterImports(content);
    // Quick substring check first for performance
    if (!content.includes("createBrowserRouter") &&
        createBrowserRouterAliases.size === 0 &&
        namespaceImports.size === 0) {
        return false;
    }
    try {
        const ast = parse(content, {
            sourceType: "module",
            plugins: ["typescript", "jsx"],
        });
        let found = false;
        traverse(ast, {
            CallExpression(path) {
                const node = path.node;
                // Check for direct call: createBrowserRouter() or alias()
                if (node.callee.type === "Identifier") {
                    if (createBrowserRouterAliases.has(node.callee.name)) {
                        found = true;
                        path.stop();
                    }
                }
                // Check for namespace call: ReactRouter.createBrowserRouter()
                else if (node.callee.type === "MemberExpression") {
                    if (node.callee.object.type === "Identifier" &&
                        namespaceImports.has(node.callee.object.name) &&
                        node.callee.property.type === "Identifier" &&
                        node.callee.property.name === "createBrowserRouter") {
                        found = true;
                        path.stop();
                    }
                }
            },
        });
        return found;
    }
    catch {
        // If parsing fails, fall back to substring check
        return content.includes("createBrowserRouter");
    }
}
/**
 * Find all TypeScript/TSX files in rootDir
 */
async function findAllTsFiles(rootDir) {
    const files = [];
    async function scan(dir, depth = 0) {
        // Limit depth to avoid deep recursion
        if (depth > 3)
            return;
        try {
            const entries = await readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = join(dir, entry.name);
                // Skip node_modules and hidden directories
                if (entry.name === "node_modules" || entry.name.startsWith(".")) {
                    continue;
                }
                if (entry.isDirectory()) {
                    await scan(fullPath, depth + 1);
                }
                else if (entry.isFile() &&
                    (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
                    files.push(fullPath);
                }
            }
        }
        catch (e) {
            // Ignore permission errors or missing directories
            getLogger().debug(`Error scanning directory ${dir}: ${e}`);
        }
    }
    await scan(rootDir);
    return files;
}
/**
 * React Router v7 Support
 *
 * This implementation parses createBrowserRouter route configurations to extract page components.
 *
 * Check the test file for supported route configurations.
 *
 * Import Aliasing:
 * - Handles: import { createBrowserRouter as createRouter } from "react-router"
 * - Handles: import * as ReactRouter from "react-router"
 *
 * Excluded Routes:
 * - Catch-all routes with path: "*"
 * - Routes with inline components (not imported from files)
 * - Layout/wrapper components that aren't page components
 *
 * File Organization:
 * - No assumptions about directory structure
 * - Page components can be located anywhere in the project
 * - Only imports with relative paths (starting with . or /) are tracked
 */
/**
 * Find the router file in the project (React Router v7)
 */
export async function findRouterFile(rootDir) {
    // Check cache first with mtime validation
    if (cachedRouterFileLocation) {
        try {
            const stats = await stat(cachedRouterFileLocation.path);
            if (stats.mtimeMs === cachedRouterFileLocation.mtime) {
                return cachedRouterFileLocation.path;
            }
            else {
                cachedRouterFileLocation = null;
            }
        }
        catch {
            cachedRouterFileLocation = null;
        }
    }
    // Try conventional locations first
    for (const conventionalPath of CONVENTIONAL_ROUTER_PATHS) {
        const fullPath = join(rootDir, conventionalPath);
        try {
            await access(fullPath);
            const content = await readFile(fullPath, "utf-8");
            if (hasCreateBrowserRouter(content)) {
                const stats = await stat(fullPath);
                cachedRouterFileLocation = { path: fullPath, mtime: stats.mtimeMs };
                getLogger().info(`Found router file at: ${conventionalPath}`);
                return fullPath;
            }
        }
        catch {
            continue;
        }
    }
    // Fall back to scanning all TypeScript files in rootDir
    getLogger().info("Router not found in conventional locations, scanning TypeScript files…");
    const allTsFiles = await findAllTsFiles(rootDir);
    for (const file of allTsFiles) {
        try {
            const content = await readFile(file, "utf-8");
            if (hasCreateBrowserRouter(content)) {
                const stats = await stat(file);
                cachedRouterFileLocation = { path: file, mtime: stats.mtimeMs };
                getLogger().info(`Found router file: ${relative(rootDir, file)}`);
                return file;
            }
        }
        catch {
            continue;
        }
    }
    throw new Error("Could not find router file with createBrowserRouter in project");
}
/**
 * Extract all static imports
 * Maps component identifier names to their import paths
 * We track ALL imports because any import can be used as a page Component
 */
function extractAllImports(content) {
    const imports = new Map();
    try {
        const staticImports = findStaticImports(content);
        for (const staticImport of staticImports) {
            const parsed = parseStaticImport(staticImport);
            // Skip imports from node_modules and react-router
            const isRelativeOrAlias = parsed.specifier.startsWith(".") ||
                parsed.specifier.startsWith("/") ||
                parsed.specifier.startsWith("@/");
            if (!isRelativeOrAlias) {
                continue;
            }
            // Handle default imports: import Page1 from "./components/Page1.tsx"
            if (parsed.defaultImport) {
                imports.set(parsed.defaultImport, parsed.specifier);
            }
            // Handle named imports: import { Page1 } from "./components/Page1.tsx"
            if (parsed.namedImports) {
                for (const [name, alias] of Object.entries(parsed.namedImports)) {
                    const localName = name === alias ? name : alias;
                    imports.set(localName, parsed.specifier);
                }
            }
        }
    }
    catch (e) {
        getLogger().debug(`Error extracting imports: ${e}`);
    }
    return imports;
}
/**
 * Parse the router file to extract route definitions (React Router v7)
 */
export async function parseRouterFile(routerFilePath, rootDir) {
    try {
        const content = await readFile(routerFilePath, "utf-8");
        const { createBrowserRouterAliases, namespaceImports } = extractReactRouterImports(content);
        const allImports = extractAllImports(content);
        const ast = parse(content, {
            sourceType: "module",
            plugins: ["typescript", "jsx"],
        });
        const routes = [];
        let routesArray = null;
        traverse(ast, {
            CallExpression(astPath) {
                const node = astPath.node;
                let isCreateBrowserRouterCall = false;
                // Check for direct call: createBrowserRouter() or alias()
                if (node.callee.type === "Identifier") {
                    if (createBrowserRouterAliases.has(node.callee.name)) {
                        isCreateBrowserRouterCall = true;
                    }
                }
                // Check for namespace call: ReactRouter.createBrowserRouter()
                else if (node.callee.type === "MemberExpression") {
                    if (node.callee.object.type === "Identifier" &&
                        namespaceImports.has(node.callee.object.name) &&
                        node.callee.property.type === "Identifier" &&
                        node.callee.property.name === "createBrowserRouter") {
                        isCreateBrowserRouterCall = true;
                    }
                }
                if (isCreateBrowserRouterCall && node.arguments.length > 0) {
                    const firstArg = node.arguments[0];
                    if (firstArg?.type === "ArrayExpression") {
                        routesArray = firstArg;
                    }
                }
            },
        });
        // Process routes asynchronously after traverse
        if (routesArray) {
            await extractRoutesFromArray(routesArray, routes, routerFilePath, rootDir, "", allImports);
        }
        return routes;
    }
    catch (e) {
        getLogger().error(`Error parsing router file: ${e}`);
        return [];
    }
}
async function extractRoutesFromArray(node, routes, routerFilePath, rootDir, parentPath, allImports) {
    for (const element of node.elements) {
        if (element && element.type === "ObjectExpression") {
            await extractRouteFromObject(element, routes, routerFilePath, rootDir, parentPath, allImports);
        }
    }
}
async function extractRouteFromObject(node, routes, routerFilePath, rootDir, parentPath, allImports) {
    let routePath = null;
    let pageImportPath = null;
    let children = null;
    let hasIndex = false;
    for (const prop of node.properties) {
        if (prop.type !== "ObjectProperty")
            continue;
        const key = prop.key.type === "Identifier" ? prop.key.name : null;
        if (key === "path" && prop.value.type === "StringLiteral") {
            routePath = prop.value.value;
        }
        else if (key === "index" && prop.value.type === "BooleanLiteral") {
            hasIndex = prop.value.value;
        }
        else if (key === "lazy" &&
            prop.value.type === "ArrowFunctionExpression") {
            // Extract import path from lazy function arrow expression
            let body = prop.value.body;
            // Case 1: Block statement body (async function with multiple statements)
            // Examples:
            //   lazy: async () => { const { default: Component } = await import("./Page.tsx"); return { Component } }
            //   lazy: async () => { const mod = await import("./Page.tsx"); return { Component: mod.default } }
            if (body.type === "BlockStatement") {
                // Search through statements for import() call in variable declarations
                for (const statement of body.body) {
                    if (statement.type !== "VariableDeclaration")
                        continue;
                    for (const declarator of statement.declarations) {
                        let init = declarator.init;
                        // Unwrap AwaitExpression if present: await import(...)
                        if (init?.type === "AwaitExpression") {
                            init = init.argument;
                        }
                        // Check if this is an import() call
                        if (init?.type === "CallExpression" &&
                            init.callee.type === "Import" &&
                            init.arguments[0]?.type === "StringLiteral") {
                            pageImportPath = init.arguments[0].value;
                            break;
                        }
                    }
                    if (pageImportPath)
                        break;
                }
                // Exit early if we found the import in block statement
                continue;
            }
            // Case 2: Expression body with await
            // Example: lazy: async () => await import("./Page.tsx")
            if (body.type === "AwaitExpression") {
                body = body.argument;
            }
            // Case 3: Direct import() call
            // Examples:
            //   lazy: () => import("./Page.tsx")
            //   lazy: async () => import("./Page.tsx")
            if (body.type === "CallExpression" && body.callee.type === "Import") {
                if (body.arguments[0]?.type === "StringLiteral") {
                    pageImportPath = body.arguments[0].value;
                }
                continue;
            }
            // Case 4: import().then(...) pattern
            // Examples:
            //   lazy: () => import("./Page.tsx").then(mod => ({ Component: mod.default }))
            //   lazy: () => import("./Page.tsx").then(mod => ({ Component: mod.SomePage }))
            if (body.type === "CallExpression" &&
                body.callee.type === "MemberExpression" &&
                body.callee.object.type === "CallExpression" &&
                body.callee.object.callee.type === "Import") {
                const importCall = body.callee.object;
                if (importCall.arguments[0]?.type === "StringLiteral") {
                    pageImportPath = importCall.arguments[0].value;
                }
                continue;
            }
        }
        else if (key === "Component" && prop.value.type === "Identifier") {
            // Component property uses an identifier: Component: HomePage
            const componentName = prop.value.name;
            if (allImports.has(componentName)) {
                pageImportPath = allImports.get(componentName);
            }
        }
        else if (key === "element" && prop.value.type === "JSXElement") {
            // element property uses JSX: element: <HomePage />
            const jsxElement = prop.value;
            if (jsxElement.openingElement.name.type === "JSXIdentifier") {
                const componentName = jsxElement.openingElement.name.name;
                if (allImports.has(componentName)) {
                    pageImportPath = allImports.get(componentName);
                }
            }
        }
        else if (key === "children" && prop.value.type === "ArrayExpression") {
            children = prop.value;
        }
    }
    // Check if any children have index: true
    let hasIndexChild = false;
    if (children) {
        for (const child of children.elements) {
            if (child && child.type === "ObjectExpression") {
                for (const prop of child.properties) {
                    if (prop.type === "ObjectProperty" &&
                        prop.key.type === "Identifier" &&
                        prop.key.name === "index" &&
                        prop.value.type === "BooleanLiteral" &&
                        prop.value.value === true) {
                        hasIndexChild = true;
                        break;
                    }
                }
            }
            if (hasIndexChild)
                break;
        }
    }
    // Add route if we found a page import (either lazy or static)
    // Skip routes with:
    // - path "*" (catch-all routes)
    // - no path and no index (path-less layout routes)
    // - routes that have an index child (layout routes)
    if (pageImportPath &&
        routePath !== "*" &&
        (routePath || hasIndex) &&
        !hasIndexChild) {
        // Build the full route path:
        // 1. If routePath is explicitly set, use it (takes precedence over index)
        // 2. If only index: true (no explicit path), use parent path
        let fullRoutePath;
        if (routePath) {
            fullRoutePath = joinURL(parentPath, routePath);
        }
        else if (hasIndex) {
            fullRoutePath = parentPath;
        }
        else {
            // This shouldn't happen due to our check above, but handle it anyway
            fullRoutePath = parentPath;
        }
        // Normalize empty path to "/"
        if (!fullRoutePath) {
            fullRoutePath = "/";
        }
        // Resolve the import path to an absolute file path, try Vite's resolver first, fall back to manual
        let pageFilePath = null;
        const viteResolved = await getFullImportPath(pageImportPath);
        if (viteResolved) {
            pageFilePath = viteResolved;
        }
        else {
            const isRelativePath = pageImportPath.startsWith("./") || pageImportPath.startsWith("../");
            if (isRelativePath) {
                const resolvedPath = resolve(dirname(routerFilePath), pageImportPath);
                pageFilePath = await resolveSourceFile(resolvedPath);
            }
        }
        if (pageFilePath) {
            routes.push({
                path: fullRoutePath,
                pageImportPath,
                pageFilePath,
            });
        }
        else {
            const errorMessage = `Failed to resolve import path "${pageImportPath}" for route "${fullRoutePath}". This route will not be available.`;
            getLogger().error(errorMessage, {
                error: {
                    kind: "ImportResolutionError",
                    message: `Import: ${pageImportPath}, Route: ${fullRoutePath}, Router: ${routerFilePath}`,
                },
            });
        }
    }
    // Recursively process children
    if (children) {
        const currentPath = routePath ? joinURL(parentPath, routePath) : parentPath;
        // Normalize empty path to "/" before passing to children
        const normalizedCurrentPath = currentPath || "/";
        await extractRoutesFromArray(children, routes, routerFilePath, rootDir, normalizedCurrentPath, allImports);
    }
}
//# sourceMappingURL=router-parser.js.map