/**
 * Phase 5 (v13) path resolution — prefer the new prose outline files, fall
 * back to legacy paths so older books keep working during transition.
 *
 * Maps:
 *   story/outline/story_frame.md  →  preferred replacement for story_bible.md
 *   story/outline/volume_map.md   →  preferred replacement for volume_outline.md
 *   story/roles/主要角色/*.md +
 *   story/roles/次要角色/*.md    →  preferred replacement for character_matrix.md
 *
 * All helpers accept a bookDir (path to a book root, containing `story/`)
 * and return a string — either the new-file content when it exists, or the
 * legacy file content, or an empty default placeholder.
 */

import { readFile, readdir, access } from "node:fs/promises";
import { join } from "node:path";

/**
 * Detect whether a book uses the Phase 5 new layout (outline/story_frame.md
 * exists on disk). If yes, story_bible.md / book_rules.md are compat shims.
 * If no, those files ARE the authoritative source.
 */
export async function isNewLayoutBook(bookDir: string): Promise<boolean> {
  try {
    await access(join(bookDir, "story", "outline", "story_frame.md"));
    return true;
  } catch {
    return false;
  }
}

async function readOr(path: string, fallback: string): Promise<string> {
  try {
    return await readFile(path, "utf-8");
  } catch {
    return fallback;
  }
}

/** Read story_frame.md, falling back to legacy story_bible.md. */
export async function readStoryFrame(
  bookDir: string,
  fallbackPlaceholder: string = "",
): Promise<string> {
  const newPath = join(bookDir, "story", "outline", "story_frame.md");
  const legacyPath = join(bookDir, "story", "story_bible.md");

  const newContent = await readOr(newPath, "");
  if (newContent.trim()) return newContent;

  return readOr(legacyPath, fallbackPlaceholder);
}

/** Read volume_map.md, falling back to legacy volume_outline.md. */
export async function readVolumeMap(
  bookDir: string,
  fallbackPlaceholder: string = "",
): Promise<string> {
  const newPath = join(bookDir, "story", "outline", "volume_map.md");
  const legacyPath = join(bookDir, "story", "volume_outline.md");

  const newContent = await readOr(newPath, "");
  if (newContent.trim()) return newContent;

  return readOr(legacyPath, fallbackPlaceholder);
}

/** Read the rhythm principles file (zh or en variant). */
export async function readRhythmPrinciples(bookDir: string): Promise<string> {
  const zhPath = join(bookDir, "story", "outline", "节奏原则.md");
  const enPath = join(bookDir, "story", "outline", "rhythm_principles.md");

  const zh = await readOr(zhPath, "");
  if (zh.trim()) return zh;
  return readOr(enPath, "");
}

export interface RoleCard {
  readonly tier: "major" | "minor";
  readonly name: string;
  readonly content: string;
}

/**
 * Read the roles/ directory. Returns [] when no roles are present (e.g. old
 * books still on character_matrix.md).
 */
export async function readRoleCards(bookDir: string): Promise<ReadonlyArray<RoleCard>> {
  const rolesRoot = join(bookDir, "story", "roles");
  const majorDirZh = join(rolesRoot, "主要角色");
  const minorDirZh = join(rolesRoot, "次要角色");
  const majorDirEn = join(rolesRoot, "major");
  const minorDirEn = join(rolesRoot, "minor");

  const cards: RoleCard[] = [];
  await Promise.all([
    collectRoleDir(majorDirZh, "major", cards),
    collectRoleDir(minorDirZh, "minor", cards),
    collectRoleDir(majorDirEn, "major", cards),
    collectRoleDir(minorDirEn, "minor", cards),
  ]);
  return cards;
}

async function collectRoleDir(
  dir: string,
  tier: "major" | "minor",
  out: RoleCard[],
): Promise<void> {
  let entries: string[];
  try {
    entries = await readdir(dir);
  } catch {
    return;
  }
  const reads = entries
    .filter((entry) => entry.endsWith(".md"))
    .map(async (entry) => {
      const content = await readOr(join(dir, entry), "");
      if (!content.trim()) return;
      out.push({
        tier,
        name: entry.replace(/\.md$/, ""),
        content,
      });
    });
  await Promise.all(reads);
}

/**
 * Render role cards in a format compatible with downstream consumers that
 * previously expected character_matrix.md prose. When no role cards exist,
 * returns the legacy character_matrix.md content or the placeholder.
 */
export async function readCharacterContext(
  bookDir: string,
  fallbackPlaceholder: string = "",
): Promise<string> {
  const cards = await readRoleCards(bookDir);
  if (cards.length > 0) {
    const groups: Record<"major" | "minor", RoleCard[]> = { major: [], minor: [] };
    for (const card of cards) groups[card.tier].push(card);

    const render = (tierCards: RoleCard[], heading: string): string => {
      if (tierCards.length === 0) return "";
      const sections = tierCards.map((card) => `### ${card.name}\n\n${card.content.trim()}`);
      return `## ${heading}\n\n${sections.join("\n\n")}`;
    };

    const blocks = [
      render(groups.major, "主要角色 / Major characters"),
      render(groups.minor, "次要角色 / Minor characters"),
    ].filter(Boolean);

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

  // Fallback: legacy character_matrix.md (may itself be a shim pointer).
  const legacyPath = join(bookDir, "story", "character_matrix.md");
  return readOr(legacyPath, fallbackPlaceholder);
}

// ---------------------------------------------------------------------------
// Phase 5 consolidation: current_state.md initial fallback
//
// After architect consolidation (7→5 sections), current_state.md is seeded
// with a tiny placeholder at book creation. Real content only arrives once
// the consolidator has appended output from chapter 1 onward. Readers that
// previously relied on architect-provided initial state (writer phase-1
// creative prompt, continuity, chapter-analyzer, reviser, composer) should
// substitute a derived initial-state block when the seed placeholder is all
// that's on disk — otherwise the "## 当前状态卡" block in prompts degenerates
// into a meta note about runtime append behaviour.
// ---------------------------------------------------------------------------

/**
 * Marker substring emitted by architect.writeFoundationFiles when seeding
 * current_state.md. Its presence is how readers detect "nothing real yet".
 */
const CURRENT_STATE_SEED_MARKERS = [
  "建书时占位",
  "Seeded at book creation",
];

export function isCurrentStateSeedPlaceholder(raw: string): boolean {
  const trimmed = raw.trim();
  if (!trimmed) return true;
  // Heuristic: short file AND contains one of the seed markers.
  if (trimmed.length > 600) return false;
  return CURRENT_STATE_SEED_MARKERS.some((marker) => trimmed.includes(marker));
}

function extractCurrentStateFromRole(content: string): string | null {
  // Accept both zh (`## 当前现状`) and en (`## Current_State` / `## Current State`).
  const pattern = /^##\s*(?:当前现状|Current[_\s]?State)[^\n]*$/im;
  const match = content.match(pattern);
  if (!match || match.index === undefined) return null;
  const after = content.slice(match.index + match[0].length);
  // Cut at next `## ` heading (same or higher level).
  const nextHeading = after.search(/^##\s/m);
  const raw = nextHeading >= 0 ? after.slice(0, nextHeading) : after;
  const text = raw.trim();
  return text.length > 0 ? text : null;
}

function extractSeedHooksFromPendingHooks(raw: string): string[] {
  if (!raw.trim()) return [];
  const lines = raw.split("\n").map((line) => line.trim()).filter(Boolean);
  const seedRows: string[] = [];
  for (const line of lines) {
    if (!line.startsWith("|")) continue;
    if (/^\|\s*-/.test(line)) continue;
    const cells = line.split("|").slice(1, -1).map((cell) => cell.trim());
    if (cells.length < 2) continue;
    if (cells[0]?.toLowerCase() === "hook_id" || cells[0] === "hookId") continue;
    const startCh = Number.parseInt(cells[1] ?? "", 10);
    if (!Number.isFinite(startCh) || startCh !== 0) continue;
    // cells[2] type, cells[5] expected payoff, last cell notes
    const notes = cells[cells.length - 1] ?? "";
    const summary = [cells[0], cells[2], notes].filter(Boolean).join(" · ");
    if (summary) seedRows.push(summary);
  }
  return seedRows;
}

/**
 * Read current_state.md; when the file is only a seed placeholder (chapter 0,
 * before consolidator has appended anything), derive an initial-state block
 * from roles/*.Current_State + pending_hooks startChapter=0 rows so callers
 * still have substantive content to feed into writer / analyzer prompts.
 */
export async function readCurrentStateWithFallback(
  bookDir: string,
  fallbackPlaceholder: string = "",
): Promise<string> {
  const storyDir = join(bookDir, "story");
  const currentStatePath = join(storyDir, "current_state.md");
  const raw = await readOr(currentStatePath, "");

  if (!isCurrentStateSeedPlaceholder(raw)) {
    return raw;
  }

  const [cards, pendingHooks] = await Promise.all([
    readRoleCards(bookDir),
    readOr(join(storyDir, "pending_hooks.md"), ""),
  ]);

  const roleLines = cards
    .map((card) => {
      const state = extractCurrentStateFromRole(card.content);
      if (!state) return null;
      const tierLabel = card.tier === "major" ? "主要" : "次要";
      return `- ${card.name}（${tierLabel}）：${state.replace(/\s+/g, " ")}`;
    })
    .filter((line): line is string => line !== null);

  const hookLines = extractSeedHooksFromPendingHooks(pendingHooks);

  if (roleLines.length === 0 && hookLines.length === 0) {
    return raw.trim() ? raw : fallbackPlaceholder;
  }

  const parts: string[] = ["# 初始状态（第 0 章，由 roles + 种子伏笔派生）"];
  if (roleLines.length > 0) {
    parts.push("\n## 角色初始位置 / 处境");
    parts.push(...roleLines);
  }
  if (hookLines.length > 0) {
    parts.push("\n## 种子伏笔（startChapter = 0）");
    parts.push(...hookLines.map((line) => `- ${line}`));
  }
  return parts.join("\n");
}
