/**
 * BiliIndex — 在限定区域内于用户昵称右侧显示 UID。
 * 范围：评论区、推荐视频、播放页 UP 主条、搜索页视频/用户卡片。
 */
(function () {
  "use strict";

  const PROCESSED = "data-bili-uid-done";
  const ITEM_DONE = "data-bili-uid-item-done";
  const BADGE_CLASS = "bili-uid-badge";
  const EXTENSION_MARK = "data-biliindex-ext";

  const COMMENT_ROOT_SELECTORS = [
    "bili-comments",
    "bili-comment-area",
    "#comment-app",
    "#comment-module",
    "#comment",
    ".bb-comment",
    ".comment-list",
    ".reply-wrapper",
    ".reply-list",
  ];

  const RECOMMEND_ROOT_SELECTORS = [
    "#reco_list",
    ".recommend-list-v1",
    ".rcmd-tab",
    ".rcmd-list",
    ".rcmd-box",
    ".rec-list",
    "#i_cecream .bili-feed4-layout",
    "#i_cecream .recommended-container",
  ];

  const UP_PANEL_ROOT_SELECTORS = [
    ".up-panel-container",
    ".up-info-container",
    ".up-info--wrapper",
    ".video-info-detail",
  ];

  const SEARCH_ROOT_SELECTORS = [
    ".search-page",
    ".search-page-wrapper",
    "#all-list",
    ".search-component-result-list",
    ".video-list.bili-grid",
    ".user-list",
    ".search-result-container",
  ];

  const ALL_ROOT_SELECTORS = [
    ...COMMENT_ROOT_SELECTORS,
    ...RECOMMEND_ROOT_SELECTORS,
    ...UP_PANEL_ROOT_SELECTORS,
    ...SEARCH_ROOT_SELECTORS,
  ];

  const COMMENT_NICKNAME_LINK_SELECTORS = [
    "#user-name a[href*='space.bilibili.com']",
    "#user-name a[href*='/space/']",
    ".user-name a[href*='space.bilibili.com']",
    ".user-name a[href*='/space/']",
    "a.name[href*='space.bilibili.com']",
    "a.name[href*='/space/']",
    "bili-comment-user-renderer a[href*='space.bilibili.com']",
    "bili-comment-user-renderer a[href*='/space/']",
    "a[href*='space.bilibili.com']",
    "a[href*='/space/']",
  ];

  const COMMENT_NICKNAME_HOST_SELECTORS = [
    "#user-name",
    ".user-name",
    "bili-comment-user-renderer",
    ".comment-user",
  ];

  const AUTHOR_NICKNAME_LINK_SELECTORS = [
    ".upname a[href*='space.bilibili.com']",
    ".video-page-card-small .upname a[href*='space.bilibili.com']",
    "a.bili-video-card__info--author[href*='space.bilibili.com']",
    ".bili-video-card__info--author a[href*='space.bilibili.com']",
    "a.bili-video-card__info--owner[href*='space.bilibili.com']",
    ".bili-feed-card__author a[href*='space.bilibili.com']",
    ".bili-feed-card__info--author a[href*='space.bilibili.com']",
    ".feed-card .author a[href*='space.bilibili.com']",
    ".bili-video-card .info--author a[href*='space.bilibili.com']",
    ".video-list-card .info a[href*='space.bilibili.com']",
  ];

  const UP_PANEL_LINK_SELECTORS = [
    "a.up-name[href*='space.bilibili.com']",
    ".up-detail-top a[href*='space.bilibili.com']",
  ];

  const SEARCH_USER_LINK_SELECTORS = [
    ".user-item a[href*='space.bilibili.com']",
    ".user-list-item a[href*='space.bilibili.com']",
    ".bili-user-card a.title[href*='space.bilibili.com']",
    ".user-card a[href*='space.bilibili.com']",
  ];

  const SPACE_LINK_PATTERNS = [
    /space\.bilibili\.com\/(\d+)/i,
    /\/space\/(\d+)(?:\/|$|\?|#)/i,
  ];

  function getShadowRoot(el) {
    if (!(el instanceof Element)) return null;
    try {
      if (typeof chrome !== "undefined" && chrome.dom?.openOrClosedShadowRoot) {
        const sr = chrome.dom.openOrClosedShadowRoot(el);
        if (sr) return sr;
      }
    } catch {
      /* ignore */
    }
    try {
      const sr = el.openOrClosedShadowRoot;
      if (sr) return sr;
    } catch {
      /* ignore */
    }
    return el.shadowRoot || null;
  }

  function extractMid(href) {
    if (!href || typeof href !== "string") return null;
    const h = href.trim();
    for (const re of SPACE_LINK_PATTERNS) {
      const m = h.match(re);
      if (m) return m[1];
    }
    return null;
  }

  function matchesAnySelector(el, selectors) {
    for (const sel of selectors) {
      try {
        if (el.matches(sel)) return true;
      } catch {
        /* ignore */
      }
    }
    return false;
  }

  function isExtensionNode(node) {
    if (!(node instanceof Element)) return false;
    if (
      node.classList?.contains(BADGE_CLASS) ||
      node.classList?.contains("biliindex-quick-wrap") ||
      node.classList?.contains("biliindex-tag-menu")
    ) {
      return true;
    }
    return !!node.closest?.(
      `.${BADGE_CLASS}, .biliindex-quick-wrap, .biliindex-tag-menu`
    );
  }

  function isCommentRelated(node) {
    if (!(node instanceof Element)) return false;
    const tag = (node.tagName || "").toLowerCase();
    if (tag.startsWith("bili-comment")) return true;
    return !!node.closest?.(
      "bili-comments, bili-comment-area, bili-comment-renderer, #comment-app, #comment"
    );
  }

  function nodeTouchesTargets(node) {
    if (!(node instanceof Element)) return false;
    if (isExtensionNode(node)) return false;
    if (isCommentRelated(node)) return true;
    if (matchesAnySelector(node, ALL_ROOT_SELECTORS)) return true;
    for (const sel of ALL_ROOT_SELECTORS) {
      try {
        if (node.querySelector?.(sel)) return true;
      } catch {
        /* ignore */
      }
    }
    return !!node.closest?.(ALL_ROOT_SELECTORS.join(", "));
  }

  function collectRoots(selectors) {
    const roots = new Set();
    for (const sel of selectors) {
      try {
        document.querySelectorAll(sel).forEach((el) => roots.add(el));
      } catch {
        /* ignore */
      }
    }
    return [...roots];
  }

  const observedShadowHosts = new WeakSet();

  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);
        new MutationObserver((mutations) => {
          for (const m of mutations) {
            for (const node of m.addedNodes) {
              if (nodeTouchesTargets(node)) {
                scheduleRun();
                return;
              }
            }
          }
        }).observe(shadow, {
          childList: true,
          subtree: true,
        });
      });
    }
  }

  function queryAllDeep(root, selector) {
    const out = [];
    const seen = new Set();

    function walk(node) {
      if (!node || seen.has(node)) return;
      seen.add(node);

      let scope = node;
      if (node instanceof Element) {
        const shadow = getShadowRoot(node);
        if (shadow) {
          walk(shadow);
          scope = shadow;
        }
      }

      try {
        scope.querySelectorAll?.(selector).forEach((el) => {
          if (!seen.has(el)) {
            seen.add(el);
            out.push(el);
          }
        });
      } catch {
        /* ignore */
      }

      const kids =
        scope instanceof DocumentFragment || scope instanceof ShadowRoot
          ? scope.children
          : scope.children;
      if (kids) {
        for (const child of kids) walk(child);
      }
    }

    walk(root);
    return out;
  }

  function midFromCommentElement(el) {
    const data = el?.__data;
    if (!data) return null;
    return (
      data.mid ??
      data.member?.mid ??
      data.user?.mid ??
      data.reply?.member?.mid ??
      null
    );
  }

  function getCommentItemRoot(anchor) {
    return (
      anchor.closest(
        "bili-comment-renderer, bili-comment-reply-renderer, .reply-item, .list-item"
      ) || anchor
    );
  }

  function getCardItemRoot(anchor) {
    return (
      anchor.closest(
        ".video-page-card-small, .bili-video-card, .feed-card, .bili-feed-card, .video-card-reco, .video-list-card, .user-item, .user-list-item, .bili-user-card"
      ) || anchor
    );
  }

  function getUpPanelItemRoot(anchor) {
    return anchor.closest(".up-panel-container, .up-info-container") || anchor;
  }

  function getNicknameEl(link) {
    return link.querySelector("span.name, .name") || link;
  }

  function hasBadgeInItem(itemRoot, mid) {
    return !!itemRoot.querySelector(
      `.${BADGE_CLASS}[data-uid="${CSS.escape(mid)}"]`
    );
  }

  function createBadge(mid) {
    const span = document.createElement("span");
    span.className = BADGE_CLASS;
    span.setAttribute(EXTENSION_MARK, "1");
    span.dataset.uid = mid;
    span.textContent = `(${mid})`;
    span.title = `UID: ${mid}`;
    return span;
  }

  /**
   * @param {"comment"|"card"|"up"} placement
   */
  function attachBadge(anchor, mid, placement) {
    if (!(anchor instanceof HTMLElement) || !mid) return;
    if (anchor.getAttribute(PROCESSED) === "1") return;

    const itemRoot =
      placement === "comment"
        ? getCommentItemRoot(anchor)
        : placement === "up"
          ? getUpPanelItemRoot(anchor)
          : getCardItemRoot(anchor);

    const nickEl =
      placement === "card" ? getNicknameEl(anchor) : anchor;

    if (hasBadgeInItem(itemRoot, mid)) {
      anchor.setAttribute(PROCESSED, "1");
      const existing = itemRoot.querySelector(
        `.${BADGE_CLASS}[data-uid="${CSS.escape(mid)}"]`
      );
      if (existing) {
        emitUidReady(mid, nickEl, existing, placement);
      }
      return;
    }

    const badge = createBadge(mid);
    nickEl.insertAdjacentElement("afterend", badge);

    anchor.setAttribute(PROCESSED, "1");
    if (itemRoot instanceof HTMLElement) {
      itemRoot.setAttribute(ITEM_DONE, "1");
    }

    emitUidReady(mid, nickEl, badge, placement);
  }

  function emitUidReady(upMid, anchor, badgeEl, placement) {
    document.dispatchEvent(
      new CustomEvent("biliindex:uid", {
        detail: { upMid, anchor, badgeEl, placement },
      })
    );
  }

  function findCommentNicknameLink(renderer) {
    const links = [];
    for (const sel of COMMENT_NICKNAME_LINK_SELECTORS) {
      queryAllDeep(renderer, sel).forEach((el) => {
        if (el instanceof HTMLAnchorElement && !links.includes(el)) {
          links.push(el);
        }
      });
    }
    return (
      links.find((l) => !l.querySelector("img") && (l.textContent || "").trim()) ||
      links[0] ||
      null
    );
  }

  function findCommentNicknameHost(renderer) {
    for (const sel of COMMENT_NICKNAME_HOST_SELECTORS) {
      const found = queryAllDeep(renderer, sel).find(
        (el) => el instanceof HTMLElement
      );
      if (found) return found;
    }
    return null;
  }

  function processComments(root) {
    const newItems = queryAllDeep(
      root,
      "bili-comment-renderer, bili-comment-reply-renderer"
    );
    const items = newItems.length
      ? newItems
      : queryAllDeep(root, ".reply-item, .list-item");

    for (const item of items) {
      if (!(item instanceof HTMLElement)) continue;
      if (item.getAttribute(ITEM_DONE) === "1") {
        if (!item.querySelector(`.${BADGE_CLASS}`)) {
          item.removeAttribute(ITEM_DONE);
        } else {
          continue;
        }
      }

      const link = findCommentNicknameLink(item);
      const mid =
        (link && extractMid(link.getAttribute("href") || link.href)) ||
        midFromCommentElement(item);

      if (!mid) continue;

      if (link) {
        attachBadge(link, String(mid), "comment");
        continue;
      }

      const nickHost = findCommentNicknameHost(item);
      if (nickHost) {
        attachBadge(nickHost, String(mid), "comment");
      }
    }
  }

  function processAuthorLinks(root, linkSelectors, placement) {
    const processedLinks = new WeakSet();

    for (const sel of linkSelectors) {
      for (const link of queryAllDeep(root, sel)) {
        if (!(link instanceof HTMLAnchorElement)) continue;
        if (processedLinks.has(link)) continue;
        if (link.querySelector("img") && !(link.textContent || "").trim()) {
          continue;
        }

        const mid = extractMid(link.getAttribute("href") || link.href);
        if (!mid) continue;

        const card = getCardItemRoot(link);
        if (hasBadgeInItem(card, mid)) {
          processedLinks.add(link);
          continue;
        }

        attachBadge(link, mid, placement);
        processedLinks.add(link);
      }
    }
  }

  function processUpPanel(root) {
    const processed = new WeakSet();
    for (const sel of UP_PANEL_LINK_SELECTORS) {
      for (const link of queryAllDeep(root, sel)) {
        if (!(link instanceof HTMLAnchorElement) || processed.has(link)) {
          continue;
        }
        const mid = extractMid(link.getAttribute("href") || link.href);
        if (!mid) continue;

        const panelRoot = getUpPanelItemRoot(link);
        if (hasBadgeInItem(panelRoot, mid)) {
          processed.add(link);
          continue;
        }

        attachBadge(link, mid, "up");
        processed.add(link);
      }
    }
  }

  function processRoot(root) {
    if (!root || !(root instanceof Element)) return;

    if (matchesAnySelector(root, COMMENT_ROOT_SELECTORS)) {
      processComments(root);
    }
    if (matchesAnySelector(root, RECOMMEND_ROOT_SELECTORS)) {
      processAuthorLinks(root, AUTHOR_NICKNAME_LINK_SELECTORS, "card");
    }
    if (matchesAnySelector(root, UP_PANEL_ROOT_SELECTORS)) {
      processUpPanel(root);
    }
    if (matchesAnySelector(root, SEARCH_ROOT_SELECTORS)) {
      processAuthorLinks(
        root,
        [...AUTHOR_NICKNAME_LINK_SELECTORS, ...SEARCH_USER_LINK_SELECTORS],
        "card"
      );
    }
  }

  function run() {
    try {
      attachShadowObservers();
      const roots = new Set([
        ...collectRoots(COMMENT_ROOT_SELECTORS),
        ...collectRoots(RECOMMEND_ROOT_SELECTORS),
        ...collectRoots(UP_PANEL_ROOT_SELECTORS),
        ...collectRoots(SEARCH_ROOT_SELECTORS),
      ]);

      for (const sel of ["bili-comments", "bili-comment-area", "#comment-app"]) {
        document.querySelectorAll(sel).forEach((host) => {
          roots.add(host);
          const shadow = getShadowRoot(host);
          if (shadow) processComments(shadow);
        });
      }

      for (const root of roots) {
        processRoot(root);
      }
    } catch (err) {
      console.warn("[BiliIndex] UID 扫描失败", err);
    }
  }

  let scheduled = false;
  let debounceTimer = null;

  function scheduleRun() {
    if (scheduled) return;
    scheduled = true;
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      scheduled = false;
      const work = () => run();
      if (typeof requestIdleCallback === "function") {
        requestIdleCallback(work, { timeout: 800 });
      } else {
        requestAnimationFrame(work);
      }
    }, 200);
  }

  scheduleRun();

  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      for (const node of m.addedNodes) {
        if (nodeTouchesTargets(node)) {
          scheduleRun();
          return;
        }
      }
    }
  });

  observer.observe(document.body || document.documentElement, {
    childList: true,
    subtree: true,
  });

  window.addEventListener("load", scheduleRun);

  let pollTicks = 0;
  const pollTimer = setInterval(() => {
    scheduleRun();
    pollTicks += 1;
    if (pollTicks >= 20) clearInterval(pollTimer);
  }, 1500);
})();
