/**
 * @wrnexus/router — file-based router.
 *
 * Maps the `app/` directory onto route tables:
 *   app/pages/index.tsx     -> GET /
 *   app/pages/about.tsx     -> GET /about
 *   app/pages/users/[id].tsx-> GET /users/:id
 *   app/api/hello.ts        -> /api/hello
 *   app/realtime/chat.ts    -> /realtime/chat
 *   app/pages/*.wrn api      -> embedded /api/* routes
 *   app/pages/*.wrn realtime -> embedded /realtime/* routes
 *   app/middleware/*.ts     -> global middleware (alphabetical)
 *   app/components/*.wrn   -> server-rendered components (by declaration),
 *                              mounted in a page via data-component="<name>"
 */

import { readFileSync } from "node:fs";
import { join, basename } from "node:path";
import { parse } from "@wrnexus/compiler";
import { isSafeIslandName, type Middleware } from "@wrnexus/core";
import { scanDir, type ScannedFile } from "./scan.ts";
import {
  compileRoutePattern,
  matchRoute,
  sortRoutes,
  findRouteConflicts,
  type Route,
  type RouteMatch,
} from "./match.ts";

export type { Route, RouteMatch } from "./match.ts";
export {
  compileRoutePattern,
  getRouteParams,
  matchRoute,
  sortRoutes,
  findRouteConflicts,
} from "./match.ts";
export { generateRoutesFile } from "./routes-gen.ts";

export interface ComponentRef {
  /** Validated component name (matches a `data-component` attribute). */
  name: string;
  /** Absolute path to the component's `.wrn` module. */
  file: string;
}

export interface Router {
  pages: Route[];
  api: Route[];
  realtime: Route[];
  /** Absolute paths of middleware modules, in execution order. */
  middlewareFiles: string[];
  /** Server-rendered `.wrn` components, mounted via `data-component`. */
  components: ComponentRef[];
  /** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
  layouts: ComponentRef[];
  /** Typed global/page stores discovered under `app/stores/`. */
  stores: ComponentRef[];
  /** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
  schemas: ComponentRef[];
  /** Authorization declarations (`app/authz/<name>.ts`) merged into the catalog. */
  authz: ComponentRef[];
  /** Service implementations (`app/services/<name>.ts`) mounted for inter-app calls. */
  services: ComponentRef[];
  matchPage(pathname: string): RouteMatch | null;
  matchApi(pathname: string): RouteMatch | null;
  matchRealtime(pathname: string): RouteMatch | null;
}

export interface ExternalRouteDefinition {
  kind: "page" | "api" | "realtime";
  path: string;
  entry: string;
  name?: string;
}

export interface RouterOptions {
  /**
   * Extra directories to scan for `.wrn` components (e.g. `@wrnexus/ui`).
   * Scanned before `app/components`, so an app component of the same name wins.
   */
  componentDirs?: string[];
  /** Package-owned routes registered by the plugin contribution system. */
  externalRoutes?: ExternalRouteDefinition[];
  /** Package-owned middleware executed before app/middleware. */
  middlewareFiles?: string[];
}

/**
 * Convert a scanned file's relative path into a URL route pattern.
 *  - strips the extension
 *  - drops a trailing `index` segment
 *  - prefixes with `prefix` (e.g. "/api")
 */
export function fileToRoute(rel: string, prefix = ""): string {
  const withoutExt = rel.replace(/\.(tsx|ts|wrn)$/, "");
  const segments = withoutExt
    .split("/")
    .filter(Boolean)
    .filter((segment) => !(segment.startsWith("(") && segment.endsWith(")")));
  if (["index", "page"].includes(segments[segments.length - 1] ?? "")) segments.pop();
  const tail = segments.join("/");
  const route = prefix + (tail ? "/" + tail : "");
  return route === "" ? "/" : route;
}

function buildRoutes(files: ScannedFile[], prefix: string): Route[] {
  const routes = files.map((f): Route => {
    const raw = fileToRoute(f.rel, prefix);
    const { regex, paramNames, paramMeta } = compileRoutePattern(raw);
    return { raw, file: f.file, regex, paramNames, paramMeta };
  });
  return sortRoutes(routes);
}

function normalizeEmbeddedApiPath(path: string): string {
  if (path === "/api" || path.startsWith("/api/")) return path;
  return `/api${path.startsWith("/") ? path : `/${path}`}`;
}

function routeFromRaw(raw: string, file: string): Route {
  const { regex, paramNames, paramMeta } = compileRoutePattern(raw);
  return { raw, file, regex, paramNames, paramMeta };
}

function embeddedWireRoutes(pageFiles: ScannedFile[]): Pick<Router, "api" | "realtime"> {
  const api: Route[] = [];
  const realtime: Route[] = [];

  for (const file of pageFiles) {
    if (!file.file.endsWith(".wrn")) continue;
    const ast = parse(readFileSync(file.file, "utf8"));

    for (const block of ast.apis) {
      api.push(routeFromRaw(normalizeEmbeddedApiPath(block.path), file.file));
    }

    for (const block of ast.realtimes) {
      if (!isSafeIslandName(block.name)) {
        console.warn(`[wrnexus] skipping realtime route with unsafe name: ${block.name}`);
        continue;
      }
      realtime.push(routeFromRaw(`/realtime/${block.name}`, file.file));
    }
  }

  return { api, realtime };
}

function warnRouteConflicts(kind: string, routes: Route[]): void {
  for (const conflict of findRouteConflicts(routes)) {
    console.warn(
      `[wrnexus] WRN-ROUTE-CONFLICT: duplicate ${kind} route '${conflict.raw}' in ${conflict.files.join(", ")}`,
    );
  }
}

/**
 * Merge package routes with application routes. Exact application paths always
 * win so a package can safely provide defaults that an app may override.
 */
function mergeExternalRoutes(
  kind: string,
  applicationRoutes: Route[],
  externalRoutes: Route[],
): Route[] {
  const applicationPaths = new Set(applicationRoutes.map((route) => route.raw));
  const retainedExternal = externalRoutes.filter((route) => {
    if (!applicationPaths.has(route.raw)) return true;
    console.warn(
      `[wrnexus] WRN-ROUTE-OVERRIDE: application ${kind} route '${route.raw}' overrides package route from ${route.file}`,
    );
    return false;
  });
  return sortRoutes([...applicationRoutes, ...retainedExternal]);
}

/** Scan an app directory and build all route tables. */
export function buildRouter(appDir: string, opts: RouterOptions = {}): Router {
  const pageFiles = scanDir(join(appDir, "pages"));
  const embedded = embeddedWireRoutes(pageFiles);
  const external = opts.externalRoutes ?? [];
  const externalPages = external
    .filter((route) => route.kind === "page")
    .map((route) => routeFromRaw(route.path, route.entry));
  const externalApi = external
    .filter((route) => route.kind === "api")
    .map((route) => routeFromRaw(route.path, route.entry));
  const externalRealtime = external
    .filter((route) => route.kind === "realtime")
    .map((route) => routeFromRaw(route.path, route.entry));
  const applicationPages = buildRoutes(pageFiles, "");
  const applicationApi = sortRoutes([
    ...buildRoutes(scanDir(join(appDir, "api")), "/api"),
    ...embedded.api,
  ]);
  const applicationRealtime = sortRoutes([
    ...buildRoutes(scanDir(join(appDir, "realtime")), "/realtime"),
    ...embedded.realtime,
  ]);
  const pages = mergeExternalRoutes("page", applicationPages, externalPages);
  const api = mergeExternalRoutes("API", applicationApi, externalApi);
  const realtime = mergeExternalRoutes("realtime", applicationRealtime, externalRealtime);

  warnRouteConflicts("page", pages);
  warnRouteConflicts("API", api);
  warnRouteConflicts("realtime", realtime);

  // Middleware runs in deterministic (alphabetical) order.
  const middlewareFiles = [
    ...new Set([
      ...(opts.middlewareFiles ?? []),
      ...scanDir(join(appDir, "middleware"))
        .map((f) => f.file)
        .sort(),
    ]),
  ];

  // Components are keyed case-insensitively by their declared name so later
  // scans override earlier ones without creating duplicate aliases. Library
  // dirs (e.g. @wrnexus/ui) are scanned first; app/components last.
  const componentMap = new Map<string, ComponentRef>();
  const scanComponents = (dir: string): void => {
    for (const f of scanDir(dir)) {
      if (!f.file.endsWith(".wrn")) continue;

      const fileName = basename(f.file).replace(/\.wrn$/, "");
      let name = fileName;

      try {
        const source = readFileSync(f.file, "utf8");
        const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(source);
        if (declaration) name = declaration[1]!;
      } catch (error) {
        console.warn(`[wrnexus] unable to inspect component declaration in ${f.file}`, error);
      }

      if (!isSafeIslandName(name)) {
        console.warn(`[wrnexus] skipping component with unsafe name: ${name}`);
        continue;
      }

      componentMap.set(name.toLowerCase(), { name, file: f.file });
    }
  };
  for (const dir of opts.componentDirs ?? []) scanComponents(dir);
  scanComponents(join(appDir, "components"));
  const components = [...componentMap.values()];

  // Named page layouts: app/layouts/<name>.wrn. A page selects one with its
  // `layout` export; the layout wraps the page body via <slot>.
  const layouts: ComponentRef[] = [];
  for (const f of scanDir(join(appDir, "layouts"))) {
    if (!f.file.endsWith(".wrn")) continue;
    const name = basename(f.file).replace(/\.wrn$/, "");
    if (!isSafeIslandName(name)) {
      console.warn(`[wrnexus] skipping layout with unsafe name: ${name}`);
      continue;
    }
    layouts.push({ name, file: f.file });
  }

  const stores: ComponentRef[] = [];
  for (const f of scanDir(join(appDir, "stores"))) {
    if (!f.file.endsWith(".wrn")) continue;
    try {
      const source = readFileSync(f.file, "utf8");
      const declaration = /\b(?:global|page)\s+store\s+([A-Za-z_$][\w$]*)\s*\{/.exec(source);
      if (!declaration) continue;
      stores.push({ name: declaration[1]!, file: f.file });
    } catch (error) {
      console.warn(`[wrnexus] unable to inspect store declaration in ${f.file}`, error);
    }
  }

  // Validation schemas: app/schemas/<name>.{ts,js}. Imported by API routes and
  // referenced by forms via `data-schema="<name>"`.
  const schemas: ComponentRef[] = [];
  for (const f of scanDir(join(appDir, "schemas"))) {
    if (!/\.(ts|js)$/.test(f.file)) continue;
    const name = basename(f.file).replace(/\.(ts|js)$/, "");
    if (!isSafeIslandName(name)) {
      console.warn(`[wrnexus] skipping schema with unsafe name: ${name}`);
      continue;
    }
    schemas.push({ name, file: f.file });
  }

  // Authorization declarations: app/authz/<name>.{ts,js}, each default-exporting
  // a defineAuthz() module. Merged into the catalog at boot.
  const authz: ComponentRef[] = [];
  for (const f of scanDir(join(appDir, "authz"), [".js"])) {
    if (!/\.(ts|js)$/.test(f.file)) continue;
    // Generated type files (permissions.gen.ts) live here too. Skip them quietly:
    // they export types only, and isSafeIslandName would otherwise reject the dot
    // and warn on every boot.
    if (/[.]gen[.](ts|js)$/.test(f.file)) continue;
    const name = basename(f.file).replace(/\.(ts|js)$/, "");
    if (!isSafeIslandName(name)) {
      console.warn(`[wrnexus] skipping authz declaration with unsafe name: ${name}`);
      continue;
    }
    authz.push({ name, file: f.file });
  }

  const services: ComponentRef[] = [];
  const serviceFilesByName = new Map<string, string>();
  for (const f of scanDir(join(appDir, "services"), [".js"])) {
    if (!/\.(ts|js)$/.test(f.file) || /[.]gen[.](ts|js)$/.test(f.file)) continue;
    const name = basename(f.file).replace(/\.(ts|js)$/, "");
    if (!isSafeIslandName(name)) {
      console.warn(`[wrnexus] skipping service with unsafe name: ${name}`);
      continue;
    }
    // A service name is a routable identity (/__wrnexus/rpc/<name>/...), so a
    // collision is a configuration error, not something to resolve by
    // directory-walk precedence like `components` silently does. Fail loudly,
    // naming both files, instead of letting whichever file is visited last
    // win non-deterministically.
    const existing = serviceFilesByName.get(name);
    if (existing) {
      throw new Error(
        `WRN-SERVICE-COLLISION: two services are both named "${name}": ${existing} and ${f.file}. ` +
          `Rename one of the files — service names must be unique across app/services.`,
      );
    }
    serviceFilesByName.set(name, f.file);
    services.push({ name, file: f.file });
  }

  return {
    pages,
    api,
    realtime,
    middlewareFiles,
    components,
    layouts,
    stores,
    schemas,
    authz,
    services,
    matchPage: (p) => matchRoute(pages, p),
    matchApi: (p) => matchRoute(api, p),
    matchRealtime: (p) => matchRoute(realtime, p),
  };
}

/** Re-export for callers that load middleware modules themselves. */
export type { Middleware };
export { routeName, nameRoutes, createRouteManifest, routeUrl, findNamedRoute } from "./named.ts";
export type { NamedRoute, RouteManifestEntry } from "./named.ts";
