/**
 * FS Routes Generator
 *
 * 스캔 결과를 RoutesManifest로 변환
 *
 * @module router/fs-routes
 */

import { writeFile, readFile, mkdir } from "fs/promises";
import { join, dirname } from "path";
import type { RoutesManifest, RouteSpec } from "../spec/schema";
import type { FSRouteConfig, FSScannerConfig, ScanResult } from "./fs-types";
import { DEFAULT_SCANNER_CONFIG } from "./fs-types";
import { scanRoutes } from "./fs-scanner";
import { loadManduConfig } from "../config";
import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
import {
  runOnRouteRegistered,
  runOnManifestBuilt,
} from "../plugins/runner";
import { shouldPreserveExistingClientModule } from "./client-entry";

// ═══════════════════════════════════════════════════════════════════════════
// Types
// ═══════════════════════════════════════════════════════════════════════════

/**
 * 매니페스트 생성 결과
 */
export interface FSGenerateResult {
  /** 생성된 매니페스트 */
  manifest: RoutesManifest;

  /** FS Routes에서 생성된 라우트 수 */
  fsRoutesCount: number;

  /** 경고 메시지 */
  warnings: string[];
}

/**
 * 매니페스트 생성 옵션
 */
export interface GenerateOptions {
  /** 스캐너 설정 */
  scanner?: Partial<FSScannerConfig>;

  /** 출력 파일 경로 (지정 시 파일로 저장) */
  outputPath?: string;

  /**
   * Phase 18.τ — optional plugins + config-level hooks bundle.
   *
   * When provided, `generateManifest()` fires two plugin hooks:
   *   - `onRouteRegistered(route)` — once per scanned route.
   *   - `onManifestBuilt(manifest)` — after assembly, BEFORE the
   *     manifest is written to disk. A plugin may return a mutated
   *     manifest (pipe semantics across plugins).
   *
   * Omitted → zero overhead, identical to pre-τ behaviour.
   */
  plugins?: readonly ManduPlugin[];
  configHooks?: Partial<ManduHooks>;
}

// ═══════════════════════════════════════════════════════════════════════════
// Conversion Functions
// ═══════════════════════════════════════════════════════════════════════════

/**
 * FSRouteConfig를 RouteSpec으로 변환
 */
/** Normalize path separators to forward slashes for cross-platform consistency */
function normalizePath(p: string): string {
  return p.replace(/\\/g, "/");
}

export function fsRouteToRouteSpec(fsRoute: FSRouteConfig): RouteSpec {
  const base = {
    id: fsRoute.id,
    pattern: fsRoute.pattern,
    module: normalizePath(fsRoute.module),
  };

  if (fsRoute.kind === "page") {
    const pageRoute: RouteSpec = {
      ...base,
      kind: "page" as const,
      componentModule: normalizePath(fsRoute.componentModule ?? ""),
      ...(fsRoute.clientModule
        ? {
            clientModule: normalizePath(fsRoute.clientModule),
            ...(fsRoute.clientExportName ? { clientExportName: fsRoute.clientExportName } : {}),
            hydration: fsRoute.hydration ?? {
              strategy: "island" as const,
              priority: "immediate" as const,
              preload: false,
            },
          }
        : fsRoute.hydration
          ? { hydration: fsRoute.hydration }
          : {}),
      ...(fsRoute.boundaries && fsRoute.boundaries.length > 0
        ? {
            boundaries: fsRoute.boundaries.map((boundary) => ({
              ...boundary,
              module: normalizePath(boundary.module),
              source: {
                ...boundary.source,
                file: normalizePath(boundary.source.file),
              },
            })),
          }
        : {}),
      ...(fsRoute.layoutChain && fsRoute.layoutChain.length > 0
        ? { layoutChain: fsRoute.layoutChain.map(normalizePath) }
        : {}),
      ...(fsRoute.loadingModule ? { loadingModule: normalizePath(fsRoute.loadingModule) } : {}),
      ...(fsRoute.errorModule ? { errorModule: normalizePath(fsRoute.errorModule) } : {}),
      ...(fsRoute.notFoundModule ? { notFoundModule: normalizePath(fsRoute.notFoundModule) } : {}),
    };
    return pageRoute;
  }

  // Issue #206: metadata 라우트 (sitemap / robots / llms-txt / manifest).
  // metadataKind + contentType는 fs-scanner에서 채워진다. 방어적으로
  // 없는 경우를 떨어뜨리지 않고 빈 값으로 통과시켜 RoutesManifest 검증
  // 단계에서 명확한 에러가 나오게 한다.
  if (fsRoute.kind === "metadata") {
    const metadataRoute: RouteSpec = {
      ...base,
      kind: "metadata" as const,
      metadataKind: fsRoute.metadataKind ?? "sitemap",
      contentType: fsRoute.contentType ?? "text/plain; charset=utf-8",
    };
    return metadataRoute;
  }

  // API 라우트
  const apiRoute: RouteSpec = {
    ...base,
    kind: "api" as const,
    ...(fsRoute.methods ? { methods: fsRoute.methods } : {}),
  };
  return apiRoute;
}

/**
 * 스캔 결과를 RoutesManifest로 변환
 */
export function scanResultToManifest(scanResult: ScanResult): RoutesManifest {
  const routes = scanResult.routes.map(fsRouteToRouteSpec);

  return {
    version: 1,
    routes,
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// Auto-Linking (spec/slots + spec/contracts → manifest routes)
// ═══════════════════════════════════════════════════════════════════════════

/**
 * 매니페스트 라우트에 slot/contract 모듈을 자동 연결
 *
 * ID 컨벤션 기반: route.id → spec/slots/{id}.slot.ts, spec/contracts/{id}.contract.ts
 */
export async function resolveAutoLinks(
  manifest: RoutesManifest,
  rootDir: string
): Promise<void> {
  // Slot = server-side data loader (.slot.ts/.tsx)
  // Client = client-side island module (.client.ts/.tsx) — sets clientModule, NOT slotModule
  const slotExtensions = [".slot.ts", ".slot.tsx"];
  const clientExtensions = [".client.ts", ".client.tsx"];

  await Promise.all(
    manifest.routes.map(async (route) => {
      // Metadata routes (sitemap/robots/llms-txt/manifest) have a
      // strict file-convention contract — no slots, no contracts.
      // Skipping them here avoids fs.access chatter on every dev
      // rescan.
      if (route.kind === "metadata") return;

      const contractPath = join(rootDir, "spec", "contracts", `${route.id}.contract.ts`);

      // Check all extensions in parallel
      const slotChecks = slotExtensions.map(async (ext) => {
        const path = join(rootDir, "spec", "slots", `${route.id}${ext}`);
        return (await Bun.file(path).exists()) ? ext : null;
      });
      const clientChecks = clientExtensions.map(async (ext) => {
        const path = join(rootDir, "spec", "slots", `${route.id}${ext}`);
        return (await Bun.file(path).exists()) ? ext : null;
      });

      const [contractExists, ...allResults] = await Promise.all([
        Bun.file(contractPath).exists(),
        ...slotChecks,
        ...clientChecks,
      ]);

      const slotResults = allResults.slice(0, slotExtensions.length);
      const clientResults = allResults.slice(slotExtensions.length);

      // Set slotModule for .slot.ts(x) files (server data loaders)
      const matchedSlotExt = slotResults.find((ext) => ext !== null);
      if (matchedSlotExt) {
        route.slotModule = `spec/slots/${route.id}${matchedSlotExt}`;
      }

      // Set clientModule for .client.ts(x) files (island hydration) — only if not already set
      const matchedClientExt = clientResults.find((ext) => ext !== null);
      if (matchedClientExt && !route.clientModule) {
        route.clientModule = `spec/slots/${route.id}${matchedClientExt}`;
        // Also set default hydration config if not present
        if (!route.hydration) {
          route.hydration = {
            strategy: "island" as const,
            priority: "immediate" as const,
            preload: false,
          };
        }
      }

      if (contractExists) {
        route.contractModule = `spec/contracts/${route.id}.contract.ts`;
      }
    })
  );
}

// ═══════════════════════════════════════════════════════════════════════════
// Main Generator
// ═══════════════════════════════════════════════════════════════════════════

/**
 * mandu.config 기반 스캐너 설정 해석
 */
async function resolveScannerConfig(
  rootDir: string,
  scannerOverrides: Partial<FSScannerConfig> = {}
): Promise<FSScannerConfig> {
  const config = await loadManduConfig(rootDir);
  const configScanner = config.fsRoutes ?? {};

  return {
    ...DEFAULT_SCANNER_CONFIG,
    ...configScanner,
    ...scannerOverrides,
  };
}

/**
 * FS Routes 기반 매니페스트 생성
 *
 * app/ 디렉토리를 스캔하여 매니페스트를 생성하고
 * spec/slots/, spec/contracts/와 자동 연결한 후
 * .mandu/routes.manifest.json에 저장
 *
 * @example
 * const result = await generateManifest("/path/to/project");
 * console.log(result.manifest.routes);
 */
export async function generateManifest(
  rootDir: string,
  options: GenerateOptions = {}
): Promise<FSGenerateResult> {
  const scannerConfig = await resolveScannerConfig(rootDir, options.scanner);

  // FS Routes 스캔
  const scanResult = await scanRoutes(rootDir, scannerConfig);

  // 스캔 에러 체크
  if (scanResult.errors.length > 0) {
    const errorMessages = scanResult.errors.map((e) => `${e.type}: ${e.message}`);
    console.warn("FS Routes scan warnings:", errorMessages);
  }

  // FS Routes 매니페스트 생성
  let manifest = scanResultToManifest(scanResult);
  const warnings: string[] = [];

  // Auto-linking: spec/slots/, spec/contracts/ 자동 연결
  await resolveAutoLinks(manifest, rootDir);

  // Phase 18.τ — fire plugin hooks AFTER auto-linking so plugins see the
  // final RouteSpec (including slot/contract/client modules). Both hooks
  // are opt-in; zero-overhead when `options.plugins` / `options.configHooks`
  // are omitted.
  const pluginArgs = {
    plugins: options.plugins ?? [],
    configHooks: options.configHooks,
  };
  if (pluginArgs.plugins.length > 0 || pluginArgs.configHooks) {
    for (const route of manifest.routes) {
      const { errors } = await runOnRouteRegistered(route, pluginArgs);
      for (const e of errors) {
        warnings.push(
          `onRouteRegistered[${e.source}] ${route.id}: ${e.error.message}`
        );
      }
    }
    const manifestReport = await runOnManifestBuilt(manifest, pluginArgs);
    manifest = manifestReport.result;
    for (const e of manifestReport.errors) {
      warnings.push(`onManifestBuilt[${e.source}]: ${e.error.message}`);
    }
  }

  // 기존 매니페스트에서 사용자 설정 필드 보존 (clientModule, hydration 등)
  const outputPath = options.outputPath ?? ".mandu/routes.manifest.json";
  const outputFullPath = join(rootDir, outputPath);
  await mkdir(dirname(outputFullPath), { recursive: true });

  try {
    const existingRaw = await readFile(outputFullPath, "utf-8");
    const existingManifest = JSON.parse(existingRaw) as RoutesManifest;
    if (existingManifest.routes) {
      const existingMap = new Map(
        existingManifest.routes.map((r) => [r.id, r])
      );
      for (const route of manifest.routes) {
        const prev = existingMap.get(route.id);
        if (!prev) continue;
        // 사용자가 설정한 clientModule/hydration 보존.
        // If app/page.tsx used to be a client page and later becomes a server
        // page, preserving the old clientModule would leak the server graph into
        // the browser bundle.
        if (
          prev.clientModule &&
          !route.clientModule &&
          !(route.kind === "page" && route.boundaries && route.boundaries.length > 0) &&
          await shouldPreserveExistingClientModule(route, prev.clientModule, rootDir)
        ) {
          route.clientModule = prev.clientModule;
          route.clientExportName = prev.clientExportName;
        }
        if (prev.hydration && !route.hydration) {
          const canPreserveHydration =
            !!route.clientModule || prev.hydration.strategy === "none";
          if (canPreserveHydration) {
            route.hydration = prev.hydration;
          }
        }
        // Issue #214 — preserve prerender-time fields (`dynamicParams`,
        // `staticParams`) across manifest rescans. `mandu build`'s prerender
        // phase stamps these onto the manifest; a subsequent `mandu dev`
        // rescan must not wipe them out.
        if (route.kind === "page" && prev.kind === "page") {
          const prevPage = prev as typeof prev & {
            dynamicParams?: boolean;
            staticParams?: unknown[];
          };
          const routePage = route as typeof route & {
            dynamicParams?: boolean;
            staticParams?: unknown[];
          };
          if (prevPage.dynamicParams !== undefined && routePage.dynamicParams === undefined) {
            routePage.dynamicParams = prevPage.dynamicParams;
          }
          if (prevPage.staticParams !== undefined && routePage.staticParams === undefined) {
            routePage.staticParams = prevPage.staticParams;
          }
        }
      }
    }
  } catch {
    // 기존 매니페스트가 없으면 무시
  }

  await writeFile(outputFullPath, JSON.stringify(manifest, null, 2), "utf-8");

  return {
    manifest,
    fsRoutesCount: scanResult.routes.length,
    warnings,
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// Watch Mode Support
// ═══════════════════════════════════════════════════════════════════════════

/**
 * 라우트 변경 콜백
 */
export type RouteChangeCallback = (result: FSGenerateResult) => void | Promise<void>;

/**
 * FS Routes 감시자 인터페이스
 */
export interface FSRoutesWatcher {
  /** 감시 중지 */
  close(): void;

  /** 수동 재스캔 */
  rescan(): Promise<FSGenerateResult>;
}

/**
 * FS Routes 감시 시작
 *
 * 파일 변경 시 자동으로 매니페스트 재생성
 *
 * @example
 * const watcher = await watchFSRoutes("/path/to/project", {
 *   onChange: (result) => {
 *     console.log("Routes updated:", result.manifest.routes.length);
 *   }
 * });
 *
 * // 나중에 중지
 * watcher.close();
 */
export async function watchFSRoutes(
  rootDir: string,
  options: GenerateOptions & { onChange?: RouteChangeCallback }
): Promise<FSRoutesWatcher> {
  const { onChange, ...generateOptions } = options;
  const scannerConfig = await resolveScannerConfig(rootDir, options.scanner);

  const routesDir = join(rootDir, scannerConfig.routesDir);
  const slotsDir = join(rootDir, "spec", "slots");
  const contractsDir = join(rootDir, "spec", "contracts");

  // chokidar 동적 import
  const chokidar = await import("chokidar");

  // Watch app/ routes directory
  const routesWatcher = chokidar.watch(routesDir, {
    ignored: Array.from(
      new Set([
        ...scannerConfig.exclude,
        "**/node_modules/**",
        "**/_*/**", // 비공개 폴더
        "**/*.test.*",
        "**/*.spec.*",
      ])
    ),
    persistent: true,
    ignoreInitial: true,
  });

  // Watch spec/slots/ and spec/contracts/ for auto-link refresh
  const specWatcher = chokidar.watch([slotsDir, contractsDir], {
    ignored: ["**/node_modules/**"],
    persistent: true,
    ignoreInitial: true,
  });

  let debounceTimer: ReturnType<typeof setTimeout> | null = null;

  const triggerRescan = async (): Promise<FSGenerateResult> => {
    const result = await generateManifest(rootDir, generateOptions);
    if (onChange) {
      await onChange(result);
    }
    return result;
  };

  const debouncedRescan = () => {
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    debounceTimer = setTimeout(() => {
      triggerRescan().catch(console.error);
    }, 100);
  };

  // 파일 변경 이벤트 핸들러 (app/ routes)
  routesWatcher.on("add", debouncedRescan);
  routesWatcher.on("unlink", debouncedRescan);
  routesWatcher.on("change", debouncedRescan);

  // spec/slots/ and spec/contracts/ 변경 시 auto-link refresh
  specWatcher.on("add", debouncedRescan);
  specWatcher.on("unlink", debouncedRescan);

  return {
    close() {
      if (debounceTimer) {
        clearTimeout(debounceTimer);
      }
      void routesWatcher.close();
      void specWatcher.close();
    },
    async rescan() {
      return triggerRescan();
    },
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// CLI Helper
// ═══════════════════════════════════════════════════════════════════════════

/**
 * CLI용 라우트 목록 출력 형식
 */
export function formatRoutesForCLI(manifest: RoutesManifest): string {
  const lines: string[] = [];

  lines.push(`📋 Routes (${manifest.routes.length} total)`);
  lines.push("─".repeat(60));

  for (const route of manifest.routes) {
    const icon = route.kind === "page" ? "📄" : route.kind === "api" ? "📡" : "🗺️";
    const hydration = route.clientModule ? " 🏝️" : "";
    lines.push(`${icon} ${route.pattern.padEnd(30)} → ${route.id}${hydration}`);
  }

  return lines.join("\n");
}
