/**
 * 评论区屏蔽（会员）：讨厌的一级 / Emoji 标签，刷新后生效。
 */
(function () {
  "use strict";

  if (window.self !== window.top) return;
  if (typeof BiliDom === "undefined" || typeof BiliIndex === "undefined") {
    console.warn("[BiliIndex] comment-block: 缺少 bili-dom / biliindex-core");
    return;
  }

  const BLOCK_MSG = "已根据规则屏蔽，如需查看请双击";
  const PASSERBY_CODE = "passerby";
  const BLOCK_CSS_URL = "src/content/comment-block.css";
  /** 仅主评论参与屏蔽；回复评论不检查标签 */
  const MAIN_COMMENT_SEL = "bili-comment-renderer";
  const REPLY_COMMENT_SEL = "bili-comment-reply-renderer";
  const THREAD_SEL = "bili-comment-thread-renderer";
  const THREAD_REPLY_EXTRA_SEL = [
    "bili-comment-replybox-renderer",
    ".reply-list",
    ".sub-reply-list",
    ".reply-container",
    ".view-more-reply",
    ".reply-wrap",
  ].join(", ");
  const COMMENT_ROOT_SELECTORS = [
    "bili-comments",
    "bili-comment-area",
    "#comment-app",
    "#comment-module",
    "#comment",
    ".bb-comment",
  ];
  const isCommentShadowHost = BiliDom.isCommentShadowHost;
  const getShadowRoot = BiliDom.getCommentShadowRoot;
  const queryAllDeep = (root, selector) => BiliDom.queryAllDeep(root, selector);
  const isReplyComment = BiliDom.isReplyComment;
  const resolveCommentTarget = BiliDom.resolveCommentTarget;


  let blockCodes = [];
  let blockReady = false;
  const DISPLAY_CACHE_MAX = 384;
  const displayCache =
    typeof BiliIndex.createLruMap === "function"
      ? BiliIndex.createLruMap(DISPLAY_CACHE_MAX)
      : new Map();
  let catchUpTimer = null;
  let catchUpPass = 0;
  const CATCH_UP_MAX = 8;
  const styledShadowRoots = new WeakSet();
  const observedShadowHosts = new WeakSet();
  let commentItemObserver = null;
  const observedCommentItems = new WeakSet();
  const offscreenBlockQueue = [];
  let offscreenBlockIdleScheduled = false;
  const BLOCK_OFFSCREEN_BATCH = 8;

  function injectBlockStylesIntoShadow(shadowRoot) {
    if (!shadowRoot || styledShadowRoots.has(shadowRoot)) return;
    styledShadowRoots.add(shadowRoot);
    try {
      if (shadowRoot.querySelector("link[data-biliindex-block-css]")) return;
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.setAttribute("data-biliindex-block-css", "1");
      link.href = chrome.runtime.getURL(BLOCK_CSS_URL);
      shadowRoot.prepend(link);
    } catch {
      /* ignore */
    }
  }

  function attachShadowObservers() {
    for (const sel of ["bili-comments", "bili-comment-area", "#comment-app"]) {
      document.querySelectorAll(sel).forEach((host) => {
        const shadow = getShadowRoot(host);
        if (!shadow || observedShadowHosts.has(host)) return;
        observedShadowHosts.add(host);
        injectBlockStylesIntoShadow(shadow);
        new MutationObserver(() => {
          if (BiliIndex.isCommentsSectionLoading?.()) return;
          scheduleScan();
        }).observe(shadow, {
          childList: true,
          subtree: true,
        });
      });
    }
  }

  function resolveUpMid(item) {
    return BiliDom.resolveCommentUpMid(item, queryAllDeep);
  }

  function iconToEmojiCode(icon) {
    if (!icon || typeof BiliIndex === "undefined") return null;
    const map = BiliIndex.EMOJI_ICON_BY_CODE || {};
    for (const [code, ic] of Object.entries(map)) {
      if (ic === icon) return code;
    }
    return null;
  }

  function barHasFilledBlockSlots(bar) {
    if (!(bar instanceof HTMLElement)) return false;
    if (bar.dataset.biliindexTag1Code || bar.dataset.biliindexEmojiCode) return true;
    return !!bar.querySelector(
      '.biliindex-slot-filled[data-slot="tag1"], .biliindex-slot-filled[data-slot="emoji"]'
    );
  }

  function needsRefetchDisplay(upMid, bar) {
    if (!displayCache.has(upMid)) return true;
    const d = displayCache.get(upMid);
    if (d?._fetchFailed) return true;
    if (shouldBlockDisplay(d) || shouldBlockFromBar(bar)) return false;
    return barHasFilledBlockSlots(bar);
  }

  const BLOCK_HOST_STYLE = `
:host(.biliindex-comment-blocked) {
  display: block !important;
  height: 15px !important;
  min-height: 15px !important;
  max-height: 15px !important;
  overflow: hidden !important;
  position: relative !important;
}
`;

  function codesMatchBlock(codes) {
    if (!blockReady || !blockCodes.length || !codes?.length) return false;
    const set = new Set(blockCodes);
    return codes.some((c) => c && set.has(c));
  }

  function shouldBlockDisplay(display) {
    if (!display) return false;
    const tag1 = display.slots?.[0];
    const emoji = display.emoji;
    return codesMatchBlock([tag1?.code, emoji?.code].filter(Boolean));
  }

  function findBarForTarget(target) {
    const bars = queryAllDeep(target, ".biliindex-bar[data-up-mid]");
    return bars[0] instanceof HTMLElement ? bars[0] : null;
  }

  function codesFromBar(bar) {
    if (!(bar instanceof HTMLElement)) return [];
    const codes = new Set();
    if (bar.dataset.biliindexTag1Code) codes.add(bar.dataset.biliindexTag1Code);
    if (bar.dataset.biliindexEmojiCode) codes.add(bar.dataset.biliindexEmojiCode);
    bar.querySelectorAll(".biliindex-slot-filled").forEach((el) => {
      const slot = el.dataset.slot;
      if (slot !== "tag1" && slot !== "emoji") return;
      if (el.dataset.tagCode) codes.add(el.dataset.tagCode);
      if (slot === "emoji") {
        const icon = el.querySelector(".biliindex-slot-label")?.textContent?.trim();
        const fromIcon = iconToEmojiCode(icon);
        if (fromIcon) codes.add(fromIcon);
      }
    });
    return [...codes];
  }

  function shouldBlockFromBar(bar) {
    return codesMatchBlock(codesFromBar(bar));
  }

  function shouldBlockTarget(target, display, bar) {
    return shouldBlockDisplay(display) || shouldBlockFromBar(bar);
  }

  function injectBlockHostStyles(shadowRoot) {
    if (!shadowRoot || shadowRoot.querySelector("style[data-biliindex-block-host]")) return;
    const style = document.createElement("style");
    style.setAttribute("data-biliindex-block-host", "1");
    style.textContent = BLOCK_HOST_STYLE;
    shadowRoot.prepend(style);
  }

  async function loadBlockRules() {
    blockCodes = [];
    blockReady = false;
    displayCache.clear();
    catchUpPass = 0;
    try {
      const stored = await chrome.storage.local.get(["blockTagCodes", "blockIsMember"]);
      if (stored.blockIsMember && Array.isArray(stored.blockTagCodes) && stored.blockTagCodes.length) {
        blockCodes = stored.blockTagCodes.filter((c) => c && c !== PASSERBY_CODE);
        blockReady = blockCodes.length > 0;
        if (blockReady) {
          console.info("[BiliIndex] 屏蔽规则已加载", blockCodes);
          return;
        }
      }
      if (typeof BiliIndex === "undefined" || !BiliIndex.getToken) return;
      const token = await BiliIndex.getToken();
      if (!token) return;
      const data = await BiliIndex.apiFetch("/api/user/block-tags");
      if (!data?.is_member || !data?.codes?.length) {
        await chrome.storage.local.set({
          blockTagCodes: [],
          blockIsMember: !!data?.is_member,
        });
        return;
      }
      blockCodes = data.codes.filter((c) => c !== PASSERBY_CODE);
      blockReady = blockCodes.length > 0;
      await chrome.storage.local.set({
        blockTagCodes: blockCodes,
        blockIsMember: true,
      });
      if (blockReady) console.info("[BiliIndex] 屏蔽规则已从 API 加载", blockCodes);
    } catch (e) {
      console.warn("[BiliIndex] 屏蔽规则加载失败", e);
    }
  }

  function touchDisplayCache(mid, row) {
    displayCache.set(String(mid), { ...row, _at: Date.now() });
  }

  function isCommentTargetNearViewport(target) {
    if (!(target instanceof Element)) return false;
    try {
      const r = target.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight || 800;
      return r.bottom >= -320 && r.top <= vh + 320;
    } catch {
      return false;
    }
  }

  function ensureCommentItemObserver() {
    if (commentItemObserver) return;
    try {
      commentItemObserver = new IntersectionObserver(
        (entries) => {
          const batch = [];
          for (const entry of entries) {
            if (!entry.isIntersecting) continue;
            const item = entry.target;
            if (!(item instanceof HTMLElement) || isReplyComment(item)) continue;
            batch.push(item);
          }
          if (batch.length && blockReady) {
            void processCommentItems(batch);
          }
        },
        { root: null, rootMargin: "300px 0px", threshold: 0 }
      );
    } catch {
      commentItemObserver = null;
    }
  }

  function watchCommentItemForBlock(item) {
    if (!(item instanceof HTMLElement) || isReplyComment(item)) return;
    ensureCommentItemObserver();
    if (!commentItemObserver || observedCommentItems.has(item)) return;
    try {
      observedCommentItems.add(item);
      commentItemObserver.observe(item);
    } catch {
      /* ignore */
    }
  }

  function scheduleOffscreenBlockScan(items) {
    if (items?.length) offscreenBlockQueue.push(...items);
    if (!offscreenBlockQueue.length || offscreenBlockIdleScheduled) return;
    offscreenBlockIdleScheduled = true;
    const run = () => {
      offscreenBlockIdleScheduled = false;
      const batch = offscreenBlockQueue.splice(0, BLOCK_OFFSCREEN_BATCH);
      if (batch.length && blockReady) void processCommentItems(batch);
      if (offscreenBlockQueue.length) scheduleOffscreenBlockScan();
    };
    if (typeof BiliIndex.runWhenIdle === "function") {
      BiliIndex.runWhenIdle(run, { timeout: 1000 });
    } else if (typeof requestIdleCallback === "function") {
      requestIdleCallback(run, { timeout: 1000 });
    } else {
      setTimeout(run, 64);
    }
  }

  async function fetchDisplaysBatch(mids) {
    if (BiliIndex.isExtensionContextInvalidated?.()) {
      BiliIndex.markExtensionContextInvalidated?.();
      return;
    }
    const need = mids.filter((m) => !displayCache.has(m));
    if (!need.length) return;
    if (typeof BiliIndex === "undefined" || !BiliIndex.fetchPublicJson) {
      console.warn("[BiliIndex] fetchPublicJson 不可用");
      return;
    }
    try {
      const data = await BiliIndex.fetchPublicJson("/api/tag/display-batch", {
        method: "POST",
        body: JSON.stringify({ up_mids: need }),
      });
      const items = data?.items || {};
      for (const mid of need) {
        const row = items[mid] || { slots: [null, null, null], emoji: null };
        touchDisplayCache(mid, { ...row, _fetchFailed: false });
      }
    } catch (e) {
      if (BiliIndex.isContextInvalidatedError?.(e)) {
        BiliIndex.markExtensionContextInvalidated?.();
        return;
      }
      const errMsg = String(e?.message || e);
      const isTimeout = /超时|timeout/i.test(errMsg);
      if (!isTimeout) {
        console.warn("[BiliIndex] 批量标签查询失败", errMsg);
      }
      for (const mid of need) {
        touchDisplayCache(mid, {
          slots: [null, null, null],
          emoji: null,
          _fetchFailed: true,
        });
      }
    }
  }

  function restoreHiddenNodes(root) {
    root.querySelectorAll("[data-biliindex-block-hide]").forEach((el) => {
      el.style.removeProperty("display");
      el.removeAttribute("data-biliindex-block-hide");
    });
  }

  function hideThreadNode(el) {
    if (!(el instanceof HTMLElement)) return;
    if (el.hasAttribute("data-biliindex-thread-hide")) return;
    el.setAttribute("data-biliindex-thread-hide", "1");
    el.style.setProperty("display", "none", "important");
  }

  function findCommentThread(mainRenderer) {
    if (!(mainRenderer instanceof Element)) return null;
    const thread = mainRenderer.closest?.(THREAD_SEL);
    return thread instanceof HTMLElement ? thread : null;
  }

  /** 主评论被屏蔽时，折叠同楼全部子评论与「查看回复」等区域 */
  function hideThreadReplies(mainRenderer) {
    if (!(mainRenderer instanceof HTMLElement)) return;
    const thread = findCommentThread(mainRenderer);

    const hideInScope = (scope) => {
      if (!(scope instanceof Element)) return;
      for (const child of scope.children) {
        if (child === mainRenderer) continue;
        if (child.contains?.(mainRenderer)) continue;
        hideThreadNode(child);
      }
      queryAllDeep(scope, REPLY_COMMENT_SEL).forEach((el) => {
        if (el === mainRenderer || mainRenderer.contains(el)) return;
        hideThreadNode(el);
      });
      if (THREAD_REPLY_EXTRA_SEL) {
        queryAllDeep(scope, THREAD_REPLY_EXTRA_SEL).forEach((el) => {
          if (mainRenderer.contains(el)) return;
          hideThreadNode(el);
        });
      }
    };

    if (thread) {
      hideInScope(thread);
      const tShadow = getShadowRoot(thread);
      if (tShadow) hideInScope(tShadow);
    }

    let sib = mainRenderer.nextElementSibling;
    while (sib) {
      hideThreadNode(sib);
      sib = sib.nextElementSibling;
    }
    const parent = mainRenderer.parentElement;
    if (parent) {
      const pShadow = getShadowRoot(parent);
      if (pShadow) {
        let seenMain = false;
        for (const child of pShadow.children) {
          if (child === mainRenderer || child.contains?.(mainRenderer)) {
            seenMain = true;
            continue;
          }
          if (seenMain) hideThreadNode(child);
        }
      }
    }

    mainRenderer.dataset.biliindexThreadCollapsed = "1";
    if (thread) thread.dataset.biliindexThreadCollapsed = "1";
  }

  function showThreadReplies(mainRenderer) {
    if (!(mainRenderer instanceof HTMLElement)) return;
    const thread = findCommentThread(mainRenderer);
    const scopes = [thread, mainRenderer.parentElement, document.body].filter(Boolean);
    for (const scope of scopes) {
      scope.querySelectorAll?.("[data-biliindex-thread-hide]").forEach((el) => {
        el.style.removeProperty("display");
        el.removeAttribute("data-biliindex-thread-hide");
      });
      const sr = scope instanceof Element ? getShadowRoot(scope) : null;
      sr?.querySelectorAll("[data-biliindex-thread-hide]").forEach((el) => {
        el.style.removeProperty("display");
        el.removeAttribute("data-biliindex-thread-hide");
      });
    }
    delete mainRenderer.dataset.biliindexThreadCollapsed;
    if (thread) delete thread.dataset.biliindexThreadCollapsed;
  }

  function hideShadowContent(shadow) {
    for (const child of shadow.children) {
      if (child.classList?.contains("biliindex-block-overlay")) continue;
      if (child.hasAttribute("data-biliindex-block-hide")) continue;
      child.setAttribute("data-biliindex-block-hide", "1");
      child.style.setProperty("display", "none", "important");
    }
  }

  function ensureShadowOverlay(renderer, shadow) {
    let overlay = shadow.querySelector(".biliindex-block-overlay");
    if (overlay) return overlay;
    overlay = document.createElement("div");
    overlay.className = "biliindex-block-overlay";
    const banner = document.createElement("div");
    banner.className = "biliindex-block-banner";
    banner.textContent = BLOCK_MSG;
    banner.title = BLOCK_MSG;
    overlay.appendChild(banner);
    shadow.appendChild(overlay);
    const onExpand = (e) => {
      e.preventDefault();
      e.stopPropagation();
      expandBlockedComment(renderer);
    };
    overlay.addEventListener("dblclick", onExpand);
    banner.addEventListener("dblclick", onExpand);
    return overlay;
  }

  function ensureLightBanner(target) {
    let banner = target.querySelector(":scope > .biliindex-block-banner");
    if (!banner) {
      banner = document.createElement("div");
      banner.className = "biliindex-block-banner";
      banner.textContent = BLOCK_MSG;
      banner.title = BLOCK_MSG;
      target.appendChild(banner);
      banner.addEventListener("dblclick", (e) => {
        e.preventDefault();
        e.stopPropagation();
        expandBlockedComment(target);
      });
    }
    return banner;
  }

  function expandBlockedComment(target) {
    target.classList.remove("biliindex-comment-blocked");
    target.classList.add("biliindex-block-expanded");
    target.dataset.biliindexBlockExpanded = "1";
    target.dataset.biliindexBlocked = "0";
    showThreadReplies(target);
    const shadow = getShadowRoot(target);
    if (shadow) {
      shadow.querySelector(".biliindex-block-overlay")?.remove();
      restoreHiddenNodes(shadow);
      return;
    }
    target.querySelector(".biliindex-block-banner")?.remove();
    restoreHiddenNodes(target);
  }

  function unblockTarget(target) {
    if (target.dataset.biliindexBlocked !== "1") return;
    target.classList.remove("biliindex-comment-blocked");
    target.dataset.biliindexBlocked = "0";
    showThreadReplies(target);
    const shadow = getShadowRoot(target);
    if (shadow) {
      shadow.querySelector(".biliindex-block-overlay")?.remove();
      restoreHiddenNodes(shadow);
      return;
    }
    target.querySelector(".biliindex-block-banner")?.remove();
    restoreHiddenNodes(target);
  }

  function applyBlockToItem(item) {
    if (isReplyComment(item)) return;
    const target = resolveCommentTarget(item);
    if (!target) return;
    if (target.dataset.biliindexBlockExpanded === "1") return;
    if (target.closest?.("bili-comment-box, bili-comment-input, .comment-send, .comment-input-wrap")) {
      return;
    }

    const upMid = resolveUpMid(target);
    if (!upMid) return;

    const display = displayCache.get(upMid);
    const bar = findBarForTarget(target);

    if (!shouldBlockTarget(target, display, bar)) {
      delete target.dataset.biliindexBlockPending;
      unblockTarget(target);
      showThreadReplies(target);
      if (barHasFilledBlockSlots(bar) && catchUpPass < CATCH_UP_MAX) {
        target.dataset.biliindexBlockPending = "1";
      }
      return;
    }

    delete target.dataset.biliindexBlockPending;

    if (target.dataset.biliindexBlocked === "1") {
      if (target.dataset.biliindexThreadCollapsed !== "1") hideThreadReplies(target);
      return;
    }
    target.dataset.biliindexBlocked = "1";
    target.classList.add("biliindex-comment-blocked");
    hideThreadReplies(target);

    const shadow = getShadowRoot(target);
    if (shadow) {
      injectBlockStylesIntoShadow(shadow);
      injectBlockHostStyles(shadow);
      hideShadowContent(shadow);
      ensureShadowOverlay(target, shadow);
      if (!target.dataset.biliindexBlockDblBound) {
        target.dataset.biliindexBlockDblBound = "1";
        target.addEventListener("dblclick", (e) => {
          e.preventDefault();
          e.stopPropagation();
          expandBlockedComment(target);
        });
      }
      return;
    }

    for (const child of target.children) {
      if (child.classList?.contains("biliindex-block-banner")) continue;
      child.setAttribute("data-biliindex-block-hide", "1");
      child.style.display = "none";
    }
    ensureLightBanner(target);
    if (!target.dataset.biliindexBlockDblBound) {
      target.dataset.biliindexBlockDblBound = "1";
      target.addEventListener("dblclick", (e) => {
        e.preventDefault();
        e.stopPropagation();
        expandBlockedComment(target);
      });
    }
  }

  async function processCommentItems(items) {
    if (!blockReady) return;
    if (BiliIndex.isExtensionContextInvalidated?.()) return;
    const mids = [];
    const itemByMid = new Map();
    for (const item of items) {
      if (isReplyComment(item)) continue;
      const target = resolveCommentTarget(item);
      if (!target || target.dataset.biliindexBlockExpanded === "1") continue;
      const upMid = resolveUpMid(item);
      if (!upMid) continue;
      const bar = findBarForTarget(target);
      if (needsRefetchDisplay(upMid, bar)) mids.push(upMid);
      if (!itemByMid.has(upMid)) itemByMid.set(upMid, []);
      itemByMid.get(upMid).push(target);
    }
    const uniqueMids = [...new Set(mids)];
    if (uniqueMids.length) await fetchDisplaysBatch(uniqueMids);
    for (const [, list] of itemByMid) {
      for (const item of list) applyBlockToItem(item);
    }
  }

  function syncThreadRepliesVisibility() {
    const mains = BiliIndex.queryInCommentHosts
      ? BiliIndex.queryInCommentHosts(MAIN_COMMENT_SEL)
      : queryAllDeep(document.body, MAIN_COMMENT_SEL);
    mains.forEach((main) => {
      if (!(main instanceof HTMLElement)) return;
      const blocked =
        main.dataset.biliindexBlocked === "1" &&
        main.dataset.biliindexBlockExpanded !== "1";
      if (blocked) hideThreadReplies(main);
      else if (main.dataset.biliindexThreadCollapsed === "1") showThreadReplies(main);
    });
  }

  function scanComments() {
    if (!blockReady) return;
    if (BiliIndex.isCommentsSectionLoading?.()) {
      clearTimeout(scanTimer);
      scanTimer = setTimeout(scanComments, 800);
      return;
    }
    attachShadowObservers();
    const items = BiliIndex.queryInCommentHosts
      ? BiliIndex.queryInCommentHosts(MAIN_COMMENT_SEL)
      : queryAllDeep(document.body, MAIN_COMMENT_SEL);
    if (items.length) {
      const viewport = [];
      const offscreen = [];
      for (const item of items) {
        watchCommentItemForBlock(item);
        const target = resolveCommentTarget(item);
        if (target && isCommentTargetNearViewport(target)) viewport.push(item);
        else offscreen.push(item);
      }
      const work = viewport.length
        ? processCommentItems(viewport).then(() => syncThreadRepliesVisibility())
        : Promise.resolve().then(() => syncThreadRepliesVisibility());
      void work;
      if (offscreen.length) scheduleOffscreenBlockScan(offscreen);
    } else {
      syncThreadRepliesVisibility();
    }
  }

  let scanTimer = null;
  function scheduleScan() {
    if (BiliIndex.isExtensionContextInvalidated?.()) return;
    clearTimeout(scanTimer);
    scanTimer = setTimeout(scanComments, 280);
  }

  function scheduleCatchUpScans() {
    if (BiliIndex.isExtensionContextInvalidated?.()) return;
    if (catchUpPass >= CATCH_UP_MAX) return;
    clearTimeout(catchUpTimer);
    catchUpTimer = setTimeout(() => {
      catchUpPass += 1;
      scanComments();
      const pending = document.querySelectorAll(
        `${MAIN_COMMENT_SEL}[data-biliindex-block-pending="1"]`
      ).length;
      if (pending > 0 || catchUpPass < 4) scheduleCatchUpScans();
    }, 900);
  }

  document.addEventListener("biliindex:up-target", () => scheduleScan());
  document.addEventListener("biliindex:bar-refreshed", () => scheduleScan());
  document.addEventListener("biliindex:comments-scanned", () => {
    scheduleScan();
    scheduleCatchUpScans();
  });

  chrome.storage.onChanged.addListener((changes, area) => {
    if (BiliIndex.isExtensionContextInvalidated?.()) return;
    if (area !== "local") return;
    if (changes.blockTagCodes || changes.blockIsMember) {
      void loadBlockRules().then(() => scheduleScan());
    }
  });

  async function init() {
    if (typeof BiliIndex === "undefined") {
      console.warn("[BiliIndex] comment-block: BiliIndex 未加载");
      return;
    }
    await loadBlockRules();
    if (!blockReady) return;
    BiliIndex.whenCommentsReady(() => {
      scheduleScan();
      scheduleCatchUpScans();
    });
    const commentRoot = document.querySelector(
      "bili-comments, #comment-app, #comment-module"
    );
    if (commentRoot) {
      try {
        new IntersectionObserver(
          (entries) => {
            if (entries.some((e) => e.isIntersecting)) {
              scheduleScan();
              scheduleCatchUpScans();
            }
          },
          { rootMargin: "200px 0px", threshold: 0.01 }
        ).observe(commentRoot);
      } catch {
        /* ignore */
      }
    }
    for (const sel of COMMENT_ROOT_SELECTORS) {
      document.querySelectorAll(sel).forEach((host) => {
        const shadow = getShadowRoot(host);
        if (shadow) injectBlockStylesIntoShadow(shadow);
        new MutationObserver(() => {
          if (BiliIndex.isCommentsSectionLoading?.()) return;
          scheduleScan();
        }).observe(host, { childList: true, subtree: false });
      });
    }
    const observedBodyForComments = new WeakSet();
    function hookCommentHosts() {
      attachShadowObservers();
      for (const sel of COMMENT_ROOT_SELECTORS) {
        document.querySelectorAll(sel).forEach((host) => {
          if (observedBodyForComments.has(host)) return;
          observedBodyForComments.add(host);
        });
      }
    }
    new MutationObserver(() => {
      hookCommentHosts();
      if (!BiliIndex.isCommentsSectionLoading?.()) scheduleScan();
    }).observe(document.body, { childList: true, subtree: false });
    hookCommentHosts();
    let hostFinderTicks = 0;
    const hostFinder = setInterval(() => {
      hookCommentHosts();
      hostFinderTicks += 1;
      if (
        document.querySelector("bili-comments, #comment-app") ||
        hostFinderTicks >= 36
      ) {
        clearInterval(hostFinder);
      }
    }, 1200);
  }

  document.addEventListener("biliindex-passerby-marked", (e) => {
    const upMid = e.detail?.up_mid;
    if (upMid) displayCache.delete(String(upMid));
    scheduleScan();
  });

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => void init());
  } else {
    void init();
  }
})();
