const $ = (sel) => document.querySelector(sel);

function applyExtensionVersionLabels() {
  const v = BiliIndex.getExtensionVersion?.() || "0.0.0";
  const label = `V${v}`;
  document.querySelectorAll(".app-version").forEach((el) => {
    el.textContent = label;
  });
  document.title = `BiliIndex-B站标签插件 ${label}`;
  const actionTitle = `BiliIndex-B站标签插件 ${label}`;
  try {
    chrome.action?.setTitle?.({ title: actionTitle });
  } catch {
    /* ignore */
  }
}

let tagGroups = null;
let subsByParent = new Map();
let selectedCategoryId = null;
let isSubmittingDraft = false;

const PASSERBY_CODE = "passerby";
const SUPER_HATE_CODE = "super_hate";
const SLOT_ORDER = ["tag1", "tag2", "tag3", "emoji"];
const draft = { tag1: null, tag2: null, tag3: null, emoji: null };

const SLOT_LABELS = {
  tag1: "一级",
  tag2: "二级",
  tag3: "风格",
  emoji: "表情",
};

/** 弹窗内可折叠标签组（默认收起） */
const expandedPopupGroups = new Set();

const BLOCK_MAX_POINTS = 3;
const BLOCK_MAX_FULL = 5;
let blockTagMax = BLOCK_MAX_POINTS;
const SUPER_HATE_TAG_MAX = 1;
const SUPER_HATE_UP_MAX_DEFAULT = 3;
let superHateUpMax = SUPER_HATE_UP_MAX_DEFAULT;
/** @type {{ id: number, code: string, name: string, tag_kind?: string }[]} */
let blockSelected = [];
/** @type {{ id: number, code: string, name: string, tag_kind?: string }[]} */
let superHateSelected = [];
/** @type {{ up_mid: string, nickname?: string | null }[]} */
let superHateUps = [];
/** @type {"block" | "super_hate" | "keyword"} */
let shieldMode = "block";
let shieldMembership = null;
let keywordBlockMax = 1;
/** @type {string[]} */
let keywordBlockValues = ["", "", "", "", ""];
let currentUserIsSuperAdmin = false;

function slotForKind(kind) {
  const map = { category: "tag1", sub: "tag2", style: "tag3", emoji: "emoji" };
  return map[kind] || null;
}

function isPasserbyDraft() {
  return draft.tag1?.code === PASSERBY_CODE;
}

function isSuperHateDraft() {
  return draft.tag1?.code === SUPER_HATE_CODE;
}

function requiredSlots() {
  if (isPasserbyDraft()) return ["tag1", "emoji"];
  if (isSuperHateDraft()) return ["tag1"];
  return SLOT_ORDER;
}

function isDraftComplete() {
  return requiredSlots().every((s) => draft[s]);
}

function clearDraft() {
  draft.tag1 = null;
  draft.tag2 = null;
  draft.tag3 = null;
  draft.emoji = null;
}

function updateDraftBar() {
  const bar = $("#tag-draft-bar");
  if (!bar) return;
  const need = requiredSlots();
  bar.innerHTML = SLOT_ORDER.map((slot) => {
    const tag = draft[slot];
    const skipPasserby = isPasserbyDraft() && (slot === "tag2" || slot === "tag3");
    const skipSuperHate = isSuperHateDraft() && slot !== "tag1";
    const label = SLOT_LABELS[slot];
    if (skipPasserby) {
      return `<span class="draft-slot">${label}：路人无需</span>`;
    }
    if (skipSuperHate) {
      return `<span class="draft-slot">${label}：超级讨厌无需</span>`;
    }
    const text = tag ? BiliIndex.formatTagLabel(tag) : "—";
    return `<span class="draft-slot${tag ? " done" : ""}">${label}：${text}</span>`;
  }).join("");
}

function refreshSelectionStyles() {
  document.querySelectorAll(".tag-special-btn").forEach((btn) => {
    const code = btn.dataset.code || "";
    btn.classList.toggle("selected", draft.tag1?.code === code);
  });
  document.querySelectorAll(".tag-l1-item").forEach((btn) => {
    const id = Number(btn.dataset.id);
    btn.classList.toggle("selected", draft.tag1?.id === id);
    btn.classList.toggle("active", selectedCategoryId === id);
  });
  document.querySelectorAll(".tag-chip[data-tag-id]").forEach((btn) => {
    const id = Number(btn.dataset.tagId);
    const slot = btn.dataset.slot;
    btn.classList.toggle("selected", draft[slot]?.id === id);
  });
  document.querySelectorAll(".tag-group[data-slot-group]").forEach((el) => {
    const g = el.dataset.slotGroup;
    const skip =
      (isPasserbyDraft() && (g === "tag2" || g === "tag3")) ||
      (isSuperHateDraft() && g !== "tag1");
    el.classList.toggle("is-passerby-skip", skip);
  });
}

function nextMissingSlotLabel() {
  const slot = requiredSlots().find((s) => !draft[s]);
  return slot ? SLOT_LABELS[slot] : "";
}

function tagKindOf(t) {
  let kind =
    t.tag_kind ||
    (t.level === 1 ? "category" : t.icon ? "emoji" : t.level === 3 ? "style" : "sub");
  if (BiliIndex.isEmojiTag?.(t)) kind = "emoji";
  return kind;
}

function buildSubsByParent(tree) {
  const map = new Map();
  for (const t of tree || []) {
    if (tagKindOf(t) !== "sub" || !t.parent_id) continue;
    if (!map.has(t.parent_id)) map.set(t.parent_id, []);
    map.get(t.parent_id).push(t);
  }
  const sortFn = (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || a.id - b.id;
  for (const list of map.values()) list.sort(sortFn);
  return map;
}

function renderSubPane(catId, categories) {
  const pane = document.getElementById("tag-sub-pane");
  if (!pane) return;
  pane.innerHTML = "";
  if (!catId) {
    pane.innerHTML = '<p class="tag-pane-placeholder">点击左侧一级分类</p>';
    return;
  }
  const cat = categories.find((c) => c.id === catId);
  const subs = subsByParent.get(catId) || [];
  if (!subs.length) {
    pane.innerHTML = `<p class="tag-pane-placeholder">${cat ? `「${cat.name}」暂无二级标签` : "暂无二级标签"}</p>`;
    return;
  }
  const chips = document.createElement("div");
  chips.className = "tag-chips tag-chips-pane";
  for (const tag of subs) {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.className = "tag-chip";
    btn.textContent = BiliIndex.formatTagLabel(tag);
    btn.title = `code: ${tag.code}`;
    btn.dataset.tagId = String(tag.id);
    btn.dataset.slot = "tag2";
    btn.addEventListener("click", () => void selectTagForSlot(tag, "sub"));
    chips.appendChild(btn);
  }
  pane.appendChild(chips);
}

function isPasserbyTagItem(t) {
  return typeof BiliIndex.isPasserbyTag === "function"
    ? BiliIndex.isPasserbyTag(t)
    : t?.code === PASSERBY_CODE || (t?.name || "").trim() === "路人";
}

function renderSpecialTag1Row(groups) {
  const all = groups?.category || [];
  const superHate = all.find((t) => t.code === SUPER_HATE_CODE);
  if (!superHate) return null;

  const block = document.createElement("div");
  block.className = "tag-group tag-group-special-tag1";
  block.dataset.slotGroup = "tag1";

  const row = document.createElement("div");
  row.className = "tag-special-tag1-row";

  if (superHate) {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.className = "tag-special-btn is-super-hate";
    btn.dataset.code = SUPER_HATE_CODE;
    btn.textContent = superHate.name || "超级讨厌";
    btn.title = "code: super_hate";
    btn.addEventListener("click", () => void selectTagForSlot(superHate, "category"));
    row.appendChild(btn);
  }

  block.appendChild(row);
  return block;
}

function renderCategorySubPicker(categories) {
  const filtered = (categories || []).filter(
    (t) => t.code !== PASSERBY_CODE && t.code !== SUPER_HATE_CODE
  );
  const block = document.createElement("div");
  block.className = "tag-group tag-l1-l2-picker";
  block.innerHTML =
    '<div class="tag-group-title">一级分类（职业） · 仅归类，点击可快速填入参考</div>';

  const picker = document.createElement("div");
  picker.className = "tag-dual-pane";

  const left = document.createElement("div");
  left.className = "tag-pane-l1";
  left.setAttribute("role", "listbox");
  left.setAttribute("aria-label", "一级分类");

  const right = document.createElement("div");
  right.className = "tag-pane-l2";
  right.id = "tag-sub-pane";
  right.setAttribute("role", "region");
  right.setAttribute("aria-label", "二级标签");

  if (!filtered.length) {
    left.innerHTML = '<p class="tag-pane-placeholder">暂无一级分类</p>';
    right.innerHTML = '<p class="tag-pane-placeholder">—</p>';
  } else {
    for (const cat of filtered) {
      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = "tag-l1-item";
      btn.textContent = cat.name;
      btn.dataset.id = String(cat.id);
      btn.setAttribute("role", "option");
      btn.addEventListener("click", () => {
        selectedCategoryId = cat.id;
        renderSubPane(cat.id, categories);
        void selectTagForSlot(cat, "category");
      });
      left.appendChild(btn);
    }
    const stillValid = filtered.some((c) => c.id === selectedCategoryId);
    if (!stillValid) selectedCategoryId = filtered[0].id;
    const activeBtn = left.querySelector(`[data-id="${selectedCategoryId}"]`);
    if (activeBtn) activeBtn.classList.add("active");
    renderSubPane(selectedCategoryId, filtered);
  }

  picker.appendChild(left);
  picker.appendChild(right);
  block.appendChild(picker);
  return block;
}

function appendTagChips(container, sec, list, slot) {
  container.className =
    sec.key === "emoji" ? "tag-chips tag-chips-emoji" : "tag-chips";
  if (!list.length) {
    container.innerHTML = '<span class="hint">暂无</span>';
    return;
  }
  for (const tag of list) {
    const btn = document.createElement("button");
    btn.type = "button";
    const isEmoji = sec.key === "emoji";
    btn.className = `tag-chip${isEmoji ? " emoji tag-chip-emoji-only" : ""}`;
    if (isEmoji) {
      const icon = BiliIndex.resolveTagIcon(tag);
      const name = (tag.name || tag.code || "").trim();
      btn.textContent = icon || "·";
      btn.title = name;
      btn.setAttribute("aria-label", name);
    } else {
      btn.textContent = BiliIndex.formatTagLabel(tag);
      btn.title = `code: ${tag.code}`;
    }
    if (slot) {
      btn.dataset.tagId = String(tag.id);
      btn.dataset.slot = slot;
    }
    btn.addEventListener("click", () => void selectTagForSlot(tag, sec.key));
    container.appendChild(btn);
  }
}

function renderCollapsibleChipGroup(sec, list) {
  const slot = slotForKind(sec.key);
  const expanded = expandedPopupGroups.has(sec.key);
  const block = document.createElement("div");
  block.className = "tag-group tag-group-collapsible";
  if (slot) block.dataset.slotGroup = slot;

  const head = document.createElement("button");
  head.type = "button";
  head.className = "tag-group-collapse-head";
  head.setAttribute("aria-expanded", String(expanded));
  const arrow = document.createElement("span");
  arrow.className = `collapse-arrow${expanded ? " expanded" : ""}`;
  arrow.setAttribute("aria-hidden", "true");
  arrow.textContent = "▶";
  const title = document.createElement("span");
  title.className = "tag-group-collapse-title";
  title.textContent = `${sec.title}${sec.hint ? ` · ${sec.hint}` : ""}`;
  head.append(arrow, title);

  const body = document.createElement("div");
  body.className = "tag-group-collapse-body";
  body.hidden = !expanded;
  const chips = document.createElement("div");
  appendTagChips(chips, sec, list, slot);
  body.appendChild(chips);

  head.addEventListener("click", () => {
    const open = !expandedPopupGroups.has(sec.key);
    if (open) expandedPopupGroups.add(sec.key);
    else expandedPopupGroups.delete(sec.key);
    body.hidden = !open;
    head.setAttribute("aria-expanded", String(open));
    arrow.classList.toggle("expanded", open);
  });

  block.append(head, body);
  return block;
}

function renderEmojiCategoryPicker(categories, emojis) {
  const slot = slotForKind("emoji");
  const block = document.createElement("div");
  block.className = "tag-group tag-emoji-picker";
  if (slot) block.dataset.slotGroup = slot;
  block.innerHTML = `<div class="tag-group-title">表情标签</div>`;

  const cats = (categories || []).slice().sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
  let activeCatId = cats[0]?.id ?? null;

  const catRow = document.createElement("div");
  catRow.className = "tag-emoji-cat-row";

  const gridWrap = document.createElement("div");
  gridWrap.className = "tag-chips tag-chips-emoji tag-emoji-grid";

  const renderGrid = () => {
    gridWrap.replaceChildren();
    const list = (emojis || []).filter(
      (t) => !activeCatId || Number(t.parent_id) === Number(activeCatId)
    );
    if (!list.length) {
      gridWrap.innerHTML = '<span class="hint">该分类暂无表情</span>';
      return;
    }
    appendTagChips(gridWrap, { key: "emoji" }, list, slot);
  };

  const renderCats = () => {
    catRow.replaceChildren();
    cats.forEach((cat) => {
      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = `tag-emoji-cat-btn${Number(cat.id) === Number(activeCatId) ? " active" : ""}`;
      btn.textContent = cat.name || cat.code;
      btn.addEventListener("click", () => {
        activeCatId = cat.id;
        renderCats();
        renderGrid();
        refreshSelectionStyles();
      });
      catRow.appendChild(btn);
    });
  };

  if (!cats.length) {
    appendTagChips(gridWrap, { key: "emoji" }, emojis || [], slot);
  } else {
    renderCats();
    renderGrid();
  }

  block.append(gridWrap, catRow);
  return block;
}

function renderChipGroup(sec, list) {
  const slot = slotForKind(sec.key);
  const block = document.createElement("div");
  block.className = "tag-group";
  if (slot) block.dataset.slotGroup = slot;
  block.innerHTML = `<div class="tag-group-title">${sec.title}${sec.hint ? ` · ${sec.hint}` : ""}</div>`;
  const chips = document.createElement("div");
  appendTagChips(chips, sec, list, slot);
  block.appendChild(chips);
  return block;
}

function showAuthError(msg, isError = true) {
  const el = $("#auth-error");
  el.textContent = msg;
  el.classList.toggle("hidden", !msg);
  el.classList.toggle("error", !!msg && isError);
  el.classList.toggle("ok", !!msg && !isError);
}

function placeCaptchaForTab(tab) {
  const wrap = document.getElementById("auth-captcha-wrap");
  if (!wrap) return;
  const slotId = tab === "register" ? "register-captcha-slot" : "login-captcha-slot";
  const slot = document.getElementById(slotId);
  if (slot && wrap.parentElement !== slot) {
    slot.appendChild(wrap);
  }
}

function switchTab(tab) {
  document.querySelectorAll(".tab").forEach((b) => {
    b.classList.toggle("active", b.dataset.tab === tab);
  });
  $("#form-login").classList.toggle("hidden", tab !== "login");
  $("#form-register").classList.toggle("hidden", tab !== "register");
  $("#form-reset").classList.toggle("hidden", tab !== "reset");
  if (tab === "login" || tab === "register") {
    placeCaptchaForTab(tab);
  }
  if (tab === "login") void updateLoginApiHint();
  if (typeof BiliIndexCaptcha !== "undefined") {
    BiliIndexCaptcha.setVisible(tab === "login" || tab === "register");
  }
  showAuthError("");
}

async function updateLoginApiHint() {
  const el = document.getElementById("login-api-hint");
  if (!el || typeof BiliIndex === "undefined") return;
  try {
    await BiliIndex.ensureFixedApiConfig?.();
    const mode = await BiliIndex.getApiMode();
    const base = await BiliIndex.getApiBase();
    el.textContent = `当前 API：${base}（${mode === "local" ? "本地" : "联网"}）`;
    if (mode === "local") {
      el.textContent += " — 其他电脑请改用联网模式";
    }
  } catch {
    el.textContent = "";
  }
}

document.querySelectorAll(".tab").forEach((btn) => {
  btn.addEventListener("click", () => switchTab(btn.dataset.tab));
});

$("#remember-password").addEventListener("change", () => {
  if (!$("#remember-password").checked) {
    $("#auto-login").checked = false;
  }
});

$("#auto-login").addEventListener("change", () => {
  if ($("#auto-login").checked) {
    $("#remember-password").checked = true;
  }
});

async function initAuthForm() {
  const prefs = await BiliIndex.getLoginPrefs();
  $("#remember-password").checked = prefs.rememberPassword;
  $("#auto-login").checked = prefs.autoLogin;
  const form = $("#form-login");
  if (prefs.rememberPassword && prefs.savedUsername) {
    form.username.value = prefs.savedUsername;
    if (prefs.savedPassword) {
      form.password.value = prefs.savedPassword;
    }
  }
}

async function tryAutoLogin() {
  const token = await BiliIndex.getToken();
  if (token) return false;
  const prefs = await BiliIndex.getLoginPrefs();
  if (
    !prefs.autoLogin ||
    !prefs.rememberPassword ||
    !prefs.savedUsername ||
    !prefs.savedPassword
  ) {
    return false;
  }
  switchTab("login");
  if (typeof BiliIndexCaptcha !== "undefined") {
    BiliIndexCaptcha.setVisible(true);
  }
  showAuthError("已填入账号，请完成滑块验证后点击登录", false);
  return false;
}

async function showBanner(membership) {
  const banner = $("#banner");
  if (!membership.is_member) {
    if (membership.label === "免费用户") {
      banner.textContent =
        "您的会员试用期已到期，部分功能已受限，请购买会员或使用积分兑换继续使用。";
      banner.className = "banner danger";
      banner.classList.remove("hidden");
    } else {
      banner.classList.add("hidden");
    }
    return;
  }
  if (
    membership.member_type === "trial" &&
    membership.remaining_days !== null &&
    membership.remaining_days <= 1
  ) {
    banner.textContent =
      "您的会员试用期将于明天到期，到期后您将无法使用高级屏蔽规则和无限打标功能，如需继续使用请购买会员或使用积分兑换。";
    banner.className = "banner warn";
    banner.classList.remove("hidden");
    return;
  }
  banner.classList.add("hidden");
}

function renderTagSections(groups, tree) {
  const root = $("#tag-sections");
  root.innerHTML = "";

  const categories = (groups.category || []).filter(
    (t) => t.code !== PASSERBY_CODE && t.code !== SUPER_HATE_CODE
  );
  const subs = groups.sub || [];
  const special = renderSpecialTag1Row(groups);
  if (special) root.appendChild(special);
  root.appendChild(renderCategorySubPicker(categories));

  const tailSections = [{ key: "style", title: "三级标签（风格）", hint: null }];
  for (const sec of tailSections) {
    const list = groups[sec.key] || [];
    root.appendChild(renderCollapsibleChipGroup(sec, list));
  }
  root.appendChild(renderEmojiCategoryPicker(groups.emoji_cat || [], groups.emoji || []));

  updateDraftBar();
  refreshSelectionStyles();

  return categories.length + subs.length + (groups.style?.length || 0) + (groups.emoji?.length || 0);
}

async function submitDraft(upMid) {
  if (isSubmittingDraft) return;
  isSubmittingDraft = true;
  const slots = requiredSlots();
  const wasPasserby = isPasserbyDraft();
  const wasSuperHate = isSuperHateDraft();
  $("#action-msg").textContent = `提交中…（${slots.length} 个槽位）`;
  const ledgerIds = [];
  let expectedPoints = 0;
  try {
    for (const slot of slots) {
      const tag = draft[slot];
      if (!tag) continue;
      const res = await BiliIndex.quickTag(upMid, tag.code, { slot });
      if (!res?.ok) {
        $("#action-msg").textContent = res?.error || `打标失败（${SLOT_LABELS[slot]}）`;
        return;
      }
      if (res.points_ledger_id) {
        ledgerIds.push(res.points_ledger_id);
        expectedPoints += Number(res.points_earned) || 0;
      }
      if (res.activation_ledger_id) {
        ledgerIds.push(res.activation_ledger_id);
        expectedPoints += Number(res.activation_bonus) || 0;
      }
      if (tag.code === SUPER_HATE_CODE && typeof BiliIndex.syncSuperHateRules === "function") {
        await BiliIndex.syncSuperHateRules();
      }
    }
    clearDraft();
    selectedCategoryId = null;
    updateDraftBar();
    refreshSelectionStyles();
    const groups = tagGroups;
    const cats = (groups?.category || []).filter(
      (t) => t.code !== PASSERBY_CODE && t.code !== SUPER_HATE_CODE
    );
    if (cats.length) {
      selectedCategoryId = cats[0].id;
      renderSubPane(selectedCategoryId, cats);
    }
    let pointsNote = "";
    if (ledgerIds.length) {
      try {
        const verify = await BiliIndex.verifyPointsCredit(ledgerIds, expectedPoints);
        if (verify.verified) {
          pointsNote = `，积分已到账 +${verify.actual_delta}`;
        } else {
          pointsNote = "，积分到账校验未通过，请稍后查看积分";
        }
        if (verify.user_points != null) {
          $("#points-status").textContent = `积分：${verify.user_points}`;
        }
      } catch {
        pointsNote = "，请刷新后查看积分是否到账";
      }
    }
    const modeLabel = wasSuperHate
      ? "超级讨厌"
      : wasPasserby
        ? "路人 + Emoji"
        : "一级 + 二级 + 风格 + Emoji";
    $("#action-msg").textContent = `已对 ${upMid} 完成打标（${modeLabel}）${pointsNote}。请刷新 B 站页面后生效`;
    const me = await BiliIndex.fetchMe();
    $("#points-status").textContent = `积分：${me.points}`;
  } catch (e) {
    $("#action-msg").textContent = e.message || "打标失败";
  } finally {
    isSubmittingDraft = false;
  }
}

async function selectTagForSlot(tag, kind) {
  const upMid = $("#tag-up-mid").value.trim();
  if (!upMid) {
    $("#action-msg").textContent = "请先填写或读取 UP主 UID";
    return;
  }

  const slot = slotForKind(kind);
  if (!slot) return;

  if (slot === "tag2" && !draft.tag1) {
    $("#action-msg").textContent = "请先选择一级分类";
    return;
  }
  if (slot === "tag2" && draft.tag1 && tag.parent_id != null && tag.parent_id !== draft.tag1.id) {
    $("#action-msg").textContent = `二级标签须属于「${draft.tag1.name}」`;
    return;
  }

  if (slot === "tag1") {
    draft.tag1 = tag;
    selectedCategoryId = tag.id;
    if (tag.code === PASSERBY_CODE) {
      draft.tag2 = null;
      draft.tag3 = null;
    } else if (tag.code === SUPER_HATE_CODE) {
      draft.tag2 = null;
      draft.tag3 = null;
      draft.emoji = null;
      selectedCategoryId = null;
    } else if (draft.tag2 && draft.tag2.parent_id !== tag.id) {
      draft.tag2 = null;
    }
    const cats = (tagGroups?.category || []).filter(
      (t) => t.code !== PASSERBY_CODE && t.code !== SUPER_HATE_CODE
    );
    if (tag.code !== SUPER_HATE_CODE && tag.code !== PASSERBY_CODE) {
      renderSubPane(tag.id, cats);
    }
  } else {
    draft[slot] = tag;
  }

  updateDraftBar();
  refreshSelectionStyles();

  const need = requiredSlots();
  const done = need.filter((s) => draft[s]).length;
  if (!isDraftComplete()) {
    const next = nextMissingSlotLabel();
    $("#action-msg").textContent = `已选 ${done}/${need.length}，请继续选择${next ? `「${next}」` : ""}`;
    return;
  }

  await submitDraft(upMid);
}

async function loadTags(force = false) {
  const status = $("#tags-load-status");
  status.textContent = "正在从服务器加载标签…";
  try {
    let tree = [];
    let emojiItems = [];
    try {
      if (force) {
        await BiliIndex.refreshTagCaches(true);
      }
      tree = await BiliIndex.getTagTree(force);
      const emojiRes = await BiliIndex.fetchPublic("/api/tag/quick-list?kind=emoji");
      emojiItems = emojiRes.items || [];
    } catch (e) {
      status.textContent = `后台加载失败，尝试直连… (${e.message})`;
      const [treeData, emojiRes] = await Promise.all([
        BiliIndex.fetchPublic("/api/tag/tree"),
        BiliIndex.fetchPublic("/api/tag/quick-list?kind=emoji"),
      ]);
      tree = treeData.items || [];
      emojiItems = emojiRes.items || [];
      await BiliIndex.storageSet({
        tagTree: tree,
        quickTags: tree.filter((t) => (t.tag_kind || "") !== "category" && t.level > 1),
        tagsCachedAt: Date.now(),
      });
    }

    const byCode = new Map(tree.map((t) => [t.code, t]));
    for (const t of emojiItems) {
      const prev = byCode.get(t.code);
      if (prev) {
        if (t.icon) prev.icon = BiliIndex.resolveTagIcon(t);
        if (t.tag_kind) prev.tag_kind = t.tag_kind;
      } else {
        byCode.set(t.code, { ...t, icon: BiliIndex.resolveTagIcon(t), tag_kind: "emoji" });
      }
    }
    tree = [...byCode.values()];

    if (!tree.length) {
      status.textContent = "未获取到标签。请确认 API 运行在 http://127.0.0.1:8000";
      $("#tag-sections").innerHTML =
        '<p class="hint">启动命令：uvicorn app.main:app --reload --port 8000</p>';
      return;
    }

    tagGroups = BiliIndex.groupTags(tree);
    subsByParent = buildSubsByParent(tree);

    const n = renderTagSections(tagGroups, tree);
    const quickN =
      (tagGroups.sub?.length || 0) +
      (tagGroups.style?.length || 0) +
      (tagGroups.emoji?.length || 0);
    status.textContent = `已加载 ${n} 个标签（可打标 ${quickN} 个）· 缓存 1 小时`;
    if (!$("#block-panel")?.classList.contains("hidden")) {
      renderShieldPool();
    }
  } catch (e) {
    status.textContent = `加载失败：${e.message}`;
    $("#tag-sections").innerHTML =
      '<p class="hint">请检查 API 地址与密钥，并确认 biliindex-api 已启动。</p>';
  }
}

function sendTabMessage(tabId, payload) {
  return new Promise((resolve, reject) => {
    chrome.tabs.sendMessage(tabId, payload, (res) => {
      const err = chrome.runtime.lastError;
      if (err) {
        reject(new Error(err.message));
        return;
      }
      resolve(res);
    });
  });
}

async function fetchUpMidFromActiveTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab?.id) {
    return { ok: false, error: "无法获取当前标签页" };
  }
  if (!tab.url || !/bilibili\.com/i.test(tab.url)) {
    return { ok: false, error: "请先切换到 B 站页面（视频页 / 空间页等）" };
  }
  let res = null;
  for (let attempt = 0; attempt < 3; attempt += 1) {
    try {
      res = await sendTabMessage(tab.id, { type: "GET_LAST_UP_MID" });
      break;
    } catch (e) {
      if (attempt < 2) {
        await new Promise((r) => setTimeout(r, 350));
        continue;
      }
      console.warn("[BiliIndex] GET_LAST_UP_MID failed", e);
      return {
        ok: false,
        error: "无法连接页面脚本，请在 B 站标签页按 Ctrl+F5 刷新后重试",
      };
    }
  }
  if (!res?.upMid) {
    return {
      ok: false,
      error: "未能识别 UP 主。请打开视频页或空间页，或点击页面上任意标签槽后再试",
    };
  }
  return { ok: true, upMid: String(res.upMid) };
}

async function pickUpMidFromPage() {
  const btn = $("#btn-pick-up-mid");
  const msg = $("#action-msg");
  msg.textContent = "正在读取当前页…";
  if (btn) btn.disabled = true;
  try {
    const result = await fetchUpMidFromActiveTab();
    if (!result.ok) {
      msg.textContent = result.error;
      return;
    }
    $("#tag-up-mid").value = result.upMid;
    msg.textContent = `已读取 UID：${result.upMid}`;
  } catch (e) {
    msg.textContent = e?.message || "读取失败";
  } finally {
    if (btn) btn.disabled = false;
  }
}

function canPickSuperHateCategory() {
  if (shieldMembership?.super_hate_category_allowed === false) return false;
  if (shieldMembership?.member_type === "points_member") return false;
  return !!shieldMembership?.is_full_member;
}

function getShieldPoolTags() {
  const groups = tagGroups || { category: [], emoji: [] };
  let list = [...(groups.category || []), ...(groups.emoji || [])].filter(
    (t) => !isPasserbyTagItem(t) && t.code !== SUPER_HATE_CODE
  );
  if (shieldMode === "super_hate" && !canPickSuperHateCategory()) {
    list = list.filter((t) => isShieldEmojiTag(t));
  }
  return list;
}

function resolveKeywordMax(membership) {
  const n = Number(membership?.keyword_blocks_max);
  if (Number.isFinite(n) && n > 0) return n;
  if (membership?.member_type === "lifetime" || membership?.keyword_blocks_lifetime) return 5;
  if (membership?.is_member) return 3;
  return 1;
}

function keywordSlotLockMessage(slotIndex, membership) {
  const max = resolveKeywordMax(membership);
  if (slotIndex < max) return "";
  if (membership?.member_type === "lifetime" || membership?.keyword_blocks_lifetime) return "";
  if (max <= 1) return "需要开通会员后可使用更多关键词槽位";
  return "需要开通买断会员后可使用第 4、5 个关键词槽位";
}

function readKeywordInputsFromDom() {
  const inputs = document.querySelectorAll("#keyword-block-inputs .keyword-slot-input");
  return [...inputs].map((el) => String(el.value || "").trim());
}

function fillKeywordInputs(values) {
  const inputs = document.querySelectorAll("#keyword-block-inputs .keyword-slot-input");
  inputs.forEach((el, i) => {
    el.value = values[i] || "";
  });
}

function bindKeywordInputFilters() {
  document.querySelectorAll("#keyword-block-inputs .keyword-slot-input").forEach((input) => {
    if (input.dataset.biliindexKwBound) return;
    input.dataset.biliindexKwBound = "1";
    input.addEventListener("input", () => {
      const filtered = [...input.value]
        .filter((ch) => /[\u4e00-\u9fff]/.test(ch))
        .slice(0, 5)
        .join("");
      if (filtered !== input.value) input.value = filtered;
    });
  });
}

function updateKeywordSlotUi(membership) {
  keywordBlockMax = resolveKeywordMax(membership);
  bindKeywordInputFilters();
  document.querySelectorAll("#keyword-block-inputs .keyword-slot-row").forEach((row, idx) => {
    const msg = keywordSlotLockMessage(idx, membership);
    row.classList.toggle("is-locked", !!msg);
    row.dataset.lockMsg = msg;
    const input = row.querySelector(".keyword-slot-input");
    if (input) {
      input.readOnly = !!msg;
      input.tabIndex = msg ? -1 : 0;
      input.classList.toggle("is-locked", !!msg);
    }
  });
}

async function loadKeywordBlocks() {
  try {
    const data = await BiliIndex.fetchKeywordBlocks();
    keywordBlockValues = (data?.keywords || ["", "", "", "", ""]).slice(0, 5);
    while (keywordBlockValues.length < 5) keywordBlockValues.push("");
    fillKeywordInputs(keywordBlockValues);
  } catch (e) {
    $("#block-save-status").textContent = e.message || "关键词屏蔽加载失败";
  }
}

function setShieldMode(mode) {
  if (mode === "super_hate") shieldMode = "super_hate";
  else if (mode === "keyword") shieldMode = "keyword";
  else shieldMode = "block";

  const tabBlock = $("#shield-tab-block");
  const tabSuper = $("#shield-tab-super");
  const tabKeyword = $("#shield-tab-keyword");
  const hintBlock = $("#shield-hint-block");
  const hintSuper = $("#shield-hint-super");
  const hintKeyword = $("#shield-hint-keyword");
  const btnSave = $("#btn-save-block-tags");
  const isBlock = shieldMode === "block";
  const isSuper = shieldMode === "super_hate";
  const isKeyword = shieldMode === "keyword";

  tabBlock?.classList.toggle("active", isBlock);
  tabSuper?.classList.toggle("active", isSuper);
  tabKeyword?.classList.toggle("active", isKeyword);
  tabBlock?.setAttribute("aria-selected", isBlock ? "true" : "false");
  tabSuper?.setAttribute("aria-selected", isSuper ? "true" : "false");
  tabKeyword?.setAttribute("aria-selected", isKeyword ? "true" : "false");

  hintBlock?.classList.toggle("hidden", !isBlock);
  hintSuper?.classList.toggle("hidden", !isSuper);
  hintKeyword?.classList.toggle("hidden", !isKeyword);
  $("#keyword-block-inputs")?.classList.toggle("hidden", !isKeyword);
  $("#block-selected")?.classList.toggle("hidden", isKeyword);
  $("#block-tag-pool")?.classList.toggle("hidden", isKeyword);
  $("#super-hate-up-row")?.classList.toggle("hidden", !isSuper);

  const panel = $("#block-panel");
  const badge = panel?.querySelector(".block-member-badge");
  if (badge) {
    badge.classList.toggle("hidden", isKeyword || !shieldMembership?.is_member);
  }

  if (btnSave) {
    btnSave.textContent = isKeyword
      ? "保存关键词屏蔽"
      : isBlock
        ? "保存屏蔽规则"
        : "保存超级讨厌标签";
    btnSave.classList.toggle("is-super-hate-save", isSuper);
  }

  if (isKeyword) {
    updateKeywordSlotUi(shieldMembership);
  } else if (isBlock) {
    renderBlockSelected();
    renderShieldPool();
  } else {
    renderShieldSelected();
    renderShieldPool();
  }
  $("#block-save-status").textContent = "";
}

function renderShieldSelected() {
  if (shieldMode === "block") {
    renderBlockSelected();
    return;
  }
  const el = $("#block-selected");
  if (!el) return;
  const parts = [];
  if (superHateSelected.length) {
    const t = superHateSelected[0];
    parts.push(
      `<span class="block-picked is-super-tag${isShieldEmojiTag(t) ? " is-emoji-picked" : ""}" title="标配标签：${escHtml(shieldTagChipTitle(t))}">标配：${escHtml(shieldTagChipText(t))} <button type="button" data-code="${t.code}" class="block-remove super-tag-remove" aria-label="移除标签">×</button></span>`
    );
  }
  for (const u of superHateUps) {
    parts.push(
      `<span class="block-picked is-super-up" title="指定 UP">UP：${u.up_mid} <button type="button" data-mid="${u.up_mid}" class="block-remove super-up-remove" aria-label="移除 UP">×</button></span>`
    );
  }
  if (!parts.length) {
    el.innerHTML =
      '<span class="hint">请选择 1 个标配标签和/或添加最多 3 个 UP</span>';
    return;
  }
  el.innerHTML = parts.join("");
  el.querySelectorAll(".super-tag-remove").forEach((btn) => {
    btn.addEventListener("click", () => {
      superHateSelected = [];
      renderShieldSelected();
      renderShieldPool();
    });
  });
  el.querySelectorAll(".super-up-remove").forEach((btn) => {
    btn.addEventListener("click", () => void removeSuperHateUpEntry(btn.dataset.mid));
  });
}

function renderBlockSelected() {
  const el = $("#block-selected");
  if (!el) return;
  if (!blockSelected.length) {
    el.innerHTML = '<span class="hint">未选择屏蔽标签</span>';
    return;
  }
  el.innerHTML = blockSelected
    .map(
      (t) =>
        `<span class="block-picked${isShieldEmojiTag(t) ? " is-emoji-picked" : ""}" title="${escHtml(shieldTagChipTitle(t))}">${escHtml(shieldTagChipText(t))} <button type="button" data-code="${t.code}" class="block-remove" aria-label="移除">×</button></span>`
    )
    .join("");
  el.querySelectorAll(".block-remove").forEach((btn) => {
    btn.addEventListener("click", () => {
      const code = btn.dataset.code;
      blockSelected = blockSelected.filter((t) => t.code !== code);
      renderBlockSelected();
      renderShieldPool();
    });
  });
}

function escHtml(s) {
  return String(s ?? "")
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function isShieldEmojiTag(tag) {
  return tag?.tag_kind === "emoji" || !!BiliIndex.isEmojiTag?.(tag);
}

/** 屏蔽/超级讨厌标配：Emoji 仅显示图标，一级标签显示名称 */
function shieldTagChipText(tag) {
  if (!tag) return "";
  if (isShieldEmojiTag(tag)) {
    return BiliIndex.resolveTagIcon?.(tag) || tag.icon || tag.name || "";
  }
  return BiliIndex.formatTagLabel(tag);
}

function shieldTagChipTitle(tag) {
  if (!tag) return "";
  if (isShieldEmojiTag(tag)) {
    const name = (tag.name || tag.code || "").trim();
    return name ? `Emoji：${name}` : "Emoji 标签";
  }
  return BiliIndex.formatTagLabel(tag);
}

function otherShieldCodes() {
  return shieldMode === "block"
    ? new Set(superHateSelected.map((t) => t.code))
    : new Set(blockSelected.map((t) => t.code));
}

function renderShieldPool() {
  const pool = $("#block-tag-pool");
  if (!pool) return;
  pool.innerHTML = "";
  const all = getShieldPoolTags();
  if (!all.length) {
    pool.innerHTML = '<span class="hint">请先加载标签</span>';
    return;
  }
  const forbidden = otherShieldCodes();
  const isBlock = shieldMode === "block";
  const maxTags = isBlock ? blockTagMax : SUPER_HATE_TAG_MAX;
  const picked = new Set(
    (isBlock ? blockSelected : superHateSelected).map((t) => t.code)
  );

  const pointsMemberSuper = shieldMode === "super_hate" && !canPickSuperHateCategory();

  for (const tag of all) {
    const btn = document.createElement("button");
    btn.type = "button";
    const active = picked.has(tag.code);
    const conflict = forbidden.has(tag.code);
    const isEmoji = isShieldEmojiTag(tag);
    const categoryBlocked =
      pointsMemberSuper && !isEmoji && tag.tag_kind === "category";
    btn.className = `block-pool-chip${active ? " active" : ""}${
      !isBlock && active ? " is-super-hate-chip" : ""
    }${conflict ? " is-shield-conflict" : ""}${isEmoji ? " block-pool-chip-emoji" : ""}`;
    btn.textContent = shieldTagChipText(tag);
    if (categoryBlocked) {
      btn.disabled = true;
      btn.title = "积分会员的超级讨厌不可选择一级分类标签";
    } else if (conflict) {
      btn.disabled = true;
      btn.title = isBlock
        ? "该标签已在「超级讨厌」中使用"
        : "该标签已在「评论区屏蔽」中使用";
    } else if (isBlock) {
      btn.disabled = false;
      btn.title = active
        ? `${shieldTagChipTitle(tag)} · 点击取消`
        : shieldTagChipTitle(tag);
    } else {
      btn.disabled = false;
      const label = shieldTagChipTitle(tag);
      btn.title = active
        ? `${label} · 点击可取消标配`
        : `${label} · 设为超级讨厌标配（仅 1 个）`;
    }
    btn.addEventListener("click", () => {
      if (conflict) return;
      const item = {
        id: tag.id,
        code: tag.code,
        name: tag.name,
        tag_kind: tag.tag_kind,
        icon: tag.icon,
      };
      if (isBlock) {
        if (active) {
          blockSelected = blockSelected.filter((t) => t.code !== tag.code);
        } else if (blockSelected.length >= maxTags) {
          $("#block-save-status").textContent = `最多 ${maxTags} 个`;
          return;
        } else {
          blockSelected.push(item);
        }
      } else if (active) {
        superHateSelected = [];
      } else {
        superHateSelected = [item];
      }
      renderShieldSelected();
      renderShieldPool();
      $("#block-save-status").textContent = "";
    });
    pool.appendChild(btn);
  }
  pool.classList.toggle(
    "block-tag-pool--emoji",
    all.length > 0 && all.every((t) => isShieldEmojiTag(t))
  );
}

function renderBlockPool() {
  renderShieldPool();
}

async function loadSuperHateTags() {
  try {
    const data = await BiliIndex.fetchSuperHateTags();
    if (data?.category_allowed === false) {
      shieldMembership = {
        ...(shieldMembership || {}),
        super_hate_category_allowed: false,
      };
    }
    let tags = data?.tags || [];
    if (!canPickSuperHateCategory()) {
      tags = tags.filter((t) => isShieldEmojiTag(t));
    }
    superHateSelected = tags.slice(0, SUPER_HATE_TAG_MAX).map((t) => ({
      id: t.id,
      code: t.code,
      name: t.name,
      tag_kind: t.tag_kind,
      icon: t.icon,
    }));
    if (shieldMode === "super_hate") renderShieldSelected();
    renderShieldPool();
  } catch (e) {
    if (shieldMode === "super_hate") {
      $("#block-save-status").textContent = e.message || "超级讨厌标签加载失败";
    }
  }
}

async function loadSuperHateUps() {
  try {
    const data = await BiliIndex.fetchSuperHateUps();
    superHateUps = (data?.items || []).slice(0, superHateUpMax || SUPER_HATE_UP_MAX_DEFAULT);
    if (shieldMode === "super_hate") renderShieldSelected();
  } catch (e) {
    if (shieldMode === "super_hate") {
      $("#block-save-status").textContent = e.message || "超级讨厌 UP 列表加载失败";
    }
  }
}

async function addSuperHateUpEntry(upMidOverride) {
  const status = $("#block-save-status");
  const raw = String(
    upMidOverride ||
      $("#super-hate-up-mid")?.value?.trim() ||
      $("#tag-up-mid")?.value?.trim() ||
      ""
  );
  if (!/^\d+$/.test(raw)) {
    status.textContent = "请填写有效的 UP主 UID（纯数字）";
    return;
  }
  if (superHateUps.length >= superHateUpMax) {
    status.textContent = `最多添加 ${superHateUpMax} 个 UP（月卡会员可指定 3 个）`;
    return;
  }
  if (superHateUps.some((u) => u.up_mid === raw)) {
    status.textContent = "该 UP 已在列表中";
    return;
  }
  status.textContent = "添加中…";
  try {
    const res = await BiliIndex.addSuperHateUp(raw);
    if (res?.ok === false) {
      status.textContent = res.error || "添加失败";
      return;
    }
    superHateUps = res.items || [];
    if (typeof BiliIndex.syncSuperHateRules === "function") {
      await BiliIndex.syncSuperHateRules();
    }
    $("#super-hate-up-mid").value = "";
    renderShieldSelected();
    status.textContent = "已添加。请刷新 B 站页面后生效";
  } catch (e) {
    status.textContent = e.message || "添加失败";
  }
}

async function addSuperHateUpFromCurrentPage() {
  const btn = $("#btn-add-super-hate-up");
  const status = $("#block-save-status");
  const input = $("#super-hate-up-mid");
  status.textContent = "正在读取当前页 UP 主…";
  if (btn) btn.disabled = true;
  try {
    const result = await fetchUpMidFromActiveTab();
    if (!result.ok) {
      status.textContent = result.error;
      if (input) input.value = "";
      return;
    }
    if (input) input.value = result.upMid;
    $("#tag-up-mid").value = result.upMid;
    status.textContent = `已读取 UID：${result.upMid}，正在添加…`;
    await addSuperHateUpEntry(result.upMid);
  } catch (e) {
    status.textContent = e?.message || "读取失败";
  } finally {
    if (btn) btn.disabled = false;
  }
}

async function removeSuperHateUpEntry(upMid) {
  const status = $("#block-save-status");
  status.textContent = "移除中…";
  try {
    const data = await BiliIndex.removeSuperHateUp(upMid);
    superHateUps = data.items || [];
    if (typeof BiliIndex.syncSuperHateRules === "function") {
      await BiliIndex.syncSuperHateRules();
    }
    renderShieldSelected();
    status.textContent = "已移除。请刷新 B 站页面后生效";
  } catch (e) {
    status.textContent = e.message || "移除失败";
  }
}

function resolveSuperHateUpMax(membership) {
  const n = Number(membership?.super_hate_max_ups);
  if (Number.isFinite(n) && n > 0) return n;
  if (membership?.is_full_member) return SUPER_HATE_UP_MAX_DEFAULT;
  if (membership?.member_type === "points_member") return 1;
  return membership?.is_member ? 1 : 0;
}

function resolveBlockTagMax(membership, blockData) {
  const fromApi = Number(blockData?.max ?? membership?.block_tags_max);
  if (Number.isFinite(fromApi) && fromApi > 0) return fromApi;
  if (membership?.is_full_member) return BLOCK_MAX_FULL;
  if (membership?.member_type === "points_member") return BLOCK_MAX_POINTS;
  return membership?.is_member ? BLOCK_MAX_POINTS : BLOCK_MAX_POINTS;
}

function updateBlockTagLimitUi(membership, blockData) {
  blockTagMax = resolveBlockTagMax(membership, blockData);
  const hint = document.getElementById("shield-hint-block");
  if (hint) {
    const n = blockTagMax;
    hint.innerHTML = `最多选 ${n} 个讨厌的一级或 Emoji 标签（全功能会员 5 个、积分会员 3 个；不可与「超级讨厌」标配重复）；路人不可选。匹配后折叠主评论；播放页推荐/首页/搜索页视频仅封面遮罩。<strong>保存后刷新页面</strong>生效。`;
  }
}

function updateSuperHateUpLimitUi(membership) {
  superHateUpMax = resolveSuperHateUpMax(membership);
  const label = document.getElementById("super-hate-up-limit-label");
  const hintSuper = document.getElementById("shield-hint-super");
  const isPoints = membership?.member_type === "points_member";
  if (label) {
    label.textContent =
      superHateUpMax > 0
        ? `指定 UP 主（最多 ${superHateUpMax} 个）`
        : "指定 UP 主（会员可用）";
  }
  if (hintSuper) {
    if (isPoints) {
      hintSuper.innerHTML =
        "<strong>积分会员</strong>：仅可指定 <strong>1 个 UP</strong> 屏蔽；不可选一级分类标配（可选 1 个 Emoji 标配）。命中后视频纯白遮挡、评论区完全折叠。<strong>保存后刷新 B 站页面</strong>。";
    } else {
      hintSuper.innerHTML =
        "<strong>标配标签</strong>：最多 1 个一级或 Emoji（不可与评论区屏蔽重复）。<strong>指定 UP</strong>：在 B 站视频/空间页打开弹窗，点「添加当前页UP主」自动读取 UID 并加入（最多 3 个）。命中标签或名单中的 UP：视频<strong>纯白遮挡</strong>，评论区<strong>完全折叠</strong>。<strong>保存标签后刷新 B 站页面</strong>。";
    }
  }
}

async function renderBlockPanel(membership, blockData) {
  const panel = $("#block-panel");
  if (!panel) return;
  shieldMembership = membership;
  updateBlockTagLimitUi(membership, blockData);
  updateSuperHateUpLimitUi(membership);
  panel.classList.remove("hidden");
  const badge = panel.querySelector(".block-member-badge");
  if (badge) badge.classList.toggle("hidden", !membership?.is_member);
  blockSelected = (blockData?.tags || []).map((t) => ({
    id: t.id,
    code: t.code,
    name: t.name,
    tag_kind: t.tag_kind,
    icon: t.icon,
  }));
  if (!membership?.is_member && shieldMode === "block") {
    setShieldMode("keyword");
  }
  setShieldMode(shieldMode);
  await Promise.all([loadSuperHateTags(), loadSuperHateUps(), loadKeywordBlocks()]);
}

async function loadAndRenderBlockPanel(membership) {
  let blockData = { tags: [], codes: [] };
  if (membership?.is_member) {
    try {
      blockData = await BiliIndex.fetchBlockTags();
    } catch (e) {
      $("#block-save-status").textContent = e.message || "屏蔽规则加载失败";
    }
  }
  await renderBlockPanel(membership, blockData);
}

function localDayKey() {
  try {
    return new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai" }).format(
      new Date()
    );
  } catch {
    const d = new Date();
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
  }
}

function normalizeEcoScanQuotaStatus(st) {
  if (!st) return null;
  const unlimited = !!st.scan_points_unlimited;
  const limit = Number(st.scan_points_limit ?? st.eco_points_limit ?? 100);
  const earned = Number(st.scan_points_earned ?? st.eco_points_earned ?? 0);
  const atCap = unlimited
    ? false
    : !!(
        st.eco_at_cap ||
        st.at_daily_cap ||
        st.scan_points_at_daily_cap ||
        earned >= limit
      );
  const remain = unlimited
    ? limit
    : atCap
      ? 0
      : Number(
          st.scan_points_remaining ??
            st.eco_points_remaining ??
            Math.max(0, limit - earned)
        );
  return {
    ...st,
    scan_points_unlimited: unlimited,
    scan_points_earned: earned,
    scan_points_limit: limit,
    scan_points_remaining: remain,
    eco_at_cap: atCap,
    at_daily_cap: atCap,
  };
}

function scanQuotaPayloadEqual(a, b) {
  if (!a || !b) return false;
  return (
    a.day === b.day &&
    a.scan_points_earned === b.scan_points_earned &&
    a.scan_points_limit === b.scan_points_limit &&
    a.scan_points_remaining === b.scan_points_remaining &&
    !!a.eco_at_cap === !!b.eco_at_cap &&
    !!a.at_daily_cap === !!b.at_daily_cap &&
    a.user_points === b.user_points &&
    a.uid_submitted_today === b.uid_submitted_today &&
    a.uid_submit_progress === b.uid_submit_progress &&
    a.uid_submit_need === b.uid_submit_need
  );
}

async function savePasserbySharedScanQuotaCache(st) {
  const norm = normalizeEcoScanQuotaStatus(st);
  if (!norm || norm.scan_points_limit == null) return null;
  const payload = {
    day: localDayKey(),
    ts: Date.now(),
    ...norm,
    daily_used: norm.scan_points_earned,
    daily_limit: norm.scan_points_limit,
    daily_remaining: norm.scan_points_remaining,
  };
  const prev = await BiliIndex.storageGet(["passerbySharedScanStatusCache"]);
  const existing = prev.passerbySharedScanStatusCache;
  if (existing && scanQuotaPayloadEqual(existing, payload)) {
    return norm;
  }
  await BiliIndex.storageSet({
    passerbySharedScanStatusCache: payload,
    passerbyEcoScanStatusCache: payload,
    passerbyScanQuotaCache: payload,
  });
  return norm;
}

async function savePasserbyEcoScanQuotaCache(st) {
  return savePasserbySharedScanQuotaCache(st);
}

async function loadPasserbyEcoScanQuotaCache() {
  const { passerbySharedScanStatusCache: shared, passerbyEcoScanStatusCache: legacy } =
    await BiliIndex.storageGet([
      "passerbySharedScanStatusCache",
      "passerbyEcoScanStatusCache",
    ]);
  const c = shared || legacy;
  if (!c || c.day !== localDayKey()) return null;
  return normalizeEcoScanQuotaStatus(c);
}

async function savePasserbyScanQuotaCache(st) {
  return savePasserbySharedScanQuotaCache(st);
}

async function loadPasserbyScanQuotaCache() {
  const { passerbySharedScanStatusCache: shared, passerbyScanQuotaCache: legacy } =
    await BiliIndex.storageGet([
      "passerbySharedScanStatusCache",
      "passerbyScanQuotaCache",
    ]);
  const c = shared || legacy;
  if (!c || c.day !== localDayKey()) return null;
  return normalizeEcoScanQuotaStatus(c);
}

async function refreshSharedPasserbyScanQuota(opts = {}) {
  const token = await BiliIndex.getToken();
  if (!token) return null;
  try {
    const st = await BiliIndex.fetchPasserbyScanStatus();
    const merged = await savePasserbySharedScanQuotaCache(st);
    applyPasserbyScanStatusCache(merged || st);
    const mode = await resolvePasserbyScanMode();
    const quotaClassic = document.getElementById("passerby-scan-quota");
    const quotaEco = document.getElementById("passerby-eco-scan-quota");
    const classicCb = document.getElementById("passerby-scan-enabled");
    const ecoCb = document.getElementById("passerby-eco-scan-enabled");
    const norm = merged || normalizeEcoScanQuotaStatus(st) || st;
    if (quotaClassic && (opts.force || classicCb?.checked)) {
      quotaClassic.textContent = formatPasserbyQuotaText(norm);
      quotaClassic.classList.remove("hidden");
    }
    if (quotaEco && (opts.force || ecoCb?.checked)) {
      quotaEco.textContent = formatEcoQuotaText(norm);
      quotaEco.classList.remove("hidden");
    }
    return merged || st;
  } catch {
    const cached = await loadPasserbyEcoScanQuotaCache();
    if (cached) applyPasserbyScanStatusCache(cached);
    return cached;
  }
}

function formatPasserbyQuotaText(st) {
  const used = st.daily_used ?? st.scan_points_earned ?? 0;
  const limit = st.daily_limit ?? st.scan_points_limit ?? 100;
  const remain = st.daily_remaining ?? st.scan_points_remaining ?? Math.max(0, limit - used);
  return `扫描积分（共用）：${used}/${limit}（剩余 ${remain}）`;
}

async function resolvePasserbyScanMode() {
  const st = await BiliIndex.storageGet([
    "passerbyScanMode",
    "passerbyScanEnabled",
    "passerbyEcoScanEnabled",
  ]);
  if (st.passerbyScanMode === "eco" || st.passerbyEcoScanEnabled) return "eco";
  if (st.passerbyScanMode === "classic" || st.passerbyScanEnabled) return "classic";
  return "";
}

async function setPasserbyScanMode(mode) {
  if (mode === "classic") {
    await BiliIndex.storageSet({
      passerbyScanMode: "classic",
      passerbyScanEnabled: true,
      passerbyEcoScanEnabled: false,
      passerbyScanHintPending: true,
    });
  } else if (mode === "eco") {
    await BiliIndex.storageSet({
      passerbyScanMode: "eco",
      passerbyScanEnabled: false,
      passerbyEcoScanEnabled: true,
      passerbyScanPausedUntil: null,
    });
  } else {
    await BiliIndex.storageSet({
      passerbyScanMode: "",
      passerbyScanEnabled: false,
      passerbyEcoScanEnabled: false,
    });
  }
}

function canUsePasserbyEcoScan(membership, isTagger) {
  return !!isTagger || !!membership?.is_full_member;
}

function formatEcoQuotaText(st) {
  const norm = normalizeEcoScanQuotaStatus(st) || st;
  const earned = norm.scan_points_earned ?? 0;
  const limit = norm.scan_points_limit ?? 100;
  const remain = norm.scan_points_remaining ?? Math.max(0, limit - earned);
  const atCap = !!norm.eco_at_cap;
  const unlimited = !!norm.scan_points_unlimited;
  const isTagger = !!norm.is_tagger;
  const batch = norm.uid_batch_size ?? norm.eco_uid_batch_size ?? norm.eco_batch_size ?? 200;
  const progress = norm.uid_submit_progress ?? 0;
  const submitted = norm.uid_submitted_today ?? 0;
  let line = unlimited
    ? isTagger
      ? `打标员 · 节能扫描不限次（今日已提交 ${submitted} 人）`
      : `扫描积分（共用）：${earned}（超级管理员 · 节能扫描不限次）`
    : `扫描积分（共用）：${earned}/${limit}（剩余 ${remain}）`;
  if (atCap) {
    line += " · 今日扫描积分已满，次日 0:00 重置";
    return line;
  }
  const need = norm.uid_submit_need ?? norm.need_for_next_point ?? 0;
  if (need > 0) {
    line += ` · 提交进度 ${progress}/${batch}，还差 ${need} 人得 1 积分（今日已提交 ${submitted} 人）`;
  } else if (submitted <= 0) {
    line += ` · 再提交 ${batch} 人可得 1 积分`;
  }
  return line;
}

const passerbyQuotaDisplayCache = { eco: "", classic: "", points: "" };

function applyPasserbyScanStatusCache(st) {
  if (!st) return;
  const norm = normalizeEcoScanQuotaStatus(st) || st;
  const quotaEco = document.getElementById("passerby-eco-scan-quota");
  const quotaClassic = document.getElementById("passerby-scan-quota");
  const ecoCb = document.getElementById("passerby-eco-scan-enabled");
  const classicCb = document.getElementById("passerby-scan-enabled");
  if (quotaEco && ecoCb?.checked) {
    const ecoText = formatEcoQuotaText(norm);
    if (passerbyQuotaDisplayCache.eco !== ecoText) {
      passerbyQuotaDisplayCache.eco = ecoText;
      quotaEco.textContent = ecoText;
    }
    quotaEco.classList.remove("hidden");
  }
  if (quotaClassic && classicCb?.checked) {
    const classicText = formatPasserbyQuotaText(norm);
    if (passerbyQuotaDisplayCache.classic !== classicText) {
      passerbyQuotaDisplayCache.classic = classicText;
      quotaClassic.textContent = classicText;
    }
    quotaClassic.classList.remove("hidden");
  }
  const earned = norm.scan_points_earned ?? norm.eco_points_earned ?? norm.daily_used;
  const pointsEl = document.getElementById("points-status");
  if (
    pointsEl &&
    earned != null &&
    String(pointsEl.textContent).startsWith("积分：") &&
    !norm.is_tagger
  ) {
    const base = st.user_points;
    if (base != null) {
      const pointsText = `积分：${base}`;
      if (passerbyQuotaDisplayCache.points !== pointsText) {
        passerbyQuotaDisplayCache.points = pointsText;
        pointsEl.textContent = pointsText;
      }
    }
  }
}

async function refreshPasserbyEcoScanQuota(opts = {}) {
  const cb = document.getElementById("passerby-eco-scan-enabled");
  if (!opts.force && !cb?.checked) {
    document.getElementById("passerby-eco-scan-quota")?.classList.add("hidden");
    return;
  }
  await refreshSharedPasserbyScanQuota(opts);
}

async function refreshPasserbyScanQuota(opts = {}) {
  const cb = document.getElementById("passerby-scan-enabled");
  const { passerbyScanPausedUntil } = await BiliIndex.storageGet([
    "passerbyScanPausedUntil",
  ]);
  const paused = Number(passerbyScanPausedUntil) > Date.now();
  if (!opts.force && !cb?.checked && !paused) {
    document.getElementById("passerby-scan-quota")?.classList.add("hidden");
    return;
  }
  await refreshSharedPasserbyScanQuota({ ...opts, force: true });
}

async function loadPasserbyScanUi(pointsOverride, membership, isTagger = false) {
  const cb = document.getElementById("passerby-scan-enabled");
  const quotaEl = document.getElementById("passerby-scan-quota");
  const ecoCb = document.getElementById("passerby-eco-scan-enabled");
  if (!cb) return;
  if (ecoCb) {
    if (!canUsePasserbyEcoScan(membership, isTagger)) {
      ecoCb.checked = false;
      ecoCb.disabled = true;
      if ((await resolvePasserbyScanMode()) === "eco") {
        await setPasserbyScanMode("");
      }
    } else {
      ecoCb.disabled = false;
    }
  }
  const mode = await resolvePasserbyScanMode();
  const sched = BiliIndex.reconcilePasserbyScanSchedule
    ? await BiliIndex.reconcilePasserbyScanSchedule()
    : null;
  const pausedUntil = isTagger ? 0 : sched?.pausedUntil || 0;
  const now = Date.now();
  if (pausedUntil > now) {
    cb.checked = false;
    cb.disabled = true;
    const hint = BiliIndex.formatLocalMidnightHint(pausedUntil);
    if (quotaEl) {
      quotaEl.textContent = `今日已满额，${hint} 自动重新开启`;
      quotaEl.classList.remove("hidden");
    }
    void refreshPasserbyScanQuota({ force: true });
  } else {
    cb.disabled = false;
    const classicOn = sched ? !!sched.enabled : false;
    cb.checked = mode === "classic" && classicOn;
    if (ecoCb) ecoCb.checked = mode === "eco";
    if (cb.checked) {
      setTimeout(() => void refreshPasserbyScanQuota(), 0);
    } else if (quotaEl) {
      quotaEl.classList.add("hidden");
    }
  }
  if (!isTagger && typeof pointsOverride === "number") {
    const last = (await BiliIndex.storageGet(["lastPasserbyScanResult"]))
      .lastPasserbyScanResult;
    const ecoLast = (await BiliIndex.storageGet(["lastPasserbyEcoScanResult"]))
      .lastPasserbyEcoScanResult;
    const recent =
      (last?.points_earned && Date.now() - (last.ts || 0) < 60000 && last) ||
      (ecoLast?.points_earned && Date.now() - (ecoLast.ts || 0) < 60000 && ecoLast);
    if (recent?.points_earned) {
      $("#points-status").textContent = `积分：${pointsOverride}（最近扫描 +${recent.points_earned}）`;
    }
  }
}

async function loadPasserbyEcoScanUi(membership, isTagger = false) {
  const cb = document.getElementById("passerby-eco-scan-enabled");
  const quotaEl = document.getElementById("passerby-eco-scan-quota");
  if (!cb) return;
  if (!canUsePasserbyEcoScan(membership, isTagger)) {
    cb.checked = false;
    cb.disabled = true;
    if (quotaEl) quotaEl.classList.add("hidden");
    return;
  }
  cb.disabled = false;
  const mode = await resolvePasserbyScanMode();
  cb.checked = mode === "eco";
  void refreshPasserbyEcoScanQuota({ force: cb.checked });
  if (!cb.checked && quotaEl) {
    quotaEl.classList.add("hidden");
  }
}

function initPasserbyScanToggle() {
  const cb = document.getElementById("passerby-scan-enabled");
  const labelBtn = document.getElementById("passerby-scan-label");
  const hintBox = document.getElementById("passerby-scan-hint-box");
  if (!cb) return;

  if (hintBox) {
    const html =
      (typeof BiliIndex !== "undefined" && BiliIndex.PASSERBY_SCAN_HINT_HTML) ||
      "请先勾选左侧开关，再打开 B 站视频页；右下角会显示扫描状态。每 4 秒处理 1 人（库内已有则快速跳过）：先标路人，粉丝≤100 或投稿≤1 则保留并 +1 积分（每日 100 次，满额后自动关闭至次日 0:00 再开）。";
    hintBox.innerHTML = html;
  }

  labelBtn?.addEventListener("click", () => {
    if (!hintBox) return;
    const open = hintBox.classList.toggle("hidden");
    const expanded = !open;
    labelBtn.setAttribute("aria-expanded", expanded ? "true" : "false");
  });

  cb.addEventListener("change", async () => {
    const { passerbyScanPausedUntil } = await BiliIndex.storageGet([
      "passerbyScanPausedUntil",
    ]);
    if (Number(passerbyScanPausedUntil) > Date.now()) {
      cb.checked = false;
      return;
    }
    if (cb.checked) {
      await setPasserbyScanMode("classic");
      const ecoCb = document.getElementById("passerby-eco-scan-enabled");
      if (ecoCb) ecoCb.checked = false;
    } else if ((await resolvePasserbyScanMode()) === "classic") {
      await setPasserbyScanMode("");
    }
    const ecoQuota = document.getElementById("passerby-eco-scan-quota");
    if (ecoQuota) ecoQuota.classList.add("hidden");
    if (cb.checked) {
      void refreshSharedPasserbyScanQuota({ force: true });
    } else {
      document.getElementById("passerby-scan-quota")?.classList.add("hidden");
    }
  });
}

function initPasserbyEcoScanToggle() {
  const cb = document.getElementById("passerby-eco-scan-enabled");
  const labelBtn = document.getElementById("passerby-eco-scan-label");
  const hintBox = document.getElementById("passerby-eco-scan-hint-box");
  if (!cb) return;

  if (hintBox) {
    hintBox.innerHTML =
      (typeof BiliIndex !== "undefined" && BiliIndex.PASSERBY_ECO_SCAN_HINT_HTML) ||
      "节能扫描：每提交 200 个不同 UID 得 1 积分，与自动扫描路人共用每日 100 积分。";
  }

  labelBtn?.addEventListener("click", () => {
    if (!hintBox) return;
    const open = hintBox.classList.toggle("hidden");
    labelBtn.setAttribute("aria-expanded", open ? "false" : "true");
  });

  cb.addEventListener("change", async () => {
    if (cb.checked) {
      try {
        const [me, membership] = await Promise.all([
          BiliIndex.fetchMe(),
          BiliIndex.fetchMembership(),
        ]);
        if (!canUsePasserbyEcoScan(membership, me?.is_tagger)) {
          cb.checked = false;
          const quotaEl = document.getElementById("passerby-eco-scan-quota");
          if (quotaEl) {
            quotaEl.textContent = membership?.is_member
              ? "节能扫描需月卡会员（4000 积分兑换）"
              : "节能扫描为月卡会员功能，请开通或兑换会员";
            quotaEl.classList.remove("hidden");
          }
          return;
        }
      } catch {
        cb.checked = false;
        return;
      }
      await setPasserbyScanMode("eco");
      const classicCb = document.getElementById("passerby-scan-enabled");
      if (classicCb) classicCb.checked = false;
      document.getElementById("passerby-scan-quota")?.classList.add("hidden");
    } else if ((await resolvePasserbyScanMode()) === "eco") {
      await setPasserbyScanMode("");
    }
    if (cb.checked) {
      void refreshSharedPasserbyScanQuota({ force: true });
    } else {
      document.getElementById("passerby-eco-scan-quota")?.classList.add("hidden");
    }
  });
}

async function renderLoggedIn() {
  const token = await BiliIndex.getToken();
  if (!token) {
    currentUserIsSuperAdmin = false;
    updateSuperAdminSettingsUi(null);
    $("#logged-out").classList.remove("hidden");
    $("#logged-in").classList.add("hidden");
    $("#block-panel")?.classList.add("hidden");
    const activeTab = document.querySelector(".tab.active")?.dataset?.tab || "login";
    if (typeof BiliIndexCaptcha !== "undefined") {
      BiliIndexCaptcha.setVisible(activeTab === "login" || activeTab === "register");
    }
    void checkAnnouncementNotifications();
    void updateLoginApiHint();
    return;
  }
  if (typeof BiliIndexCaptcha !== "undefined") {
    BiliIndexCaptcha.setVisible(false);
  }

  try {
    const me = await BiliIndex.fetchMe();
    const membership = await BiliIndex.fetchMembership();
    const isTagger = !!me.is_tagger;
    $("#logged-out").classList.add("hidden");
    $("#logged-in").classList.remove("hidden");
    $("#username-label").textContent = me.username + (isTagger ? "（打标员）" : "");
    if (isTagger) {
      $("#membership-status").textContent = "打标员 · 扫描不限次";
      $("#points-status").textContent = "打标员无积分";
      $("#block-panel")?.classList.add("hidden");
      document.querySelectorAll(".member-purchase-row, .points-exchange-row, #btn-buy-member").forEach((el) => {
        el.classList.add("hidden");
      });
    } else {
      $("#membership-status").textContent = membership.label || "免费用户";
      $("#points-status").textContent = `积分：${me.points}`;
      document.querySelectorAll(".member-purchase-row, .points-exchange-row, #btn-buy-member").forEach((el) => {
        el.classList.remove("hidden");
      });
    }
    void loadPasserbyScanUi(me.points, membership, isTagger);
    void loadPasserbyEcoScanUi(membership, isTagger);
    if (!isTagger) {
      await showBanner(membership);
    }
    await checkBugNotifications();
    await checkAnnouncementNotifications();
    await loadTags(false);
    if (!isTagger) {
      await loadAndRenderBlockPanel(membership);
      if (typeof BiliIndex.syncKeywordBlocks === "function") {
        await BiliIndex.syncKeywordBlocks();
      }
    }
    updateSuperAdminSettingsUi(me);
  } catch (e) {
    await BiliIndex.clearSession();
    await BiliIndex.invalidateTagCache();
    renderLoggedIn();
  }
}

$("#form-login").addEventListener("submit", async (e) => {
  e.preventDefault();
  showAuthError("");
  const fd = new FormData(e.target);
  const username = String(fd.get("username") || "").trim();
  const password = String(fd.get("password") || "");
  const rememberPassword = $("#remember-password").checked;
  const autoLogin = $("#auto-login").checked;
  try {
    const captcha = BiliIndexCaptcha.getPayload();
    await BiliIndex.login(username, password, captcha);
    BiliIndexCaptcha.resetAfterSubmit();
    await BiliIndex.saveLoginPrefs({
      username,
      password,
      rememberPassword,
      autoLogin,
    });
    await BiliIndex.refreshTagCaches(true);
    await renderLoggedIn();
  } catch (err) {
    showAuthError(err.message);
    if (typeof BiliIndexCaptcha !== "undefined") {
      if (BiliIndexCaptcha.isCaptchaError(err.message)) {
        BiliIndexCaptcha.setVisible(true);
        void BiliIndexCaptcha.refresh();
      }
    }
  }
});

$("#form-reset").addEventListener("submit", async (e) => {
  e.preventDefault();
  showAuthError("");
  const fd = new FormData(e.target);
  const username = String(fd.get("username") || "").trim();
  const resetKey = String(fd.get("reset_key") || "").replace(/\D/g, "");
  const password = String(fd.get("password") || "");
  const password2 = String(fd.get("password2") || "");
  if (resetKey.length !== 16) {
    showAuthError("重置密钥须为 16 位数字");
    return;
  }
  if (password !== password2) {
    showAuthError("两次输入的新密码不一致");
    return;
  }
  try {
    const res = await BiliIndex.resetPassword(username, resetKey, password);
    showAuthError(res.message || "密码已重置，请使用新密码登录", false);
    e.target.reset();
    switchTab("login");
    $("#form-login input[name=username]").value = username;
  } catch (err) {
    showAuthError(err.message);
  }
});

$("#form-register").addEventListener("submit", async (e) => {
  e.preventDefault();
  showAuthError("");
  const fd = new FormData(e.target);
  const acceptPrivacy = document.getElementById("register-accept-privacy")?.checked;
  if (!acceptPrivacy) {
    showAuthError("请先阅读并同意隐私协议");
    return;
  }
  try {
    const captcha = BiliIndexCaptcha.getPayload();
    await BiliIndex.register(fd.get("username"), fd.get("password"), captcha, {
      accept_privacy: true,
    });
    BiliIndexCaptcha.resetAfterSubmit();
    await BiliIndex.refreshTagCaches(true);
    await renderLoggedIn();
  } catch (err) {
    showAuthError(err.message);
    if (typeof BiliIndexCaptcha !== "undefined") {
      if (BiliIndexCaptcha.isCaptchaError(err.message)) {
        BiliIndexCaptcha.setVisible(true);
        void BiliIndexCaptcha.refresh();
      }
    }
  }
});

$("#btn-logout").addEventListener("click", async () => {
  await BiliIndex.clearSession();
  await BiliIndex.invalidateTagCache();
  $("#action-msg").textContent = "";
  await renderLoggedIn();
});

async function runPointsExchange(plan) {
  $("#action-msg").textContent = "兑换中…";
  try {
    const res = await BiliIndex.exchangeMembership(plan);
    $("#action-msg").textContent = res.message || "兑换成功";
    await renderLoggedIn();
  } catch (err) {
    $("#action-msg").textContent = err.message;
  }
}

$("#btn-exchange-points-member")?.addEventListener("click", () => {
  void runPointsExchange("points_member");
});

$("#btn-exchange-monthly")?.addEventListener("click", () => {
  void runPointsExchange("monthly");
});

$("#btn-refresh-tags").addEventListener("click", async () => {
  await loadTags(true);
  const membership = await BiliIndex.fetchMembership();
  shieldMembership = membership;
  renderShieldPool();
  await Promise.all([loadSuperHateTags(), loadSuperHateUps()]);
  void refreshPasserbyScanQuota();
});

$("#btn-add-super-hate-up")?.addEventListener("click", () => void addSuperHateUpFromCurrentPage());

$("#shield-tab-block")?.addEventListener("click", () => {
  if (!shieldMembership?.is_member) {
    $("#block-save-status").textContent =
      "评论区屏蔽为会员功能，请开通或兑换会员后使用";
  }
  setShieldMode("block");
});

$("#shield-tab-super")?.addEventListener("click", () => setShieldMode("super_hate"));

$("#shield-tab-keyword")?.addEventListener("click", () => setShieldMode("keyword"));

$("#keyword-block-inputs")?.addEventListener("click", (e) => {
  const row = e.target.closest?.(".keyword-slot-row.is-locked");
  if (!row?.dataset?.lockMsg) return;
  $("#block-save-status").textContent = row.dataset.lockMsg;
});

$("#btn-save-block-tags")?.addEventListener("click", async () => {
  const status = $("#block-save-status");
  if (shieldMode === "keyword") {
    const keywords = readKeywordInputsFromDom();
    const max = resolveKeywordMax(shieldMembership);
    const seen = new Set();
    for (let i = 0; i < keywords.length; i++) {
      const word = keywords[i];
      if (!word) continue;
      if (i >= max) {
        status.textContent = keywordSlotLockMessage(i, shieldMembership);
        return;
      }
      if (!/^[\u4e00-\u9fff]{1,5}$/.test(word)) {
        status.textContent = `关键词 ${i + 1} 须为 1～5 个汉字`;
        return;
      }
      if (seen.has(word)) {
        status.textContent = "关键词不可重复";
        return;
      }
      seen.add(word);
    }
    status.textContent = "保存中…";
    try {
      const res = await BiliIndex.saveKeywordBlocks(keywords);
      keywordBlockValues = (res.keywords || keywords).slice(0, 5);
      fillKeywordInputs(keywordBlockValues);
      if (typeof BiliIndex.syncKeywordBlocks === "function") {
        await BiliIndex.syncKeywordBlocks();
      }
      status.textContent = "已保存。请刷新 B 站页面后生效";
    } catch (e) {
      status.textContent = e.message || "保存失败";
    }
    return;
  }
  if (shieldMode === "super_hate") {
    const overlap = superHateSelected.filter((t) =>
      blockSelected.some((b) => b.code === t.code)
    );
    if (overlap.length) {
      status.textContent = "所选标签与评论区屏蔽重复，请更换";
      return;
    }
    status.textContent = "保存中…";
    try {
      const codes = superHateSelected.map((t) => t.code).slice(0, SUPER_HATE_TAG_MAX);
      const res = await BiliIndex.saveSuperHateTags(codes);
      superHateSelected = (res.tags || []).map((t) => ({
        id: t.id,
        code: t.code,
        name: t.name,
        tag_kind: t.tag_kind,
        icon: t.icon,
      }));
      const tagPayload = superHateSelected.map((t) => ({
        id: t.id,
        code: t.code,
        name: t.name,
        tag_kind: t.tag_kind,
        icon: BiliIndex.resolveTagIcon?.(t) || t.icon || "",
      }));
      await chrome.storage.local.set({
        superHateTagCodes: tagPayload.map((t) => t.code),
        superHateTags: tagPayload,
      });
      if (typeof BiliIndex.syncSuperHateRules === "function") {
        await BiliIndex.syncSuperHateRules();
      }
      renderShieldSelected();
      renderShieldPool();
      status.textContent = "已保存。若当前页未遮挡请刷新一次 B 站页面";
    } catch (e) {
      status.textContent = e.message || "保存失败";
    }
    return;
  }
  if (!shieldMembership?.is_member) {
    status.textContent = "评论区屏蔽为会员功能，请开通或兑换会员后使用";
    return;
  }
  const overlap = blockSelected.filter((t) =>
    superHateSelected.some((s) => s.code === t.code)
  );
  if (overlap.length) {
    status.textContent = "所选标签与超级讨厌标配重复，请更换";
    return;
  }
  status.textContent = "保存中…";
  try {
    const codes = blockSelected.map((t) => t.code);
    const res = await BiliIndex.saveBlockTags(codes);
    await chrome.storage.local.set({
      blockTagCodes: res.codes || codes,
      blockIsMember: !!res.is_member,
    });
    blockSelected = (res.tags || []).map((t) => ({
      id: t.id,
      code: t.code,
      name: t.name,
      tag_kind: t.tag_kind,
      icon: t.icon,
    }));
    renderBlockSelected();
    renderShieldPool();
    status.textContent = "已保存。请刷新 B 站页面后生效";
  } catch (e) {
    status.textContent = e.message || "保存失败";
  }
});

$("#btn-pick-up-mid").addEventListener("click", pickUpMidFromPage);

function syncApiModePanelUi(mode) {
  const isLocal = mode === "local";
  document.getElementById("api-mode-local")?.classList.toggle("active", isLocal);
  document.getElementById("api-mode-cloud")?.classList.toggle("active", !isLocal);
  document
    .getElementById("api-mode-local")
    ?.setAttribute("aria-checked", isLocal ? "true" : "false");
  document
    .getElementById("api-mode-cloud")
    ?.setAttribute("aria-checked", !isLocal ? "true" : "false");
  document.getElementById("api-mode-local-pane")?.classList.toggle("hidden", !isLocal);
  document.getElementById("api-mode-cloud-pane")?.classList.toggle("hidden", isLocal);
  const localEl = document.getElementById("local-api-base-display");
  const cloudEl = document.getElementById("cloud-api-base-display");
  if (localEl) localEl.textContent = BiliIndex.LOCAL_API_BASE;
  void BiliIndex.resolveCloudApiBase?.().then((base) => {
    if (cloudEl) cloudEl.textContent = `当前生效：${base}`;
  });
}

async function loadApiModeSettingsUi() {
  const mode = await BiliIndex.getApiMode();
  const { customCloudApiBase } = await BiliIndex.storageGet(["customCloudApiBase"]);
  const input = document.getElementById("custom-cloud-api-base");
  if (input) input.value = customCloudApiBase || "";
  syncApiModePanelUi(mode);
}

async function saveCustomCloudApiBase() {
  const input = document.getElementById("custom-cloud-api-base");
  const status = document.getElementById("cloud-api-status");
  const raw = input?.value?.trim() || "";
  try {
    const base = await BiliIndex.setCustomCloudApiBase(raw);
    if (status) status.textContent = raw ? `已保存：${base}` : "已恢复默认官方地址";
    syncApiModePanelUi(await BiliIndex.getApiMode());
    await BiliIndex.invalidateTagCache?.();
    if (await BiliIndex.getToken()) {
      await loadTags(true);
    }
  } catch (e) {
    if (status) status.textContent = e.message || "保存失败";
  }
}

async function testCloudApiConnection() {
  const input = document.getElementById("custom-cloud-api-base");
  const status = document.getElementById("cloud-api-status");
  const raw = input?.value?.trim() || "";
  const base =
    BiliIndex._normalizeApiBaseUrl(raw) || BiliIndex.CLOUD_API_BASE;
  if (status) status.textContent = "测试中…";
  try {
    const okPerm = await BiliIndex.ensureApiHostPermission(base);
    if (!okPerm) throw new Error("未授予访问该 API 的权限");
    const data = await BiliIndex.probeApiHealth(base);
    const bind = data?.device_binding_enabled;
    const bindHint =
      bind === false
        ? " · 多机登录已开启"
        : bind === true
          ? " · 一机一号已开启"
          : "";
    if (status) {
      status.textContent = `连接成功：${base}（${data?.status || "ok"}${bindHint}）`;
    }
  } catch (e) {
    if (status) {
      status.textContent = `连接失败：${BiliIndex.humanizeApiError(e, base)}`;
    }
  }
}

async function applyApiMode(mode) {
  const status = document.getElementById("cloud-api-status");
  try {
    await BiliIndex.setApiMode(mode);
    syncApiModePanelUi(mode);
    await BiliIndex.invalidateTagCache();
    if (await BiliIndex.getToken()) {
      await loadTags(true);
    }
    if (status && mode === "cloud") status.textContent = "";
  } catch (e) {
    const current = await BiliIndex.getApiMode();
    syncApiModePanelUi(current);
    if (status) status.textContent = e.message || "切换联网模式失败";
  }
}

function initApiModeSettings() {
  document.querySelectorAll("[data-api-mode]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const mode = btn.getAttribute("data-api-mode");
      if (mode !== "local" && mode !== "cloud") return;
      void applyApiMode(mode);
    });
  });
  document.getElementById("btn-save-cloud-api")?.addEventListener("click", () => {
    void saveCustomCloudApiBase();
  });
  document.getElementById("btn-test-cloud-api")?.addEventListener("click", () => {
    void testCloudApiConnection();
  });
}

const POPUP_HEIGHT_KEY = "popupHeight";
const POPUP_HEIGHT_MIN = 400;
/** Chrome 扩展图标弹窗高度上限约 600px */
const POPUP_HEIGHT_MAX_ACTION = 600;
const POPUP_HEIGHT_MAX_STANDALONE = 1300;
const POPUP_HEIGHT_DEFAULT = 580;

const isStandaloneMode = () =>
  new URLSearchParams(location.search).get("mode") === "window";

function getPopupHeightMax() {
  return isStandaloneMode() ? POPUP_HEIGHT_MAX_STANDALONE : POPUP_HEIGHT_MAX_ACTION;
}

function clampPopupHeight(px) {
  return Math.min(getPopupHeightMax(), Math.max(POPUP_HEIGHT_MIN, Math.round(px)));
}

function applyPopupHeight(px) {
  const height = clampPopupHeight(px);
  document.documentElement.style.height = `${height}px`;
  document.body.style.height = `${height}px`;
  return height;
}

function updateResizeHint() {
  const hint = document.getElementById("popup-resize-hint");
  if (!hint) return;
  if (isStandaloneMode()) {
    hint.textContent = "大窗口模式：可拖至约 1300px 高";
    return;
  }
  hint.textContent = `浏览器限制图标弹窗最高约 ${POPUP_HEIGHT_MAX_ACTION}px，需要更高请点「大窗口」`;
}

function openStandaloneWindow() {
  const url = chrome.runtime.getURL("popup/popup.html?mode=window");
  chrome.tabs.create({ url });
}

function initPopupResize() {
  if (isStandaloneMode()) {
    document.documentElement.classList.add("popup-standalone-mode");
    document.documentElement.style.height = "100vh";
    document.body.style.height = "100vh";
    updateResizeHint();
    return;
  }

  const handle = document.getElementById("popup-resize-handle");
  if (!handle) return;
  updateResizeHint();

  chrome.storage.local.get([POPUP_HEIGHT_KEY], (stored) => {
    const h = stored[POPUP_HEIGHT_KEY];
    const raw = typeof h === "number" ? h : POPUP_HEIGHT_DEFAULT;
    applyPopupHeight(Math.min(raw, POPUP_HEIGHT_MAX_ACTION));
  });

  const onMove = (e) => {
    applyPopupHeight(e.clientY);
  };

  const onUp = () => {
    document.body.classList.remove("is-resizing-popup");
    document.removeEventListener("mousemove", onMove);
    document.removeEventListener("mouseup", onUp);
    let height = applyPopupHeight(document.documentElement.clientHeight);
    if (!isStandaloneMode()) {
      height = Math.min(height, POPUP_HEIGHT_MAX_ACTION);
      applyPopupHeight(height);
    }
    chrome.storage.local.set({ [POPUP_HEIGHT_KEY]: height });
  };

  handle.addEventListener("mousedown", (e) => {
    e.preventDefault();
    document.body.classList.add("is-resizing-popup");
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  });
}

function openPopupModal(title, html) {
  const modal = document.getElementById("popup-modal");
  const titleEl = document.getElementById("popup-modal-title");
  const bodyEl = document.getElementById("popup-modal-body");
  if (!modal || !titleEl || !bodyEl) return;
  titleEl.textContent = title;
  bodyEl.innerHTML = html;
  modal.classList.remove("hidden");
  modal.setAttribute("aria-hidden", "false");
}

function closePopupModal() {
  const modal = document.getElementById("popup-modal");
  if (!modal) return;
  modal.classList.add("hidden");
  modal.setAttribute("aria-hidden", "true");
}

const LAST_SEEN_ANNOUNCEMENT_KEY = "lastSeenAnnouncementId";
let settingsHasBugUnread = false;
let settingsHasAnnounceUnread = false;

function syncSettingsGearDot() {
  const show = settingsHasBugUnread || settingsHasAnnounceUnread;
  document
    .getElementById("settings-notify-dot")
    ?.classList.toggle("hidden", !show);
}

function setSettingsNotifyBadge(hasUnread) {
  settingsHasBugUnread = !!hasUnread;
  document
    .getElementById("settings-menu-notify-dot")
    ?.classList.toggle("hidden", !hasUnread);
  syncSettingsGearDot();
}

function setSettingsAnnounceBadge(hasUnread) {
  settingsHasAnnounceUnread = !!hasUnread;
  document
    .getElementById("settings-menu-announce-dot")
    ?.classList.toggle("hidden", !hasUnread);
  syncSettingsGearDot();
}

async function getLastSeenAnnouncementId() {
  try {
    const stored = await chrome.storage.local.get([LAST_SEEN_ANNOUNCEMENT_KEY]);
    const n = parseInt(stored[LAST_SEEN_ANNOUNCEMENT_KEY], 10);
    return Number.isFinite(n) && n > 0 ? n : 0;
  } catch {
    return 0;
  }
}

async function setLastSeenAnnouncementId(id) {
  const n = parseInt(id, 10);
  if (!Number.isFinite(n) || n <= 0) return;
  try {
    await chrome.storage.local.set({ [LAST_SEEN_ANNOUNCEMENT_KEY]: n });
  } catch {
    /* ignore */
  }
}

function renderRichHtmlInto(el, html) {
  if (!el) return;
  const text = (html || "").trim();
  if (!text) {
    el.innerHTML = '<p class="empty-hint">暂无正文</p>';
    return;
  }
  if (/<[a-z][\s\S]*>/i.test(text)) {
    el.innerHTML = text;
  } else {
    const esc = (s) =>
      String(s)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
    el.innerHTML = `<div class="author-note-text">${esc(text)}</div>`;
  }
}

function renderAnnouncementBody(html) {
  const bodyEl = document.getElementById("popup-modal-body");
  renderRichHtmlInto(bodyEl, html);
}

async function loadPrivacyPolicyRevision(revisionId = null) {
  const bodyEl = document.getElementById("popup-modal-body");
  if (!bodyEl) return;
  try {
    const base = await BiliIndex.getApiBase();
    const versionsRes = await fetch(`${base}/api/content/privacy-policy/versions?limit=100`);
    const versionsData = await versionsRes.json().catch(() => ({}));
    if (!versionsRes.ok) {
      throw new Error(versionsData.detail || versionsData.message || versionsRes.statusText);
    }
    const versions = Array.isArray(versionsData.items) ? versionsData.items : [];
    const detailUrl = revisionId
      ? `${base}/api/content/privacy-policy/versions/${revisionId}`
      : `${base}/api/content/privacy-policy`;
    const detailRes = await fetch(detailUrl);
    const data = await detailRes.json().catch(() => ({}));
    if (!detailRes.ok) {
      throw new Error(data.detail || data.message || detailRes.statusText);
    }
    const selectedId = data.revision_id || data.id || null;
    let barHtml = "";
    if (versions.length > 1) {
      const esc = (s) =>
        String(s)
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/"/g, "&quot;");
      const opts = versions
        .map((v) => {
          const when = v.published_at
            ? new Date(v.published_at).toLocaleString()
            : `#${v.id}`;
          const label = v.is_current ? `${when}（当前）` : when;
          const sel = selectedId && v.id === selectedId ? " selected" : "";
          return `<option value="${v.id}"${sel}>${esc(label)}</option>`;
        })
        .join("");
      barHtml = `<div class="privacy-version-bar"><label for="privacy-version-select">历史版本</label><select id="privacy-version-select">${opts}</select></div>`;
    }
    bodyEl.innerHTML = `${barHtml}<div id="privacy-policy-content"></div>`;
    renderRichHtmlInto(document.getElementById("privacy-policy-content"), data.body || "");
    const select = document.getElementById("privacy-version-select");
    select?.addEventListener("change", () => {
      const id = parseInt(select.value, 10);
      if (Number.isFinite(id) && id > 0) void loadPrivacyPolicyRevision(id);
    });
  } catch (err) {
    bodyEl.innerHTML = `<p class="empty-hint">加载失败：${err.message}</p>`;
  }
}

async function openPrivacyPolicyModal() {
  openPopupModal("隐私协议", '<p class="empty-hint">加载中…</p>');
  await loadPrivacyPolicyRevision(null);
}

async function checkAnnouncementNotifications() {
  try {
    const base = await BiliIndex.getApiBase();
    const lastSeen = await getLastSeenAnnouncementId();
    const res = await fetch(`${base}/api/content/announcements?limit=1`);
    const data = await res.json().catch(() => ({}));
    if (!res.ok) return;
    const latest = parseInt(data.latest_id, 10) || 0;
    setSettingsAnnounceBadge(latest > lastSeen);
  } catch {
    setSettingsAnnounceBadge(false);
  }
}

function updateSuperAdminSettingsUi(me) {
  currentUserIsSuperAdmin = !!me?.is_super_admin;
  const btn = document.getElementById("settings-menu-admin");
  if (btn) btn.classList.toggle("hidden", !currentUserIsSuperAdmin);
}

async function refreshSuperAdminFingerprintBlock() {
  const block = document.getElementById("super-admin-fp-block");
  const input = document.getElementById("super-admin-fp-display");
  if (!block || !input) return;
  if (!currentUserIsSuperAdmin) {
    block.classList.add("hidden");
    return;
  }
  block.classList.remove("hidden");
  try {
    const fp = await BiliIndex.collectFingerprint();
    input.value = fp;
  } catch {
    input.value = "";
  }
}

function openSettingsDrawer() {
  const root = document.getElementById("popup-settings");
  if (!root) return;
  showSettingsMenu();
  root.classList.add("is-open");
  root.setAttribute("aria-hidden", "false");
}

function closeSettingsDrawer() {
  const root = document.getElementById("popup-settings");
  if (!root) return;
  root.classList.remove("is-open");
  root.setAttribute("aria-hidden", "true");
  showSettingsMenu();
}

function showSettingsMenu() {
  document.getElementById("popup-settings-title").textContent = "设置";
  document.getElementById("popup-settings-back")?.classList.add("hidden");
  document.getElementById("popup-settings-menu")?.classList.remove("hidden");
  document.getElementById("popup-settings-panel")?.classList.add("hidden");
  document
    .querySelectorAll(".popup-settings-panel-view")
    .forEach((el) => el.classList.add("hidden"));
}

function showSettingsSubPanel(viewId, title) {
  document.getElementById("popup-settings-title").textContent = title;
  document.getElementById("popup-settings-back")?.classList.remove("hidden");
  document.getElementById("popup-settings-menu")?.classList.add("hidden");
  const panel = document.getElementById("popup-settings-panel");
  panel?.classList.remove("hidden");
  document
    .querySelectorAll(".popup-settings-panel-view")
    .forEach((el) => el.classList.add("hidden"));
  document.getElementById(viewId)?.classList.remove("hidden");
}

async function openUsageGuideModal() {
  closeSettingsDrawer();
  openPopupModal("使用说明", '<p class="empty-hint">加载中…</p>');
  try {
    const base = await BiliIndex.getApiBase();
    const res = await fetch(`${base}/api/content/usage-guide`);
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      throw new Error(data.detail || data.message || res.statusText);
    }
    const html = (data.body || "").trim();
    const bodyEl = document.getElementById("popup-modal-body");
    if (!bodyEl) return;
    if (!html) {
      bodyEl.innerHTML =
        '<p class="empty-hint">暂无使用说明，请在管理后台编辑。</p>';
      return;
    }
    bodyEl.innerHTML = html;
  } catch (e) {
    const bodyEl = document.getElementById("popup-modal-body");
    if (bodyEl) {
      bodyEl.innerHTML = `<p class="empty-hint">加载失败：${e.message}</p>`;
    }
  }
}

async function openAuthorNoteModal() {
  closeSettingsDrawer();
  openPopupModal("作者的话", '<p class="empty-hint">加载中…</p>');
  try {
    const base = await BiliIndex.getApiBase();
    const res = await fetch(`${base}/api/content/author-note`);
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      throw new Error(data.detail || data.message || res.statusText);
    }
    const text = (data.body || "").trim();
    const bodyEl = document.getElementById("popup-modal-body");
    if (!bodyEl) return;
    if (!text) {
      bodyEl.innerHTML =
        '<p class="empty-hint">作者尚未发布内容，请稍后再看。</p>';
      return;
    }
    const esc = (s) =>
      String(s)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
    bodyEl.innerHTML = `<div class="author-note-text">${esc(text)}</div>`;
  } catch (e) {
    const bodyEl = document.getElementById("popup-modal-body");
    if (bodyEl) {
      bodyEl.innerHTML = `<p class="empty-hint">加载失败：${e.message}</p>`;
    }
  }
}

function openSettingsAnnouncePanel() {
  showSettingsSubPanel("popup-settings-panel-announcements", "公告");
  void loadSettingsAnnounceList();
}

async function loadSettingsAnnounceList() {
  const statusEl = document.getElementById("settings-announce-status");
  const listEl = document.getElementById("settings-announce-list");
  if (!statusEl || !listEl) return;
  statusEl.textContent = "加载中…";
  listEl.innerHTML = "";
  try {
    const base = await BiliIndex.getApiBase();
    const res = await fetch(`${base}/api/content/announcements?limit=50`);
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      throw new Error(data.detail || data.message || res.statusText);
    }
    const items = data.items || [];
    const latestId = parseInt(data.latest_id, 10) || 0;
    if (!items.length) {
      statusEl.textContent = "暂无公告";
      listEl.innerHTML = "";
    } else {
      statusEl.textContent = `共 ${items.length} 条（最新在前）`;
      const esc = (s) =>
        String(s)
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;");
      listEl.innerHTML = items
        .map((item) => {
          const when = item.created_at
            ? new Date(item.created_at).toLocaleString()
            : "";
          const preview = esc((item.body || "").replace(/\s+/g, " ").slice(0, 80));
          return `<li class="settings-announce-item" data-id="${item.id}">
            <strong>${esc(item.title || "公告")}</strong>
            <span class="hint">${esc(when)}</span>
            <p class="settings-announce-preview">${preview}${(item.body || "").length > 80 ? "…" : ""}</p>
          </li>`;
        })
        .join("");
      listEl.querySelectorAll(".settings-announce-item").forEach((li) => {
        li.addEventListener("click", () => {
          const id = parseInt(li.getAttribute("data-id"), 10);
          const item = items.find((x) => x.id === id);
          if (!item) return;
          openPopupModal(item.title || "公告", "");
          renderAnnouncementBody(item.body || "");
        });
      });
    }
    if (latestId > 0) {
      await setLastSeenAnnouncementId(latestId);
      setSettingsAnnounceBadge(false);
    }
  } catch (err) {
    statusEl.textContent = `加载失败：${err.message}`;
    listEl.innerHTML = "";
  }
}

async function loadSettingsNotifyList() {
  const statusEl = document.getElementById("settings-notify-status");
  const listEl = document.getElementById("settings-notify-list");
  if (!statusEl || !listEl) return;
  statusEl.textContent = "加载中…";
  listEl.innerHTML = "";
  const token = await BiliIndex.getToken();
  if (!token) {
    statusEl.textContent = "请先登录后查看消息";
    setSettingsNotifyBadge(false);
    return;
  }
  try {
    const data = await BiliIndex.apiFetch("/api/user/bugs/notifications");
    const items = data.items || [];
    if (!items.length) {
      statusEl.textContent = "暂无新消息";
      setSettingsNotifyBadge(false);
      return;
    }
    statusEl.textContent = `共 ${items.length} 条未读`;
    setSettingsNotifyBadge(true);
    listEl.innerHTML = items
      .map(
        (item) =>
          `<li class="settings-notify-item" data-bug-id="${item.id}"><strong>BUG #${item.id}</strong>${escHtml(
            (item.admin_reply || item.body || "").slice(0, 80)
          )}${(item.admin_reply || item.body || "").length > 80 ? "…" : ""}</li>`
      )
      .join("");
    listEl.querySelectorAll(".settings-notify-item").forEach((li) => {
      li.addEventListener("click", () => {
        const id = Number(li.dataset.bugId);
        const item = items.find((x) => x.id === id);
        if (item) void showBugReplyModal(item);
      });
    });
  } catch (e) {
    statusEl.textContent = e.message || "加载失败";
    setSettingsNotifyBadge(false);
  }
}

function openSettingsApiPanel() {
  showSettingsSubPanel("popup-settings-panel-api", "API 地址");
  void loadApiModeSettingsUi();
  void refreshSuperAdminFingerprintBlock();
}

function openSettingsNotifyPanel() {
  showSettingsSubPanel("popup-settings-panel-notify", "消息提醒");
  void loadSettingsNotifyList();
}

function initSettingsDrawer() {
  document.getElementById("btn-open-settings")?.addEventListener("click", () => {
    openSettingsDrawer();
  });
  document.getElementById("popup-settings-close")?.addEventListener("click", () => {
    closeSettingsDrawer();
  });
  document.getElementById("popup-settings-back")?.addEventListener("click", () => {
    showSettingsMenu();
  });
  document.querySelectorAll("[data-settings-close]").forEach((el) => {
    el.addEventListener("click", () => closeSettingsDrawer());
  });
  document.querySelectorAll("[data-settings-action]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const action = btn.getAttribute("data-settings-action");
      if (action === "admin") {
        closeSettingsDrawer();
        void openAdminPortalFromSettings();
        return;
      }
      if (action === "api") {
        openSettingsApiPanel();
        return;
      }
      if (action === "notify") {
        openSettingsNotifyPanel();
        return;
      }
      if (action === "usage") {
        void openUsageGuideModal();
        return;
      }
      if (action === "author") {
        void openAuthorNoteModal();
        return;
      }
      if (action === "announcements") {
        openSettingsAnnouncePanel();
        return;
      }
      if (action === "bug") {
        void openBugReportModal();
        return;
      }
    });
  });
  document.getElementById("btn-copy-super-admin-fp")?.addEventListener("click", async () => {
    const val = document.getElementById("super-admin-fp-display")?.value?.trim() || "";
    if (!val) {
      alert("无法读取指纹");
      return;
    }
    try {
      await navigator.clipboard.writeText(val);
      $("#action-msg").textContent = "指纹已复制，可粘贴到管理后台";
    } catch {
      alert("复制失败，请手动选择文本框内容复制");
    }
  });
}

function initPopupModals() {
  document.getElementById("link-privacy-policy")?.addEventListener("click", async (e) => {
    e.preventDefault();
    void openPrivacyPolicyModal();
  });
  document.getElementById("link-privacy-register")?.addEventListener("click", async (e) => {
    e.preventDefault();
    void openPrivacyPolicyModal();
  });

  document.getElementById("btn-buy-member")?.addEventListener("click", () => {
    openBuyMemberModal();
  });

  document.getElementById("popup-modal-close")?.addEventListener("click", closePopupModal);
  document.querySelectorAll("[data-modal-close]").forEach((el) => {
    el.addEventListener("click", closePopupModal);
  });
}

async function checkBugNotifications() {
  const token = await BiliIndex.getToken();
  if (!token) {
    setSettingsNotifyBadge(false);
    return;
  }
  try {
    const data = await BiliIndex.apiFetch("/api/user/bugs/notifications");
    const items = data.items || [];
    setSettingsNotifyBadge(items.length > 0);
  } catch {
    setSettingsNotifyBadge(false);
  }
}

async function showBugReplyModal(item) {
  const esc = (s) =>
    String(s)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  openPopupModal(
    `BUG 反馈 #${item.id} · 管理员回复`,
    `<p><strong>你的描述：</strong></p><p>${esc(item.body)}</p>
     <p><strong>管理员回复：</strong></p><p class="author-note-text">${esc(item.admin_reply || "")}</p>
     <p class="hint">时间：${item.replied_at || "—"}</p>`
  );
  try {
    await BiliIndex.apiFetch(`/api/user/bugs/${item.id}/read`, { method: "POST" });
    void checkBugNotifications();
    const listEl = document.getElementById("settings-notify-list");
    if (listEl && !document.getElementById("popup-settings-panel-notify")?.classList.contains("hidden")) {
      void loadSettingsNotifyList();
    }
  } catch {
    /* ignore */
  }
}

const MEMBER_PLAN_PURCHASE_URLS = {
  monthly: "https://www.kufaka.com/item/nj7kmf",
  yearly: "https://www.kufaka.com/item/hr3s8z",
  lifetime: "https://www.kufaka.com/item/9jpfnh",
};

function openMemberPlanPurchase(url) {
  if (!url) return;
  chrome.tabs.create({ url });
}

function openBuyMemberModal() {
  openPopupModal(
    "购买会员",
    `<div class="member-purchase">
      <div class="member-plan-row">
        <button type="button" class="member-plan-btn" data-plan-url="${MEMBER_PLAN_PURCHASE_URLS.monthly}">月卡 4.9 元</button>
        <button type="button" class="member-plan-btn" data-plan-url="${MEMBER_PLAN_PURCHASE_URLS.yearly}">年卡 38 元</button>
        <button type="button" class="member-plan-btn" data-plan-url="${MEMBER_PLAN_PURCHASE_URLS.lifetime}">买断 58 元</button>
      </div>
      <div class="member-activation">
        <p class="member-activation-title">激活码</p>
        <label class="member-activation-label">
          输入激活码
          <input
            id="activation-code-input"
            type="text"
            class="member-activation-input"
            placeholder="例如 M-XXXX-XXXX-XXXX-XXXX"
            autocomplete="off"
            spellcheck="false"
          />
        </label>
        <button type="button" id="activation-code-submit" class="primary member-activation-submit">
          激活
        </button>
        <p id="activation-code-status" class="activation-code-status hint" aria-live="polite"></p>
      </div>
      <div class="member-benefits">
        <p class="member-benefits-title">权益</p>
        <ul class="member-benefits-list">
          <li><strong>月卡</strong>：可以使用完整功能。</li>
          <li><strong>年卡</strong>：相当于月卡连续购买 12 个月价格的 64.5%。</li>
          <li><strong>买断</strong>：加入服务 QQ 群，24 小时内解答；含探针功能。</li>
        </ul>
        <p class="member-probe-title">探针功能（待开发）</p>
        <p class="member-probe-desc">若服务器难以为继或停止运营并关闭数据库更新，所有买断用户可下载数据库并在本地使用插件；同时与其他买断用户进行 P2P 连接，同步数据库，以在插件离线状态下继续使用屏蔽功能。</p>
      </div>
      <p class="hint member-purchase-hint">点击上方方案跳转购买；购买后可在下方输入激活码开通。</p>
    </div>`
  );
  document.querySelectorAll(".member-plan-btn[data-plan-url]").forEach((btn) => {
    btn.addEventListener("click", () => {
      openMemberPlanPurchase(btn.getAttribute("data-plan-url"));
    });
  });
  document.getElementById("activation-code-submit")?.addEventListener("click", () => {
    void redeemActivationCode();
  });
  document.getElementById("activation-code-input")?.addEventListener("keydown", (e) => {
    if (e.key === "Enter") void redeemActivationCode();
  });
}

async function redeemActivationCode() {
  const statusEl = document.getElementById("activation-code-status");
  const inputEl = document.getElementById("activation-code-input");
  const code = inputEl?.value?.trim() || "";
  if (!code) {
    if (statusEl) statusEl.textContent = "请输入激活码";
    return;
  }
  const token = await BiliIndex.getToken();
  if (!token) {
    if (statusEl) statusEl.textContent = "请先登录后再激活";
    return;
  }
  if (statusEl) statusEl.textContent = "正在验证…";
  try {
    const res = await BiliIndex.apiFetch("/api/user/activation-codes/redeem", {
      method: "POST",
      body: JSON.stringify({ code }),
    });
    const benefit = res.benefit_summary || "";
    const label = res.membership?.label || res.plan_label || "";
    if (statusEl) {
      statusEl.textContent = `${res.message || "激活成功"}。${benefit}${label ? ` 当前：${label}` : ""}`;
      statusEl.classList.add("is-success");
    }
    if (inputEl) inputEl.value = "";
    $("#action-msg").textContent = res.message || "激活成功";
    await renderLoggedIn();
  } catch (e) {
    if (statusEl) {
      statusEl.textContent = e.message || "激活失败";
      statusEl.classList.remove("is-success");
    }
  }
}

const BUG_MAX_CHARS = 500;
const BUG_MAX_IMAGE_BYTES = 1024 * 1024;

async function openAdminPortalFromSettings() {
  try {
    await BiliIndex.openAdminPortal();
    $("#action-msg").textContent = "已打开管理后台（新标签页）";
  } catch (e) {
    alert(e.message || "无法打开管理后台");
  }
}

async function openBugReportModal() {
  closeSettingsDrawer();
  const token = await BiliIndex.getToken();
  if (!token) {
    $("#action-msg").textContent = "请先登录后再提交 BUG";
    return;
  }
  let quota = {
    can_submit: true,
    max_body_chars: BUG_MAX_CHARS,
    min_body_chars: 5,
    max_image_bytes: BUG_MAX_IMAGE_BYTES,
  };
  try {
    quota = { ...quota, ...(await BiliIndex.fetchBugQuota()) };
  } catch {
    /* 配额接口失败时仍允许尝试提交，由服务端校验 */
  }
  if (!quota.can_submit) {
    openPopupModal(
      "BUG 提交",
      `<p class="hint">每日仅可提交 1 次 BUG，您今日已提交过，请明日再试。</p>`
    );
    return;
  }
  const maxChars = Number(quota.max_body_chars) || BUG_MAX_CHARS;
  const minChars = Number(quota.min_body_chars) || 5;
  const maxImg = Number(quota.max_image_bytes) || BUG_MAX_IMAGE_BYTES;
  openPopupModal(
    "BUG 提交",
    `<p class="hint">每日 1 次；描述最多 ${maxChars} 字；可附 1 张截图（≤1MB，PNG/JPEG/WebP/GIF）。</p>
     <label>BUG 描述
       <textarea id="bug-report-input" class="bug-form" maxlength="${maxChars}" placeholder="例如：评论区标签槽不显示…"></textarea>
       <span id="bug-report-counter" class="bug-char-counter">0 / ${maxChars}</span>
     </label>
     <label class="bug-image-label">截图（可选，1 张）
       <input type="file" id="bug-report-image" accept="image/png,image/jpeg,image/webp,image/gif" />
       <span id="bug-report-image-hint" class="hint"></span>
     </label>
     <button type="button" id="bug-report-submit" class="primary" style="width:100%;margin-top:8px">提交</button>`
  );
  const input = document.getElementById("bug-report-input");
  const counter = document.getElementById("bug-report-counter");
  const syncCounter = () => {
    const n = input?.value?.length || 0;
    if (counter) counter.textContent = `${n} / ${maxChars}`;
  };
  input?.addEventListener("input", syncCounter);
  syncCounter();

  const fileInput = document.getElementById("bug-report-image");
  const fileHint = document.getElementById("bug-report-image-hint");
  fileInput?.addEventListener("change", () => {
    const file = fileInput.files?.[0];
    if (!file) {
      if (fileHint) fileHint.textContent = "";
      return;
    }
    if (file.size > maxImg) {
      fileInput.value = "";
      if (fileHint) fileHint.textContent = "图片不能超过 1MB";
      return;
    }
    if (fileHint) {
      fileHint.textContent = `${file.name}（${(file.size / 1024).toFixed(0)} KB）`;
    }
  });

  document.getElementById("bug-report-submit")?.addEventListener("click", async () => {
    const text = input?.value?.trim() || "";
    if (text.length < minChars) {
      alert(`请至少输入 ${minChars} 个字`);
      return;
    }
    if (text.length > maxChars) {
      alert(`描述不能超过 ${maxChars} 字`);
      return;
    }
    const imageFile = fileInput?.files?.[0];
    if (imageFile && imageFile.size > maxImg) {
      alert("图片不能超过 1MB");
      return;
    }
    const btn = document.getElementById("bug-report-submit");
    if (btn) {
      btn.disabled = true;
      btn.textContent = "提交中…";
    }
    try {
      const res = await BiliIndex.submitBugReport({ body: text, imageFile: imageFile || null });
      closePopupModal();
      $("#action-msg").textContent = res.message || "BUG 已提交";
    } catch (e) {
      alert(e.message || "提交失败");
    } finally {
      if (btn) {
        btn.disabled = false;
        btn.textContent = "提交";
      }
    }
  });
}

function initBugReport() {
  document.getElementById("btn-open-standalone")?.addEventListener("click", () => {
    openStandaloneWindow();
  });
}

initPopupResize();
initApiModeSettings();
initPasserbyScanToggle();
initPasserbyEcoScanToggle();
initSettingsDrawer();
initPopupModals();
initBugReport();

chrome.storage.onChanged.addListener((changes, area) => {
  if (area !== "local") return;
  if (changes.passerbySharedScanStatusCache?.newValue) {
    applyPasserbyScanStatusCache(changes.passerbySharedScanStatusCache.newValue);
    return;
  }
  if (changes.passerbyEcoScanStatusCache?.newValue) {
    applyPasserbyScanStatusCache(changes.passerbyEcoScanStatusCache.newValue);
    return;
  }
  if (changes.lastPasserbyEcoScanResult?.newValue) {
    const r = changes.lastPasserbyEcoScanResult.newValue;
    applyPasserbyScanStatusCache(r);
    if (r.user_points != null) {
      const pointsEl = document.getElementById("points-status");
      if (pointsEl) {
        const extra = r.points_earned ? `（最近 +${r.points_earned}）` : "";
        const pointsText = `积分：${r.user_points}${extra}`;
        if (passerbyQuotaDisplayCache.points !== pointsText) {
          passerbyQuotaDisplayCache.points = pointsText;
          pointsEl.textContent = pointsText;
        }
      }
    }
  }
});

(async () => {
  applyExtensionVersionLabels();
  if (typeof BiliIndexCaptcha !== "undefined") {
    const loginSlot = document.getElementById("login-captcha-slot");
    let wrap = document.getElementById("auth-captcha-wrap");
    if (!wrap && loginSlot) {
      wrap = document.createElement("div");
      wrap.id = "auth-captcha-wrap";
      wrap.className = "auth-captcha-wrap hidden";
      wrap.setAttribute("aria-hidden", "true");
      loginSlot.appendChild(wrap);
    }
    if (wrap) {
      BiliIndexCaptcha.mount(wrap);
      placeCaptchaForTab("login");
    }
  }
  await initAuthForm();
  await tryAutoLogin();
  await renderLoggedIn();
  void checkAnnouncementNotifications();
})();
