/**
 * Filesystem scanning for the file-based router.
 *
 * Scanning happens once at startup. We build a table of routes from the files
 * that exist on disk; request paths are later matched against that table.
 * Crucially, request input is NEVER turned into a file path — this is what
 * makes the router immune to path-traversal.
 */

import { existsSync, readdirSync, statSync } from "node:fs";
import { join, relative, sep } from "node:path";

/** Extensions we are willing to load as route modules. */
const ALLOWED_EXTENSIONS = [".ts", ".tsx", ".wrn"] as const;

export interface ScannedFile {
  /** Absolute path to the file on disk. */
  file: string;
  /** Path relative to the scanned base directory, using forward slashes. */
  rel: string;
}

function hasAllowedExtension(name: string): boolean {
  return ALLOWED_EXTENSIONS.some((ext) => name.endsWith(ext));
}

/** Hidden files/dirs (dotfiles) and underscore-prefixed files are ignored. */
function isIgnored(name: string): boolean {
  return name.startsWith(".") || name.startsWith("_");
}

/**
 * Recursively collect allowed route files under `baseDir`.
 * Returns [] if the directory does not exist (a route kind may be unused).
 *
 * `extraExtensions` widens the allow-list for a caller that scans a non-route
 * directory and accepts plain `.js` modules (currently only `app/authz`); it
 * defaults to empty so every other caller — route scanning (`app/pages`,
 * `app/api`, `app/realtime`, ...) as well as `app/schemas`, which does not
 * pass it and so still only sees `.ts`/`.tsx`/`.wrn` — is unaffected.
 */
export function scanDir(baseDir: string, extraExtensions: readonly string[] = []): ScannedFile[] {
  if (!existsSync(baseDir)) return [];

  const out: ScannedFile[] = [];

  const walk = (dir: string): void => {
    for (const entry of readdirSync(dir)) {
      if (isIgnored(entry)) continue;
      const abs = join(dir, entry);
      const stats = statSync(abs);
      if (stats.isDirectory()) {
        walk(abs);
      } else if (
        stats.isFile() &&
        (hasAllowedExtension(entry) || extraExtensions.some((ext) => entry.endsWith(ext)))
      ) {
        out.push({
          file: abs,
          rel: relative(baseDir, abs).split(sep).join("/"),
        });
      }
    }
  };

  walk(baseDir);
  return out;
}
