import { readFile, writeFile, mkdir, readdir, rm, stat, unlink, open } from "node:fs/promises";
import { join } from "node:path";
import { bootstrapStructuredStateFromMarkdown, resolveDurableStoryProgress } from "./state-bootstrap.js";
export class StateManager {
    projectRoot;
    /** Books actively being written by this process — used for same-process stale lock detection. */
    activeWrites = new Set();
    constructor(projectRoot) {
        this.projectRoot = projectRoot;
    }
    static defaultAuthorIntent(language) {
        return language === "zh"
            ? "# 作者意图\n\n（在这里描述这本书的长期创作方向。）\n"
            : "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n";
    }
    static defaultCurrentFocus(language) {
        return language === "zh"
            ? "# 当前聚焦\n\n## 当前重点\n\n（描述接下来 1-3 章最需要优先推进的内容。）\n"
            : "# Current Focus\n\n## Active Focus\n\n(Describe what the next 1-3 chapters should prioritize.)\n";
    }
    async ensureControlDocuments(bookId, authorIntent) {
        const language = await this.resolveControlDocumentLanguage(bookId);
        await this.ensureControlDocumentsAt(this.bookDir(bookId), language, authorIntent);
    }
    async ensureControlDocumentsAt(bookDir, language, authorIntent) {
        const storyDir = join(bookDir, "story");
        const runtimeDir = join(storyDir, "runtime");
        const outlineDir = join(storyDir, "outline");
        const rolesMajorDir = join(storyDir, "roles", "主要角色");
        const rolesMinorDir = join(storyDir, "roles", "次要角色");
        await mkdir(storyDir, { recursive: true });
        await mkdir(runtimeDir, { recursive: true });
        await mkdir(outlineDir, { recursive: true });
        await mkdir(rolesMajorDir, { recursive: true });
        await mkdir(rolesMinorDir, { recursive: true });
        await this.writeIfMissing(join(storyDir, "author_intent.md"), authorIntent?.trim()
            ? authorIntent.trimEnd() + "\n"
            : StateManager.defaultAuthorIntent(language));
        await this.writeIfMissing(join(storyDir, "current_focus.md"), StateManager.defaultCurrentFocus(language));
        // Ensure style_guide includes writing methodology even without reference text
        const styleGuidePath = join(storyDir, "style_guide.md");
        try {
            const existing = await readFile(styleGuidePath, "utf-8");
            if (!existing.includes("写作方法论") && !existing.includes("Writing Methodology")) {
                const { buildWritingMethodologySection } = await import("../utils/writing-methodology.js");
                await writeFile(styleGuidePath, `${existing}\n\n${buildWritingMethodologySection(language)}`, "utf-8");
            }
        }
        catch {
            const { buildWritingMethodologySection } = await import("../utils/writing-methodology.js");
            await writeFile(styleGuidePath, buildWritingMethodologySection(language), "utf-8");
        }
    }
    async loadControlDocuments(bookId) {
        await this.ensureControlDocuments(bookId);
        const storyDir = join(this.bookDir(bookId), "story");
        const runtimeDir = join(storyDir, "runtime");
        const [authorIntent, currentFocus] = await Promise.all([
            readFile(join(storyDir, "author_intent.md"), "utf-8"),
            readFile(join(storyDir, "current_focus.md"), "utf-8"),
        ]);
        return { authorIntent, currentFocus, runtimeDir };
    }
    async resolveControlDocumentLanguage(bookId) {
        try {
            const raw = await readFile(join(this.bookDir(bookId), "book.json"), "utf-8");
            const parsed = JSON.parse(raw);
            return parsed.language === "zh" ? "zh" : "en";
        }
        catch {
            return "en";
        }
    }
    async acquireBookLock(bookId) {
        await mkdir(this.bookDir(bookId), { recursive: true });
        const lockPath = join(this.bookDir(bookId), ".write.lock");
        try {
            const handle = await open(lockPath, "wx");
            try {
                await handle.writeFile(`pid:${process.pid} ts:${Date.now()}`, "utf-8");
            }
            catch (error) {
                await handle.close().catch(() => undefined);
                await unlink(lockPath).catch(() => undefined);
                throw error;
            }
            await handle.close();
        }
        catch (e) {
            const code = e?.code;
            if (code === "EEXIST") {
                const lockData = await readFile(lockPath, "utf-8").catch(() => "pid:unknown ts:unknown");
                const lockPid = this.extractLockPid(lockData);
                const isStale = (lockPid !== undefined && !this.isProcessAlive(lockPid)) ||
                    (lockPid === process.pid && !this.activeWrites.has(bookId));
                if (isStale) {
                    await unlink(lockPath).catch(() => undefined);
                    return this.acquireBookLock(bookId);
                }
                throw new Error(`Book "${bookId}" is locked by another process (${lockData}). ` +
                    `If this is stale, delete ${lockPath}`);
            }
            throw e;
        }
        this.activeWrites.add(bookId);
        return async () => {
            this.activeWrites.delete(bookId);
            try {
                await unlink(lockPath);
            }
            catch {
                // ignore
            }
        };
    }
    extractLockPid(lockData) {
        const match = lockData.match(/pid:(\d+)/);
        if (!match)
            return undefined;
        const pid = Number.parseInt(match[1] ?? "", 10);
        return Number.isInteger(pid) && pid > 0 ? pid : undefined;
    }
    isProcessAlive(pid) {
        try {
            process.kill(pid, 0);
            return true;
        }
        catch (error) {
            const code = error?.code;
            if (code === "ESRCH") {
                return false;
            }
            return true;
        }
    }
    get booksDir() {
        return join(this.projectRoot, "books");
    }
    bookDir(bookId) {
        return join(this.booksDir, bookId);
    }
    stateDir(bookId) {
        return join(this.bookDir(bookId), "story", "state");
    }
    async loadProjectConfig() {
        const configPath = join(this.projectRoot, "inkos.json");
        const raw = await readFile(configPath, "utf-8");
        return JSON.parse(raw);
    }
    async saveProjectConfig(config) {
        const configPath = join(this.projectRoot, "inkos.json");
        await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
    }
    async loadBookConfig(bookId) {
        const configPath = join(this.bookDir(bookId), "book.json");
        const raw = await readFile(configPath, "utf-8");
        if (!raw.trim()) {
            throw new Error(`book.json is empty for book "${bookId}"`);
        }
        return JSON.parse(raw);
    }
    async saveBookConfig(bookId, config) {
        await this.saveBookConfigAt(this.bookDir(bookId), config);
    }
    async saveBookConfigAt(bookDir, config) {
        await mkdir(bookDir, { recursive: true });
        await writeFile(join(bookDir, "book.json"), JSON.stringify(config, null, 2), "utf-8");
    }
    async ensureRuntimeState(bookId, fallbackChapter = 0) {
        await bootstrapStructuredStateFromMarkdown({
            bookDir: this.bookDir(bookId),
            fallbackChapter,
        });
    }
    async listBooks() {
        try {
            const entries = await readdir(this.booksDir);
            const bookIds = [];
            for (const entry of entries) {
                const bookJsonPath = join(this.booksDir, entry, "book.json");
                try {
                    await stat(bookJsonPath);
                    bookIds.push(entry);
                }
                catch {
                    // not a book directory
                }
            }
            return bookIds;
        }
        catch {
            return [];
        }
    }
    async getNextChapterNumber(bookId) {
        const durableChapter = await resolveDurableStoryProgress({
            bookDir: this.bookDir(bookId),
        });
        // Ensure structured state is bootstrapped (side-effect: creates missing
        // JSON files), but do NOT trust its chapter number for progress — only
        // the contiguous durable artifact chain is authoritative.
        await bootstrapStructuredStateFromMarkdown({
            bookDir: this.bookDir(bookId),
            fallbackChapter: durableChapter,
        });
        return durableChapter + 1;
    }
    async getPersistedChapterCount(bookId) {
        const chaptersDir = join(this.bookDir(bookId), "chapters");
        const chapterNumbers = new Set();
        try {
            const files = await readdir(chaptersDir);
            for (const file of files) {
                const match = file.match(/^(\d+)_.*\.md$/);
                if (!match)
                    continue;
                chapterNumbers.add(parseInt(match[1], 10));
            }
        }
        catch {
            return 0;
        }
        return chapterNumbers.size;
    }
    async loadChapterIndex(bookId) {
        const indexPath = join(this.bookDir(bookId), "chapters", "index.json");
        try {
            const raw = await readFile(indexPath, "utf-8");
            return JSON.parse(raw);
        }
        catch {
            return [];
        }
    }
    async saveChapterIndex(bookId, index) {
        await this.saveChapterIndexAt(this.bookDir(bookId), index);
    }
    async saveChapterIndexAt(bookDir, index) {
        const chaptersDir = join(bookDir, "chapters");
        await mkdir(chaptersDir, { recursive: true });
        await writeFile(join(chaptersDir, "index.json"), JSON.stringify(index, null, 2), "utf-8");
    }
    async snapshotState(bookId, chapterNumber) {
        await this.snapshotStateAt(this.bookDir(bookId), chapterNumber);
    }
    async snapshotStateAt(bookDir, chapterNumber) {
        const storyDir = join(bookDir, "story");
        const snapshotDir = join(storyDir, "snapshots", String(chapterNumber));
        await mkdir(snapshotDir, { recursive: true });
        const files = [
            "current_state.md", "particle_ledger.md", "pending_hooks.md",
            "chapter_summaries.md", "subplot_board.md", "emotional_arcs.md", "character_matrix.md",
        ];
        await Promise.all(files.map(async (f) => {
            try {
                const content = await readFile(join(storyDir, f), "utf-8");
                await writeFile(join(snapshotDir, f), content, "utf-8");
            }
            catch {
                // file doesn't exist yet
            }
        }));
        const stateDir = join(bookDir, "story", "state");
        const snapshotStateDir = join(snapshotDir, "state");
        try {
            const stateFiles = await readdir(stateDir);
            if (stateFiles.length > 0) {
                await mkdir(snapshotStateDir, { recursive: true });
                await Promise.all(stateFiles.map(async (fileName) => {
                    const content = await readFile(join(stateDir, fileName), "utf-8");
                    await writeFile(join(snapshotStateDir, fileName), content, "utf-8");
                }));
            }
        }
        catch {
            // state directory missing — skip
        }
    }
    async isCompleteBookDirectory(bookDir) {
        // Phase 5 cleanup: prefer outline/* paths, fall back to legacy flat files
        // so older books on disk still resolve as complete.
        const requiredSingle = [
            join(bookDir, "book.json"),
            join(bookDir, "story", "book_rules.md"),
            join(bookDir, "story", "current_state.md"),
            join(bookDir, "story", "pending_hooks.md"),
            join(bookDir, "chapters", "index.json"),
        ];
        const eitherOr = [
            // story_frame (new) OR story_bible (legacy)
            [
                join(bookDir, "story", "outline", "story_frame.md"),
                join(bookDir, "story", "story_bible.md"),
            ],
            // volume_map (new) OR volume_outline (legacy)
            [
                join(bookDir, "story", "outline", "volume_map.md"),
                join(bookDir, "story", "volume_outline.md"),
            ],
        ];
        for (const requiredPath of requiredSingle) {
            try {
                await stat(requiredPath);
            }
            catch {
                return false;
            }
        }
        for (const alternatives of eitherOr) {
            let found = false;
            for (const candidate of alternatives) {
                try {
                    await stat(candidate);
                    found = true;
                    break;
                }
                catch {
                    // try next alternative
                }
            }
            if (!found)
                return false;
        }
        return true;
    }
    async restoreState(bookId, chapterNumber) {
        const storyDir = join(this.bookDir(bookId), "story");
        const snapshotDir = join(storyDir, "snapshots", String(chapterNumber));
        const files = [
            "current_state.md", "particle_ledger.md", "pending_hooks.md",
            "chapter_summaries.md", "subplot_board.md", "emotional_arcs.md", "character_matrix.md",
        ];
        try {
            // current_state.md and pending_hooks.md are required;
            // particle_ledger.md is optional (numericalSystem=false genres don't have it)
            // the rest are optional (may not exist in older snapshots)
            const requiredFiles = ["current_state.md", "pending_hooks.md"];
            const optionalFiles = files.filter((f) => !requiredFiles.includes(f));
            await Promise.all(requiredFiles.map(async (f) => {
                const content = await readFile(join(snapshotDir, f), "utf-8");
                await writeFile(join(storyDir, f), content, "utf-8");
            }));
            await Promise.all(optionalFiles.map(async (f) => {
                const targetPath = join(storyDir, f);
                try {
                    const content = await readFile(join(snapshotDir, f), "utf-8");
                    await writeFile(targetPath, content, "utf-8");
                }
                catch {
                    await rm(targetPath, { force: true });
                }
            }));
            const stateDir = this.stateDir(bookId);
            let restoredStructuredState = false;
            try {
                const snapshotStateDir = join(snapshotDir, "state");
                const stateFiles = await readdir(snapshotStateDir);
                if (stateFiles.length > 0) {
                    restoredStructuredState = true;
                    await mkdir(stateDir, { recursive: true });
                    await Promise.all(stateFiles.map(async (fileName) => {
                        const content = await readFile(join(snapshotStateDir, fileName), "utf-8");
                        await writeFile(join(stateDir, fileName), content, "utf-8");
                    }));
                }
            }
            catch {
                // snapshot structured state missing — skip
            }
            if (!restoredStructuredState) {
                await rm(stateDir, { recursive: true, force: true });
            }
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Roll back state to the snapshot at `targetChapter`, removing all chapters
     * after it and their associated files (chapter markdown, snapshots, runtime).
     * Used by review reject to undo a bad chapter and everything that followed.
     *
     * Returns the list of chapter numbers that were discarded.
     */
    async rollbackToChapter(bookId, targetChapter) {
        const restored = await this.restoreState(bookId, targetChapter);
        if (!restored) {
            throw new Error(`Cannot restore snapshot for chapter ${targetChapter} in "${bookId}"`);
        }
        const bookDir = this.bookDir(bookId);
        const chaptersDir = join(bookDir, "chapters");
        const index = await this.loadChapterIndex(bookId);
        const kept = [];
        const discarded = [];
        for (const entry of index) {
            if (entry.number <= targetChapter) {
                kept.push(entry);
            }
            else {
                discarded.push(entry.number);
            }
        }
        // Delete chapter markdown files for discarded chapters
        try {
            const files = await readdir(chaptersDir);
            for (const file of files) {
                const match = file.match(/^(\d+)_.*\.md$/);
                if (!match)
                    continue;
                const num = parseInt(match[1], 10);
                if (num > targetChapter) {
                    await unlink(join(chaptersDir, file)).catch(() => { });
                }
            }
        }
        catch {
            // chapters directory missing
        }
        // Delete snapshots for discarded chapters
        const snapshotsDir = join(bookDir, "story", "snapshots");
        try {
            const snapshots = await readdir(snapshotsDir);
            for (const snap of snapshots) {
                const num = parseInt(snap, 10);
                if (Number.isFinite(num) && num > targetChapter) {
                    await rm(join(snapshotsDir, snap), { recursive: true, force: true });
                }
            }
        }
        catch {
            // snapshots directory missing
        }
        // Delete runtime artifacts for discarded chapters
        const runtimeDir = join(bookDir, "story", "runtime");
        try {
            const runtimeFiles = await readdir(runtimeDir);
            for (const file of runtimeFiles) {
                const match = file.match(/^chapter-(\d+)\./);
                if (!match)
                    continue;
                const num = parseInt(match[1], 10);
                if (num > targetChapter) {
                    await unlink(join(runtimeDir, file)).catch(() => { });
                }
            }
        }
        catch {
            // runtime directory missing
        }
        // Also check story/drafts/ for discarded chapter files
        const draftsDir = join(bookDir, "story", "drafts");
        try {
            const draftFiles = await readdir(draftsDir);
            for (const file of draftFiles) {
                const match = file.match(/^(\d+)_.*\.md$/);
                if (!match)
                    continue;
                const num = parseInt(match[1], 10);
                if (num > targetChapter) {
                    await unlink(join(draftsDir, file)).catch(() => { });
                }
            }
        }
        catch {
            // drafts directory missing
        }
        // Drop any persisted sqlite acceleration index so discarded chapters
        // cannot leak back into retrieval after the markdown/state rollback.
        await Promise.all([
            rm(join(bookDir, "story", "memory.db"), { force: true }),
            rm(join(bookDir, "story", "memory.db-shm"), { force: true }),
            rm(join(bookDir, "story", "memory.db-wal"), { force: true }),
        ]);
        await this.saveChapterIndex(bookId, kept);
        return discarded;
    }
    async writeIfMissing(path, content) {
        try {
            await stat(path);
        }
        catch {
            await writeFile(path, content, "utf-8");
        }
    }
}
//# sourceMappingURL=manager.js.map