import { extension_settings, getContext } from "../../../../extensions.js";
import { saveSettingsDebounced, eventSource, event_types } from "../../../../../script.js";
import { EXT_ID } from "../core/constants.js";

const C = { MAX_HISTORY: 10, CHECK: 200, DEBOUNCE: 300, CLEAN: 300000, TARGET: "/api/backends/chat-completions/generate", TIMEOUT: 30, ASSOC_DELAY: 1000, REQ_WINDOW: 30000 };
const S = { active: false, isPreview: false, isLong: false, isHistoryUiBound: false, previewData: null, previewIds: new Set(), interceptedIds: [], history: [], listeners: [], resolve: null, reject: null, sendBtnWasDisabled: false, longPressTimer: null, longPressDelay: 1000, chatLenBefore: 0, restoreLong: null, cleanTimer: null, previewAbort: null, tailAPI: null, genEndedOff: null, cleanupFallback: null, pendingPurge: false };

const $q = (sel) => $(sel);
const ON = (e, c) => eventSource.on(e, c);
const OFF = (e, c) => eventSource.removeListener(e, c);
const now = () => Date.now();
const geEnabled = () => { try { return ("isXiaobaixEnabled" in window) ? !!window.isXiaobaixEnabled : true; } catch { return true; } };
const debounce = (fn, w) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), w); }; };
const safeJson = (t) => { try { return JSON.parse(t); } catch { return null; } };

const readText = async (b) => { try { if (!b) return ""; if (typeof b === "string") return b; if (b instanceof Blob) return await b.text(); if (b instanceof URLSearchParams) return b.toString(); if (typeof b === "object" && typeof b.text === "function") return await b.text(); } catch { } return ""; };

function isSafeBody(body) { if (!body) return true; return (typeof body === "string" || body instanceof Blob || body instanceof URLSearchParams || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || (typeof FormData !== "undefined" && body instanceof FormData)); }

async function safeReadBodyFromInput(input, options) { try { if (input instanceof Request) return await readText(input.clone()); const body = options?.body; if (!isSafeBody(body)) return ""; return await readText(body); } catch { return ""; } }

const isGen = (u) => String(u || "").includes(C.TARGET);
const isTarget = async (input, opt = {}) => { try { const url = input instanceof Request ? input.url : input; if (!isGen(url)) return false; const text = await safeReadBodyFromInput(input, opt); return text ? text.includes('"messages"') : true; } catch { return input instanceof Request ? isGen(input.url) : isGen(input); } };
const getSettings = () => { const d = extension_settings[EXT_ID] || (extension_settings[EXT_ID] = {}); d.preview = d.preview || { enabled: false, timeoutSeconds: C.TIMEOUT }; d.recorded = d.recorded || { enabled: true }; d.preview.timeoutSeconds = C.TIMEOUT; return d; };

function injectPreviewModalStyles() {
  if (document.getElementById('message-preview-modal-styles')) return;
  const style = document.createElement('style');
  style.id = 'message-preview-modal-styles';
  style.textContent = `
    .mp-overlay{position:fixed;inset:0;background:none;z-index:9999;display:flex;align-items:center;justify-content:center;pointer-events:none}
    .mp-modal{
      width:clamp(360px,55vw,860px);
      max-width:95vw;
      background:var(--SmartThemeBlurTintColor);
      border:2px solid var(--SmartThemeBorderColor);
      border-radius:10px;
      box-shadow:0 8px 16px var(--SmartThemeShadowColor);
      pointer-events:auto;
      display:flex;
      flex-direction:column;
      height:80vh;
      max-height:calc(100vh - 60px);
      resize:both;
      overflow:hidden;
    }
    .mp-header{display:flex;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--SmartThemeBorderColor);font-weight:600;cursor:move;flex-shrink:0}
    .mp-body{height:60vh;overflow:auto;padding:10px;flex:1;min-height:160px}
    .mp-footer{display:flex;gap:8px;justify-content:flex-end;padding:12px 14px;border-top:1px solid var(--SmartThemeBorderColor);flex-shrink:0}
    .mp-close{cursor:pointer}
    .mp-btn{cursor:pointer;border:1px solid var(--SmartThemeBorderColor);background:var(--SmartThemeBlurTintColor);padding:6px 10px;border-radius:6px}
    .mp-search-input{padding:4px 8px;border:1px solid var(--SmartThemeBorderColor);border-radius:4px;background:var(--SmartThemeShadowColor);color:inherit;font-size:12px;width:120px}
    .mp-search-btn{padding:4px 6px;font-size:12px;min-width:24px;text-align:center}
    .mp-search-info{font-size:12px;opacity:.8;white-space:nowrap}
    .message-preview-container{height:100%}
    .message-preview-content-box{height:100%;overflow:auto}
    .mp-highlight{background-color:yellow;color:black;padding:1px 2px;border-radius:2px}
    .mp-highlight.current{background-color:orange;font-weight:bold}
    @media (max-width:999px){
      .mp-overlay{position:absolute;inset:0;align-items:flex-start}
      .mp-modal{width:100%;max-width:100%;max-height:100%;margin:0;border-radius:10px 10px 0 0;height:100vh;resize:none}
      .mp-header{padding:8px 14px}
      .mp-body{padding:8px}
      .mp-footer{padding:8px 14px;flex-wrap:wrap;gap:6px}
      .mp-search-input{width:150px}
    }
  `;
  document.head.appendChild(style);
}

function setupModalDrag(modal, overlay, header) {
  modal.style.position = 'absolute';
  modal.style.left = '50%';
  modal.style.top = '50%';
  modal.style.transform = 'translate(-50%, -50%)';

  let dragging = false, sx = 0, sy = 0, sl = 0, st = 0;

  function onDown(e) {
    if (!(e instanceof PointerEvent) || e.button !== 0) return;
    dragging = true;
    const overlayRect = overlay.getBoundingClientRect();
    const rect = modal.getBoundingClientRect();
    modal.style.left = (rect.left - overlayRect.left) + 'px';
    modal.style.top = (rect.top - overlayRect.top) + 'px';
    modal.style.transform = '';
    sx = e.clientX; sy = e.clientY;
    sl = parseFloat(modal.style.left) || 0;
    st = parseFloat(modal.style.top) || 0;
    window.addEventListener('pointermove', onMove, { passive: true });
    window.addEventListener('pointerup', onUp, { once: true });
    e.preventDefault();
  }

  function onMove(e) {
    if (!dragging) return;
    const dx = e.clientX - sx, dy = e.clientY - sy;
    let nl = sl + dx, nt = st + dy;
    const maxLeft = (overlay.clientWidth || overlay.getBoundingClientRect().width) - modal.offsetWidth;
    const maxTop = (overlay.clientHeight || overlay.getBoundingClientRect().height) - modal.offsetHeight;
    nl = Math.max(0, Math.min(maxLeft, nl));
    nt = Math.max(0, Math.min(maxTop, nt));
    modal.style.left = nl + 'px';
    modal.style.top = nt + 'px';
  }

  function onUp() {
    dragging = false;
    window.removeEventListener('pointermove', onMove);
  }

  header.addEventListener('pointerdown', onDown);
}

function createMovableModal(title, content) {
  injectPreviewModalStyles();
  const overlay = document.createElement('div');
  overlay.className = 'mp-overlay';
  const modal = document.createElement('div');
  modal.className = 'mp-modal';
  const header = document.createElement('div');
  header.className = 'mp-header';
  // Template-only UI markup (title is escaped by caller).
  // eslint-disable-next-line no-unsanitized/property
  header.innerHTML = `<span>${title}</span><span class="mp-close">✕</span>`;
  const body = document.createElement('div');
  body.className = 'mp-body';
  // Content is already escaped before building the preview.
  // eslint-disable-next-line no-unsanitized/property
  body.innerHTML = content;
  const footer = document.createElement('div');
  footer.className = 'mp-footer';
  // Template-only UI markup.
  // eslint-disable-next-line no-unsanitized/property
  footer.innerHTML = `
    <input type="text" class="mp-search-input" placeholder="搜索..." />
    <button class="mp-btn mp-search-btn" id="mp-search-prev">↑</button>
    <button class="mp-btn mp-search-btn" id="mp-search-next">↓</button>
    <span class="mp-search-info" id="mp-search-info"></span>
    <button class="mp-btn" id="mp-toggle-format">切换原始格式</button>
    <button class="mp-btn" id="mp-focus-search">搜索</button>
    <button class="mp-btn" id="mp-close">关闭</button>
  `;
  modal.appendChild(header);
  modal.appendChild(body);
  modal.appendChild(footer);
  overlay.appendChild(modal);
  setupModalDrag(modal, overlay, header);

  let searchResults = [];
  let currentIndex = -1;
  const searchInput = footer.querySelector('.mp-search-input');
  const searchInfo = footer.querySelector('#mp-search-info');
  const prevBtn = footer.querySelector('#mp-search-prev');
  const nextBtn = footer.querySelector('#mp-search-next');

  function clearHighlights() {
    body.querySelectorAll('.mp-highlight').forEach(el => {
      // Controlled markup generated locally.
      // eslint-disable-next-line no-unsanitized/property
      el.outerHTML = el.innerHTML;
    });
  }
  function performSearch(query) {
    clearHighlights();
    searchResults = [];
    currentIndex = -1;
    if (!query.trim()) { searchInfo.textContent = ''; return; }
    const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false);
    const nodes = [];
    let node;
    while ((node = walker.nextNode())) { nodes.push(node); }
    const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
    nodes.forEach(textNode => {
      const text = textNode.textContent;
      if (!text || !regex.test(text)) return;
      let html = text;
      let offset = 0;
      regex.lastIndex = 0;
      const matches = [...text.matchAll(regex)];
      matches.forEach((m) => {
        const start = m.index + offset;
        const end = start + m[0].length;
        const before = html.slice(0, start);
        const mid = html.slice(start, end);
        const after = html.slice(end);
        const span = `<span class="mp-highlight" data-search-index="${searchResults.length}">${mid}</span>`;
        html = before + span + after;
        offset += span.length - m[0].length;
        searchResults.push({});
      });
      const parent = textNode.parentElement;
      // Controlled markup generated locally.
      // eslint-disable-next-line no-unsanitized/property
      parent.innerHTML = parent.innerHTML.replace(text, html);
    });
    updateSearchInfo();
    if (searchResults.length > 0) { currentIndex = 0; highlightCurrent(); }
  }
  function updateSearchInfo() { if (!searchResults.length) searchInfo.textContent = searchInput.value.trim() ? '无结果' : ''; else searchInfo.textContent = `${currentIndex + 1}/${searchResults.length}`; }
  function highlightCurrent() {
    body.querySelectorAll('.mp-highlight.current').forEach(el => el.classList.remove('current'));
    if (currentIndex >= 0 && currentIndex < searchResults.length) {
      const el = body.querySelector(`.mp-highlight[data-search-index="${currentIndex}"]`);
      if (el) { el.classList.add('current'); el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
    }
  }
  function navigateSearch(direction) {
    if (!searchResults.length) return;
    if (direction === 'next') currentIndex = (currentIndex + 1) % searchResults.length;
    else currentIndex = currentIndex <= 0 ? searchResults.length - 1 : currentIndex - 1;
    updateSearchInfo();
    highlightCurrent();
  }
  let searchTimeout;
  searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => performSearch(e.target.value), 250); });
  searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); if (e.shiftKey) navigateSearch('prev'); else navigateSearch('next'); } else if (e.key === 'Escape') { searchInput.value = ''; performSearch(''); } });
  prevBtn.addEventListener('click', () => navigateSearch('prev'));
  nextBtn.addEventListener('click', () => navigateSearch('next'));
  footer.querySelector('#mp-focus-search')?.addEventListener('click', () => { searchInput.focus(); if (searchInput.value) navigateSearch('next'); });

  const close = () => overlay.remove();
  header.querySelector('.mp-close').addEventListener('click', close);
  footer.querySelector('#mp-close').addEventListener('click', close);
  footer.querySelector('#mp-toggle-format').addEventListener('click', (e) => {
    const box = body.querySelector(".message-preview-content-box");
    const f = box?.querySelector(".mp-state-formatted");
    const r = box?.querySelector(".mp-state-raw");
    if (!(f && r)) return;
    const showRaw = r.style.display === "none";
    r.style.display = showRaw ? "block" : "none";
    f.style.display = showRaw ? "none" : "block";
    e.currentTarget.textContent = showRaw ? "切换整理格式" : "切换原始格式";
    searchInput.value = "";
    clearHighlights();
    searchInfo.textContent = "";
    searchResults = [];
    currentIndex = -1;
  });

  document.body.appendChild(overlay);
  return { overlay, modal, body, close };
}

const MIRROR = { MERGE: "merge", MERGE_TOOLS: "merge_tools", SEMI: "semi", SEMI_TOOLS: "semi_tools", STRICT: "strict", STRICT_TOOLS: "strict_tools", SINGLE: "single" };
const roleMap = { system: { label: "SYSTEM:", color: "#F7E3DA" }, user: { label: "USER:", color: "#F0ADA7" }, assistant: { label: "ASSISTANT:", color: "#6BB2CC" } };
const escapeHtml = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]));
const colorXml = (t) => {
  const safe = escapeHtml(t);
  return safe.replace(/&lt;([^&]+?)&gt;/g, '<span style="color:#999;font-weight:bold;">&lt;$1&gt;</span>');
};
const getNames = (req) => { const n = { charName: String(req?.char_name || ""), userName: String(req?.user_name || ""), groupNames: Array.isArray(req?.group_names) ? req.group_names.map(String) : [] }; n.startsWithGroupName = (m) => n.groupNames.some((g) => String(m || "").startsWith(`${g}: `)); return n; };
const toText = (m) => { const c = m?.content; if (typeof c === "string") return c; if (Array.isArray(c)) return c.map((p) => p?.type === "text" ? String(p.text || "") : p?.type === "image_url" ? "[image]" : p?.type === "video_url" ? "[video]" : typeof p === "string" ? p : (typeof p?.content === "string" ? p.content : "")).filter(Boolean).join("\n\n"); return String(c || ""); };
const applyName = (m, n) => { const { role, name } = m; let t = toText(m); if (role === "system" && name === "example_assistant") { if (n.charName && !t.startsWith(`${n.charName}: `) && !n.startsWithGroupName(t)) t = `${n.charName}: ${t}`; } else if (role === "system" && name === "example_user") { if (n.userName && !t.startsWith(`${n.userName}: `)) t = `${n.userName}: ${t}`; } else if (name && role !== "system" && !t.startsWith(`${name}: `)) t = `${name}: ${t}`; return { ...m, content: t, name: undefined }; };
function mergeMessages(messages, names, { strict = false, placeholders = false, single = false, tools = false } = {}) {
  if (!Array.isArray(messages)) return [];
  let mapped = messages.map((m) => applyName({ ...m }, names)).map((x) => { const m = { ...x }; if (!tools) { if (m.role === "tool") m.role = "user"; delete m.tool_calls; delete m.tool_call_id; } if (single) { if (m.role === "assistant") { const t = String(m.content || ""); if (names.charName && !t.startsWith(`${names.charName}: `) && !names.startsWithGroupName(t)) m.content = `${names.charName}: ${t}`; } if (m.role === "user") { const t = String(m.content || ""); if (names.userName && !t.startsWith(`${names.userName}: `)) m.content = `${names.userName}: ${t}`; } m.role = "user"; } return m; });
  const squash = (arr) => { const out = []; for (const m of arr) { if (out.length && out[out.length - 1].role === m.role && String(m.content || "").length && m.role !== "tool") out[out.length - 1].content += `\n\n${m.content}`; else out.push(m); } return out; };
  let sq = squash(mapped);
  if (strict) { for (let i = 0; i < sq.length; i++) if (i > 0 && sq[i].role === "system") sq[i].role = "user"; if (placeholders) { if (!sq.length) sq.push({ role: "user", content: "[Start a new chat]" }); else if (sq[0].role === "system" && (sq.length === 1 || sq[1].role !== "user")) sq.splice(1, 0, { role: "user", content: "[Start a new chat]" }); else if (sq[0].role !== "system" && sq[0].role !== "user") sq.unshift({ role: "user", content: "[Start a new chat]" }); } return squash(sq); }
  if (!sq.length) sq.push({ role: "user", content: "[Start a new chat]" });
  return sq;
}
function mirror(requestData) {
  try {
    let type = String(requestData?.custom_prompt_post_processing || "").toLowerCase();
    const source = String(requestData?.chat_completion_source || "").toLowerCase();
    if (source === "perplexity") type = MIRROR.STRICT;
    const names = getNames(requestData || {}), src = Array.isArray(requestData?.messages) ? JSON.parse(JSON.stringify(requestData.messages)) : [];
    const mk = (o) => mergeMessages(src, names, o);
    switch (type) {
      case MIRROR.MERGE: return mk({ strict: false });
      case MIRROR.MERGE_TOOLS: return mk({ strict: false, tools: true });
      case MIRROR.SEMI: return mk({ strict: true });
      case MIRROR.SEMI_TOOLS: return mk({ strict: true, tools: true });
      case MIRROR.STRICT: return mk({ strict: true, placeholders: true });
      case MIRROR.STRICT_TOOLS: return mk({ strict: true, placeholders: true, tools: true });
      case MIRROR.SINGLE: return mk({ strict: true, single: true });
      default: return src;
    }
  } catch { return Array.isArray(requestData?.messages) ? requestData.messages : []; }
}
const finalMsgs = (d) => { try { if (d?.requestData?.messages) return mirror(d.requestData); if (Array.isArray(d?.messages)) return d.messages; return []; } catch { return Array.isArray(d?.messages) ? d.messages : []; } };
const formatPreview = (d) => {
  const msgs = finalMsgs(d);
  let out = `↓酒馆日志↓(${msgs.length})\n${"-".repeat(30)}\n`;
  msgs.forEach((m, i) => {
    const txt = String(m.content || "");
    const safeTxt = escapeHtml(txt);
    const rm = roleMap[m.role] || { label: `${String(m.role || "").toUpperCase()}:`, color: "#FFF" };
    out += `<div style="color:${rm.color};font-weight:bold;margin-top:${i ? "15px" : "0"};">${rm.label}</div>`;
    out += /<[^>]+>/g.test(txt) ? `<pre style="white-space:pre-wrap;margin:5px 0;color:${rm.color};">${colorXml(txt)}</pre>` : `<div style="margin:5px 0;color:${rm.color};white-space:pre-wrap;">${safeTxt}</div>`;
  });
  return out;
};
const stripTop = (o) => { try { if (!o || typeof o !== "object") return o; if (Array.isArray(o)) return o; const messages = Array.isArray(o.messages) ? JSON.parse(JSON.stringify(o.messages)) : undefined; return typeof messages !== "undefined" ? { messages } : {}; } catch { return {}; } };
const formatRaw = (d) => { try { const hasReq = Array.isArray(d?.requestData?.messages), hasMsgs = !hasReq && Array.isArray(d?.messages); let obj; if (hasReq) { const req = JSON.parse(JSON.stringify(d.requestData)); try { req.messages = mirror(req); } catch { } obj = req; } else if (hasMsgs) { const fake = { ...(d || {}), messages: d.messages }; let mm = null; try { mm = mirror(fake); } catch { } obj = { ...(d || {}), messages: mm || d.messages }; } else obj = d?.requestData ?? d; obj = stripTop(obj); return colorXml(JSON.stringify(obj, null, 2)); } catch { try { return colorXml(String(d)); } catch { return ""; } } };
const buildPreviewHtml = (d) => { const formatted = formatPreview(d), raw = formatRaw(d); return `<div class="message-preview-container"><div class="message-preview-content-box"><div class="mp-state-formatted">${formatted}</div><pre class="mp-state-raw" style="display:none;">${raw}</pre></div></div>`; };
const openPopup = async (html, title) => { createMovableModal(title, html); };
const displayPreview = async (d) => { try { await openPopup(buildPreviewHtml(d), "消息拦截"); } catch { toastr.error("显示拦截失败"); } };

const pushHistory = (r) => { S.history.unshift(r); if (S.history.length > C.MAX_HISTORY) S.history.length = C.MAX_HISTORY; };
const extractUser = (ms) => { if (!Array.isArray(ms)) return ""; for (let i = ms.length - 1; i >= 0; i--) if (ms[i]?.role === "user") return ms[i].content || ""; return ""; };

async function recordReal(input, options) {
  try {
    const url = input instanceof Request ? input.url : input;
    const body = await safeReadBodyFromInput(input, options);
    if (!body) return;
    const data = safeJson(body) || {}, ctx = getContext();
    pushHistory({ url, method: options?.method || (input instanceof Request ? input.method : "POST"), requestData: data, messages: data.messages || [], model: data.model || "Unknown", timestamp: now(), messageId: ctx.chat?.length || 0, characterName: ctx.characters?.[ctx.characterId]?.name || "Unknown", userInput: extractUser(data.messages || []), isRealRequest: true });
    setTimeout(() => { if (S.history[0] && !S.history[0].associatedMessageId) S.history[0].associatedMessageId = ctx.chat?.length || 0; }, C.ASSOC_DELAY);
  } catch { }
}

const findRec = (id) => {
  if (!S.history.length) return null;
  const preds = [(r) => r.associatedMessageId === id, (r) => r.messageId === id, (r) => r.messageId === id - 1, (r) => Math.abs(r.messageId - id) <= 1];
  for (const p of preds) { const m = S.history.find(p); if (m) return m; }
  const cs = S.history.filter((r) => r.messageId <= id + 2);
  return cs.length ? cs.sort((a, b) => b.messageId - a.messageId)[0] : S.history[0];
};

// Improved purgePreviewArtifacts - follows SillyTavern's batch delete pattern
async function purgePreviewArtifacts() {
  try {
    if (!S.pendingPurge) return;
    S.pendingPurge = false;
    const ctx = getContext();
    const chat = Array.isArray(ctx.chat) ? ctx.chat : [];
    const start = Math.max(0, Number(S.chatLenBefore) || 0);
    if (start >= chat.length) return;

    // 1. Remove DOM elements (following SillyTavern's pattern from #dialogue_del_mes_ok)
    const $chat = $('#chat');
    $chat.find(`.mes[mesid="${start}"]`).nextAll('.mes').addBack().remove();

    // 2. Truncate chat array
    chat.length = start;

    // 3. Update last_mes class
    $('#chat .mes').removeClass('last_mes');
    $('#chat .mes').last().addClass('last_mes');

    // 4. Save chat and emit MESSAGE_DELETED event (critical for other plugins)
    ctx.saveChat?.();
    await eventSource.emit(event_types.MESSAGE_DELETED, start);
  } catch (e) {
    console.error('[message-preview] purgePreviewArtifacts error', e);
  }
}



function oneShotOnLast(ev, handler) {
  const wrapped = (...args) => {
    try { handler(...args); } finally { off(); }
  };
  let off = () => { };
  if (typeof eventSource.makeLast === "function") {
    eventSource.makeLast(ev, wrapped);
    off = () => {
      try { eventSource.removeListener?.(ev, wrapped); } catch { }
      try { eventSource.off?.(ev, wrapped); } catch { }
    };
  } else if (S.tailAPI?.onLast) {
    const disposer = S.tailAPI.onLast(ev, wrapped);
    off = () => { try { disposer?.(); } catch { } };
  } else {
    eventSource.on(ev, wrapped);
    off = () => { try { eventSource.removeListener?.(ev, wrapped); } catch { } };
  }
  return off;
}

function installEventSourceTail(es) {
  if (!es || es.__lw_tailInstalled) return es?.__lw_tailAPI || null;
  const SYM = { MW_STACK: Symbol.for("lwbox.es.emitMiddlewareStack"), BASE: Symbol.for("lwbox.es.emitBase"), ORIG_DESC: Symbol.for("lwbox.es.emit.origDesc"), COMPOSED: Symbol.for("lwbox.es.emit.composed"), ID: Symbol.for("lwbox.middleware.identity") };
  const getFnFromDesc = (d) => { try { if (typeof d?.value === "function") return d.value; if (typeof d?.get === "function") { const v = d.get.call(es); if (typeof v === "function") return v; } } catch { } return es.emit?.bind?.(es) || es.emit; };
  const compose = (base, stack) => stack.reduce((acc, mw) => mw(acc), base);
  const tails = new Map();
  const addTail = (ev, fn) => { if (typeof fn !== "function") return () => { }; const arr = tails.get(ev) || []; arr.push(fn); tails.set(ev, arr); return () => { const a = tails.get(ev); if (!a) return; const i = a.indexOf(fn); if (i >= 0) a.splice(i, 1); }; };
  const runTails = (ev, args) => { const arr = tails.get(ev); if (!arr?.length) return; for (const h of arr.slice()) { try { h(...args); } catch (e) { } } };
  const makeTailMw = () => { const mw = (next) => function patchedEmit(ev, ...args) { let r; try { r = next.call(this, ev, ...args); } catch (e) { queueMicrotask(() => runTails(ev, args)); throw e; } if (r && typeof r.then === "function") r.finally(() => runTails(ev, args)); else queueMicrotask(() => runTails(ev, args)); return r; }; Object.defineProperty(mw, SYM.ID, { value: true }); return Object.freeze(mw); };
  const ensureAccessor = () => { try { const d = Object.getOwnPropertyDescriptor(es, "emit"); if (!es[SYM.ORIG_DESC]) es[SYM.ORIG_DESC] = d || null; es[SYM.BASE] ||= getFnFromDesc(d); Object.defineProperty(es, "emit", { configurable: true, enumerable: d?.enumerable ?? true, get() { return reapply(); }, set(v) { if (typeof v === "function") { es[SYM.BASE] = v; queueMicrotask(reapply); } } }); } catch { } };
  const reapply = () => { try { const base = es[SYM.BASE] || getFnFromDesc(Object.getOwnPropertyDescriptor(es, "emit")) || es.emit.bind(es); const stack = es[SYM.MW_STACK] || (es[SYM.MW_STACK] = []); let idx = stack.findIndex((m) => m && m[SYM.ID]); if (idx === -1) { stack.push(makeTailMw()); idx = stack.length - 1; } if (idx !== stack.length - 1) { const mw = stack[idx]; stack.splice(idx, 1); stack.push(mw); } const composed = compose(base, stack) || base; if (!es[SYM.COMPOSED] || es[SYM.COMPOSED]._base !== base || es[SYM.COMPOSED]._stack !== stack) { composed._base = base; composed._stack = stack; es[SYM.COMPOSED] = composed; } return es[SYM.COMPOSED]; } catch { return es.emit; } };
  ensureAccessor();
  queueMicrotask(reapply);
  const api = { onLast: (e, h) => addTail(e, h), removeLast: (e, h) => { const a = tails.get(e); if (!a) return; const i = a.indexOf(h); if (i >= 0) a.splice(i, 1); }, uninstall() { try { const s = es[SYM.MW_STACK]; const i = Array.isArray(s) ? s.findIndex((m) => m && m[SYM.ID]) : -1; if (i >= 0) s.splice(i, 1); const orig = es[SYM.ORIG_DESC]; if (orig) { try { Object.defineProperty(es, "emit", orig); } catch { Object.defineProperty(es, "emit", { configurable: true, enumerable: true, writable: true, value: es[SYM.BASE] || es.emit }); } } else { Object.defineProperty(es, "emit", { configurable: true, enumerable: true, writable: true, value: es[SYM.BASE] || es.emit }); } } catch { } delete es.__lw_tailInstalled; delete es.__lw_tailAPI; tails.clear(); } };
  Object.defineProperty(es, "__lw_tailInstalled", { value: true });
  Object.defineProperty(es, "__lw_tailAPI", { value: api });
  return api;
}

let __installed = false;
const MW_KEY = Symbol.for("lwbox.fetchMiddlewareStack");
const BASE_KEY = Symbol.for("lwbox.fetchBase");
const ORIG_KEY = Symbol.for("lwbox.fetch.origDesc");
const CMP_KEY = Symbol.for("lwbox.fetch.composed");
const ID = Symbol.for("lwbox.middleware.identity");
const getFetchFromDesc = (d) => { try { if (typeof d?.value === "function") return d.value; if (typeof d?.get === "function") { const v = d.get.call(window); if (typeof v === "function") return v; } } catch { } return globalThis.fetch; };
const compose = (base, stack) => stack.reduce((acc, mw) => mw(acc), base);
const withTimeout = (p, ms = 200) => { try { return Promise.race([p, new Promise((r) => setTimeout(r, ms))]); } catch { return p; } };
const ensureAccessor = () => { try { const d = Object.getOwnPropertyDescriptor(window, "fetch"); if (!window[ORIG_KEY]) window[ORIG_KEY] = d || null; window[BASE_KEY] ||= getFetchFromDesc(d); Object.defineProperty(window, "fetch", { configurable: true, enumerable: d?.enumerable ?? true, get() { return reapply(); }, set(v) { if (typeof v === "function") { window[BASE_KEY] = v; queueMicrotask(reapply); } } }); } catch { } };
const reapply = () => { try { const base = window[BASE_KEY] || getFetchFromDesc(Object.getOwnPropertyDescriptor(window, "fetch")); const stack = window[MW_KEY] || (window[MW_KEY] = []); let idx = stack.findIndex((m) => m && m[ID]); if (idx === -1) { stack.push(makeMw()); idx = stack.length - 1; } if (idx !== window[MW_KEY].length - 1) { const mw = window[MW_KEY][idx]; window[MW_KEY].splice(idx, 1); window[MW_KEY].push(mw); } const composed = compose(base, stack) || base; if (!window[CMP_KEY] || window[CMP_KEY]._base !== base || window[CMP_KEY]._stack !== stack) { composed._base = base; composed._stack = stack; window[CMP_KEY] = composed; } return window[CMP_KEY]; } catch { return globalThis.fetch; } };
function makeMw() {
  const mw = (next) => async function f(input, options = {}) {
    try {
      if (await isTarget(input, options)) {
        if (S.isPreview || S.isLong) {
          const url = input instanceof Request ? input.url : input;
          return interceptPreview(url, options).catch(() => new Response(JSON.stringify({ error: { message: "拦截失败，请手动中止消息生成。" } }), { status: 200, headers: { "Content-Type": "application/json" } }));
        } else { try { await withTimeout(recordReal(input, options)); } catch { } }
      }
    } catch { }
    return Reflect.apply(next, this, arguments);
  };
  Object.defineProperty(mw, ID, { value: true, enumerable: false });
  return Object.freeze(mw);
}
function installFetch() {
  if (__installed) return; __installed = true;
  try {
    window[MW_KEY] ||= [];
    window[BASE_KEY] ||= getFetchFromDesc(Object.getOwnPropertyDescriptor(window, "fetch"));
    ensureAccessor();
    if (!window[MW_KEY].some((m) => m && m[ID])) window[MW_KEY].push(makeMw());
    else {
      const i = window[MW_KEY].findIndex((m) => m && m[ID]);
      if (i !== window[MW_KEY].length - 1) {
        const mw = window[MW_KEY][i];
        window[MW_KEY].splice(i, 1);
        window[MW_KEY].push(mw);
      }
    }
    queueMicrotask(reapply);
    window.addEventListener("pageshow", reapply, { passive: true });
    document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") reapply(); }, { passive: true });
    window.addEventListener("focus", reapply, { passive: true });
  } catch { }
}
function uninstallFetch() {
  if (!__installed) return;
  try {
    const s = window[MW_KEY];
    const i = Array.isArray(s) ? s.findIndex((m) => m && m[ID]) : -1;
    if (i >= 0) s.splice(i, 1);
    const others = Array.isArray(window[MW_KEY]) && window[MW_KEY].length;
    const orig = window[ORIG_KEY];
    if (!others) {
      if (orig) {
        try { Object.defineProperty(window, "fetch", orig); }
        catch { Object.defineProperty(window, "fetch", { configurable: true, enumerable: true, writable: true, value: window[BASE_KEY] || globalThis.fetch }); }
      } else {
        Object.defineProperty(window, "fetch", { configurable: true, enumerable: true, writable: true, value: window[BASE_KEY] || globalThis.fetch });
      }
    } else {
      reapply();
    }
  } catch { }
  __installed = false;
}
const setupFetch = () => { if (!S.active) { installFetch(); S.active = true; } };
const restoreFetch = () => { if (S.active) { uninstallFetch(); S.active = false; } };
const updateFetchState = () => { const st = getSettings(), need = (st.preview.enabled || st.recorded.enabled); if (need && !S.active) setupFetch(); if (!need && S.active) restoreFetch(); };

async function interceptPreview(url, options) {
  const body = await safeReadBodyFromInput(url, options);
  const data = safeJson(body) || {};
  const userInput = extractUser(data?.messages || []);
  const ctx = getContext();

  if (S.isLong) {
    const chat = Array.isArray(ctx.chat) ? ctx.chat : [];
    let start = chat.length;
    if (chat.length > 0 && chat[chat.length - 1]?.is_user === true) start = chat.length - 1;
    S.chatLenBefore = start;
    S.pendingPurge = true;
    oneShotOnLast(event_types.GENERATION_ENDED, () => setTimeout(() => purgePreviewArtifacts(), 0));
  }

  S.previewData = { url, method: options?.method || "POST", requestData: data, messages: data?.messages || [], model: data?.model || "Unknown", timestamp: now(), userInput, isPreview: true };
  if (S.isLong) { setTimeout(() => { displayPreview(S.previewData); }, 100); } else if (S.resolve) { S.resolve({ success: true, data: S.previewData }); S.resolve = S.reject = null; }
  const payload = S.isLong ? { choices: [{ message: { content: "【小白X】已拦截消息" }, finish_reason: "stop" }], intercepted: true } : { choices: [{ message: { content: "" }, finish_reason: "stop" }] };
  return new Response(JSON.stringify(payload), { status: 200, headers: { "Content-Type": "application/json" } });
}

const addHistoryButtonsDebounced = debounce(() => {
  const set = getSettings(); if (!set.recorded.enabled || !geEnabled()) return;
  $(".mes_history_preview").remove();
  $("#chat .mes").each(function () {
    const id = parseInt($(this).attr("mesid")), isUser = $(this).attr("is_user") === "true";
    if (id <= 0 || isUser) return;
    const btn = $(`<div class="mes_btn mes_history_preview" title="查看历史API请求"><i class="fa-regular fa-note-sticky"></i></div>`).on("click", (e) => { e.preventDefault(); e.stopPropagation(); showHistoryPreview(id); });
    if (window.registerButtonToSubContainer && window.registerButtonToSubContainer(id, btn[0])) return;
    $(this).find(".flex-container.flex1.alignitemscenter").append(btn);
  });
}, C.DEBOUNCE);

const disableSend = (dis = true) => {
  const $b = $q("#send_but");
  if (dis) { S.sendBtnWasDisabled = $b.prop("disabled"); $b.prop("disabled", true).off("click.preview-block").on("click.preview-block", (e) => { e.preventDefault(); e.stopImmediatePropagation(); return false; }); }
  else { $b.prop("disabled", S.sendBtnWasDisabled).off("click.preview-block"); S.sendBtnWasDisabled = false; }
};
const triggerSend = () => {
  const $b = $q("#send_but"), $t = $q("#send_textarea"), txt = String($t.val() || ""); if (!txt.trim()) return false;
  const was = $b.prop("disabled"); $b.prop("disabled", false); $b[0].dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window })); if (was) $b.prop("disabled", true); return true;
};

async function showPreview() {
  let toast = null, backup = null;
  try {
    const set = getSettings(); if (!set.preview.enabled || !geEnabled()) return toastr.warning("消息拦截功能未启用");
    const text = String($q("#send_textarea").val() || "").trim(); if (!text) return toastr.error("请先输入消息内容");

    backup = text; disableSend(true);
    const ctx = getContext();
    S.chatLenBefore = Array.isArray(ctx.chat) ? ctx.chat.length : 0;
    S.isPreview = true; S.previewData = null; S.previewIds.clear(); S.previewAbort = new AbortController();
    S.pendingPurge = true;

    const endHandler = () => {
      try { if (S.genEndedOff) { S.genEndedOff(); S.genEndedOff = null; } } catch { }
      if (S.pendingPurge) {
        setTimeout(() => purgePreviewArtifacts(), 0);
      }
    };

    S.genEndedOff = oneShotOnLast(event_types.GENERATION_ENDED, endHandler);
    clearTimeout(S.cleanupFallback);
    S.cleanupFallback = setTimeout(() => {
      try { if (S.genEndedOff) { S.genEndedOff(); S.genEndedOff = null; } } catch { }
      purgePreviewArtifacts();
    }, 1500);

    toast = toastr.info(`正在拦截请求...（${set.preview.timeoutSeconds}秒超时）`, "消息拦截", { timeOut: 0, tapToDismiss: false });

    if (!triggerSend()) throw new Error("无法触发发送事件");

    const res = await waitIntercept().catch((e) => ({ success: false, error: e?.message || e }));
    if (toast) { toastr.clear(toast); toast = null; }
    if (res.success) { await displayPreview(res.data); toastr.success("拦截成功！", "", { timeOut: 3000 }); }
    else toastr.error(`拦截失败: ${res.error}`, "", { timeOut: 5000 });
  } catch (e) {
    if (toast) toastr.clear(toast); toastr.error(`拦截异常: ${e.message}`, "", { timeOut: 5000 });
  } finally {
    try { S.previewAbort?.abort("拦截结束"); } catch { } S.previewAbort = null;
    if (S.resolve) S.resolve({ success: false, error: "拦截已取消" }); S.resolve = S.reject = null;
    clearTimeout(S.cleanupFallback); S.cleanupFallback = null;
    S.isPreview = false; S.previewData = null;
    disableSend(false); if (backup) $q("#send_textarea").val(backup);
  }
}

async function showHistoryPreview(messageId) {
  try {
    const set = getSettings(); if (!set.recorded.enabled || !geEnabled()) return;
    const rec = findRec(messageId);
    if (rec?.messages?.length || rec?.requestData?.messages?.length) await openPopup(buildPreviewHtml({ ...rec, isHistoryPreview: true, targetMessageId: messageId }), `消息历史查看 - 第 ${messageId + 1} 条消息`);
    else toastr.warning(`未找到第 ${messageId + 1} 条消息的API请求记录`);
  } catch { toastr.error("查看历史消息失败"); }
}

const cleanupMemory = () => {
  if (S.history.length > C.MAX_HISTORY) S.history = S.history.slice(0, C.MAX_HISTORY);
  S.previewIds.clear(); S.previewData = null; $(".mes_history_preview").each(function () { if (!$(this).closest(".mes").length) $(this).remove(); });
  if (!S.isLong) S.interceptedIds = [];
};

function onLast(ev, handler) {
  if (typeof eventSource.makeLast === "function") { eventSource.makeLast(ev, handler); S.listeners.push({ e: ev, h: handler, off: () => { } }); return; }
  if (S.tailAPI?.onLast) { const off = S.tailAPI.onLast(ev, handler); S.listeners.push({ e: ev, h: handler, off }); return; }
  const tail = (...args) => queueMicrotask(() => { try { handler(...args); } catch { } });
  eventSource.on(ev, tail);
  S.listeners.push({ e: ev, h: tail, off: () => eventSource.removeListener?.(ev, tail) });
}

const addEvents = () => {
  removeEvents();
  [
    { e: event_types.MESSAGE_RECEIVED, h: addHistoryButtonsDebounced },
    { e: event_types.CHARACTER_MESSAGE_RENDERED, h: addHistoryButtonsDebounced },
    { e: event_types.USER_MESSAGE_RENDERED, h: addHistoryButtonsDebounced },
    { e: event_types.CHAT_CHANGED, h: () => { S.history = []; setTimeout(addHistoryButtonsDebounced, C.CHECK); } },
    { e: event_types.MESSAGE_RECEIVED, h: (messageId) => setTimeout(() => { const r = S.history.find((x) => !x.associatedMessageId && now() - x.timestamp < C.REQ_WINDOW); if (r) r.associatedMessageId = messageId; }, 100) },
  ].forEach(({ e, h }) => onLast(e, h));
  const late = (payload) => {
    try {
      const ctx = getContext();
      pushHistory({
        url: C.TARGET, method: "POST", requestData: payload, messages: payload?.messages || [], model: payload?.model || "Unknown",
        timestamp: now(), messageId: ctx.chat?.length || 0, characterName: ctx.characters?.[ctx.characterId]?.name || "Unknown",
        userInput: extractUser(payload?.messages || []), isRealRequest: true, source: "settings_ready",
      });
    } catch { }
    queueMicrotask(() => updateFetchState());
  };
  if (typeof eventSource.makeLast === "function") { eventSource.makeLast(event_types.CHAT_COMPLETION_SETTINGS_READY, late); S.listeners.push({ e: event_types.CHAT_COMPLETION_SETTINGS_READY, h: late, off: () => { } }); }
  else if (S.tailAPI?.onLast) { const off = S.tailAPI.onLast(event_types.CHAT_COMPLETION_SETTINGS_READY, late); S.listeners.push({ e: event_types.CHAT_COMPLETION_SETTINGS_READY, h: late, off }); }
  else { ON(event_types.CHAT_COMPLETION_SETTINGS_READY, late); S.listeners.push({ e: event_types.CHAT_COMPLETION_SETTINGS_READY, h: late, off: () => OFF(event_types.CHAT_COMPLETION_SETTINGS_READY, late) }); queueMicrotask(() => { try { OFF(event_types.CHAT_COMPLETION_SETTINGS_READY, late); } catch { } try { ON(event_types.CHAT_COMPLETION_SETTINGS_READY, late); } catch { } }); }
};
const removeEvents = () => { S.listeners.forEach(({ e, h, off }) => { if (typeof off === "function") { try { off(); } catch { } } else { try { OFF(e, h); } catch { } } }); S.listeners = []; };

const toggleLong = () => {
  S.isLong = !S.isLong;
  const $b = $q("#message_preview_btn");
  if (S.isLong) {
    $b.css("color", "red");
    toastr.info("持续拦截已开启", "", { timeOut: 2000 });
  } else {
    $b.css("color", "");
    S.pendingPurge = false;
    toastr.info("持续拦截已关闭", "", { timeOut: 2000 });
  }
};
const bindBtn = () => {
  const $b = $q("#message_preview_btn");
  $b.on("mousedown touchstart", () => { S.longPressTimer = setTimeout(() => toggleLong(), S.longPressDelay); });
  $b.on("mouseup touchend mouseleave", () => { if (S.longPressTimer) { clearTimeout(S.longPressTimer); S.longPressTimer = null; } });
  $b.on("click", () => { if (S.longPressTimer) { clearTimeout(S.longPressTimer); S.longPressTimer = null; return; } if (!S.isLong) showPreview(); });
};

const waitIntercept = () => new Promise((resolve, reject) => {
  const t = setTimeout(() => { if (S.resolve) { S.resolve({ success: false, error: `等待超时 (${getSettings().preview.timeoutSeconds}秒)` }); S.resolve = S.reject = null; } }, getSettings().preview.timeoutSeconds * 1000);
  S.resolve = (v) => { clearTimeout(t); resolve(v); }; S.reject = (e) => { clearTimeout(t); reject(e); };
});

function cleanup() {
  removeEvents(); restoreFetch(); disableSend(false);
  $(".mes_history_preview").remove(); $("#message_preview_btn").remove(); cleanupMemory();
  Object.assign(S, { resolve: null, reject: null, isPreview: false, isLong: false, interceptedIds: [], chatLenBefore: 0, sendBtnWasDisabled: false, pendingPurge: false });
  if (S.longPressTimer) { clearTimeout(S.longPressTimer); S.longPressTimer = null; }
  if (S.restoreLong) { try { S.restoreLong(); } catch { } S.restoreLong = null; }
  if (S.genEndedOff) { try { S.genEndedOff(); } catch { } S.genEndedOff = null; }
  if (S.cleanupFallback) { clearTimeout(S.cleanupFallback); S.cleanupFallback = null; }
}

function initMessagePreview() {
  try {
    cleanup(); S.tailAPI = installEventSourceTail(eventSource);
    const set = getSettings();
    const btn = $(`<div id="message_preview_btn" class="fa-regular fa-note-sticky interactable" title="预览消息"></div>`);
    $("#send_but").before(btn); bindBtn();
    $("#xiaobaix_preview_enabled").prop("checked", set.preview.enabled).on("change", function () {
      if (!geEnabled()) return; set.preview.enabled = $(this).prop("checked"); saveSettingsDebounced();
      $("#message_preview_btn").toggle(set.preview.enabled);
      if (set.preview.enabled) { if (!S.cleanTimer) S.cleanTimer = setInterval(cleanupMemory, C.CLEAN); }
      else { if (S.cleanTimer) { clearInterval(S.cleanTimer); S.cleanTimer = null; } }
      updateFetchState();
      if (!set.preview.enabled && set.recorded.enabled) { addEvents(); addHistoryButtonsDebounced(); }
    });
    $("#xiaobaix_recorded_enabled").prop("checked", set.recorded.enabled).on("change", function () {
      if (!geEnabled()) return; set.recorded.enabled = $(this).prop("checked"); saveSettingsDebounced();
      if (set.recorded.enabled) { addEvents(); addHistoryButtonsDebounced(); }
      else { $(".mes_history_preview").remove(); S.history.length = 0; if (!set.preview.enabled) removeEvents(); }
      updateFetchState();
    });
    if (!set.preview.enabled) $("#message_preview_btn").hide();
    updateFetchState(); if (set.recorded.enabled) addHistoryButtonsDebounced();
    if (set.preview.enabled || set.recorded.enabled) addEvents();
    if (window.registerModuleCleanup) window.registerModuleCleanup("messagePreview", cleanup);
    if (set.preview.enabled) S.cleanTimer = setInterval(cleanupMemory, C.CLEAN);
  } catch { toastr.error("模块初始化失败"); }
}

window.addEventListener("beforeunload", cleanup);
window.messagePreviewCleanup = cleanup;

export { initMessagePreview, addHistoryButtonsDebounced, cleanup };
