/**
 * Wiki generator orchestrator.
 *
 * Thin entry point that imports all building blocks from the wiki/ sub-modules
 * and wires them together in the correct execution order.
 *
 * Phase 1: Scan repository, build module tree, plan documentation map
 * Phase 2: Generate leaf module documentation (bounded concurrency)
 * Phase 3: Synthesize parent module docs bottom-up, then root overview
 * Phase 4+5: Generate guide pages AND DD documents in parallel
 */

import { join } from "node:path";
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { RepoScanner } from "../repopilot/scanner/scanner";
import { RepoIndex } from "../repopilot/graph/index";
import {
  buildModuleTree,
  getLeafModules,
  getParentModulesBottomUp,
} from "../repopilot/module-tree";
import { EdgeType, NodeType } from "../repopilot/types";
import type { KnowledgeGraph } from "../repopilot/graph/knowledge-graph";
import type { ModuleNode } from "../repopilot/module-tree";
import type { WikiNavigationNode, WikiPage } from "../types/wiki-model";
import { validateWikiPageMermaid } from "./mermaid-validation";
import { WIKI_DIR_NAME, CAP_DD_METHODS_PER_CLASS, CAP_DD_PAGES } from "./constants";
import { countModules, getString, getNumber } from "./utils";
import type { ProgressCallback, WikiGenerationResult, WikiProgressUpdate } from "./types";
import type { WikiCheckpointStore } from "./checkpoint-store";
import type { CachedStepResult } from "./checkpoint-store";
import type { DDMethodInfo } from "./prompts";
import type { GeneratedWikiArtifacts } from "./types";
import {
  detectCrossCuttingPatterns,
  findKeyClassCandidates,
  discoverResourceFiles,
} from "./analysis";
import {
  generateModuleDoc,
  synthesizeParentDoc,
  synthesizeRootOverview,
  generateCrossCuttingPage,
  generateGettingStartedPage,
  generateClassDeepDivePage,
  generateDDPage,
} from "./agents";
import {
  buildNavigationTree,
  writeStructuredWikiArtifacts,
} from "./pages";

/* --- Re-export public types so existing consumers don't break --- */
export type {
  WikiGenerationResult,
  WikiProgressUpdate,
  ProgressCallback,
} from "./types";

/* ------------------------------------------------------------------ */
/*  selectDDCandidates                                                 */
/* ------------------------------------------------------------------ */

function selectDDCandidates(graph: KnowledgeGraph): DDMethodInfo[] {
  const ddMethods: DDMethodInfo[] = [];
  let ddGlobalIndex = 1;

  // Build list of classes sorted by method count descending (largest classes first)
  const ddClassCandidates: Array<{ nodeId: string; methodCount: number }> = [];
  for (const nodeType of [NodeType.CLASS, NodeType.INTERFACE]) {
    for (const nodeId of graph.getNodesByType(nodeType)) {
      const memberIds = graph.getNeighbors(nodeId, EdgeType.CONTAINS, "out");
      let methodCount = 0;
      for (const memberId of memberIds) {
        try {
          const mAttrs = graph.getNodeAttrs(memberId);
          if (String(mAttrs.type) === NodeType.METHOD) methodCount++;
        } catch { /* skip */ }
      }
      if (methodCount >= 3) {
        ddClassCandidates.push({ nodeId, methodCount });
      }
    }
  }
  ddClassCandidates.sort((a, b) => b.methodCount - a.methodCount);

  // Issue #7: iterate sorted array directly instead of going through an intermediate Set
  for (const { nodeId: classNodeId } of ddClassCandidates) {
    if (ddMethods.length >= CAP_DD_PAGES) break;

    try {
      const classAttrs = graph.getNodeAttrs(classNodeId);
      const className = String(classAttrs.name);
      const classFilePath = getString(classAttrs, "file_path") || "";

      const memberIds = graph.getNeighbors(classNodeId, EdgeType.CONTAINS, "out");
      const classMethods: Array<{
        name: string;
        lineStart?: number;
        lineEnd?: number;
        returnType?: string;
        parameters: string[];
        javadoc?: string;
        loc: number;
      }> = [];

      for (const memberId of memberIds) {
        try {
          const mAttrs = graph.getNodeAttrs(memberId);
          if (String(mAttrs.type) !== NodeType.METHOD) continue;

          const lineStart = getNumber(mAttrs, "line_start");
          const lineEnd = getNumber(mAttrs, "line_end");
          const loc = lineStart && lineEnd ? lineEnd - lineStart + 1 : 0;

          if (loc < 5) continue;

          classMethods.push({
            name: String(mAttrs.name),
            lineStart: lineStart ?? undefined,
            lineEnd: lineEnd ?? undefined,
            returnType: getString(mAttrs, "return_type") || undefined,
            parameters: Array.isArray(mAttrs.parameters)
              ? mAttrs.parameters.filter((v): v is string => typeof v === "string")
              : [],
            javadoc: getString(mAttrs, "javadoc") || undefined,
            loc,
          });
        } catch { /* skip */ }
      }

      classMethods.sort((a, b) => b.loc - a.loc);
      const selectedMethods = classMethods.slice(0, CAP_DD_METHODS_PER_CLASS);

      for (const m of selectedMethods) {
        if (ddMethods.length >= CAP_DD_PAGES) break;
        ddMethods.push({
          ddIndex: ddGlobalIndex++,
          className,
          methodName: m.name,
          filePath: classFilePath,
          lineStart: m.lineStart,
          lineEnd: m.lineEnd,
          returnType: m.returnType,
          parameters: m.parameters,
          javadoc: m.javadoc,
        });
      }
    } catch { /* skip class */ }
  }

  return ddMethods;
}

/* ------------------------------------------------------------------ */
/*  Semaphore — simple bounded-concurrency primitive                   */
/* ------------------------------------------------------------------ */

class Semaphore {
  private queue: Array<{ resolve: () => void; reject: (err: Error) => void }> = [];
  private active = 0;
  constructor(private readonly limit: number) {}

  async acquire(abortSignal?: AbortSignal): Promise<void> {
    if (abortSignal?.aborted) throw new Error("Wiki generation cancelled");
    if (this.active < this.limit) {
      this.active++;
      return;
    }
    return new Promise<void>((resolve, reject) => {
      const entry = {
        resolve: () => { this.active++; resolve(); },
        reject,
      };
      this.queue.push(entry);
      if (abortSignal) {
        const onAbort = () => {
          const idx = this.queue.indexOf(entry);
          if (idx >= 0) {
            this.queue.splice(idx, 1);
            reject(new Error("Wiki generation cancelled"));
          }
        };
        abortSignal.addEventListener("abort", onAbort, { once: true });
      }
    });
  }

  release(): void {
    this.active--;
    const next = this.queue.shift();
    if (next) next.resolve();
  }
}

/* ------------------------------------------------------------------ */
/*  generateWiki                                                       */
/* ------------------------------------------------------------------ */

export async function generateWiki(
  sourcePath: string,
  onProgress?: ProgressCallback,
  checkpoint?: WikiCheckpointStore,
  abortSignal?: AbortSignal,
): Promise<WikiGenerationResult> {
  const errors: string[] = [];
  const structuredPages = new Map<string, WikiPage>();
  const supplementalPageMap = new Map<string, WikiNavigationNode[]>();
  const guidePages: WikiPage[] = [];
  const ddPages: WikiPage[] = [];
  let pagesGenerated = 0;
  let completedSteps = 0;
  let totalSteps = 0;
  const completedPageIds: string[] = [];
  const wikiDir = join(sourcePath, WIKI_DIR_NAME);

  // Issue #4: track which pages have been validated to avoid redundant work
  const validatedPageIds = new Set<string>();

  // Issue #10: ETA tracking
  const startTime = Date.now();

  function checkAborted(): void {
    if (abortSignal?.aborted) {
      throw new Error("Wiki generation cancelled");
    }
  }

  function isAbortError(): boolean {
    return !!abortSignal?.aborted;
  }

  const markPagesCompleted = (pages: WikiPage[]) => {
    for (const page of pages) {
      completedPageIds.push(page.id);
    }
  };

  const withGenerationMetadata = (
    pages: WikiPage[],
    warning: string | null,
  ): WikiPage[] =>
    pages.map((page) => ({
      ...page,
      status: warning ? "fallback" : (page.status || "generated"),
      generationWarnings: warning
        ? Array.from(new Set([...(page.generationWarnings || []), warning]))
        : (page.generationWarnings || []),
    }));

  const progress = (
    phase: WikiProgressUpdate["phase"],
    message: string,
  ) => {
    const elapsedMs = Date.now() - startTime;
    const etaMs =
      completedSteps > 0 && totalSteps > 0
        ? Math.round((elapsedMs / completedSteps) * (totalSteps - completedSteps))
        : undefined;

    console.log(`[wiki-gen] ${message}`);
    onProgress?.({
      phase,
      message,
      pagesGenerated,
      errors: errors.length,
      completedSteps,
      totalSteps,
      completedPageIds,
      elapsedMs,
      etaMs,
    });
  };

  /* ---------------------------------------------------------------- */
  /*  processStep — generic checkpoint-aware generation step          */
  /*  Issue #2: eliminates ~300 lines of duplicated boilerplate       */
  /* ---------------------------------------------------------------- */

  interface StepOptions {
    stepKey: string;
    stepType: string;
    phase: WikiProgressUpdate["phase"];
    label: string;
    generate: () => Promise<GeneratedWikiArtifacts>;
    /** Extra array to push generated pages into (e.g. guidePages, ddPages). */
    extraTarget?: WikiPage[];
    /** Module whose status should be updated during generation. */
    module?: ModuleNode;
    /** Whether to compute supplemental nav from entity/workflow pages. */
    computeSupplementalNav?: boolean;
  }

  async function processStep(opts: StepOptions): Promise<void> {
    checkAborted();
    const { stepKey, stepType, phase, label, generate, extraTarget, module: mod, computeSupplementalNav } = opts;
    try {
      const cached = await checkpoint?.get(stepKey);
      if (cached && !cached.warning) {
        // Only use cache if the previous run succeeded (no warning).
        // Entries with warnings contain fallback content and should be retried.
        progress(phase, `Using cached ${label}`);
        restoreCachedResult(cached, label, extraTarget, mod);
      } else {
        progress(phase, `Generating ${label}...`);
        if (mod) mod.status = "generating";
        const generated = await generate();
        const artifact = {
          ...generated,
          pages: withGenerationMetadata(generated.pages, generated.warning),
        };
        if (mod) mod.status = "done";
        pagesGenerated++;
        markPagesCompleted(artifact.pages);
        for (const page of artifact.pages) {
          structuredPages.set(page.id, page);
          extraTarget?.push(page);
        }
        let suppNav: WikiNavigationNode[] | undefined;
        if (computeSupplementalNav && mod) {
          suppNav = artifact.pages
            .filter((p) => p.pageType === "entity" || p.pageType === "workflow")
            .map((p) => ({
              id: p.id,
              title: p.title,
              pageId: p.id,
              pageType: p.pageType,
              path: p.path,
              children: [],
            }));
          supplementalPageMap.set(mod.id, suppNav);
        }
        if (artifact.warning) {
          errors.push(`${label}: ${artifact.warning}`);
        }
        // Only cache successful results. Skip caching when:
        // - Generation was aborted (fallback content should be regenerated)
        // - Agent failed (warning set) — retry on resume instead of
        //   serving stale fallback forever
        if (!abortSignal?.aborted && !artifact.warning) {
          await checkpoint?.set(stepKey, stepType, {
            pages: artifact.pages,
            supplementalNav: suppNav,
            warning: artifact.warning,
          });
        }
      }
    } catch (err) {
      if (isAbortError()) throw err;
      const msg = `Failed to generate ${label}: ${err instanceof Error ? err.message : String(err)}`;
      errors.push(msg);
      progress(phase, `Error: ${msg}`);
      if (mod) mod.status = "done";
    } finally {
      completedSteps++;
      progress(phase, `${completedSteps}/${totalSteps} steps complete`);
    }
  }

  function restoreCachedResult(
    cached: CachedStepResult,
    label: string,
    extraTarget?: WikiPage[],
    mod?: ModuleNode,
  ): void {
    const pages = withGenerationMetadata(cached.pages, cached.warning);
    if (mod) mod.status = "done";
    pagesGenerated++;
    markPagesCompleted(pages);
    for (const page of pages) {
      structuredPages.set(page.id, page);
      extraTarget?.push(page);
    }
    if (mod && cached.supplementalNav) {
      supplementalPageMap.set(mod.id, cached.supplementalNav);
    }
    if (cached.warning) {
      errors.push(`${label}: ${cached.warning}`);
    }
  }

  /* ---- Phase 1: Scan & build tree ---- */
  let indexDir = join(sourcePath, ".repopilot");
  const indexPath = join(indexDir, "index.rpidx");

  let index: RepoIndex;
  if (existsSync(indexPath)) {
    progress("scanning", "Loading cached index...");
    index = RepoIndex.load(indexPath);
  } else {
    progress("scanning", "Scanning repository...");
    const scanner = new RepoScanner(sourcePath);
    index = scanner.scan();
    try {
      mkdirSync(indexDir, { recursive: true });
      index.save(indexPath);
    } catch {
      // Source path may be read-only (e.g. mounted volume) — save to temp
      indexDir = mkdtempSync(join(tmpdir(), "repopilot-idx-"));
      index.save(join(indexDir, "index.rpidx"));
      console.log(`[wiki-gen] Index saved to fallback path: ${indexDir}`);
    }
  }
  const graph = index.graph;

  progress("building-tree", "Building module tree...");
  const tree = buildModuleTree(graph);
  const totalModules = countModules(tree);
  progress("building-tree", `${totalModules} modules found`);

  mkdirSync(wikiDir, { recursive: true });
  writeFileSync(join(wikiDir, "tree.json"), JSON.stringify(tree, null, 2));

  if (tree.children.length === 0) {
    progress("done", "No modules found — skipping documentation generation.");
    return { pagesGenerated: 0, moduleTree: tree, errors };
  }

  /* ---- Phase 1.5: Plan documentation map ---- */
  progress("planning", "Planning documentation coverage...");

  const crossCuttingPatterns = detectCrossCuttingPatterns(graph, tree);
  const keyClasses = findKeyClassCandidates(graph, tree);
  const resourceFiles = discoverResourceFiles(sourcePath);
  const ddMethods = selectDDCandidates(graph);

  progress(
    "planning",
    `Planned: ${crossCuttingPatterns.length} cross-cutting pages, ` +
      `${keyClasses.length} key classes, ${ddMethods.length} DD methods`,
  );

  /* ---- Phase 2: Leaf docs (with bounded concurrency) ---- */
  const leaves = getLeafModules(tree);
  const parents = getParentModulesBottomUp(tree);
  const guideCount = 1 + crossCuttingPatterns.length + keyClasses.length;
  totalSteps = leaves.length + parents.length + 1 + guideCount + ddMethods.length;
  progress(
    "leaf-docs",
    `Generating docs for ${leaves.length} leaf modules...`,
  );

  // Issue #1: Process leaf modules with bounded concurrency instead of sequentially
  const LEAF_CONCURRENCY = 4;
  const leafSemaphore = new Semaphore(LEAF_CONCURRENCY);
  await Promise.all(
    leaves.map(async (leaf) => {
      await leafSemaphore.acquire(abortSignal);
      try {
        await processStep({
          stepKey: `leaf:${leaf.id}`,
          stepType: "leaf",
          phase: "leaf-docs",
          label: `${leaf.name} (${leaf.path})`,
          generate: () => generateModuleDoc(leaf, tree, graph, sourcePath, wikiDir, abortSignal),
          module: leaf,
          computeSupplementalNav: true,
        });
      } finally {
        leafSemaphore.release();
      }
    }),
  );

  progress(
    "leaf-docs",
    `${pagesGenerated} leaf docs generated`,
  );

  /* ---- Phase 3: Parent docs ---- */
  progress(
    "parent-docs",
    `Synthesizing ${parents.length} parent modules...`,
  );

  // Parents must be processed bottom-up (sequential) since parents read child docs
  for (const parent of parents) {
    await processStep({
      stepKey: `parent:${parent.id}`,
      stepType: "parent",
      phase: "parent-docs",
      label: `${parent.name} (${parent.path})`,
      generate: () => synthesizeParentDoc(parent, tree, graph, sourcePath, wikiDir, abortSignal),
      module: parent,
      computeSupplementalNav: true,
    });
  }

  /* ---- Phase 3b: Root overview ---- */
  const extraPageIds: string[] = [
    "guide:getting-started",
    ...crossCuttingPatterns.map((p) => `guide:${p.id}`),
    ...keyClasses.map((kc) => `deep-dive:${kc.name}`),
  ];

  await processStep({
    stepKey: "overview",
    stepType: "overview",
    phase: "overview",
    label: "root overview",
    generate: () => synthesizeRootOverview(tree, index, sourcePath, wikiDir, resourceFiles, extraPageIds, abortSignal),
  });

  // Helper: write manifest and page files from current state.
  // Issue #4: only validate pages that haven't been validated yet.
  async function writeWikiArtifacts(): Promise<void> {
    if (abortSignal?.aborted) return;
    try {
      // Snapshot pages to avoid interleaving with concurrent mutations
      const allPages = [...structuredPages.values()];
      const needsValidation = allPages.filter((p) => !validatedPageIds.has(p.id));
      const alreadyValidated = allPages.filter((p) => validatedPageIds.has(p.id));

      const freshlyValidated = await Promise.all(
        needsValidation.map((page) => validateWikiPageMermaid(page)),
      );

      // Update the structuredPages map with validated versions
      for (const page of freshlyValidated) {
        structuredPages.set(page.id, page);
        validatedPageIds.add(page.id);
      }

      const validatedPages = [...alreadyValidated, ...freshlyValidated];
      const allExtraPages = [...guidePages, ...ddPages];
      const navigation = buildNavigationTree(
        tree,
        supplementalPageMap,
        validatedPages.filter((page) => allExtraPages.some((extra) => extra.id === page.id)),
      );
      console.log(`[wiki-gen] Writing manifest: ${validatedPages.length} pages, ${allExtraPages.length} extra`);
      writeStructuredWikiArtifacts(wikiDir, navigation, validatedPages);
      writeFileSync(join(wikiDir, "tree.json"), JSON.stringify(tree, null, 2));
    } catch (err) {
      const msg = err instanceof Error ? err.stack || err.message : String(err);
      console.error(`[wiki-gen] writeWikiArtifacts failed: ${msg}`);
      errors.push(`Failed to write wiki artifacts: ${msg}`);
    }
  }

  // Write intermediate manifest so wiki is viewable even if later phases are interrupted.
  await writeWikiArtifacts();

  /* ---- Phase 4+5: Guides AND DD documents in parallel ---- */
  // Issue #8: Guides and DD pages are independent — run them concurrently.

  const guidesPromise = (async () => {
    progress(
      "guides",
      `Generating ${guideCount} guide pages...`,
    );

    // Issue #8: All guide sub-tasks are independent — run them concurrently
    const guideSteps: StepOptions[] = [
      // Getting Started page
      {
        stepKey: "guide:getting-started",
        stepType: "guide",
        phase: "guides" as const,
        label: "Getting Started guide",
        generate: () => generateGettingStartedPage(tree, graph, sourcePath, wikiDir, keyClasses, resourceFiles, abortSignal),
        extraTarget: guidePages,
      },
      // Cross-cutting pages
      ...crossCuttingPatterns.map((pattern) => ({
        stepKey: `guide:${pattern.id}`,
        stepType: "cross-cutting",
        phase: "guides" as const,
        label: `${pattern.title} guide`,
        generate: () => generateCrossCuttingPage(pattern, graph, sourcePath, wikiDir, abortSignal),
        extraTarget: guidePages,
      })),
      // Class deep-dive pages
      ...keyClasses.map((candidate) => ({
        stepKey: `deep-dive:${candidate.name}`,
        stepType: "deep-dive",
        phase: "guides" as const,
        label: `deep-dive for ${candidate.name} (${candidate.connectionCount} connections)`,
        generate: () => generateClassDeepDivePage(candidate, graph, sourcePath, wikiDir, abortSignal),
        extraTarget: guidePages,
      })),
    ];

    const GUIDE_CONCURRENCY = 4;
    const guideSemaphore = new Semaphore(GUIDE_CONCURRENCY);
    await Promise.all(
      guideSteps.map(async (step) => {
        await guideSemaphore.acquire(abortSignal);
        try {
          await processStep(step);
        } finally {
          guideSemaphore.release();
        }
      }),
    );
  })();

  const ddPromise = (async () => {
    progress(
      "dd-docs",
      `Generating ${ddMethods.length} DD documents...`,
    );

    // Issue #5: Use semaphore-based concurrency instead of batch-wait model
    const DD_CONCURRENCY = 4;
    const ddSemaphore = new Semaphore(DD_CONCURRENCY);
    let ddManifestCounter = 0;

    await Promise.all(
      ddMethods.map(async (method) => {
        await ddSemaphore.acquire(abortSignal);
        try {
          await processStep({
            stepKey: `dd:${method.className}:${method.methodName}`,
            stepType: "dd",
            phase: "dd-docs",
            label: `DD${String(method.ddIndex).padStart(2, "0")} of ${ddMethods.length}: ${method.className}.${method.methodName}`,
            generate: () => generateDDPage(method, sourcePath, wikiDir, abortSignal),
            extraTarget: ddPages,
          });
          // Write manifest periodically so DD pages appear in the UI
          ddManifestCounter++;
          if (ddManifestCounter % DD_CONCURRENCY === 0) {
            await writeWikiArtifacts();
          }
        } finally {
          ddSemaphore.release();
        }
      }),
    );
  })();

  await Promise.all([guidesPromise, ddPromise]);

  progress(
    "dd-docs",
    `${ddPages.length} DD documents generated`,
  );

  progress(
    "done",
    `Wiki generation complete: ${pagesGenerated} pages, ${errors.length} warnings/errors`,
  );

  /* ---- Write final artifacts ---- */
  await writeWikiArtifacts();

  return { pagesGenerated, moduleTree: tree, errors };
}
