import fs from "fs";
import path from "path";

// File patterns for API and auxiliary files
const FILE_PATTERNS = {
  ROUTE: "route.ts",
  LOADING: "loading.tsx",
  ERROR: "error.tsx",
};

// Allowed page file extensions (and for layout files)
const allowedExtensions = ["tsx", "ts", "jsx", "js", "md", "mdx"];

// Determine the app directory
const appDirectory =
  fs.existsSync(path.join(process.cwd(), "app"))
    ? path.join(process.cwd(), "app")
    : fs.existsSync(path.join(process.cwd(), "src/app"))
    ? path.join(process.cwd(), "src/app")
    : null;

if (!appDirectory) {
  throw new Error("App directory not found");
}

const fileContentsCache = new Map<string, string>();

let currentFilePath: string | null = null;
let currentFileContent: string | null = null;

function setCurrentFile(filePath: string) {
  currentFilePath = filePath;
  if (fileContentsCache.has(filePath)) {
    currentFileContent = fileContentsCache.get(filePath)!;
  } else {
    currentFileContent = fs.readFileSync(filePath, "utf8");
    fileContentsCache.set(filePath, currentFileContent);
  }
}

/**
 * Represents a URL segment computed from a folder name.
 */
interface RouteSegment {
  urlSegment: string; // What appears in the URL (for example: "blog", "..." or ".../..." for catch-all)
  displayName: string; // A “clean” name for display (e.g. dynamic segments without brackets)
}

/**
 * Given a folder name, returns a RouteSegment or null if that folder is not meant to affect the URL.
 *
 * Rules:
 * - Folders starting with "@" (parallel routes) are ignored.
 * - Folders that are entirely wrapped in parentheses (e.g. `(group)`) are route groups and are omitted.
 * - Intercepting folders such as `(.)photo` have their intercept prefix stripped.
 * - Optional catch-all segments (e.g. `[[...slug]]`) and catch-all segments (e.g. `[...slug]`)
 *   are rendered as two sets of ellipsis (".../..."), and the display name is the parameter name.
 * - Standard dynamic segments (e.g. `[post]`) are rendered as a single ellipsis ("...").
 * - Otherwise, the folder name is used as a static segment.
 */
function getRouteSegment(folderName: string): RouteSegment | null {
  // Ignore parallel route folders.
  if (folderName.startsWith("@")) return null;

  // Intercepting routes: e.g. "(..)photo" or "(.)photo" → remove the intercept prefix.
  const interceptMatch = folderName.match(/^\((\.+)\)(.+)$/);
  if (interceptMatch) {
    return { urlSegment: interceptMatch[2], displayName: interceptMatch[2] };
  }

  // Route groups: if the folder is entirely wrapped in parentheses (e.g. "(showcase)"), ignore it.
  if (/^\(.*\)$/.test(folderName)) {
    return null;
  }

  // Optional catch-all: [[...slug]]
  const optionalCatchAllMatch = folderName.match(/^\[\[\.\.\.(.+)\]\]$/);
  if (optionalCatchAllMatch) {
    const paramName = optionalCatchAllMatch[1];
    return { urlSegment: ".../...", displayName: paramName };
  }

  // Catch-all: [...slug]
  const catchAllMatch = folderName.match(/^\[\.\.\.(.+)\]$/);
  if (catchAllMatch) {
    const paramName = catchAllMatch[1];
    return { urlSegment: ".../...", displayName: paramName };
  }

  // Standard dynamic segment: [param]
  if (folderName.startsWith("[") && folderName.endsWith("]")) {
    const paramName = folderName.slice(1, -1);
    return { urlSegment: "...", displayName: paramName };
  }

  // Otherwise, return the static folder name.
  return { urlSegment: folderName, displayName: folderName };
}

/**
 * Searches upward from a given page file's directory for a layout file.
 * Returns the first layout file found (relative to process.cwd()), or null if none is found.
 */
function getActiveLayoutFile(pageFilePath: string): string | null {
  let dir = path.dirname(pageFilePath);
  // Loop until we reach the appDirectory (or beyond)
  while (dir.startsWith(appDirectory)) {
    for (const ext of allowedExtensions) {
      const layoutPath = path.join(dir, `layout.${ext}`);
      if (fs.existsSync(layoutPath)) {
        return path.relative(process.cwd(), layoutPath);
      }
    }
    // Move one directory up.
    const parentDir = path.dirname(dir);
    if (parentDir === dir) break;
    dir = parentDir;
  }
  return null;
}

/**
 * Checks for a metadata export.
 */
function hasMetadata() {
  const metadataPatterns = [
    { pattern: "export const metadata", returnVal: "metadata" },
    { pattern: "export async function generateMetadata", returnVal: "generateMetadata" },
    { pattern: "export function generateMetadata", returnVal: "generateMetadata" },
    { pattern: "export const generateMetadata", returnVal: "generateMetadata" },
    { pattern: "export let metadata", returnVal: "metadata" },
    { pattern: "export var metadata", returnVal: "metadata" },
    { pattern: "export const metadata:", returnVal: "metadata" },
    { pattern: "export const generateMetadata:", returnVal: "generateMetadata" },
  ];

  const found = metadataPatterns.find(({ pattern }) =>
    currentFileContent!.includes(pattern)
  );
  return found ? found.returnVal : null;
}

/**
 * Checks if the file is a client component.
 * Only checks the very top of the file (after trimming whitespace).
 */
function isClientComponent() {
  const trimmed = currentFileContent!.trimStart();
  return trimmed.startsWith('"use client"') || trimmed.startsWith("'use client'");
}

/**
 * Checks if the file has a server action directive.
 * Only checks the very top of the file.
 */
function hasServerAction() {
  const trimmed = currentFileContent!.trimStart();
  return trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'");
}

function extractDynamicValue() {
  const match = currentFileContent!.match(
    /export (?:const|let|var) dynamic\s*=\s*['"]([^'"]+)['"]/
  );
  return match ? match[1] : "";
}

function extractRevalidateValue() {
  const match = currentFileContent!.match(
    /export (?:const|let|var) revalidate\s*=\s*(\d+)/
  );
  return match ? match[1] : "";
}

function extractFetchCacheValue() {
  const match = currentFileContent!.match(
    /export (?:const|let|var) fetchCache\s*=\s*['"]([^'"]+)['"]/
  );
  return match ? match[1] : "";
}

function hasParallelRoute() {
  return currentFilePath!.includes("@");
}

function hasInterceptingRoute() {
  return (
    currentFilePath!.includes("(.)") ||
    currentFilePath!.includes("(..)") ||
    currentFilePath!.includes("(...)")
  );
}

/**
 * Extracts exported functions from the current file.
 *
 * Returns an object with:
 * - defaultExport: the name of the default export (if any)
 * - namedExports: an array of names for other exported functions
 *
 * It looks for function declarations (including async) as well as arrow function exports.
 */
function extractExportedFunctions() {
  let defaultExport = "";
  const defaultMatch =
    currentFileContent!.match(/export default (async\s+function\s+|function\s+)?(\w+)/);
  if (defaultMatch) {
    defaultExport = defaultMatch[2] || defaultMatch[1] || "";
  }

  // Find non-default function declarations.
  const namedDeclarationMatches = [
    ...currentFileContent!.matchAll(/export\s+(?!default)(?:async\s+)?function\s+(\w+)/g),
  ].map(match => match[1]);

  // Find non-default arrow function exports.
  const arrowMatches = [
    ...currentFileContent!.matchAll(/export\s+(?!default)(?:const|let|var)\s+(\w+)\s*=\s*\(?.*?\)?\s*=>/g),
  ].map(match => match[1]);

  // Merge and remove duplicates.
  const namedExports = Array.from(new Set([...namedDeclarationMatches, ...arrowMatches]));

  return { defaultExport, namedExports };
}

/**
 * Extracts the HTTP methods from an API route file.
 */
function extractHttpMethods() {
  const methods: string[] = [];
  const methodPatterns = [
    "GET",
    "POST",
    "PUT",
    "DELETE",
    "PATCH",
    "OPTIONS",
    "HEAD",
  ];

  // Check for a destructured handlers pattern (e.g. export const { GET, POST } = handlers)
  const handlersMatch = currentFileContent!.match(
    /export const \{([^}]+)\}\s*=\s*handlers/
  );
  if (handlersMatch) {
    const destructuredMethods = handlersMatch[1]
      .split(",")
      .map((m) => m.trim());
    return [destructuredMethods.join(" | ")];
  }

  // Otherwise, look for individual async function exports.
  methodPatterns.forEach((method) => {
    if (currentFileContent!.includes(`export async function ${method}`)) {
      methods.push(method);
    }
  });

  return methods.length > 0 ? methods : ["GET"];
}

/**
 * Recursively lists all page routes as JSON objects.
 *
 * For each page file, we compute:
 * - routePath: the URL a user would type (joining computed segments).
 * - routeName: the display name from the last segment.
 * - parentRoute: the URL of the parent route (all segments except the last).
 * - activeLayout: the active layout file for that page (if one is found).
 * - exportedFunctions: an object with the default export and any named exports.
 */
function listRoutes(
  dir: string,
  segments: RouteSegment[] = []
): any[] {
  let routes: any[] = [];
  try {
    fs.readdirSync(dir, { withFileTypes: true }).forEach((dirent) => {
      const fullPath = path.join(dir, dirent.name);
      if (dirent.isDirectory() && !dirent.name.startsWith("_")) {
        const seg = getRouteSegment(dirent.name);
        const newSegments = seg ? [...segments, seg] : segments;
        routes = routes.concat(listRoutes(fullPath, newSegments));
      } else if (dirent.isFile()) {
        const match = dirent.name.match(
          /^page\.((?:tsx|ts|jsx|js|md|mdx))$/
        );
        if (match) {
          setCurrentFile(fullPath);
          // Compute the routePath by joining all segments.
          let routePath =
            segments.length > 0
              ? "/" + segments.map((s) => s.urlSegment).join("/")
              : "/";
          // If the last segment is a standard dynamic segment ("...") and the routePath doesn’t already end with a slash, add one.
          if (
            segments.length > 0 &&
            segments[segments.length - 1].urlSegment === "..." &&
            !routePath.endsWith("/")
          ) {
            routePath += "/";
          }
          const parentRoute =
            segments.length > 1
              ? "/" +
                segments
                  .slice(0, segments.length - 1)
                  .map((s) => s.urlSegment)
                  .join("/")
              : "/";
          const routeName =
            segments.length > 0 ? segments[segments.length - 1].displayName : "/";

          const exportedFunctions = extractExportedFunctions();
          const componentType = isClientComponent() ? "use client" : "server";
          const metadataExport = hasMetadata();
          const serverActionDirective = hasServerAction();
          const dynamicValue = extractDynamicValue();
          const revalidateValue = extractRevalidateValue();
          const fetchCacheValue = extractFetchCacheValue();
          const isParallel = hasParallelRoute();
          const isIntercepting = hasInterceptingRoute();
          const loadingFile = hasLoadingFile();
          const errorFile = hasErrorFile();
          const fileLocation = path.relative(process.cwd(), fullPath);
          const activeLayout = getActiveLayoutFile(fullPath);

          routes.push({
            type: "page",
            routeName,
            routePath,
            parentRoute,
            file: fileLocation,
            extension: match[1],
            exportedFunctions, // contains { defaultExport, namedExports }
            componentType,
            metadata: metadataExport,
            serverAction: serverActionDirective,
            dynamic: dynamicValue,
            revalidate: revalidateValue,
            fetchCache: fetchCacheValue,
            isParallel,
            isIntercepting,
            hasLoadingFile: loadingFile,
            hasErrorFile: errorFile,
            activeLayout,
          });
        }
      }
    });
  } catch (error: any) {
    console.error(`Error reading directory ${dir}:`, error.message);
  }
  return routes;
}

/**
 * Recursively lists all API routes as JSON objects.
 *
 * For consistency, the API routes also compute a URL from the folder structure and track the parent route,
 * and also extract exported functions.
 */
function listApiRoutes(
  dir: string,
  segments: RouteSegment[] = []
): any[] {
  let apiRoutes: any[] = [];
  fs.readdirSync(dir, { withFileTypes: true }).forEach((dirent) => {
    const fullPath = path.join(dir, dirent.name);
    if (dirent.isDirectory() && !dirent.name.startsWith("_")) {
      const seg = getRouteSegment(dirent.name);
      const newSegments = seg ? [...segments, seg] : segments;
      apiRoutes = apiRoutes.concat(listApiRoutes(fullPath, newSegments));
    } else if (dirent.isFile() && dirent.name === FILE_PATTERNS.ROUTE) {
      setCurrentFile(fullPath);
      const routePath =
        segments.length > 0
          ? "/" + segments.map((s) => s.urlSegment).join("/")
          : "/";
      const parentRoute =
        segments.length > 1
          ? "/" +
            segments
              .slice(0, segments.length - 1)
              .map((s) => s.urlSegment)
              .join("/")
          : "/";
      const exportedFunctions = extractExportedFunctions();
      const functionName = exportedFunctions.defaultExport; // primary export for API routes
      const methods = extractHttpMethods();
      const fileLocation = path.relative(process.cwd(), fullPath);
      methods.forEach((method) => {
        apiRoutes.push({
          type: "api",
          httpMethod: method,
          exportedFunctions, // includes defaultExport and any namedExports
          routePath,
          parentRoute,
          file: fileLocation,
        });
      });
    }
  });
  return apiRoutes;
}

/**
 * The main function that explores routes.
 * Returns a JSON object with two keys:
 * - routes: an array of page route objects.
 * - apiRoutes: an array of API route objects.
 */
export async function exploreRoutes() {
  const routes = listRoutes(appDirectory);
  const apiRoutes = listApiRoutes(appDirectory);
  return {
    routes,
    apiRoutes,
  };
}

/**
 * Checks for the existence of a loading file in the current file's directory.
 */
function hasLoadingFile() {
  const directory = path.dirname(currentFilePath!);
  return fs.existsSync(path.join(directory, FILE_PATTERNS.LOADING));
}

/**
 * Checks for the existence of an error file in the current file's directory.
 */
function hasErrorFile() {
  const directory = path.dirname(currentFilePath!);
  return fs.existsSync(path.join(directory, FILE_PATTERNS.ERROR));
}

