import { readFile, writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { BaseAgent } from "./base.js";
import { readBookRules as readAuthoritativeBookRules } from "./rules-reader.js";
import { ChapterIntentSchema, } from "../models/input-governance.js";
import { renderHookSnapshot, renderSummarySnapshot, } from "../utils/memory-retrieval.js";
import { gatherPlanningMaterials, loadPlanningSeedMaterials, } from "../utils/planning-materials.js";
import { parseMemo, PlannerParseError } from "../utils/chapter-memo-parser.js";
import { buildPlannerUserMessage, getPlannerMemoSystemPrompt, } from "./planner-prompts.js";
import { composeCurrentArcProse, extractCollaboratorRows, extractOpponentRows, extractProtagonistRow, extractRelevantThreads, formatRecentSummaries, formatRecyclableHooks, readBookRules, readCharacterMatrix, readEmotionalArcs, readPendingHooks, readSubplotBoard, } from "./planner-context.js";
const MEMO_RETRY_LIMIT = 3;
/**
 * Phase 3 planner.
 *
 * Produces:
 *   - a simplified ChapterIntent (goal + outline + keep/avoid/style) —
 *     still deterministic, used for retrieval hints and the intent markdown.
 *   - a full ChapterMemo (YAML frontmatter + 7-section markdown body) via
 *     LLM call + strict parser.
 *
 * Retry policy: up to 3 attempts. Each failed parse appends an error
 * feedback block to the user message and re-invokes the LLM. On the third
 * failure we surface `PlannerParseError` — never silently truncate or
 * rename fields.
 */
export class PlannerAgent extends BaseAgent {
    get name() {
        return "planner";
    }
    async planChapter(input) {
        const storyDir = join(input.bookDir, "story");
        const runtimeDir = join(storyDir, "runtime");
        await mkdir(runtimeDir, { recursive: true });
        const seedMaterials = await loadPlanningSeedMaterials({
            bookDir: input.bookDir,
            chapterNumber: input.chapterNumber,
        });
        const outlineNode = this.findOutlineNode(seedMaterials.volumeOutline, input.chapterNumber);
        const goal = this.deriveGoal(input.externalContext, seedMaterials.currentFocus, seedMaterials.authorIntent, outlineNode, input.chapterNumber);
        // Phase hotfix 5: read structured rules through the Phase 5 authoritative
        // loader. It prefers outline/story_frame.md frontmatter, falls back to
        // legacy book_rules.md, and refuses to silently zero out rules when the
        // legacy file is just a compat shim. Reading raw bookRulesRaw via
        // parseBookRules() bypassed all of that.
        const parsedRules = await readAuthoritativeBookRules(input.bookDir);
        const prohibitions = parsedRules?.rules.prohibitions ?? [];
        const mustKeep = this.collectMustKeep(seedMaterials.currentState, seedMaterials.storyBible);
        const mustAvoid = this.collectMustAvoid(seedMaterials.currentFocus, prohibitions);
        const styleEmphasis = this.collectStyleEmphasis(seedMaterials.authorIntent, seedMaterials.currentFocus);
        const materials = await gatherPlanningMaterials({
            bookDir: input.bookDir,
            chapterNumber: input.chapterNumber,
            goal,
            outlineNode,
            mustKeep,
            seed: seedMaterials,
        });
        const memorySelection = materials.memorySelection;
        const activeHookCount = memorySelection.activeHooks.filter((hook) => hook.status !== "resolved" && hook.status !== "deferred").length;
        const arcContext = this.buildArcContext(input.book.language, seedMaterials.volumeOutline, outlineNode);
        const intent = ChapterIntentSchema.parse({
            chapter: input.chapterNumber,
            goal,
            outlineNode,
            arcContext,
            mustKeep,
            mustAvoid,
            styleEmphasis,
        });
        const isGoldenOpening = this.isGoldenOpeningChapter(input.book.language, input.chapterNumber);
        const memo = await this.planChapterMemo({
            storyDir,
            bookDir: input.bookDir,
            chapterNumber: input.chapterNumber,
            isGoldenOpening,
            fallbackGoal: goal,
            chapterSummariesRaw: seedMaterials.chapterSummariesRaw,
            previousEndingExcerpt: seedMaterials.previousEndingExcerpt,
            brief: seedMaterials.brief,
            chapterContext: input.externalContext,
            recyclableHooks: memorySelection.recyclableHooks,
            // Phase hotfix 4: thread book language through so the planner uses
            // English prompts (system + user template + golden opening guidance)
            // for English books instead of always-Chinese.
            language: input.book.language ?? "zh",
        });
        // memo.goal is LLM-produced and specific (<=50 chars, validated).
        // Overwrite intent.goal so downstream composer/retrieval gets the
        // concrete task statement instead of the outline-derived fallback.
        intent.goal = memo.goal;
        const runtimePath = join(runtimeDir, `chapter-${String(input.chapterNumber).padStart(4, "0")}.intent.md`);
        const intentMarkdown = this.renderIntentMarkdown(intent, memo, input.book.language ?? "zh", renderHookSnapshot(memorySelection.hooks, input.book.language ?? "zh"), renderSummarySnapshot(memorySelection.summaries, input.book.language ?? "zh"), activeHookCount);
        await writeFile(runtimePath, intentMarkdown, "utf-8");
        return {
            intent,
            memo,
            intentMarkdown,
            plannerInputs: materials.plannerInputs,
            runtimePath,
        };
    }
    /**
     * Invoke the LLM to produce a 7-section memo and parse it. Retries up to
     * 3 times on parse failure, injecting the error message back into the user
     * prompt so the LLM can correct itself.
     */
    async planChapterMemo(input) {
        const [characterMatrix, subplotBoard, emotionalArcs, pendingHooks, bookRulesRaw] = await Promise.all([
            readCharacterMatrix(input.storyDir),
            readSubplotBoard(input.storyDir),
            readEmotionalArcs(input.storyDir),
            readPendingHooks(input.storyDir),
            readBookRules(input.storyDir),
        ]);
        const language = input.language ?? "zh";
        const noPriorChapter = language === "en"
            ? "(this is the opening chapter — no prior chapter)"
            : "（本章为起始章，无前章）";
        const noBookRules = language === "en"
            ? "(no book_rules entries)"
            : "（暂无 book_rules 条目）";
        const retryFeedbackHeader = language === "en"
            ? "## Error from previous output"
            : "## 上次输出的错误";
        const retryFeedbackTrailer = language === "en"
            ? "Fix and re-emit."
            : "请修正后重新输出。";
        const userMessage = buildPlannerUserMessage({
            chapterNumber: input.chapterNumber,
            previousChapterEndingExcerpt: input.previousEndingExcerpt?.trim()
                ? input.previousEndingExcerpt.trim()
                : noPriorChapter,
            recentSummaries: formatRecentSummaries(input.chapterSummariesRaw, input.chapterNumber, 3),
            currentArcProse: composeCurrentArcProse(subplotBoard, emotionalArcs, input.chapterNumber),
            protagonistMatrixRow: extractProtagonistRow(characterMatrix),
            opponentRows: extractOpponentRows(characterMatrix, 3),
            collaboratorRows: extractCollaboratorRows(characterMatrix, 3),
            relevantThreads: extractRelevantThreads(pendingHooks, subplotBoard),
            recyclableHooks: formatRecyclableHooks(input.recyclableHooks ?? [], input.chapterNumber, language),
            isGoldenOpening: input.isGoldenOpening,
            bookRulesRelevant: bookRulesRaw.trim().length > 0 ? bookRulesRaw.trim() : noBookRules,
            brief: input.brief ?? "",
            chapterContext: input.chapterContext ?? "",
            language,
        });
        const systemPrompt = getPlannerMemoSystemPrompt(language);
        let currentUserMessage = userMessage;
        let lastError;
        for (let attempt = 0; attempt < MEMO_RETRY_LIMIT; attempt += 1) {
            const response = await this.chat([
                { role: "system", content: systemPrompt },
                { role: "user", content: currentUserMessage },
            ], { temperature: 0.7 });
            try {
                return parseMemo(response.content, input.chapterNumber, input.isGoldenOpening);
            }
            catch (error) {
                if (!(error instanceof PlannerParseError)) {
                    throw error;
                }
                lastError = error;
                this.log?.warn(`[planner] memo parse failed (attempt ${attempt + 1}/${MEMO_RETRY_LIMIT}): ${error.message}`);
                currentUserMessage = `${userMessage}\n\n${retryFeedbackHeader}\n${error.message}\n${retryFeedbackTrailer}`;
            }
        }
        throw lastError ?? new PlannerParseError("memo planner exhausted retries without a specific error");
    }
    isGoldenOpeningChapter(language, chapterNumber) {
        const isZh = (language ?? "zh").toLowerCase().startsWith("zh");
        return isZh ? chapterNumber <= 3 : chapterNumber <= 5;
    }
    buildArcContext(language, volumeOutline, outlineNode) {
        if (!outlineNode)
            return undefined;
        if (volumeOutline === "(文件尚未创建)")
            return undefined;
        return this.isChineseLanguage(language)
            ? `卷纲节点：${outlineNode}`
            : `Outline node: ${outlineNode}`;
    }
    deriveGoal(externalContext, currentFocus, authorIntent, outlineNode, chapterNumber) {
        const first = this.extractFirstDirective(externalContext);
        if (first)
            return first;
        const localOverride = this.extractLocalOverrideGoal(currentFocus);
        if (localOverride)
            return localOverride;
        const outline = this.extractFirstDirective(outlineNode);
        if (outline)
            return outline;
        const focus = this.extractFocusGoal(currentFocus);
        if (focus)
            return focus;
        const author = this.extractFirstDirective(authorIntent);
        if (author)
            return author;
        return `Advance chapter ${chapterNumber} with clear narrative focus.`;
    }
    collectMustKeep(currentState, storyBible) {
        return this.unique([
            ...this.extractListItems(currentState, 2),
            ...this.extractListItems(storyBible, 2),
        ]).slice(0, 4);
    }
    collectMustAvoid(currentFocus, prohibitions) {
        const avoidSection = this.extractSection(currentFocus, [
            "avoid",
            "must avoid",
            "禁止",
            "避免",
            "避雷",
        ]);
        const focusAvoids = avoidSection
            ? this.extractListItems(avoidSection, 10)
            : currentFocus
                .split("\n")
                .map((line) => line.trim())
                .filter((line) => line.startsWith("-") &&
                /avoid|don't|do not|不要|别|禁止/i.test(line))
                .map((line) => this.cleanListItem(line))
                .filter((line) => Boolean(line));
        return this.unique([...focusAvoids, ...prohibitions]).slice(0, 6);
    }
    collectStyleEmphasis(authorIntent, currentFocus) {
        return this.unique([
            ...this.extractFocusStyleItems(currentFocus),
            ...this.extractListItems(authorIntent, 2),
        ]).slice(0, 4);
    }
    extractFirstDirective(content) {
        if (!content)
            return undefined;
        return content
            .split("\n")
            .map((line) => line.trim())
            .find((line) => line.length > 0
            && !line.startsWith("#")
            && !line.startsWith("-")
            && !this.isTemplatePlaceholder(line));
    }
    extractListItems(content, limit) {
        return content
            .split("\n")
            .map((line) => line.trim())
            .filter((line) => line.startsWith("-"))
            .map((line) => this.cleanListItem(line))
            .filter((line) => Boolean(line))
            .slice(0, limit);
    }
    extractFocusGoal(currentFocus) {
        const focusSection = this.extractSection(currentFocus, [
            "active focus",
            "focus",
            "当前聚焦",
            "当前焦点",
            "近期聚焦",
        ]) ?? currentFocus;
        const directives = this.extractFocusStyleItems(focusSection, 3);
        if (directives.length === 0) {
            return this.extractFirstDirective(focusSection);
        }
        return directives.join(this.containsChinese(focusSection) ? "；" : "; ");
    }
    extractLocalOverrideGoal(currentFocus) {
        const overrideSection = this.extractSection(currentFocus, [
            "local override",
            "explicit override",
            "chapter override",
            "local task override",
            "局部覆盖",
            "本章覆盖",
            "临时覆盖",
            "当前覆盖",
        ]);
        if (!overrideSection) {
            return undefined;
        }
        const directives = this.extractListItems(overrideSection, 3);
        if (directives.length > 0) {
            return directives.join(this.containsChinese(overrideSection) ? "；" : "; ");
        }
        return this.extractFirstDirective(overrideSection);
    }
    extractFocusStyleItems(currentFocus, limit = 3) {
        const focusSection = this.extractSection(currentFocus, [
            "active focus",
            "focus",
            "当前聚焦",
            "当前焦点",
            "近期聚焦",
        ]) ?? currentFocus;
        return this.extractListItems(focusSection, limit);
    }
    renderHookBudget(activeCount, language) {
        const cap = 12;
        if (activeCount < 10) {
            return language === "en"
                ? `### Hook Budget\n- ${activeCount} active hooks (capacity: ${cap})`
                : `### 伏笔预算\n- 当前 ${activeCount} 条活跃伏笔（容量：${cap}）`;
        }
        const remaining = Math.max(0, cap - activeCount);
        return language === "en"
            ? `### Hook Budget\n- ${activeCount} active hooks — approaching capacity (${cap}). Only ${remaining} new hook(s) allowed. Prioritize resolving existing debt over opening new threads.`
            : `### 伏笔预算\n- 当前 ${activeCount} 条活跃伏笔——接近容量上限（${cap}）。仅剩 ${remaining} 个新坑位。优先回收旧债，不要轻易开新线。`;
    }
    extractSection(content, headings) {
        const targets = headings.map((heading) => this.normalizeHeading(heading));
        const lines = content.split("\n");
        let buffer = null;
        let sectionLevel = 0;
        for (const line of lines) {
            const headingMatch = line.match(/^(#+)\s*(.+?)\s*$/);
            if (headingMatch) {
                const level = headingMatch[1].length;
                const heading = this.normalizeHeading(headingMatch[2]);
                if (buffer && level <= sectionLevel) {
                    break;
                }
                if (targets.includes(heading)) {
                    buffer = [];
                    sectionLevel = level;
                    continue;
                }
            }
            if (buffer) {
                buffer.push(line);
            }
        }
        const section = buffer?.join("\n").trim();
        return section && section.length > 0 ? section : undefined;
    }
    normalizeHeading(heading) {
        return heading
            .toLowerCase()
            .replace(/[*_`:#]/g, "")
            .replace(/\s+/g, " ")
            .trim();
    }
    cleanListItem(line) {
        const cleaned = line.replace(/^-\s*/, "").trim();
        if (cleaned.length === 0)
            return undefined;
        if (/^[-|]+$/.test(cleaned))
            return undefined;
        if (this.isTemplatePlaceholder(cleaned))
            return undefined;
        return cleaned;
    }
    isTemplatePlaceholder(line) {
        const normalized = line.trim();
        if (!normalized)
            return false;
        return (/^\((describe|briefly describe|write)\b[\s\S]*\)$/i.test(normalized)
            || /^（(?:在这里描述|描述|填写|写下)[\s\S]*）$/u.test(normalized));
    }
    containsChinese(content) {
        return /[\u4e00-\u9fff]/.test(content);
    }
    findOutlineNode(volumeOutline, chapterNumber) {
        const lines = volumeOutline.split("\n").map((line) => line.trim()).filter(Boolean);
        for (let index = 0; index < lines.length; index += 1) {
            const line = lines[index];
            const match = this.matchExactOutlineLine(line, chapterNumber);
            if (!match)
                continue;
            const inlineContent = this.cleanOutlineContent(match[1]);
            if (inlineContent) {
                return inlineContent;
            }
            const nextContent = this.findNextOutlineContent(lines, index + 1);
            if (nextContent) {
                return nextContent;
            }
        }
        for (let index = 0; index < lines.length; index += 1) {
            const line = lines[index];
            const match = this.matchRangeOutlineLine(line, chapterNumber);
            if (!match)
                continue;
            const inlineContent = this.cleanOutlineContent(match[3]);
            if (inlineContent) {
                return inlineContent;
            }
            const rangeStart = Number(match[1]);
            const sectionContent = this.extractSectionAroundRange(lines, index);
            if (sectionContent) {
                const beatIndex = chapterNumber - rangeStart;
                const specificBeat = this.extractNumberedBeat(sectionContent, beatIndex);
                return specificBeat ?? sectionContent;
            }
            const nextContent = this.findNextOutlineContent(lines, index + 1);
            if (nextContent) {
                return nextContent;
            }
        }
        for (let index = 0; index < lines.length; index += 1) {
            const line = lines[index];
            if (!this.isOutlineAnchorLine(line))
                continue;
            const exactMatch = this.matchAnyExactOutlineLine(line);
            if (exactMatch) {
                const inlineContent = this.cleanOutlineContent(exactMatch[1]);
                if (inlineContent) {
                    return inlineContent;
                }
            }
            const rangeMatch = this.matchAnyRangeOutlineLine(line);
            if (rangeMatch) {
                const inlineContent = this.cleanOutlineContent(rangeMatch[3]);
                if (inlineContent) {
                    return inlineContent;
                }
            }
            const nextContent = this.findNextOutlineContent(lines, index + 1);
            if (nextContent) {
                return nextContent;
            }
            break;
        }
        return this.extractFirstDirective(volumeOutline);
    }
    cleanOutlineContent(content) {
        const cleaned = content?.trim();
        if (!cleaned)
            return undefined;
        if (/^[*_`~:：-]+$/.test(cleaned))
            return undefined;
        return cleaned;
    }
    extractSectionAroundRange(lines, rangeLineIndex) {
        let headingIndex = -1;
        for (let i = rangeLineIndex - 1; i >= 0; i--) {
            if (lines[i].startsWith("#")) {
                headingIndex = i;
                break;
            }
            if (this.matchAnyRangeOutlineLine(lines[i]) || this.matchAnyExactOutlineLine(lines[i])) {
                break;
            }
        }
        if (headingIndex < 0) {
            return undefined;
        }
        const headingLine = lines[headingIndex];
        const headingLevel = headingLine.match(/^(#+)/)?.[1]?.length ?? 3;
        const sectionLines = [];
        for (let i = headingIndex; i < lines.length; i++) {
            if (i > headingIndex) {
                const nextHeadingMatch = lines[i].match(/^(#+)/);
                if (nextHeadingMatch && (nextHeadingMatch[1]?.length ?? 0) <= headingLevel) {
                    break;
                }
            }
            sectionLines.push(lines[i]);
        }
        const content = sectionLines.join("\n").trim();
        return content.length > 0 ? content : undefined;
    }
    extractNumberedBeat(section, beatIndex) {
        if (beatIndex < 0)
            return undefined;
        const beats = [];
        for (const line of section.split("\n")) {
            const trimmed = line.trim();
            if (/^\d+[.)]\s/.test(trimmed)) {
                beats.push(trimmed.replace(/^\d+[.)]\s*/, ""));
            }
        }
        if (beats.length === 0 || beatIndex >= beats.length)
            return undefined;
        return beats[beatIndex];
    }
    findNextOutlineContent(lines, startIndex) {
        for (let index = startIndex; index < lines.length; index += 1) {
            const line = lines[index];
            if (!line) {
                continue;
            }
            if (this.isOutlineAnchorLine(line)) {
                return undefined;
            }
            if (line.startsWith("#")) {
                continue;
            }
            const cleaned = this.cleanOutlineContent(line);
            if (cleaned) {
                return cleaned;
            }
        }
        return undefined;
    }
    matchExactOutlineLine(line, chapterNumber) {
        const patterns = [
            new RegExp(`^(?:#+\\s*)?(?:[-*]\\s+)?(?:\\*\\*)?Chapter\\s*${chapterNumber}(?!\\d|\\s*[-~–—]\\s*\\d)(?:[:：-])?(?:\\*\\*)?\\s*(.*)$`, "i"),
            new RegExp(`^(?:#+\\s*)?(?:[-*]\\s+)?(?:\\*\\*)?第\\s*${chapterNumber}\\s*章(?!\\d|\\s*[-~–—]\\s*\\d)(?:[:：-])?(?:\\*\\*)?\\s*(.*)$`),
        ];
        return patterns
            .map((pattern) => line.match(pattern))
            .find((result) => Boolean(result));
    }
    matchAnyExactOutlineLine(line) {
        const patterns = [
            /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?Chapter\s*\d+(?!\s*[-~–—]\s*\d)(?:[:：-])?(?:\*\*)?\s*(.*)$/i,
            /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?第\s*\d+\s*章(?!\s*[-~–—]\s*\d)(?:[:：-])?(?:\*\*)?\s*(.*)$/i,
        ];
        return patterns
            .map((pattern) => line.match(pattern))
            .find((result) => Boolean(result));
    }
    matchRangeOutlineLine(line, chapterNumber) {
        const match = this.matchAnyRangeOutlineLine(line);
        if (!match)
            return undefined;
        if (this.isChapterWithinRange(match[1], match[2], chapterNumber)) {
            return match;
        }
        return undefined;
    }
    matchAnyRangeOutlineLine(line) {
        const patterns = [
            /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?Chapter\s*(\d+)\s*[-~–—]\s*(\d+)\b(?:[:：-])?(?:\*\*)?\s*(.*)$/i,
            /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?第\s*(\d+)\s*[-~–—]\s*(\d+)\s*章(?:[:：-])?(?:\*\*)?\s*(.*)$/i,
            /^(?:[-*]\s+)?(?:\*\*)?章节范围(?:\*\*)?[：:]\s*(\d+)\s*[-~–—]\s*(\d+)\s*章\s*(.*)$/,
            /^(?:[-*]\s+)?(?:\*\*)?Chapter\s*[Rr]ange(?:\*\*)?[：:]\s*(\d+)\s*[-~–—]\s*(\d+)\b\s*(.*)$/i,
        ];
        return patterns
            .map((pattern) => line.match(pattern))
            .find((result) => Boolean(result));
    }
    isOutlineAnchorLine(line) {
        return this.matchAnyExactOutlineLine(line) !== undefined
            || this.matchAnyRangeOutlineLine(line) !== undefined;
    }
    isChapterWithinRange(startText, endText, chapterNumber) {
        const start = Number.parseInt(startText ?? "", 10);
        const end = Number.parseInt(endText ?? "", 10);
        if (!Number.isFinite(start) || !Number.isFinite(end))
            return false;
        const lower = Math.min(start, end);
        const upper = Math.max(start, end);
        return chapterNumber >= lower && chapterNumber <= upper;
    }
    renderIntentMarkdown(intent, memo, language, pendingHooks, chapterSummaries, activeHookCount) {
        const mustKeep = intent.mustKeep.length > 0
            ? intent.mustKeep.map((item) => `- ${item}`).join("\n")
            : "- none";
        const mustAvoid = intent.mustAvoid.length > 0
            ? intent.mustAvoid.map((item) => `- ${item}`).join("\n")
            : "- none";
        const styleEmphasis = intent.styleEmphasis.length > 0
            ? intent.styleEmphasis.map((item) => `- ${item}`).join("\n")
            : "- none";
        const memoBody = memo.body.trim();
        const threadRefsLine = memo.threadRefs.length > 0
            ? memo.threadRefs.map((id) => `- ${id}`).join("\n")
            : "- (none)";
        return [
            "# Chapter Intent",
            "",
            "## Goal",
            intent.goal,
            "",
            "## Outline Node",
            intent.outlineNode ?? "(not found)",
            "",
            "## Arc Context",
            intent.arcContext ?? "(none)",
            "",
            "## Must Keep",
            mustKeep,
            "",
            "## Must Avoid",
            mustAvoid,
            "",
            "## Style Emphasis",
            styleEmphasis,
            "",
            "## Chapter Memo",
            `- isGoldenOpening: ${memo.isGoldenOpening ? "true" : "false"}`,
            "",
            "### Thread Refs",
            threadRefsLine,
            "",
            "### Body",
            memoBody,
            "",
            this.renderHookBudget(activeHookCount, language),
            "",
            "## Pending Hooks Snapshot",
            pendingHooks,
            "",
            "## Chapter Summaries Snapshot",
            chapterSummaries,
            "",
        ].join("\n");
    }
    unique(values) {
        return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
    }
    isChineseLanguage(language) {
        return (language ?? "zh").toLowerCase().startsWith("zh");
    }
    // Kept for potential subclasses reading seed files directly.
    async readFileOrDefault(path) {
        try {
            return await readFile(path, "utf-8");
        }
        catch {
            return "(文件尚未创建)";
        }
    }
}
//# sourceMappingURL=planner.js.map