import { readFile, readdir, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { BaseAgent } from "./base.js";
import { ContextPackageSchema, } from "../models/input-governance.js";
import { parseChapterSummariesMarkdown, retrieveMemorySelection, } from "../utils/memory-retrieval.js";
import { buildGovernedRuleStack, buildGovernedTrace, } from "../utils/context-assembly.js";
import { writeGovernedRuntimeArtifacts } from "../utils/runtime-writer.js";
export async function composeGovernedChapter(input) {
    const storyDir = join(input.bookDir, "story");
    const runtimeDir = join(storyDir, "runtime");
    await mkdir(runtimeDir, { recursive: true });
    const selectedContext = await collectSelectedContext(storyDir, input.plan, input.book.language ?? "zh");
    const contextPackage = ContextPackageSchema.parse({
        chapter: input.chapterNumber,
        selectedContext,
    });
    const ruleStack = buildGovernedRuleStack(input.plan, input.chapterNumber);
    const trace = buildGovernedTrace({
        chapterNumber: input.chapterNumber,
        plan: input.plan,
        contextPackage,
        composerInputs: [input.plan.runtimePath],
    });
    const { contextPath, ruleStackPath, tracePath, } = await writeGovernedRuntimeArtifacts({
        runtimeDir,
        chapterNumber: input.chapterNumber,
        contextPackage,
        ruleStack,
        trace,
    });
    return {
        contextPackage,
        ruleStack,
        trace,
        contextPath,
        ruleStackPath,
        tracePath,
    };
}
export class ComposerAgent extends BaseAgent {
    get name() {
        return "composer";
    }
    async composeChapter(input) {
        return composeGovernedChapter(input);
    }
}
async function collectSelectedContext(storyDir, plan, language) {
    const retrievalHints = deriveRetrievalHints(plan);
    const memoBodyExcerpt = plan.memo.body.trim();
    const chapterMemoEntry = memoBodyExcerpt.length > 0
        ? [{
                source: "runtime/chapter_memo",
                reason: "Carry the planner's chapter memo into governed writing.",
                excerpt: [
                    `goal=${plan.memo.goal}`,
                    plan.memo.isGoldenOpening ? "golden-opening=true" : undefined,
                    memoBodyExcerpt,
                ].filter(Boolean).join(" | "),
            }]
        : [{
                source: "runtime/chapter_memo",
                reason: "Carry the planner's chapter memo into governed writing.",
                excerpt: `goal=${plan.memo.goal}`,
            }];
    const entries = await Promise.all([
        maybeContextSource(storyDir, "current_focus.md", "Current task focus for this chapter."),
        maybeContextSource(storyDir, "audit_drift.md", "Carry forward audit drift guidance from the previous chapter without polluting hard state facts."),
        maybeContextSource(storyDir, "current_state.md", "Preserve hard state facts referenced by the active chapter brief or hard constraints.", retrievalHints),
        maybeContextSource(storyDir, "outline/story_frame.md", "Preserve canon constraints referenced by the active chapter brief or hard constraints.", retrievalHints),
        maybeContextSource(storyDir, "outline/volume_map.md", "Anchor the default planning node for this chapter.", plan.intent.outlineNode ? [plan.intent.outlineNode] : []),
        maybeContextSource(storyDir, "parent_canon.md", "Preserve parent canon constraints for governed continuation or fanfic writing."),
        maybeContextSource(storyDir, "fanfic_canon.md", "Preserve extracted fanfic canon constraints for governed writing."),
    ]);
    const trailEntries = await buildRecentChapterTrailEntries(storyDir, plan.intent.chapter);
    const memorySelection = await retrieveMemorySelection({
        bookDir: dirname(storyDir),
        chapterNumber: plan.intent.chapter,
        goal: plan.intent.goal,
        outlineNode: plan.intent.outlineNode,
        mustKeep: retrievalHints,
    });
    const hookDebtEntries = await buildHookDebtEntries(storyDir, plan, memorySelection.activeHooks, language);
    const summaryEntries = memorySelection.summaries.map((summary) => ({
        source: `story/chapter_summaries.md#${summary.chapter}`,
        reason: "Relevant episodic memory retrieved for the current chapter goal.",
        excerpt: [summary.title, summary.events, summary.stateChanges, summary.hookActivity]
            .filter(Boolean)
            .join(" | "),
    }));
    const factEntries = memorySelection.facts.map((fact) => ({
        source: `story/current_state.md#${toFactAnchor(fact.predicate)}`,
        reason: "Relevant current-state fact retrieved for the current chapter goal.",
        excerpt: `${fact.predicate} | ${fact.object}`,
    }));
    const hookEntries = memorySelection.hooks.map((hook) => ({
        source: `story/pending_hooks.md#${hook.hookId}`,
        reason: "Carry forward unresolved hooks that match the chapter focus.",
        excerpt: [hook.type, hook.status, hook.expectedPayoff, hook.payoffTiming, hook.notes]
            .filter(Boolean)
            .join(" | "),
    }));
    const volumeSummaryEntries = memorySelection.volumeSummaries.map((summary) => ({
        source: `story/volume_summaries.md#${summary.anchor}`,
        reason: "Carry forward long-span arc memory compressed from earlier volumes.",
        excerpt: `${summary.heading} | ${summary.content}`,
    }));
    return [
        ...chapterMemoEntry,
        ...entries.filter((entry) => entry !== null),
        ...trailEntries,
        ...hookDebtEntries,
        ...factEntries,
        ...summaryEntries,
        ...volumeSummaryEntries,
        ...hookEntries,
    ];
}
function deriveRetrievalHints(plan) {
    return [
        plan.intent.goal,
        plan.intent.outlineNode,
        ...plan.memo.threadRefs,
    ].filter((value) => Boolean(value));
}
async function buildRecentChapterTrailEntries(storyDir, chapterNumber) {
    const content = await readFileOrDefault(join(storyDir, "chapter_summaries.md"));
    if (!content || content === "(文件尚未创建)") {
        return [];
    }
    const recentSummaries = parseChapterSummariesMarkdown(content)
        .filter((summary) => summary.chapter < chapterNumber)
        .sort((left, right) => right.chapter - left.chapter)
        .slice(0, 5);
    if (recentSummaries.length === 0) {
        return [];
    }
    const entries = [];
    const recentTitles = recentSummaries
        .map((summary) => [summary.chapter, summary.title].filter(Boolean).join(": "))
        .filter(Boolean)
        .join(" | ");
    if (recentTitles) {
        entries.push({
            source: "story/chapter_summaries.md#recent_titles",
            reason: "Keep recent title history visible to avoid repetitive chapter naming.",
            excerpt: recentTitles,
        });
    }
    const moodTrail = recentSummaries
        .filter((summary) => summary.mood || summary.chapterType)
        .map((summary) => `${summary.chapter}: ${summary.mood || "(none)"} / ${summary.chapterType || "(none)"}`)
        .join(" | ");
    if (moodTrail) {
        entries.push({
            source: "story/chapter_summaries.md#recent_mood_type_trail",
            reason: "Keep recent mood and chapter-type cadence visible before writing the next chapter.",
            excerpt: moodTrail,
        });
    }
    const endingTrail = await buildRecentEndingTrail(storyDir, chapterNumber);
    if (endingTrail) {
        entries.push({
            source: "story/chapters#recent_endings",
            reason: "Show how recent chapters ended so the writer avoids structural repetition (e.g. 3 consecutive collapse endings).",
            excerpt: endingTrail,
        });
    }
    return entries;
}
async function buildRecentEndingTrail(storyDir, chapterNumber) {
    const chaptersDir = join(dirname(storyDir), "chapters");
    try {
        const files = await readdir(chaptersDir);
        const chapterFiles = files
            .filter((file) => file.endsWith(".md"))
            .map((file) => ({ file, num: parseInt(file.slice(0, 4), 10) }))
            .filter((entry) => Number.isFinite(entry.num) && entry.num < chapterNumber)
            .sort((a, b) => b.num - a.num)
            .slice(0, 3);
        const endings = [];
        for (const entry of chapterFiles.reverse()) {
            const content = await readFile(join(chaptersDir, entry.file), "utf-8");
            const lastLine = extractLastMeaningfulSentence(content);
            if (lastLine) {
                endings.push(`ch${entry.num}: ${lastLine}`);
            }
        }
        return endings.length >= 2 ? endings.join(" | ") : undefined;
    }
    catch {
        return undefined;
    }
}
function extractLastMeaningfulSentence(content) {
    const lines = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 5 && !line.startsWith("#") && !line.startsWith("|") && !line.startsWith("==="));
    const last = lines.at(-1);
    if (!last)
        return undefined;
    return last.length > 60 ? last.slice(0, 57) + "..." : last;
}
async function buildHookDebtEntries(storyDir, plan, activeHooks, language) {
    const targetHookIds = [...new Set(plan.memo.threadRefs)];
    if (targetHookIds.length === 0) {
        return [];
    }
    const summaries = parseChapterSummariesMarkdown(await readFileOrDefault(join(storyDir, "chapter_summaries.md")));
    return targetHookIds.flatMap((hookId) => {
        const hook = activeHooks.find((entry) => entry.hookId === hookId);
        if (!hook) {
            return [];
        }
        const seedSummary = findHookSummary(summaries, hook.hookId, hook.startChapter, "seed");
        const latestSummary = findHookSummary(summaries, hook.hookId, hook.lastAdvancedChapter, "latest");
        const role = language === "en" ? "memo-referenced debt" : "备忘引用旧债";
        const promise = hook.expectedPayoff || (language === "en" ? "(unspecified)" : "（未写明）");
        const seedBeat = seedSummary
            ? renderHookDebtBeat(seedSummary)
            : (hook.notes || promise);
        const latestBeat = latestSummary && latestSummary !== seedSummary
            ? renderHookDebtBeat(latestSummary)
            : undefined;
        const age = Math.max(0, plan.intent.chapter - Math.max(1, hook.startChapter));
        return [{
                source: `runtime/hook_debt#${hook.hookId}`,
                reason: language === "en"
                    ? "Narrative debt brief with original seed text for this hook agenda target."
                    : "含原始种子文本的叙事债务简报。",
                excerpt: language === "en"
                    ? [
                        `${hook.hookId} (${hook.type}, ${role}, open ${age} chapters)`,
                        `reader promise: ${promise}`,
                        `original seed (ch${hook.startChapter}): ${seedBeat}`,
                        latestBeat ? `latest turn (ch${hook.lastAdvancedChapter}): ${latestBeat}` : undefined,
                    ].filter(Boolean).join(" | ")
                    : [
                        `${hook.hookId}（${hook.type}，${role}，已开${age}章）`,
                        `读者承诺：${promise}`,
                        `种于第${hook.startChapter}章：${seedBeat}`,
                        latestBeat ? `推进于第${hook.lastAdvancedChapter}章：${latestBeat}` : undefined,
                    ].filter(Boolean).join(" | "),
            }];
    });
}
async function maybeContextSource(storyDir, fileName, reason, preferredExcerpts = []) {
    const path = join(storyDir, fileName);
    let content = await readFileOrDefault(path);
    let resolvedFileName = fileName;
    if ((!content || content === "(文件尚未创建)")) {
        // Phase 5 back-compat: the new outline/ files may be absent on legacy
        // books. Fall back to the deprecated paths transparently.
        const legacyFallback = outlineFallback(fileName);
        if (legacyFallback) {
            const legacyPath = join(storyDir, legacyFallback);
            const legacyContent = await readFileOrDefault(legacyPath);
            if (legacyContent && legacyContent !== "(文件尚未创建)") {
                content = legacyContent;
                resolvedFileName = legacyFallback;
            }
        }
    }
    if (!content || content === "(文件尚未创建)")
        return null;
    return {
        source: `story/${resolvedFileName}`,
        reason,
        excerpt: pickExcerpt(content, preferredExcerpts),
    };
}
function outlineFallback(fileName) {
    if (fileName === "outline/story_frame.md")
        return "story_bible.md";
    if (fileName === "outline/volume_map.md")
        return "volume_outline.md";
    return null;
}
function pickExcerpt(content, preferredExcerpts) {
    for (const preferred of preferredExcerpts) {
        if (preferred && content.includes(preferred))
            return preferred;
    }
    return content
        .split("\n")
        .map((line) => line.trim())
        .find((line) => line.length > 0 && !line.startsWith("#"));
}
function toFactAnchor(predicate) {
    return predicate
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
        .replace(/^-+|-+$/g, "")
        || "fact";
}
async function readFileOrDefault(path) {
    try {
        return await readFile(path, "utf-8");
    }
    catch {
        return "(文件尚未创建)";
    }
}
function findHookSummary(summaries, hookId, chapter, mode) {
    const directChapterHit = summaries.find((summary) => summary.chapter === chapter);
    const hookMentions = summaries.filter((summary) => summaryMentionsHook(summary, hookId));
    if (mode === "seed") {
        return hookMentions.find((summary) => summary.chapter === chapter)
            ?? hookMentions.at(0)
            ?? directChapterHit;
    }
    return [...hookMentions].reverse().find((summary) => summary.chapter === chapter)
        ?? hookMentions.at(-1)
        ?? directChapterHit;
}
function summaryMentionsHook(summary, hookId) {
    return [
        summary.title,
        summary.events,
        summary.stateChanges,
        summary.hookActivity,
    ].some((text) => text.includes(hookId));
}
function renderHookDebtBeat(summary) {
    return `ch${summary.chapter} ${summary.title} - ${summary.events || summary.hookActivity || summary.stateChanges || "(none)"}`;
}
//# sourceMappingURL=composer.js.map