// ═══════════════════════════════════════════════════════════════════════════
// Story Summary - 主入口（干净版）
// - 注入只在 GENERATION_STARTED 发生
// - 向量关闭：注入全量总结（L3+L2+Arcs）
// - 向量开启：召回 + 1万预算装配注入
// - 删除所有 updateSummaryExtensionPrompt() 调用，避免覆盖/残留/竞态
// ═══════════════════════════════════════════════════════════════════════════

import { getContext } from "../../../../../extensions.js";
import { eventSource, event_types } from "../../../../../../script.js";
import { extensionFolderPath } from "../../core/constants.js";
import { xbLog, CacheRegistry } from "../../core/debug-core.js";
import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js";
import { CommonSettingStorage } from "../../core/server-storage.js";

// config/store
import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js";
import {
    getSummaryStore,
    saveSummaryStore,
    calcHideRange,
    rollbackSummaryIfNeeded,
    clearSummaryData,
} from "./data/store.js";

// prompt injection (ONLY on generation started)
import {
    recallAndInjectPrompt,
    clearSummaryExtensionPrompt,
    injectNonVectorPrompt,
} from "./generate/prompt.js";

// summary generation
import { runSummaryGeneration } from "./generate/generator.js";

// vector service
import {
    embed,
    getEngineFingerprint,
    checkLocalModelStatus,
    downloadLocalModel,
    cancelDownload,
    deleteLocalModelCache,
    testOnlineService,
    fetchOnlineModels,
    isLocalModelLoaded,
    DEFAULT_LOCAL_MODEL,
} from "./vector/embedder.js";

import {
    getMeta,
    updateMeta,
    getAllEventVectors,
    saveEventVectors as saveEventVectorsToDb,
    clearEventVectors,
    clearAllChunks,
    saveChunks,
    saveChunkVectors,
    getStorageStats,
    ensureFingerprintMatch,
} from "./vector/chunk-store.js";

import {
    buildIncrementalChunks,
    getChunkBuildStatus,
    chunkMessage,
    syncOnMessageDeleted,
    syncOnMessageSwiped,
    syncOnMessageReceived,
} from "./vector/chunk-builder.js";

// ═══════════════════════════════════════════════════════════════════════════
// 常量
// ═══════════════════════════════════════════════════════════════════════════

const MODULE_ID = "storySummary";
const SUMMARY_CONFIG_KEY = "storySummaryPanelConfig";
const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`;
const VALID_SECTIONS = ["keywords", "events", "characters", "arcs", "world"];
const MESSAGE_EVENT = "message";

// ═══════════════════════════════════════════════════════════════════════════
// 状态变量
// ═══════════════════════════════════════════════════════════════════════════

let summaryGenerating = false;
let overlayCreated = false;
let frameReady = false;
let currentMesId = null;
let pendingFrameMessages = [];
let eventsRegistered = false;
let vectorGenerating = false;
let vectorCancelled = false;

let hideApplyTimer = null;
const HIDE_APPLY_DEBOUNCE_MS = 250;

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// ═══════════════════════════════════════════════════════════════════════════
// 工具：执行斜杠命令
// ═══════════════════════════════════════════════════════════════════════════

async function executeSlashCommand(command) {
    try {
        const executeCmd =
            window.executeSlashCommands ||
            window.executeSlashCommandsOnChatInput ||
            (typeof SillyTavern !== "undefined" && SillyTavern.getContext()?.executeSlashCommands);

        if (executeCmd) {
            await executeCmd(command);
        } else if (typeof window.STscript === "function") {
            await window.STscript(command);
        }
    } catch (e) {
        xbLog.error(MODULE_ID, `执行命令失败: ${command}`, e);
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// 生成状态管理
// ═══════════════════════════════════════════════════════════════════════════

function setSummaryGenerating(flag) {
    summaryGenerating = !!flag;
    postToFrame({ type: "GENERATION_STATE", isGenerating: summaryGenerating });
}

function isSummaryGenerating() {
    return summaryGenerating;
}

// ═══════════════════════════════════════════════════════════════════════════
// iframe 通讯
// ═══════════════════════════════════════════════════════════════════════════

function postToFrame(payload) {
    const iframe = document.getElementById("xiaobaix-story-summary-iframe");
    if (!iframe?.contentWindow || !frameReady) {
        pendingFrameMessages.push(payload);
        return;
    }
    postToIframe(iframe, payload, "LittleWhiteBox");
}

function flushPendingFrameMessages() {
    if (!frameReady) return;
    const iframe = document.getElementById("xiaobaix-story-summary-iframe");
    if (!iframe?.contentWindow) return;
    pendingFrameMessages.forEach((p) => postToIframe(iframe, p, "LittleWhiteBox"));
    pendingFrameMessages = [];
}

// ═══════════════════════════════════════════════════════════════════════════
// 向量功能：UI 交互/状态
// ═══════════════════════════════════════════════════════════════════════════

function sendVectorConfigToFrame() {
    const cfg = getVectorConfig();
    postToFrame({ type: "VECTOR_CONFIG", config: cfg });
}

async function sendVectorStatsToFrame() {
    const { chatId, chat } = getContext();
    if (!chatId) return;

    const store = getSummaryStore();
    const eventCount = store?.json?.events?.length || 0;
    const stats = await getStorageStats(chatId);
    const chunkStatus = await getChunkBuildStatus();
    const totalMessages = chat?.length || 0;

    const cfg = getVectorConfig();
    let mismatch = false;
    if (cfg?.enabled && (stats.eventVectors > 0 || stats.chunks > 0)) {
        const fingerprint = getEngineFingerprint(cfg);
        const meta = await getMeta(chatId);
        mismatch = meta.fingerprint && meta.fingerprint !== fingerprint;
    }

    postToFrame({
        type: "VECTOR_STATS",
        stats: {
            eventCount,
            eventVectors: stats.eventVectors,
            chunkCount: stats.chunkVectors,
            builtFloors: chunkStatus.builtFloors,
            totalFloors: chunkStatus.totalFloors,
            totalMessages,
        },
        mismatch,
    });
}

async function sendLocalModelStatusToFrame(modelId) {
    if (!modelId) {
        const cfg = getVectorConfig();
        modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
    }
    const status = await checkLocalModelStatus(modelId);
    postToFrame({
        type: "VECTOR_LOCAL_MODEL_STATUS",
        status: status.status,
        message: status.message,
    });
}

async function handleDownloadLocalModel(modelId) {
    try {
        postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "downloading", message: "下载中..." });

        await downloadLocalModel(modelId, (percent) => {
            postToFrame({ type: "VECTOR_LOCAL_MODEL_PROGRESS", percent });
        });

        postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "ready", message: "已就绪" });
    } catch (e) {
        if (e.message === "下载已取消") {
            postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "已取消" });
        } else {
            postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
        }
    }
}

function handleCancelDownload() {
    cancelDownload();
    postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "已取消" });
}

async function handleDeleteLocalModel(modelId) {
    try {
        await deleteLocalModelCache(modelId);
        postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "未下载" });
    } catch (e) {
        postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
    }
}

async function handleTestOnlineService(provider, config) {
    try {
        postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "连接中..." });
        const result = await testOnlineService(provider, config);
        postToFrame({
            type: "VECTOR_ONLINE_STATUS",
            status: "success",
            message: `连接成功 (${result.dims}维)`,
        });
    } catch (e) {
        postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
    }
}

async function handleFetchOnlineModels(config) {
    try {
        postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "拉取中..." });
        const models = await fetchOnlineModels(config);
        postToFrame({ type: "VECTOR_ONLINE_MODELS", models });
        postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "success", message: `找到 ${models.length} 个模型` });
    } catch (e) {
        postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
    }
}

async function handleGenerateVectors(vectorCfg) {
    if (vectorGenerating) return;

    if (!vectorCfg?.enabled) {
        postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
        postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
        return;
    }

    const { chatId, chat } = getContext();
    if (!chatId || !chat?.length) return;

    if (vectorCfg.engine === "online") {
        if (!vectorCfg.online?.key || !vectorCfg.online?.model) {
            postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置在线服务 API" });
            return;
        }
    }

    if (vectorCfg.engine === "local") {
        const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
        const status = await checkLocalModelStatus(modelId);
        if (status.status !== "ready") {
            postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "downloading", message: "正在加载模型..." });
            try {
                await downloadLocalModel(modelId, (percent) => {
                    postToFrame({ type: "VECTOR_LOCAL_MODEL_PROGRESS", percent });
                });
                postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "ready", message: "已就绪" });
            } catch (e) {
                xbLog.error(MODULE_ID, "模型加载失败", e);
                postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
                return;
            }
        }
    }

    vectorGenerating = true;
    vectorCancelled = false;

    const fingerprint = getEngineFingerprint(vectorCfg);
    const isLocal = vectorCfg.engine === "local";
    const batchSize = isLocal ? 5 : 20;
    const concurrency = isLocal ? 1 : 2;

    await clearAllChunks(chatId);
    await updateMeta(chatId, { lastChunkFloor: -1, fingerprint });

    const allChunks = [];
    for (let floor = 0; floor < chat.length; floor++) {
        const chunks = chunkMessage(floor, chat[floor]);
        allChunks.push(...chunks);
    }

    if (allChunks.length > 0) {
        await saveChunks(chatId, allChunks);
    }

    const l1Texts = allChunks.map((c) => c.text);
    const l1Batches = [];
    for (let i = 0; i < l1Texts.length; i += batchSize) {
        l1Batches.push({
            phase: "L1",
            texts: l1Texts.slice(i, i + batchSize),
            startIdx: i,
        });
    }

    const store = getSummaryStore();
    const events = store?.json?.events || [];

    await ensureFingerprintMatch(chatId, fingerprint);
    const existingVectors = await getAllEventVectors(chatId);
    const existingIds = new Set(existingVectors.map((v) => v.eventId));

    const l2Pairs = events
        .filter((e) => !existingIds.has(e.id))
        .map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() }))
        .filter((p) => p.text);

    const l2Batches = [];
    for (let i = 0; i < l2Pairs.length; i += batchSize) {
        const batch = l2Pairs.slice(i, i + batchSize);
        l2Batches.push({
            phase: "L2",
            texts: batch.map((p) => p.text),
            ids: batch.map((p) => p.id),
            startIdx: i,
        });
    }

    const l1Total = allChunks.length;
    const l2Total = events.length;
    let l1Completed = 0;
    let l2Completed = existingIds.size;

    postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: 0, total: l1Total });
    postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Total });

    const allTasks = [...l1Batches, ...l2Batches];
    const l1Vectors = new Array(l1Texts.length);
    const l2VectorItems = [];

    let taskIndex = 0;

    async function worker() {
        while (taskIndex < allTasks.length) {
            if (vectorCancelled) break;

            const i = taskIndex++;
            if (i >= allTasks.length) break;

            const task = allTasks[i];

            try {
                const vectors = await embed(task.texts, vectorCfg);

                if (task.phase === "L1") {
                    for (let j = 0; j < vectors.length; j++) {
                        l1Vectors[task.startIdx + j] = vectors[j];
                    }
                    l1Completed += task.texts.length;
                    postToFrame({
                        type: "VECTOR_GEN_PROGRESS",
                        phase: "L1",
                        current: Math.min(l1Completed, l1Total),
                        total: l1Total,
                    });
                } else {
                    for (let j = 0; j < vectors.length; j++) {
                        l2VectorItems.push({ eventId: task.ids[j], vector: vectors[j] });
                    }
                    l2Completed += task.texts.length;
                    postToFrame({
                        type: "VECTOR_GEN_PROGRESS",
                        phase: "L2",
                        current: Math.min(l2Completed, l2Total),
                        total: l2Total,
                    });
                }
            } catch (e) {
                xbLog.error(MODULE_ID, `${task.phase} batch 向量化失败`, e);
            }
        }
    }

    await Promise.all(
        Array(Math.min(concurrency, allTasks.length))
            .fill(null)
            .map(() => worker())
    );

    if (allChunks.length > 0 && l1Vectors.filter(Boolean).length > 0) {
        const chunkVectorItems = allChunks
            .map((chunk, idx) => (l1Vectors[idx] ? { chunkId: chunk.chunkId, vector: l1Vectors[idx] } : null))
            .filter(Boolean);
        await saveChunkVectors(chatId, chunkVectorItems, fingerprint);
        await updateMeta(chatId, { lastChunkFloor: chat.length - 1 });
    }

    if (l2VectorItems.length > 0) {
        await saveEventVectorsToDb(chatId, l2VectorItems, fingerprint);
    }

    postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
    postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
    await sendVectorStatsToFrame();

    vectorGenerating = false;
    vectorCancelled = false;

    xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`);
}

async function handleClearVectors() {
    const { chatId } = getContext();
    if (!chatId) return;

    await clearEventVectors(chatId);
    await clearAllChunks(chatId);
    await updateMeta(chatId, { lastChunkFloor: -1 });
    await sendVectorStatsToFrame();
    xbLog.info(MODULE_ID, "向量数据已清除");
}

async function maybeAutoBuildChunks() {
    const cfg = getVectorConfig();
    if (!cfg?.enabled) return;

    const { chat, chatId } = getContext();
    if (!chatId || !chat?.length) return;

    const status = await getChunkBuildStatus();
    if (status.pending <= 0) return;

    if (cfg.engine === "local") {
        const modelId = cfg.local?.modelId || DEFAULT_LOCAL_MODEL;
        if (!isLocalModelLoaded(modelId)) return;
    }

    xbLog.info(MODULE_ID, `auto L1 chunks: pending=${status.pending}`);

    try {
        await buildIncrementalChunks({ vectorConfig: cfg });
    } catch (e) {
        xbLog.error(MODULE_ID, "自动 L1 构建失败", e);
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// Overlay 面板
// ═══════════════════════════════════════════════════════════════════════════

function createOverlay() {
    if (overlayCreated) return;
    overlayCreated = true;

    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent);
    const isNarrow = window.matchMedia?.("(max-width: 768px)").matches;
    const overlayHeight = (isMobile || isNarrow) ? "92.5vh" : "100vh";

    const $overlay = $(`
        <div id="xiaobaix-story-summary-overlay" style="
            position: fixed !important; inset: 0 !important;
            width: 100vw !important; height: ${overlayHeight} !important;
            z-index: 99999 !important; display: none; overflow: hidden !important;
        ">
            <div class="xb-ss-backdrop" style="
                position: absolute !important; inset: 0 !important;
                background: rgba(0,0,0,.55) !important;
                backdrop-filter: blur(4px) !important;
            "></div>
            <div class="xb-ss-frame-wrap" style="
                position: absolute !important; inset: 12px !important; z-index: 1 !important;
            ">
                <iframe id="xiaobaix-story-summary-iframe" class="xiaobaix-iframe"
                    src="${iframePath}"
                    style="width:100% !important; height:100% !important; border:none !important;
                           border-radius:12px !important; box-shadow:0 0 30px rgba(0,0,0,.4) !important;
                           background:#fafafa !important;">
                </iframe>
            </div>
            <button class="xb-ss-close-btn" style="
                position: absolute !important; top: 20px !important; right: 20px !important;
                z-index: 2 !important; width: 36px !important; height: 36px !important;
                border-radius: 50% !important; border: none !important;
                background: rgba(0,0,0,.6) !important; color: #fff !important;
                font-size: 20px !important; cursor: pointer !important;
                display: flex !important; align-items: center !important;
                justify-content: center !important;
            ">✕</button>
        </div>
    `);

    $overlay.on("click", ".xb-ss-backdrop, .xb-ss-close-btn", hideOverlay);
    document.body.appendChild($overlay[0]);
    window.addEventListener(MESSAGE_EVENT, handleFrameMessage);
}

function showOverlay() {
    if (!overlayCreated) createOverlay();
    $("#xiaobaix-story-summary-overlay").show();
}

function hideOverlay() {
    $("#xiaobaix-story-summary-overlay").hide();
}

// ═══════════════════════════════════════════════════════════════════════════
// 楼层按钮
// ═══════════════════════════════════════════════════════════════════════════

function createSummaryBtn(mesId) {
    const btn = document.createElement("div");
    btn.className = "mes_btn xiaobaix-story-summary-btn";
    btn.title = "剧情总结";
    btn.dataset.mesid = mesId;
    btn.innerHTML = '<i class="fa-solid fa-chart-line"></i>';
    btn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (!getSettings().storySummary?.enabled) return;
        currentMesId = Number(mesId);
        openPanelForMessage(currentMesId);
    });
    return btn;
}

function addSummaryBtnToMessage(mesId) {
    if (!getSettings().storySummary?.enabled) return;
    const msg = document.querySelector(`#chat .mes[mesid="${mesId}"]`);
    if (!msg || msg.querySelector(".xiaobaix-story-summary-btn")) return;

    const btn = createSummaryBtn(mesId);
    if (window.registerButtonToSubContainer?.(mesId, btn)) return;

    msg.querySelector(".flex-container.flex1.alignitemscenter")?.appendChild(btn);
}

function initButtonsForAll() {
    if (!getSettings().storySummary?.enabled) return;
    $("#chat .mes").each((_, el) => {
        const mesId = el.getAttribute("mesid");
        if (mesId != null) addSummaryBtnToMessage(mesId);
    });
}

// ═══════════════════════════════════════════════════════════════════════════
// 面板数据发送
// ═══════════════════════════════════════════════════════════════════════════

async function sendSavedConfigToFrame() {
    try {
        const savedConfig = await CommonSettingStorage.get(SUMMARY_CONFIG_KEY, null);
        if (savedConfig) {
            postToFrame({ type: "LOAD_PANEL_CONFIG", config: savedConfig });
            xbLog.info(MODULE_ID, "已从服务器加载面板配置");
        }
    } catch (e) {
        xbLog.warn(MODULE_ID, "加载面板配置失败", e);
    }
}

async function sendFrameBaseData(store, totalFloors) {
    const boundary = await getHideBoundaryFloor(store);
    const range = calcHideRange(boundary);
    const hiddenCount = (store?.hideSummarizedHistory && range) ? (range.end + 1) : 0;

    const lastSummarized = store?.lastSummarizedMesId ?? -1;
    postToFrame({
        type: "SUMMARY_BASE_DATA",
        stats: {
            totalFloors,
            summarizedUpTo: lastSummarized + 1,
            eventsCount: store?.json?.events?.length || 0,
            pendingFloors: totalFloors - lastSummarized - 1,
            hiddenCount,
        },
        hideSummarized: store?.hideSummarizedHistory || false,
        keepVisibleCount: store?.keepVisibleCount ?? 3,
    });
}

function sendFrameFullData(store, totalFloors) {
    const lastSummarized = store?.lastSummarizedMesId ?? -1;
    if (store?.json) {
        postToFrame({
            type: "SUMMARY_FULL_DATA",
            payload: {
                keywords: store.json.keywords || [],
                events: store.json.events || [],
                characters: store.json.characters || { main: [], relationships: [] },
                arcs: store.json.arcs || [],
                world: store.json.world || [],
                lastSummarizedMesId: lastSummarized,
            },
        });
    } else {
        postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } });
    }
}

function openPanelForMessage(mesId) {
    createOverlay();
    showOverlay();

    const { chat } = getContext();
    const store = getSummaryStore();
    const totalFloors = chat.length;

    sendFrameBaseData(store, totalFloors); // 不需要 await，fire-and-forget
    sendFrameFullData(store, totalFloors);
    setSummaryGenerating(summaryGenerating);

    sendVectorConfigToFrame();
    sendVectorStatsToFrame();

    const cfg = getVectorConfig();
    const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
    sendLocalModelStatusToFrame(modelId);
}

// ═══════════════════════════════════════════════════════════════════════════
// Hide/Unhide：向量模式联动（"已总结"的定义切换）
// - 非向量：boundary = lastSummarizedMesId（LLM总结边界）
// - 向量：boundary = meta.lastChunkFloor（已向量化）
// ═══════════════════════════════════════════════════════════════════════════

async function getHideBoundaryFloor(store) {
    const vectorCfg = getVectorConfig();
    if (!vectorCfg?.enabled) {
        return store?.lastSummarizedMesId ?? -1;
    }

    const { chatId } = getContext();
    if (!chatId) return -1;

    const meta = await getMeta(chatId);
    return meta?.lastChunkFloor ?? -1;
}

async function applyHideState() {
    const store = getSummaryStore();
    if (!store?.hideSummarizedHistory) return;

    const boundary = await getHideBoundaryFloor(store);
    if (boundary < 0) return;

    const range = calcHideRange(boundary);
    if (!range) return;

    await executeSlashCommand(`/hide ${range.start}-${range.end}`);
}

function applyHideStateDebounced() {
    clearTimeout(hideApplyTimer);
    hideApplyTimer = setTimeout(() => {
        applyHideState().catch((e) => xbLog.warn(MODULE_ID, "applyHideState failed", e));
    }, HIDE_APPLY_DEBOUNCE_MS);
}

async function clearHideState() {
    const store = getSummaryStore();
    const boundary = await getHideBoundaryFloor(store);
    if (boundary < 0) return;

    await executeSlashCommand(`/unhide 0-${boundary}`);
}

// ═══════════════════════════════════════════════════════════════════════════
// 自动总结（保持原逻辑；不做 prompt 注入）
// ═══════════════════════════════════════════════════════════════════════════

async function maybeAutoRunSummary(reason) {
    const { chatId, chat } = getContext();
    if (!chatId || !Array.isArray(chat)) return;
    if (!getSettings().storySummary?.enabled) return;

    const cfgAll = getSummaryPanelConfig();
    const trig = cfgAll.trigger || {};

    if (trig.timing === "manual") return;
    if (!trig.enabled) return;
    if (trig.timing === "after_ai" && reason !== "after_ai") return;
    if (trig.timing === "before_user" && reason !== "before_user") return;

    if (isSummaryGenerating()) return;

    const store = getSummaryStore();
    const lastSummarized = store?.lastSummarizedMesId ?? -1;
    const pending = chat.length - lastSummarized - 1;
    if (pending < (trig.interval || 1)) return;

    xbLog.info(MODULE_ID, `自动触发剧情总结: reason=${reason}, pending=${pending}`);
    await autoRunSummaryWithRetry(chat.length - 1, { api: cfgAll.api, gen: cfgAll.gen, trigger: trig });
}

async function autoRunSummaryWithRetry(targetMesId, configForRun) {
    setSummaryGenerating(true);

    for (let attempt = 1; attempt <= 3; attempt++) {
        const result = await runSummaryGeneration(targetMesId, configForRun, {
            onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
            onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
            onComplete: ({ merged, endMesId }) => {
                postToFrame({
                    type: "SUMMARY_FULL_DATA",
                    payload: {
                        keywords: merged.keywords || [],
                        events: merged.events || [],
                        characters: merged.characters || { main: [], relationships: [] },
                        arcs: merged.arcs || [],
                        world: merged.world || [],
                        lastSummarizedMesId: endMesId,
                    },
                });
                postToFrame({
                    type: "SUMMARY_STATUS",
                    statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
                });
                updateFrameStatsAfterSummary(endMesId, merged);
            },
        });

        if (result.success) {
            setSummaryGenerating(false);
            return;
        }

        if (attempt < 3) await sleep(1000);
    }

    setSummaryGenerating(false);
    xbLog.error(MODULE_ID, "自动总结失败（已重试3次）");
    await executeSlashCommand("/echo severity=error 剧情总结失败（已自动重试 3 次）。请稍后再试。");
}

function updateFrameStatsAfterSummary(endMesId, merged) {
    const { chat } = getContext();
    const totalFloors = Array.isArray(chat) ? chat.length : 0;
    const store = getSummaryStore();
    const range = calcHideRange(endMesId);
    const hiddenCount = store?.hideSummarizedHistory && range ? range.end + 1 : 0;

    postToFrame({
        type: "SUMMARY_BASE_DATA",
        stats: {
            totalFloors,
            summarizedUpTo: endMesId + 1,
            eventsCount: merged.events?.length || 0,
            pendingFloors: totalFloors - endMesId - 1,
            hiddenCount,
        },
    });
}

// ═══════════════════════════════════════════════════════════════════════════
// iframe 消息处理
// ═══════════════════════════════════════════════════════════════════════════

function handleFrameMessage(event) {
    const iframe = document.getElementById("xiaobaix-story-summary-iframe");
    if (!isTrustedMessage(event, iframe, "LittleWhiteBox-StoryFrame")) return;

    const data = event.data;

    switch (data.type) {
        case "FRAME_READY": {
            frameReady = true;
            flushPendingFrameMessages();
            setSummaryGenerating(summaryGenerating);
            sendSavedConfigToFrame();
            sendVectorConfigToFrame();
            sendVectorStatsToFrame();

            const cfg = getVectorConfig();
            const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
            sendLocalModelStatusToFrame(modelId);
            break;
        }

        case "SETTINGS_OPENED":
        case "FULLSCREEN_OPENED":
        case "EDITOR_OPENED":
            $(".xb-ss-close-btn").hide();
            break;

        case "SETTINGS_CLOSED":
        case "FULLSCREEN_CLOSED":
        case "EDITOR_CLOSED":
            $(".xb-ss-close-btn").show();
            break;

        case "REQUEST_GENERATE": {
            const ctx = getContext();
            currentMesId = (ctx.chat?.length ?? 1) - 1;
            handleManualGenerate(currentMesId, data.config || {});
            break;
        }

        case "REQUEST_CANCEL":
            window.xiaobaixStreamingGeneration?.cancel?.("xb9");
            setSummaryGenerating(false);
            postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" });
            break;

        // vector UI
        case "VECTOR_DOWNLOAD_MODEL":
            handleDownloadLocalModel(data.modelId);
            break;

        case "VECTOR_CANCEL_DOWNLOAD":
            handleCancelDownload();
            break;

        case "VECTOR_DELETE_MODEL":
            handleDeleteLocalModel(data.modelId);
            break;

        case "VECTOR_CHECK_LOCAL_MODEL":
            sendLocalModelStatusToFrame(data.modelId);
            break;

        case "VECTOR_TEST_ONLINE":
            handleTestOnlineService(data.provider, data.config);
            break;

        case "VECTOR_FETCH_MODELS":
            handleFetchOnlineModels(data.config);
            break;

        case "VECTOR_GENERATE":
            if (data.config) saveVectorConfig(data.config);
            clearSummaryExtensionPrompt(); // 防残留
            handleGenerateVectors(data.config);
            break;

        case "VECTOR_CLEAR":
            clearSummaryExtensionPrompt(); // 防残留
            handleClearVectors();
            break;

        case "VECTOR_CANCEL_GENERATE":
            vectorCancelled = true;
            break;

        // summary actions
        case "REQUEST_CLEAR": {
            const { chat, chatId } = getContext();
            clearSummaryData(chatId);
            clearSummaryExtensionPrompt(); // 防残留
            postToFrame({
                type: "SUMMARY_CLEARED",
                payload: { totalFloors: Array.isArray(chat) ? chat.length : 0 },
            });
            break;
        }

        case "CLOSE_PANEL":
            hideOverlay();
            break;

        case "UPDATE_SECTION": {
            const store = getSummaryStore();
            if (!store) break;
            store.json ||= {};
            if (VALID_SECTIONS.includes(data.section)) {
                store.json[data.section] = data.data;
            }
            store.updatedAt = Date.now();
            saveSummaryStore();
            break;
        }

        case "TOGGLE_HIDE_SUMMARIZED": {
            const store = getSummaryStore();
            if (!store) break;

            store.hideSummarizedHistory = !!data.enabled;
            saveSummaryStore();

            (async () => {
                if (data.enabled) {
                    await applyHideState();
                } else {
                    await clearHideState();
                }
            })();
            break;
        }

        case "UPDATE_KEEP_VISIBLE": {
            const store = getSummaryStore();
            if (!store) break;

            const oldCount = store.keepVisibleCount ?? 3;
            const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3));

            if (newCount === oldCount) break;

            store.keepVisibleCount = newCount;
            saveSummaryStore();

            (async () => {
                // 先清掉原隐藏，再按新 keepCount 重算隐藏
                if (store.hideSummarizedHistory) {
                    await clearHideState();
                    await applyHideState();
                }
                const { chat } = getContext();
                await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
            })();
            break;
        }

        case "SAVE_PANEL_CONFIG":
            if (data.config) {
                CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config);
                clearSummaryExtensionPrompt(); // 配置变化立即清除注入，避免残留
                xbLog.info(MODULE_ID, "面板配置已保存到服务器");
            }
            break;

        case "REQUEST_PANEL_CONFIG":
            sendSavedConfigToFrame();
            break;
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// 手动总结
// ═══════════════════════════════════════════════════════════════════════════

async function handleManualGenerate(mesId, config) {
    if (isSummaryGenerating()) {
        postToFrame({ type: "SUMMARY_STATUS", statusText: "上一轮总结仍在进行中..." });
        return;
    }

    setSummaryGenerating(true);
    xbLog.info(MODULE_ID, `开始手动总结 mesId=${mesId}`);

    await runSummaryGeneration(mesId, config, {
        onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
        onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
        onComplete: ({ merged, endMesId }) => {
            postToFrame({
                type: "SUMMARY_FULL_DATA",
                payload: {
                    keywords: merged.keywords || [],
                    events: merged.events || [],
                    characters: merged.characters || { main: [], relationships: [] },
                    arcs: merged.arcs || [],
                    world: merged.world || [],
                    lastSummarizedMesId: endMesId,
                },
            });

            postToFrame({
                type: "SUMMARY_STATUS",
                statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
            });

            // 隐藏逻辑：统一走 boundary（vector on/off 自动切换定义）
            applyHideStateDebounced();

            updateFrameStatsAfterSummary(endMesId, merged);
        },
    });

    setSummaryGenerating(false);
}

// ═══════════════════════════════════════════════════════════════════════════
// 事件处理器（不做 prompt 注入）
// ═══════════════════════════════════════════════════════════════════════════

async function handleChatChanged() {
    const { chat } = getContext();
    const newLength = Array.isArray(chat) ? chat.length : 0;

    await rollbackSummaryIfNeeded();
    initButtonsForAll();

    const store = getSummaryStore();
    applyHideStateDebounced();

    if (frameReady) {
        await sendFrameBaseData(store, newLength);
        sendFrameFullData(store, newLength);
    }
}

async function handleMessageDeleted() {
    const { chat, chatId } = getContext();
    const newLength = chat?.length || 0;

    await rollbackSummaryIfNeeded();
    await syncOnMessageDeleted(chatId, newLength);
}

async function handleMessageSwiped() {
    const { chat, chatId } = getContext();
    const lastFloor = (chat?.length || 1) - 1;

    await syncOnMessageSwiped(chatId, lastFloor);
    initButtonsForAll();
}

async function handleMessageReceived() {
    const { chat, chatId } = getContext();
    const lastFloor = (chat?.length || 1) - 1;
    const message = chat?.[lastFloor];
    const vectorConfig = getVectorConfig();

    initButtonsForAll();

    await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
    await maybeAutoBuildChunks();

    // 向量模式下，lastChunkFloor 会持续推进；如果勾选隐藏，自动扩展隐藏范围
    applyHideStateDebounced();

    setTimeout(() => maybeAutoRunSummary("after_ai"), 1000);
}

function handleMessageSent() {
    initButtonsForAll();
    setTimeout(() => maybeAutoRunSummary("before_user"), 1000);
}

async function handleMessageUpdated() {
    await rollbackSummaryIfNeeded();
    initButtonsForAll();
}

function handleMessageRendered(data) {
    const mesId = data?.element ? $(data.element).attr("mesid") : data?.messageId;
    if (mesId != null) addSummaryBtnToMessage(mesId);
    else initButtonsForAll();
}

// ═══════════════════════════════════════════════════════════════════════════
// ✅ 唯一注入入口：GENERATION_STARTED
// ═══════════════════════════════════════════════════════════════════════════

async function handleGenerationStarted(type, _params, isDryRun) {
    if (isDryRun) return;
    if (!getSettings().storySummary?.enabled) return;

    const excludeLastAi = type === "swipe" || type === "regenerate";
    const vectorCfg = getVectorConfig();

    // 向量模式：召回 + 预算装配
    if (vectorCfg?.enabled) {
        await recallAndInjectPrompt(excludeLastAi, {
            postToFrame,
            echo: executeSlashCommand, // recall failure notification
        });
        return;
    }

    // 非向量模式：全量总结注入（不召回）
    await injectNonVectorPrompt(postToFrame);
}

// ═══════════════════════════════════════════════════════════════════════════
// 事件注册
// ═══════════════════════════════════════════════════════════════════════════

function registerEvents() {
    if (eventsRegistered) return;
    eventsRegistered = true;

    xbLog.info(MODULE_ID, "模块初始化");

    CacheRegistry.register(MODULE_ID, {
        name: "待发送消息队列",
        getSize: () => pendingFrameMessages.length,
        getBytes: () => {
            try {
                return JSON.stringify(pendingFrameMessages || []).length * 2;
            } catch {
                return 0;
            }
        },
        clear: () => {
            pendingFrameMessages = [];
            frameReady = false;
        },
    });

    initButtonsForAll();

    eventSource.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80));
    eventSource.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 50));
    eventSource.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150));
    eventSource.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150));
    eventSource.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageSwiped, 100));
    eventSource.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
    eventSource.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100));
    eventSource.on(event_types.USER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
    eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));

    // ✅ 只在生成开始时注入
    eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
}

function unregisterEvents() {
    xbLog.info(MODULE_ID, "模块清理");
    CacheRegistry.unregister(MODULE_ID);
    eventsRegistered = false;

    $(".xiaobaix-story-summary-btn").remove();
    hideOverlay();

    // 禁用时清理注入，避免残留
    clearSummaryExtensionPrompt();
}

// ═══════════════════════════════════════════════════════════════════════════
// Toggle 监听
// ═══════════════════════════════════════════════════════════════════════════

$(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => {
    if (enabled) {
        registerEvents();
        initButtonsForAll();

        // 开启时清一次，防止旧注入残留
        clearSummaryExtensionPrompt();
    } else {
        unregisterEvents();
    }
});

// ═══════════════════════════════════════════════════════════════════════════
// 初始化
// ═══════════════════════════════════════════════════════════════════════════

jQuery(() => {
    if (!getSettings().storySummary?.enabled) {
        clearSummaryExtensionPrompt();
        return;
    }
    registerEvents();

    // 初始化也清一次，保证干净（注入只在生成开始发生）
    clearSummaryExtensionPrompt();
});
