/**
 * Generator functions — each invokes a Rust agent subprocess to produce
 * wiki documentation artifacts for a specific scope (module, parent,
 * root overview, cross-cutting pattern, getting-started guide, or
 * class deep-dive).
 */

import { join } from "node:path";
import {
  existsSync,
  mkdirSync,
  rmSync,
} from "node:fs";
import { readFile, writeFile } from "node:fs/promises";

import type { KnowledgeGraph } from "../../repopilot/graph/knowledge-graph";
import type { RepoIndex } from "../../repopilot/graph/index";
import type { ModuleNode } from "../../repopilot/module-tree";
import { EdgeType, NodeType } from "../../repopilot/types";
import type {
  WikiCodeReference,
  WikiPage,
  WikiPageType,
} from "../../types/wiki-model";

import type {
  CrossCuttingPattern,
  GeneratedWikiArtifacts,
  KeyClassCandidate,
} from "../types";
import { WIKI_DIR_NAME } from "../constants";

import { runSubagent } from "./runner";
import {
  buildModuleEvidence,
  buildRootEvidence,
} from "../analysis";
import {
  buildLeafPrompt,
  buildParentPrompt,
  buildRootPrompt,
  buildCrossCuttingPrompt,
  buildGettingStartedPrompt,
  buildClassDeepDivePrompt,
  buildDDPrompt,
  type DDMethodInfo,
} from "../prompts";
import {
  buildModuleStructuredPages,
  buildOverviewStructuredPage,
  writeFinalDocument,
  buildModuleFallbackBody,
  buildRootFallbackBody,
  buildAppendixSection,
} from "../pages";
import { buildCodeReference } from "../code-links";
import {
  toRelativePath,
  stripManagedAppendix,
  getString,
  getNumber,
} from "../utils";

/* ------------------------------------------------------------------ */
/*  generateModuleDoc                                                  */
/* ------------------------------------------------------------------ */

export async function generateModuleDoc(
  module: ModuleNode,
  tree: ModuleNode,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  const evidence = buildModuleEvidence(module, tree, graph, sourcePath);
  const prompt = buildLeafPrompt(
    module,
    tree,
    graph,
    sourcePath,
    WIKI_DIR_NAME,
    evidence,
  );
  const docFullPath = join(wikiDir, module.docPath || "");
  mkdirSync(join(docFullPath, ".."), { recursive: true });
  // Remove stale file from a previous aborted run so writeFinalDocument
  // doesn't preserve old fallback content on resume.
  if (existsSync(docFullPath)) rmSync(docFullPath);

  let warning: string | null = null;
  // Skip agent for modules with zero indexed content — they produce only fallback
  const hasContent = evidence.classCount > 0 || evidence.methodCount > 0 || evidence.fileCount > 0;
  if (!hasContent) {
    warning = `Module has no indexed content (${evidence.fileCount} files, ${evidence.classCount} classes)`;
    console.log(`[wiki-gen] Skipping agent for ${module.name}: ${warning}`);
  } else {
    try {
      await runSubagent(prompt, sourcePath, "module-documenter", undefined, abortSignal);
      // Agent completed without error but didn't write the file — treat as failure
      if (!existsSync(docFullPath)) {
        warning = "Agent completed but did not write output file";
        console.warn(`[wiki-gen] Agent for ${module.name} did not write output`);
      }
    } catch (err) {
      // Re-throw abort errors so processStep skips caching fallback content
      if (abortSignal?.aborted) throw err;
      warning = err instanceof Error ? err.message : String(err);
      console.warn(`[wiki-gen] Agent for ${module.name} failed: ${warning}`);
    }
  }

  await writeFinalDocument(
    docFullPath,
    buildModuleFallbackBody(module, evidence, warning),
    [
      buildAppendixSection("Module Facts", evidence.facts),
      buildAppendixSection("Related Pages", evidence.childLinks),
      buildAppendixSection("Code References", evidence.classReferences),
      buildAppendixSection("Method References", evidence.methodReferences),
      buildAppendixSection("Dependencies", evidence.dependencySummary),
      buildAppendixSection(
        "Cross-Module Relationships",
        evidence.crossModuleRelationships,
      ),
      buildAppendixSection("Import Patterns", evidence.importPatterns),
      buildAppendixSection("Source Files", evidence.fileInventory),
      evidence.structureDiagram
        ? buildAppendixSection(
            "Module Structure Diagram",
            evidence.structureDiagram,
            "mermaid",
          )
        : "",
      evidence.dependencyDiagram
        ? buildAppendixSection(
            "Dependency Diagram",
            evidence.dependencyDiagram,
            "mermaid",
          )
        : "",
    ],
  );

  const narrativeMarkdown = stripManagedAppendix(
    await readFile(docFullPath, "utf-8"),
  );
  const pages = buildModuleStructuredPages(
    module,
    narrativeMarkdown,
    evidence,
    sourcePath,
    new Date().toISOString(),
    graph,
    "module",
  );

  return { warning, pages };
}

/* ------------------------------------------------------------------ */
/*  synthesizeParentDoc                                                */
/* ------------------------------------------------------------------ */

export async function synthesizeParentDoc(
  module: ModuleNode,
  tree: ModuleNode,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  const evidence = buildModuleEvidence(module, tree, graph, sourcePath);
  const prompt = buildParentPrompt(
    module,
    tree,
    sourcePath,
    WIKI_DIR_NAME,
    evidence,
  );
  const docFullPath = join(wikiDir, module.docPath || "");
  mkdirSync(join(docFullPath, ".."), { recursive: true });
  // Remove stale file from a previous aborted run so writeFinalDocument
  // doesn't preserve old fallback content on resume.
  if (existsSync(docFullPath)) rmSync(docFullPath);

  let warning: string | null = null;
  const hasContent = evidence.classCount > 0 || evidence.methodCount > 0 || evidence.fileCount > 0;
  if (!hasContent && module.children.length === 0) {
    warning = `Module has no indexed content (${evidence.fileCount} files, ${evidence.classCount} classes)`;
    console.log(`[wiki-gen] Skipping agent for parent ${module.name}: ${warning}`);
  } else {
    try {
      await runSubagent(prompt, sourcePath, "module-synthesizer", undefined, abortSignal);
      if (!existsSync(docFullPath)) {
        warning = "Agent completed but did not write output file";
        console.warn(`[wiki-gen] Agent for parent ${module.name} did not write output`);
      }
    } catch (err) {
      if (abortSignal?.aborted) throw err;
      warning = err instanceof Error ? err.message : String(err);
      console.warn(`[wiki-gen] Agent for parent ${module.name} failed: ${warning}`);
    }
  }

  await writeFinalDocument(
    docFullPath,
    buildModuleFallbackBody(module, evidence, warning),
    [
      buildAppendixSection("Module Facts", evidence.facts),
      buildAppendixSection("Sub-module Navigation", evidence.childLinks),
      buildAppendixSection("Code References", evidence.classReferences),
      buildAppendixSection("Method References", evidence.methodReferences),
      buildAppendixSection("Dependencies", evidence.dependencySummary),
      buildAppendixSection(
        "Cross-Module Relationships",
        evidence.crossModuleRelationships,
      ),
      buildAppendixSection("Import Patterns", evidence.importPatterns),
      buildAppendixSection("Source Files", evidence.fileInventory),
      evidence.structureDiagram
        ? buildAppendixSection(
            "Module Structure Diagram",
            evidence.structureDiagram,
            "mermaid",
          )
        : "",
      evidence.dependencyDiagram
        ? buildAppendixSection(
            "Dependency Diagram",
            evidence.dependencyDiagram,
            "mermaid",
          )
        : "",
    ],
  );

  const narrativeMarkdown = stripManagedAppendix(
    await readFile(docFullPath, "utf-8"),
  );
  const pages = buildModuleStructuredPages(
    module,
    narrativeMarkdown,
    evidence,
    sourcePath,
    new Date().toISOString(),
    graph,
    "module",
  );

  return { warning, pages };
}

/* ------------------------------------------------------------------ */
/*  synthesizeRootOverview                                             */
/* ------------------------------------------------------------------ */

export async function synthesizeRootOverview(
  tree: ModuleNode,
  index: RepoIndex,
  sourcePath: string,
  wikiDir: string,
  resourceFiles: string[],
  extraPageIds: string[],
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  const evidence = buildRootEvidence(tree, index, index.graph, sourcePath);
  const prompt = buildRootPrompt(
    tree,
    index,
    sourcePath,
    WIKI_DIR_NAME,
    evidence,
    resourceFiles,
  );

  // Remove stale overview from a previous aborted run
  const overviewPath = join(wikiDir, "overview.md");
  if (existsSync(overviewPath)) rmSync(overviewPath);

  let warning: string | null = null;
  try {
    await runSubagent(prompt, sourcePath, "wiki-synthesizer", undefined, abortSignal);
    if (!existsSync(overviewPath)) {
      warning = "Agent completed but did not write output file";
    }
  } catch (err) {
    if (abortSignal?.aborted) throw err;
    warning = err instanceof Error ? err.message : String(err);
  }

  await writeFinalDocument(
    join(wikiDir, "overview.md"),
    buildRootFallbackBody(evidence, warning),
    [
      buildAppendixSection("Repository Facts", evidence.repoStats),
      buildAppendixSection("Technology Stack", evidence.technologyStack),
      buildAppendixSection("Module Navigation", evidence.topLevelLinks),
      buildAppendixSection("Module Summary", evidence.topLevelSummary),
      buildAppendixSection("Code References", evidence.referenceCatalog),
      evidence.dependencyDiagram
        ? buildAppendixSection(
            "Module Dependency Diagram",
            evidence.dependencyDiagram,
            "mermaid",
          )
        : "",
    ],
  );

  const narrativeMarkdown = stripManagedAppendix(
    await readFile(join(wikiDir, "overview.md"), "utf-8"),
  );
  const pages = [
    buildOverviewStructuredPage(
      narrativeMarkdown,
      evidence,
      tree,
      index.graph,
      sourcePath,
      new Date().toISOString(),
      extraPageIds,
    ),
  ];

  return { warning, pages };
}

/* ------------------------------------------------------------------ */
/*  generateCrossCuttingPage                                           */
/* ------------------------------------------------------------------ */

export async function generateCrossCuttingPage(
  pattern: CrossCuttingPattern,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  const prompt = buildCrossCuttingPrompt(
    pattern,
    graph,
    sourcePath,
    WIKI_DIR_NAME,
  );
  const docPath = `guides/${pattern.id}.md`;
  const docFullPath = join(wikiDir, docPath);
  mkdirSync(join(docFullPath, ".."), { recursive: true });
  // Remove stale file from a previous aborted run so writeFinalDocument
  // doesn't preserve old fallback content on resume.
  if (existsSync(docFullPath)) rmSync(docFullPath);

  let warning: string | null = null;
  try {
    await runSubagent(prompt, sourcePath, "cross-cutting-doc", undefined, abortSignal);
  } catch (err) {
    if (abortSignal?.aborted) throw err;
    warning = err instanceof Error ? err.message : String(err);
  }

  // Fallback if agent didn't write
  if (!existsSync(docFullPath)) {
    if (!warning) warning = "Agent completed but did not write output file";
    const fallback = [
      `# ${pattern.title}`,
      "",
      pattern.description,
      "",
      "## Relevant Classes",
      "",
      ...pattern.files.slice(0, 15).map((f) => `- \`${f}\``),
      "",
      warning
        ? `> **Note:** AI narrative generation failed: ${warning}`
        : "",
    ]
      .join("\n")
      .trim();
    await writeFile(docFullPath, fallback + "\n");
  }

  const narrativeMarkdown = stripManagedAppendix(
    await readFile(docFullPath, "utf-8"),
  );
  const generatedAt = new Date().toISOString();
  const pageId = `guide:${pattern.id}`;

  const page: WikiPage = {
    id: pageId,
    title: pattern.title,
    pageType: "module",
    kind: "concept" as never, // Will be inferred by wiki-store normalization
    path: `guides/${pattern.id}`,
    summary: pattern.description,
    sourceNodeIds: pattern.nodeIds.slice(0, 30),
    sourceEdgeKeys: [],
    primaryFiles: pattern.files
      .slice(0, 20)
      .map((fp) => toRelativePath(sourcePath, fp)),
    childPageIds: [],
    generatedAt,
    sections: [
      {
        id: "narrative",
        title: "Documentation",
        type: "markdown",
        sourceNodeIds: pattern.nodeIds.slice(0, 30),
        markdown: narrativeMarkdown,
      },
      {
        id: "related-classes",
        title: "Related Classes",
        type: "references",
        sourceNodeIds: pattern.nodeIds.slice(0, 20),
        references: pattern.nodeIds.slice(0, 20).map((nodeId) => {
          try {
            const attrs = graph.getNodeAttrs(nodeId);
            const fp = getString(attrs, "file_path") || "";
            return buildCodeReference(
              String(attrs.name),
              sourcePath,
              fp,
              getNumber(attrs, "line_start"),
              getNumber(attrs, "line_end"),
              String(attrs.name),
              nodeId,
            );
          } catch {
            return buildCodeReference(nodeId, sourcePath, "");
          }
        }),
      },
    ],
  };

  return { warning, pages: [page] };
}

/* ------------------------------------------------------------------ */
/*  generateGettingStartedPage                                         */
/* ------------------------------------------------------------------ */

export async function generateGettingStartedPage(
  tree: ModuleNode,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
  keyClasses: KeyClassCandidate[],
  resourceFiles: string[],
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  const prompt = buildGettingStartedPrompt(
    tree,
    keyClasses,
    graph,
    sourcePath,
    WIKI_DIR_NAME,
    resourceFiles,
  );
  const docFullPath = join(wikiDir, "guides", "getting-started.md");
  mkdirSync(join(docFullPath, ".."), { recursive: true });
  // Remove stale file from a previous aborted run so writeFinalDocument
  // doesn't preserve old fallback content on resume.
  if (existsSync(docFullPath)) rmSync(docFullPath);

  let warning: string | null = null;
  try {
    await runSubagent(prompt, sourcePath, "getting-started-doc", undefined, abortSignal);
  } catch (err) {
    // Re-throw abort errors so processStep skips caching fallback content
    if (abortSignal?.aborted) throw err;
    warning = err instanceof Error ? err.message : String(err);
  }

  // Fallback
  if (!existsSync(docFullPath)) {
    if (!warning) warning = "Agent completed but did not write output file";
    const modules = tree.children
      .map((c) => `- **${c.name}**: ${c.classes.length} classes`)
      .join("\n");
    const fallback = [
      "# Getting Started",
      "",
      "Welcome to this codebase. Here's a quick orientation.",
      "",
      "## Project Structure",
      "",
      modules || "No modules found.",
      "",
      "## Key Classes",
      "",
      ...keyClasses
        .slice(0, 8)
        .map(
          (kc) => `- **${kc.name}** — ${kc.connectionCount} connections`,
        ),
      "",
      warning
        ? `> **Note:** AI narrative generation failed: ${warning}`
        : "",
    ]
      .join("\n")
      .trim();
    await writeFile(docFullPath, fallback + "\n");
  }

  const narrativeMarkdown = stripManagedAppendix(
    await readFile(docFullPath, "utf-8"),
  );
  const generatedAt = new Date().toISOString();

  const page: WikiPage = {
    id: "guide:getting-started",
    title: "Getting Started",
    pageType: "module",
    path: "guides/getting-started",
    summary: "Orientation guide for engineers new to this codebase.",
    sourceNodeIds: keyClasses.map((kc) => kc.nodeId),
    sourceEdgeKeys: [],
    primaryFiles: keyClasses.map((kc) =>
      toRelativePath(sourcePath, kc.filePath),
    ),
    childPageIds: [],
    generatedAt,
    sections: [
      {
        id: "narrative",
        title: "Documentation",
        type: "markdown",
        sourceNodeIds: keyClasses.map((kc) => kc.nodeId),
        markdown: narrativeMarkdown,
      },
      {
        id: "key-classes",
        title: "Key Classes",
        type: "references",
        sourceNodeIds: keyClasses.map((kc) => kc.nodeId),
        references: keyClasses.slice(0, 12).map((kc) =>
          buildCodeReference(
            kc.name,
            sourcePath,
            kc.filePath,
            kc.lineStart,
            kc.lineEnd,
            kc.name,
            kc.nodeId,
          ),
        ),
      },
    ],
  };

  return { warning, pages: [page] };
}

/* ------------------------------------------------------------------ */
/*  generateClassDeepDivePage                                          */
/* ------------------------------------------------------------------ */

export async function generateClassDeepDivePage(
  candidate: KeyClassCandidate,
  graph: KnowledgeGraph,
  sourcePath: string,
  wikiDir: string,
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  const prompt = buildClassDeepDivePrompt(
    candidate,
    graph,
    sourcePath,
    WIKI_DIR_NAME,
  );
  const docPath = `deep-dive/${candidate.name}.md`;
  const docFullPath = join(wikiDir, docPath);
  mkdirSync(join(docFullPath, ".."), { recursive: true });
  // Remove stale file from a previous aborted run so writeFinalDocument
  // doesn't preserve old fallback content on resume.
  if (existsSync(docFullPath)) rmSync(docFullPath);

  let warning: string | null = null;
  try {
    await runSubagent(prompt, sourcePath, "class-deep-dive", undefined, abortSignal);
  } catch (err) {
    // Re-throw abort errors so processStep skips caching fallback content
    if (abortSignal?.aborted) throw err;
    warning = err instanceof Error ? err.message : String(err);
  }

  // Fallback if agent didn't write
  if (!existsSync(docFullPath)) {
    if (!warning) warning = "Agent completed but did not write output file";
    const attrs = graph.getNodeAttrs(candidate.nodeId);
    const allMembers = graph.getNeighbors(
      candidate.nodeId,
      EdgeType.CONTAINS,
      "out",
    );
    const methodMembers = allMembers
      .map((memberId) => {
        try {
          const mAttrs = graph.getNodeAttrs(memberId);
          const mType = String(mAttrs.type);
          if (mType !== NodeType.METHOD && mType !== NodeType.CONSTRUCTOR)
            return null;
          const mName = String(mAttrs.name);
          const mReturn = getString(mAttrs, "return_type") || "void";
          const mParams = Array.isArray(mAttrs.parameters)
            ? mAttrs.parameters.filter(
                (v): v is string => typeof v === "string",
              )
            : [];
          const lineStart = getNumber(mAttrs, "line_start");
          const lineEnd = getNumber(mAttrs, "line_end");
          const lineInfo = lineStart
            ? ` (lines ${lineStart}–${lineEnd ?? "?"})`
            : "";
          return `- **${mName}**(${mParams.join(", ")}) → \`${mReturn}\`${lineInfo}`;
        } catch {
          return null;
        }
      })
      .filter(Boolean) as string[];

    const shownMethods = methodMembers.slice(0, 50);
    const remaining = methodMembers.length - shownMethods.length;

    const fallback = [
      `# ${candidate.name}`,
      "",
      `\`${candidate.name}\` is a ${String(attrs.type)} located in \`${toRelativePath(sourcePath, candidate.filePath)}\`.`,
      `It has ${candidate.connectionCount} inbound connections, making it one of the most-referenced types in the codebase.`,
      "",
      `## Methods (${methodMembers.length} total)`,
      "",
      ...shownMethods,
      remaining > 0 ? `\n*... and ${remaining} more methods*` : "",
      "",
      warning
        ? `> **Note:** AI narrative generation failed: ${warning}`
        : "",
    ]
      .join("\n")
      .trim();
    await writeFile(docFullPath, fallback + "\n");
  }

  const narrativeMarkdown = stripManagedAppendix(
    await readFile(docFullPath, "utf-8"),
  );
  const generatedAt = new Date().toISOString();
  const pageId = `deep-dive:${candidate.name}`;

  // Build method references for the structured page
  const methodRefs: WikiCodeReference[] = [];
  for (const memberId of graph.getNeighbors(
    candidate.nodeId,
    EdgeType.CONTAINS,
    "out",
  )) {
    try {
      const mAttrs = graph.getNodeAttrs(memberId);
      if (
        String(mAttrs.type) !== NodeType.METHOD &&
        String(mAttrs.type) !== NodeType.CONSTRUCTOR
      )
        continue;
      methodRefs.push(
        buildCodeReference(
          `${candidate.name}.${String(mAttrs.name)}`,
          sourcePath,
          candidate.filePath,
          getNumber(mAttrs, "line_start"),
          getNumber(mAttrs, "line_end"),
          `${candidate.name}.${String(mAttrs.name)}`,
          memberId,
        ),
      );
    } catch {
      /* skip */
    }
  }

  const page: WikiPage = {
    id: pageId,
    title: candidate.name,
    pageType: "entity" as WikiPageType,
    path: `deep-dive/${candidate.name}`,
    summary: `In-depth documentation for ${candidate.name} (${candidate.connectionCount} connections)`,
    sourceNodeIds: [candidate.nodeId],
    sourceEdgeKeys: [],
    primaryFiles: [toRelativePath(sourcePath, candidate.filePath)],
    childPageIds: [],
    generatedAt,
    sections: [
      {
        id: "narrative",
        title: "Documentation",
        type: "markdown",
        sourceNodeIds: [candidate.nodeId],
        markdown: narrativeMarkdown,
      },
      {
        id: "source-location",
        title: "Source",
        type: "references",
        sourceNodeIds: [candidate.nodeId],
        references: [
          buildCodeReference(
            candidate.name,
            sourcePath,
            candidate.filePath,
            candidate.lineStart,
            candidate.lineEnd,
            candidate.name,
            candidate.nodeId,
          ),
        ],
      },
      {
        id: "methods",
        title: "Methods",
        type: "references",
        sourceNodeIds: methodRefs
          .map((r) => r.nodeId)
          .filter(Boolean) as string[],
        references: methodRefs,
      },
    ],
  };

  return { warning, pages: [page] };
}

/* ------------------------------------------------------------------ */
/*  generateDDPage                                                     */
/* ------------------------------------------------------------------ */

/**
 * Generate a Detailed Design (DD) page for a single method.
 *
 * The agent reads the method's source code and produces a DD document
 * following a structured template with flowchart, sequence diagram,
 * class/method list, and detailed descriptions.
 */
export async function generateDDPage(
  method: DDMethodInfo,
  sourcePath: string,
  wikiDir: string,
  abortSignal?: AbortSignal,
): Promise<GeneratedWikiArtifacts> {
  // Pre-read the method source code to embed in the prompt.
  // This avoids the agent needing to read Shift-JIS/CP932 files via tool calls
  // which causes context overflow and LLM stream errors.
  if (!method.sourceCode && method.lineStart && method.lineEnd) {
    try {
      const raw = await readFile(method.filePath);
      let text: string;
      try {
        text = new TextDecoder("utf-8", { fatal: true }).decode(raw);
      } catch {
        // CP932/Shift-JIS file — try Shift-JIS decoder first, fall back to lossy ASCII
        try {
          text = new TextDecoder("shift_jis", { fatal: false }).decode(raw);
        } catch {
          // Runtime doesn't support shift_jis — strip to ASCII as last resort
          const chars: string[] = [];
          for (let i = 0; i < raw.length; i++) {
            const b = raw[i];
            if ((b >= 0x20 && b <= 0x7E) || b === 0x0A || b === 0x0D || b === 0x09) {
              chars.push(String.fromCharCode(b));
            } else {
              chars.push(" ");
            }
          }
          text = chars.join("");
        }
      }
      text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
      const lines = text.split("\n");
      const start = Math.max(0, method.lineStart - 1);
      const end = Math.min(lines.length, start + 300);
      method = { ...method, sourceCode: lines.slice(start, end).join("\n") };
    } catch {
      // If pre-read fails, agent will read via tool
    }
  }

  const prompt = buildDDPrompt(method, sourcePath, WIKI_DIR_NAME);
  const docPath = `dd/${method.className}/${method.methodName}.md`;
  const docFullPath = join(wikiDir, docPath);
  mkdirSync(join(docFullPath, ".."), { recursive: true });
  // Remove stale file from a previous aborted run so writeFinalDocument
  // doesn't preserve old fallback content on resume.
  if (existsSync(docFullPath)) rmSync(docFullPath);

  let warning: string | null = null;
  try {
    // With pre-loaded source, agent needs ~3-5 iterations; without, ~10-15
    // Extra iterations for verify_mermaid tool calls (2 diagrams × up to 2 retries each)
    await runSubagent(prompt, sourcePath, "dd-generator", method.sourceCode ? 14 : 20, abortSignal);
  } catch (err) {
    // Re-throw abort errors so processStep skips caching fallback content
    if (abortSignal?.aborted) throw err;
    warning = err instanceof Error ? err.message : String(err);
  }

  // Fallback if agent didn't write the file
  if (!existsSync(docFullPath)) {
    if (!warning) warning = "Agent completed but did not write output file";
    const ddLabel = `DD${String(method.ddIndex).padStart(2, "0")}`;
    const sig = `${method.className}.${method.methodName}(${method.parameters.join(", ")})`;
    const fallback = [
      `# (${ddLabel}) ${method.methodName}`,
      "",
      `Detailed design for \`${sig}\`.`,
      "",
      `- File: \`${method.filePath}\``,
      `- Lines: ${method.lineStart ?? "?"}–${method.lineEnd ?? "?"}`,
      method.javadoc ? `- Description: ${method.javadoc}` : "",
      "",
      warning
        ? `> **Note:** AI narrative generation failed: ${warning}`
        : "",
    ]
      .join("\n")
      .trim();
    await writeFile(docFullPath, fallback + "\n");
  }

  const rawMarkdown = await readFile(docFullPath, "utf-8");
  const narrativeMarkdown = stripManagedAppendix(rawMarkdown);

  // Extract English title from the generated markdown's H1 heading
  const h1Match = narrativeMarkdown.match(/^#\s+(.+)$/m);
  const englishTitle = h1Match
    ? h1Match[1]
        .replace(/^\s*\(DD\d+\)\s*/, "")  // strip leading (DDxx) prefix
        .replace(/\s*\(.*?\)\s*$/, "")      // strip trailing parentheticals
        .trim()
    : null;

  // Build a clean English summary from the first paragraph after the heading
  let englishSummary: string | undefined;
  const lines = narrativeMarkdown.split("\n");
  for (let i = 1; i < Math.min(lines.length, 20); i++) {
    const line = lines[i].trim();
    if (line && !line.startsWith("#") && !line.startsWith("-") && !line.startsWith("`") && !line.startsWith(">") && !line.startsWith("|")) {
      englishSummary = line.slice(0, 200);
      break;
    }
  }

  const generatedAt = new Date().toISOString();
  const pageId = `dd:${method.className}:${method.methodName}`;
  const ddLabel = `DD${String(method.ddIndex).padStart(2, "0")}`;
  const displayTitle = englishTitle || `${method.className}.${method.methodName}`;
  const loc = method.lineStart && method.lineEnd ? method.lineEnd - method.lineStart + 1 : 0;

  const page: WikiPage = {
    id: pageId,
    title: `(${ddLabel}) ${displayTitle}`,
    pageType: "dd" as WikiPageType,
    path: `dd/${method.className}/${method.methodName}`,
    summary: englishSummary || `Detailed design for ${method.className}.${method.methodName} (${loc} LOC)`,
    sourceNodeIds: [],
    sourceEdgeKeys: [],
    primaryFiles: [toRelativePath(sourcePath, method.filePath)],
    childPageIds: [],
    generatedAt,
    sections: [
      {
        id: "dd-narrative",
        title: displayTitle,
        type: "markdown",
        markdown: narrativeMarkdown,
      },
      {
        id: "dd-source",
        title: "Source Method",
        type: "references",
        references: [
          buildCodeReference(
            `${method.className}.${method.methodName}`,
            sourcePath,
            method.filePath,
            method.lineStart,
            method.lineEnd,
            `${method.className}.${method.methodName}`,
          ),
        ],
      },
    ],
  };

  return { warning, pages: [page] };
}
