import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { chatCompletion, chatWithTools } from "../index.js";
import { executeEditTransaction } from "./edit-controller.js";
import { writeExportArtifact } from "./export-artifact.js";
import { safeChildPath } from "../utils/path-safety.js";
import { deriveBookIdFromTitle } from "../utils/book-id.js";
import { normalizePlatformOrOther } from "../models/book.js";
const SAFE_TRUTH_FLAT_FILE_NAMES = new Set([
    "author_intent.md",
    "current_focus.md",
    "story_bible.md",
    "volume_outline.md",
    "book_rules.md",
    "particle_ledger.md",
    "subplot_board.md",
    "emotional_arcs.md",
    "style_guide.md",
    "parent_canon.md",
    "fanfic_canon.md",
    "character_matrix.md",
    "current_state.md",
    "pending_hooks.md",
    "chapter_summaries.md",
]);
const SAFE_TRUTH_OUTLINE_FILE_NAMES = new Set([
    "outline/story_frame.md",
    "outline/volume_map.md",
    "outline/节奏原则.md",
    "outline/rhythm_principles.md",
]);
const SAFE_ROLE_TRUTH_FILE_RE = /^roles\/(主要角色|次要角色|major|minor)\/[^/\\]+\.md$/u;
export function assertSafeTruthFileName(fileName) {
    const trimmed = fileName.trim();
    const withExtension = trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`;
    const lower = withExtension.toLowerCase();
    if (!trimmed ||
        withExtension.startsWith("/") ||
        withExtension.includes("\\") ||
        withExtension.includes("\0") ||
        withExtension.includes("..")) {
        throw new Error(`Invalid truth file name: ${JSON.stringify(fileName)}`);
    }
    if (SAFE_TRUTH_FLAT_FILE_NAMES.has(lower))
        return lower;
    if (SAFE_TRUTH_OUTLINE_FILE_NAMES.has(lower))
        return lower;
    if (SAFE_ROLE_TRUTH_FILE_RE.test(withExtension))
        return withExtension;
    throw new Error(`Invalid truth file name: ${JSON.stringify(fileName)}`);
}
function buildBookConfig(input) {
    const now = new Date().toISOString();
    return {
        id: deriveBookIdFromTitle(input.title) || `book-${Date.now().toString(36)}`,
        title: input.title,
        platform: normalizePlatformOrOther(input.platform),
        genre: input.genre ?? "other",
        status: "outlining",
        targetChapters: input.targetChapters ?? 200,
        chapterWordCount: input.chapterWordCount ?? 3000,
        ...(input.language ? { language: input.language } : {}),
        createdAt: now,
        updatedAt: now,
    };
}
function buildCreationExternalContext(input) {
    const sections = [
        input.worldPremise ? `## 世界观与核心设定\n${input.worldPremise}` : undefined,
        input.settingNotes ? `## 补充设定\n${input.settingNotes}` : undefined,
        input.protagonist ? `## 主角设定\n${input.protagonist}` : undefined,
        input.supportingCast ? `## 关键角色与势力\n${input.supportingCast}` : undefined,
        input.conflictCore ? `## 核心冲突\n${input.conflictCore}` : undefined,
        input.volumeOutline ? `## 卷纲方向\n${input.volumeOutline}` : undefined,
        input.blurb ? `## 简介卖点\n${input.blurb}` : undefined,
        input.constraints ? `## 创作约束\n${input.constraints}` : undefined,
    ].filter((section) => Boolean(section?.trim()));
    if (sections.length === 0) {
        return undefined;
    }
    return sections.join("\n\n");
}
export function buildChapterFileLookup(files) {
    const lookup = new Map();
    for (const file of files) {
        if (!file.endsWith(".md") || !/^\d{4}/.test(file)) {
            continue;
        }
        const chapterNumber = parseInt(file.slice(0, 4), 10);
        if (!lookup.has(chapterNumber)) {
            lookup.set(chapterNumber, file);
        }
    }
    return lookup;
}
async function exportBookToPath(state, bookId, options) {
    return writeExportArtifact(state, bookId, options);
}
function mapStageMessageToStatus(message) {
    const lower = message.trim().toLowerCase();
    if (lower.includes("planning next chapter")
        || lower.includes("generating foundation")
        || lower.includes("reviewing foundation")
        || lower.includes("preparing chapter inputs")
        || message.includes("规划下一章意图")
        || message.includes("生成基础设定")
        || message.includes("审核基础设定")
        || message.includes("准备章节输入")) {
        return "planning";
    }
    if (lower.includes("composing chapter runtime context")
        || message.includes("组装章节运行时上下文")) {
        return "composing";
    }
    if (lower.includes("writing chapter draft")
        || message.includes("撰写章节草稿")) {
        return "writing";
    }
    if (lower.includes("auditing draft")
        || message.includes("审计草稿")) {
        return "assessing";
    }
    if (lower.includes("fixing")
        || lower.includes("revising chapter")
        || lower.includes("rewrite")
        || lower.includes("repair")
        || message.includes("自动修复")
        || message.includes("整章改写")
        || message.includes("修订第")) {
        return "repairing";
    }
    if (lower.includes("persist")
        || lower.includes("saving")
        || lower.includes("snapshot")
        || lower.includes("rebuilding final truth files")
        || lower.includes("validating truth file updates")
        || lower.includes("syncing memory indexes")
        || message.includes("落盘")
        || message.includes("保存")
        || message.includes("快照")
        || message.includes("校验真相文件变更")
        || message.includes("生成最终真相文件")
        || message.includes("同步记忆索引")) {
        return "persisting";
    }
    return undefined;
}
function extractStageDetail(message) {
    if (message.startsWith("Stage: ")) {
        return message.slice("Stage: ".length).trim();
    }
    if (message.startsWith("阶段：")) {
        return message.slice("阶段：".length).trim();
    }
    return undefined;
}
function createInteractionLogger(original, events, bookId) {
    const emit = (level, message) => {
        const stageDetail = extractStageDetail(message);
        const stageStatus = stageDetail ? mapStageMessageToStatus(stageDetail) : undefined;
        if (stageDetail && stageStatus) {
            events.push({
                kind: "stage.changed",
                timestamp: Date.now(),
                status: stageStatus,
                bookId,
                detail: stageDetail,
            });
            return;
        }
        if (level === "warn") {
            events.push({
                kind: "task.warning",
                timestamp: Date.now(),
                status: "blocked",
                bookId,
                detail: message,
            });
            return;
        }
        if (level === "error") {
            events.push({
                kind: "task.failed",
                timestamp: Date.now(),
                status: "failed",
                bookId,
                detail: message,
            });
        }
    };
    const wrap = (base) => ({
        debug: (msg, ctx) => {
            emit("debug", msg);
            base?.debug(msg, ctx);
        },
        info: (msg, ctx) => {
            emit("info", msg);
            base?.info(msg, ctx);
        },
        warn: (msg, ctx) => {
            emit("warn", msg);
            base?.warn(msg, ctx);
        },
        error: (msg, ctx) => {
            emit("error", msg);
            base?.error(msg, ctx);
        },
        child: (tag, extraCtx) => wrap(base?.child(tag, extraCtx)),
    });
    return wrap(original);
}
async function withPipelineInteractionTelemetry(pipeline, bookId, executor) {
    const events = [];
    const originalLogger = pipeline.config?.logger;
    if (pipeline.config) {
        pipeline.config.logger = createInteractionLogger(originalLogger, events, bookId);
    }
    try {
        const result = await executor();
        return {
            ...result,
            __interaction: {
                events,
                ...(typeof result.chapterNumber === "number"
                    ? { activeChapterNumber: result.chapterNumber }
                    : {}),
            },
        };
    }
    finally {
        if (pipeline.config) {
            pipeline.config.logger = originalLogger;
        }
    }
}
const CREATE_BOOK_TOOL = {
    name: "create_book",
    description: "根据用户描述生成建书参数。系统会将参数渲染为可编辑表单，用户确认后建书。",
    parameters: {
        type: "object",
        properties: {
            title: { type: "string", description: "书名" },
            genre: { type: "string", description: "题材标识，如 xuanhuan, urban, romance, scifi, mystery" },
            platform: { type: "string", enum: ["tomato", "qidian", "feilu", "other"], description: "发布平台" },
            targetChapters: { type: "number", description: "目标章数，默认 200" },
            chapterWordCount: { type: "number", description: "每章字数，默认 3000" },
            language: { type: "string", enum: ["zh", "en"], description: "写作语言，默认 zh" },
            brief: { type: "string", description: "创意简述，会传给 Architect 智能体生成完整的世界观、主角、冲突等 foundation 文件。把用户提到的所有创意要素都写进这里。" },
        },
        required: ["title", "genre", "platform", "brief"],
    },
};
const BOOK_DRAFT_SYSTEM_PROMPT = [
    "你是 InkOS 的建书助手。用户会描述想写的书，你需要调用 create_book 工具来生成建书参数。",
    "",
    "规则：",
    "1. 从用户描述中推断所有字段，大胆预填合理默认值。",
    "2. brief 字段要详细——它会传给 Architect 智能体生成完整的世界观、主角、冲突等 foundation 文件。把用户提到的所有创意要素都写进 brief。",
    "3. 如果用户后续要求修改某些字段，重新调用 create_book 工具，只更新被提到的字段，其余保持不变。",
    "4. 不要只回复文字讨论——必须调用 create_book 工具输出结构化参数。",
].join("\n");
/** Map directive field keys to BookCreationDraft property names. */
function applyFieldsToDraft(existing, fields, concept) {
    const draft = {
        concept,
        missingFields: [],
        readyToCreate: false,
        ...(existing ?? {}),
    };
    for (const [key, value] of Object.entries(fields)) {
        if (!value)
            continue;
        switch (key) {
            case "title":
                draft.title = value;
                break;
            case "genre":
                draft.genre = value;
                break;
            case "platform":
                draft.platform = value;
                break;
            case "language":
                if (value === "zh" || value === "en")
                    draft.language = value;
                break;
            case "targetChapters": {
                const n = parseInt(value, 10);
                if (!Number.isNaN(n) && n > 0)
                    draft.targetChapters = n;
                break;
            }
            case "chapterWordCount":
            case "chapterLength": {
                const n = parseInt(value, 10);
                if (!Number.isNaN(n) && n > 0)
                    draft.chapterWordCount = n;
                break;
            }
            case "blurb":
                draft.blurb = value;
                break;
            case "worldPremise":
                draft.worldPremise = value;
                break;
            case "settingNotes":
                draft.settingNotes = value;
                break;
            case "protagonist":
                draft.protagonist = value;
                break;
            case "supportingCast":
                draft.supportingCast = value;
                break;
            case "conflictCore":
                draft.conflictCore = value;
                break;
            case "volumeOutline":
                draft.volumeOutline = value;
                break;
            case "constraints":
                draft.constraints = value;
                break;
            case "authorIntent":
                draft.authorIntent = value;
                break;
            case "currentFocus":
                draft.currentFocus = value;
                break;
            // Unknown keys are silently ignored — the LLM may emit
            // application-level keys we don't map to the draft struct.
        }
    }
    return draft;
}
function formatDraftForUserMessage(existingDraft, userMessage) {
    const parts = [];
    if (existingDraft) {
        parts.push("## 当前草案状态");
        const entries = Object.entries(existingDraft).filter(([, v]) => v !== undefined && v !== "" && !(Array.isArray(v) && v.length === 0));
        for (const [key, value] of entries) {
            parts.push(`- **${key}**: ${typeof value === "object" ? JSON.stringify(value) : String(value)}`);
        }
        parts.push("");
    }
    parts.push("## 用户输入");
    parts.push(userMessage);
    return parts.join("\n");
}
export function createInteractionToolsFromDeps(pipeline, state, hooks) {
    const instrumentedPipeline = pipeline;
    return {
        listBooks: () => state.listBooks(),
        developBookDraft: async (input, existingDraft) => {
            const concept = existingDraft?.concept ?? input;
            if (!instrumentedPipeline.config?.client || !instrumentedPipeline.config?.model) {
                // Fallback: no LLM configured
                return {
                    __interaction: {
                        responseText: "请先配置 LLM 模型，然后再创建书籍。",
                        details: {
                            creationDraft: {
                                concept,
                                missingFields: ["title", "genre", "targetChapters"],
                                readyToCreate: false,
                            },
                        },
                    },
                };
            }
            // Build messages - include existing draft context if present
            const userContent = existingDraft
                ? `当前草案参数：${JSON.stringify(existingDraft, null, 2)}\n\n用户输入：${input}`
                : input;
            const result = await chatWithTools(instrumentedPipeline.config.client, instrumentedPipeline.config.model, [
                { role: "system", content: BOOK_DRAFT_SYSTEM_PROMPT },
                { role: "user", content: userContent },
            ], [CREATE_BOOK_TOOL], { temperature: 0.4 });
            // Extract tool call if present
            const toolCall = result.toolCalls[0];
            let parsedArgs = {};
            if (toolCall) {
                try {
                    parsedArgs = JSON.parse(toolCall.arguments);
                }
                catch {
                    // If parsing fails, use empty args
                }
            }
            // Build a draft from tool call arguments
            const draft = {
                concept,
                title: parsedArgs.title ?? existingDraft?.title,
                genre: parsedArgs.genre ?? existingDraft?.genre,
                platform: parsedArgs.platform ?? existingDraft?.platform,
                language: parsedArgs.language ?? existingDraft?.language,
                targetChapters: parsedArgs.targetChapters ?? existingDraft?.targetChapters,
                chapterWordCount: parsedArgs.chapterWordCount ?? existingDraft?.chapterWordCount,
                blurb: parsedArgs.brief ?? existingDraft?.blurb,
                missingFields: [],
                readyToCreate: Boolean(parsedArgs.title && parsedArgs.genre && parsedArgs.platform),
            };
            return {
                __interaction: {
                    responseText: result.content || "已生成建书参数，请确认或修改。",
                    details: {
                        creationDraft: draft,
                        toolCall: toolCall ? { name: toolCall.name, arguments: parsedArgs } : undefined,
                    },
                },
            };
        },
        createBook: async (input) => {
            const book = buildBookConfig(input);
            if (!pipeline.initBook) {
                throw new Error("Pipeline does not support shared book creation.");
            }
            await pipeline.initBook(book, {
                externalContext: buildCreationExternalContext(input),
                authorIntent: input.authorIntent,
                currentFocus: input.currentFocus,
            });
            return {
                bookId: book.id,
                title: book.title,
                __interaction: {
                    responseText: `Created ${book.title} (${book.id}).`,
                    details: {
                        bookId: book.id,
                        title: book.title,
                    },
                },
            };
        },
        exportBook: async (bookId, options) => {
            const result = await exportBookToPath(state, bookId, options);
            return {
                ...result,
                __interaction: {
                    responseText: `Exported ${bookId} to ${result.outputPath} (${result.chaptersExported} chapters).`,
                    details: {
                        outputPath: result.outputPath,
                        chaptersExported: result.chaptersExported,
                        totalWords: result.totalWords,
                        format: result.format,
                    },
                },
            };
        },
        chat: async (input, options) => {
            const bookLabel = options.bookId ?? "none";
            const chatRequestOptions = hooks?.getChatRequestOptions?.() ?? {};
            let response;
            if (instrumentedPipeline.config?.client && instrumentedPipeline.config?.model) {
                try {
                    response = await chatCompletion(instrumentedPipeline.config.client, instrumentedPipeline.config.model, [
                        {
                            role: "system",
                            content: [
                                "You are InkOS inside the terminal workbench.",
                                "Respond conversationally and briefly.",
                                "If there is no active book, help the user decide what to write next.",
                                "If there is an active book, keep the answer grounded in that book context.",
                            ].join(" "),
                        },
                        {
                            role: "user",
                            content: `activeBook=${bookLabel}\nautomationMode=${options.automationMode}\nmessage=${input}`,
                        },
                    ], {
                        temperature: chatRequestOptions.temperature ?? 0.4,
                        ...(chatRequestOptions.maxTokens !== undefined && { maxTokens: chatRequestOptions.maxTokens }),
                        onTextDelta: hooks?.onChatTextDelta,
                    });
                }
                catch (err) {
                    // Thinking models (e.g. kimi-k2.5) may return empty content for simple inputs.
                    // Only swallow empty-content errors; re-throw everything else (network, auth, etc.)
                    const msg = err instanceof Error ? err.message : "";
                    if (!msg.includes("empty") && !msg.includes("content")) {
                        throw err;
                    }
                }
            }
            return {
                __interaction: {
                    responseText: response?.content?.trim()
                        || (options.bookId
                            ? `I’m here. Active book is ${options.bookId}.`
                            : "I’m here. No active book yet."),
                },
            };
        },
        writeNextChapter: (bookId) => withPipelineInteractionTelemetry(instrumentedPipeline, bookId, () => pipeline.writeNextChapter(bookId)),
        reviseDraft: (bookId, chapterNumber, mode) => withPipelineInteractionTelemetry(instrumentedPipeline, bookId, () => pipeline.reviseDraft(bookId, chapterNumber, mode)),
        patchChapterText: async (bookId, chapterNumber, targetText, replacementText) => {
            const execution = await executeEditTransaction({
                bookDir: (targetBookId) => state.bookDir(targetBookId),
                loadChapterIndex: (targetBookId) => state.loadChapterIndex(targetBookId),
                saveChapterIndex: (targetBookId, index) => state.saveChapterIndex(targetBookId, index),
            }, {
                kind: "chapter-local-edit",
                bookId,
                chapterNumber,
                instruction: `Replace ${targetText} with ${replacementText}`,
                targetText,
                replacementText,
            });
            return {
                __interaction: {
                    activeChapterNumber: chapterNumber,
                    responseText: execution.summary,
                },
            };
        },
        renameEntity: async (bookId, oldValue, newValue) => {
            const execution = await executeEditTransaction({
                bookDir: (targetBookId) => state.bookDir(targetBookId),
                loadChapterIndex: (targetBookId) => state.loadChapterIndex(targetBookId),
                saveChapterIndex: (targetBookId, index) => state.saveChapterIndex(targetBookId, index),
            }, {
                kind: "entity-rename",
                bookId,
                entityType: "character",
                oldValue,
                newValue,
            });
            return {
                __interaction: {
                    responseText: execution.summary,
                },
            };
        },
        updateCurrentFocus: async (bookId, content) => {
            await state.ensureControlDocuments(bookId);
            await writeFile(join(state.bookDir(bookId), "story", "current_focus.md"), content, "utf-8");
        },
        updateAuthorIntent: async (bookId, content) => {
            await state.ensureControlDocuments(bookId);
            await writeFile(join(state.bookDir(bookId), "story", "author_intent.md"), content, "utf-8");
        },
        writeTruthFile: async (bookId, fileName, content) => {
            await state.ensureControlDocuments(bookId);
            const storyDir = join(state.bookDir(bookId), "story");
            const safeFileName = assertSafeTruthFileName(fileName);
            const targetPath = safeChildPath(storyDir, safeFileName);
            await mkdir(dirname(targetPath), { recursive: true });
            await writeFile(targetPath, content, "utf-8");
        },
    };
}
//# sourceMappingURL=project-tools.js.map