/**
 * BiliIndex 共享库（content / popup / service worker 共用）
 */
const BiliIndex = (globalThis.BiliIndex = globalThis.BiliIndex || {});

/** demo1 冻结基线；manifest version 须与此一致。见 VERSIONING.md */
BiliIndex.EXTENSION_VERSION = "1.0.10";
BiliIndex.DEMO_CODENAME = "demo1";

BiliIndex.DEFAULT_API_BASE = "http://127.0.0.1:8000";
BiliIndex.TAG_CACHE_MS = 60 * 60 * 1000;

/** 与 API EMOJI_PRELOAD 一致，用于前端展示兜底 */
BiliIndex.EMOJI_ICON_BY_CODE = {
  emoji_fist: "👊",
  emoji_turtle: "🐢",
  flag_us: "🇺🇸",
  flag_cn: "🇨🇳",
  flag_jp: "🇯🇵",
  flag_kr: "🇰🇷",
  flag_eu: "🇪🇺",
};

/** 后台误存 US/CN 文本时的展示兜底（与 API ICON_TEXT_TO_FLAG 一致） */
BiliIndex.ICON_TEXT_TO_FLAG = {
  US: "🇺🇸",
  USA: "🇺🇸",
  CN: "🇨🇳",
  CHN: "🇨🇳",
  JP: "🇯🇵",
  JPN: "🇯🇵",
  KR: "🇰🇷",
  KOR: "🇰🇷",
  EU: "🇪🇺",
};

BiliIndex.resolveTagIcon = (tag) => {
  if (!tag) return "";
  const fix = BiliIndex.EMOJI_ICON_BY_CODE[tag.code];
  if (fix) return fix;
  const ic = (tag.icon || "").trim();
  if (ic) {
    const upper = ic.toUpperCase();
    if (BiliIndex.ICON_TEXT_TO_FLAG[upper]) return BiliIndex.ICON_TEXT_TO_FLAG[upper];
    if (!/^[A-Z]{2,4}$/.test(ic)) return ic;
  }
  return "";
};

/** 展示用文案：避免 icon 与 name 相同时出现「游戏 游戏」 */
BiliIndex.formatTagLabel = (tag) => {
  if (!tag) return "";
  const name = (tag.name || "").trim();
  const icon = BiliIndex.resolveTagIcon(tag);
  if (BiliIndex.isEmojiTag(tag)) {
    if (icon && name && icon !== name) return `${icon} ${name}`;
    return icon || name || tag.code || "";
  }
  const rawIcon = (tag.icon || "").trim();
  if (rawIcon && rawIcon !== name && !/^[A-Z]{2,4}$/.test(rawIcon)) {
    return `${rawIcon} ${name}`.trim();
  }
  return name || tag.code || "";
};

BiliIndex.parseFanCount = (raw) => {
  if (raw == null || raw === "") return null;
  if (typeof raw === "number" && Number.isFinite(raw)) {
    return Math.max(0, Math.floor(raw));
  }
  const s = String(raw).trim().replace(/,/g, "").replace(/\s/g, "");
  if (!s) return null;
  const wan = s.match(/^([\d.]+)万$/);
  if (wan) {
    const n = Math.round(parseFloat(wan[1]) * 10000);
    return Number.isFinite(n) ? n : null;
  }
  const n = parseInt(s, 10);
  return Number.isFinite(n) ? Math.max(0, n) : null;
};

BiliIndex.formatFanCount = (fanCount, opts = {}) => {
  if (opts.deactivated) return "—";
  if (fanCount == null || fanCount === "") return "—";
  const n = Number(fanCount);
  if (!Number.isFinite(n) || n < 0) return "—";
  if (n < 100) return "<100";
  if (n < 10000) return String(Math.floor(n));
  return `${(n / 10000).toFixed(1)}万`;
};

BiliIndex.fanCountFromCommentItem = (item) => {
  if (!(item instanceof HTMLElement)) return null;
  const d = item.__data;
  if (!d) return null;
  const m = d.member || d.reply?.member || d.user;
  return BiliIndex.parseFanCount(
    m?.fans ?? m?.follower ?? m?.follower_num ?? d.fans ?? null
  );
};

BiliIndex.fanCountFromVideoPage = (upMid) => {
  try {
    const st = window.__INITIAL_STATE__;
    const candidates = [
      st?.elecFullInfo?.up_info,
      st?.videoData?.owner,
      st?.upData,
    ].filter(Boolean);
    for (const up of candidates) {
      if (up && String(up.mid ?? up.uid ?? "") === String(upMid)) {
        return BiliIndex.parseFanCount(up.fans ?? up.follower ?? up.follower_num);
      }
    }
  } catch {
    /* ignore */
  }
  return null;
};

/** 从 B 站公开接口拉取粉丝数（需 manifest host_permissions） */
BiliIndex.fetchFanCountAsync = async (upMid) => {
  const mid = String(upMid || "").trim();
  if (!mid) return null;
  try {
    const res = await fetch(
      `https://api.bilibili.com/x/relation/stat?vmid=${encodeURIComponent(mid)}`,
      { credentials: "omit" }
    );
    const data = await res.json();
    if (data?.code === 0 && data.data) {
      return BiliIndex.parseFanCount(data.data.follower);
    }
  } catch {
    /* ignore */
  }
  return null;
};

BiliIndex.fanCountFromSpacePage = (upMid) => {
  try {
    const st = window.__INITIAL_STATE__;
    const info = st?.spaceInfo || st?.space || st?.h;
    const mid = String(info?.mid ?? info?.uid ?? st?.mid ?? "");
    if (mid && String(upMid) === mid) {
      return BiliIndex.parseFanCount(
        info?.fans ?? info?.follower ?? st?.fans ?? null
      );
    }
  } catch {
    /* ignore */
  }
  return null;
};

BiliIndex.isEmojiTag = (tag) => {
  if (!tag) return false;
  if (tag.tag_kind === "emoji") return true;
  return Object.prototype.hasOwnProperty.call(BiliIndex.EMOJI_ICON_BY_CODE, tag.code);
};

BiliIndex.storageGet = (keys) =>
  new Promise((resolve) => chrome.storage.local.get(keys, resolve));

BiliIndex.storageSet = (obj) =>
  new Promise((resolve) => chrome.storage.local.set(obj, resolve));

BiliIndex.getApiBase = async () => {
  const { apiBase } = await BiliIndex.storageGet(["apiBase"]);
  return apiBase || BiliIndex.DEFAULT_API_BASE;
};

BiliIndex.getToken = async () => {
  const { accessToken } = await BiliIndex.storageGet(["accessToken"]);
  return accessToken || null;
};

BiliIndex.setSession = async (token, user) => {
  await BiliIndex.storageSet({ accessToken: token, user });
};

BiliIndex.clearSession = async () => {
  await BiliIndex.storageSet({ accessToken: null, user: null });
};

/** 登录偏好：记住密码 / 自动登录（仅存本机 chrome.storage.local） */
BiliIndex.getLoginPrefs = async () => {
  const data = await BiliIndex.storageGet([
    "rememberPassword",
    "autoLogin",
    "savedUsername",
    "savedPassword",
  ]);
  return {
    rememberPassword: !!data.rememberPassword,
    autoLogin: !!data.autoLogin,
    savedUsername: data.savedUsername || "",
    savedPassword: data.savedPassword || "",
  };
};

BiliIndex.saveLoginPrefs = async ({
  username,
  password,
  rememberPassword,
  autoLogin,
}) => {
  const patch = {
    rememberPassword: !!rememberPassword,
    autoLogin: !!autoLogin,
  };
  if (rememberPassword && username) {
    patch.savedUsername = username;
    patch.savedPassword = password || "";
  } else {
    patch.savedUsername = "";
    patch.savedPassword = "";
    patch.autoLogin = false;
  }
  await BiliIndex.storageSet(patch);
};

BiliIndex.getApiLinks = async () => {
  const base = (await BiliIndex.getApiBase()).replace(/\/$/, "");
  return {
    base,
    loginPage: `${base}/login`,
    loginApi: `${base}/api/auth/login`,
    registerApi: `${base}/api/auth/register`,
    docs: `${base}/docs`,
    admin: `${base}/admin/`,
  };
};

/** 内容脚本在 https 页面上不能直接访问本机 API（Chrome Private Network Access） */
BiliIndex.shouldProxyApi = () => {
  try {
    if (typeof chrome === "undefined" || !chrome.runtime?.id) return false;
    const href = globalThis.location?.href;
    if (!href) return false;
    return !href.startsWith("chrome-extension://");
  } catch {
    return false;
  }
};

BiliIndex.proxyApiMessage = (type, path, options) =>
  new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type, path, options }, (resp) => {
      const err = chrome.runtime.lastError;
      if (err) {
        reject(new Error(err.message));
        return;
      }
      if (!resp?.ok) {
        reject(new Error(resp?.error || "API 请求失败"));
        return;
      }
      resolve(resp.data);
    });
  });

BiliIndex._apiFetchDirect = async (path, options = {}) => {
  const base = await BiliIndex.getApiBase();
  const token = await BiliIndex.getToken();
  const headers = {
    "Content-Type": "application/json",
    ...(options.headers || {}),
  };
  if (token) headers.Authorization = `Bearer ${token}`;

  const res = await fetch(`${base}${path}`, { ...options, headers });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const msg = data.detail || data.error || data.message || res.statusText;
    throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg));
  }
  return data;
};

BiliIndex.apiFetch = async (path, options = {}) => {
  if (BiliIndex.shouldProxyApi()) {
    return BiliIndex.proxyApiMessage("API_FETCH", path, options);
  }
  return BiliIndex._apiFetchDirect(path, options);
};

/** 无需登录的公开接口（标签树、快捷列表） */
BiliIndex._fetchPublicDirect = async (path) => {
  const base = await BiliIndex.getApiBase();
  const res = await fetch(`${base}${path}`);
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    const msg = data.detail || data.message || res.statusText;
    throw new Error(typeof msg === "string" ? msg : String(msg));
  }
  return data;
};

BiliIndex.fetchPublic = async (path) => {
  if (BiliIndex.shouldProxyApi()) {
    return BiliIndex.proxyApiMessage("FETCH_PUBLIC", path);
  }
  return BiliIndex._fetchPublicDirect(path);
};

BiliIndex.cacheValid = (cachedAt, items) =>
  !!(
    cachedAt &&
    Array.isArray(items) &&
    items.length > 0 &&
    Date.now() - cachedAt < BiliIndex.TAG_CACHE_MS
  );

BiliIndex.hashString = async (str) => {
  const buf = new TextEncoder().encode(str);
  const hash = await crypto.subtle.digest("SHA-256", buf);
  return Array.from(new Uint8Array(hash))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
};

BiliIndex.getCanvasFingerprint = () => {
  try {
    const c = document.createElement("canvas");
    const ctx = c.getContext("2d");
    if (!ctx) return "";
    ctx.textBaseline = "top";
    ctx.font = "14px Arial";
    ctx.fillText("biliindex-fp", 2, 2);
    return c.toDataURL();
  } catch {
    return "";
  }
};

BiliIndex.getWebGLFingerprint = () => {
  try {
    const c = document.createElement("canvas");
    const gl = c.getContext("webgl") || c.getContext("experimental-webgl");
    if (!gl) return "";
    const dbg = gl.getExtension("WEBGL_debug_renderer_info");
    if (!dbg) return gl.getParameter(gl.VERSION) || "";
    return [
      gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL),
      gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL),
    ].join("|");
  } catch {
    return "";
  }
};

BiliIndex.getAudioFingerprint = () => {
  return new Promise((resolve) => {
    try {
      const ctx = new OfflineAudioContext(1, 5000, 44100);
      const osc = ctx.createOscillator();
      const comp = ctx.createDynamicsCompressor();
      osc.connect(comp);
      comp.connect(ctx.destination);
      osc.start(0);
      ctx.startRendering().then((buf) => {
        const data = buf.getChannelData(0).slice(0, 30);
        resolve(data.reduce((a, b) => a + Math.abs(b), 0).toString());
      });
    } catch {
      resolve("");
    }
  });
};

BiliIndex.collectFingerprint = async () => {
  const audio = await BiliIndex.getAudioFingerprint();
  const parts = [
    navigator.userAgent,
    `${screen.width}x${screen.height}x${screen.colorDepth}`,
    navigator.hardwareConcurrency,
    navigator.deviceMemory || "",
    Intl.DateTimeFormat().resolvedOptions().timeZone,
    navigator.language,
    BiliIndex.getCanvasFingerprint(),
    BiliIndex.getWebGLFingerprint(),
    audio,
    (document.fonts && document.fonts.size) || 0,
  ];
  return BiliIndex.hashString(parts.join("|||"));
};

BiliIndex.register = async (username, password) => {
  const fingerprint_hash = await BiliIndex.collectFingerprint();
  const data = await BiliIndex.apiFetch("/api/auth/register", {
    method: "POST",
    body: JSON.stringify({ username, password, fingerprint_hash }),
  });
  const me = await BiliIndex.apiFetch("/api/user/me", {
    headers: { Authorization: `Bearer ${data.access_token}` },
  });
  await BiliIndex.setSession(data.access_token, me);
  return me;
};

BiliIndex.resetPassword = async (username, resetKey, newPassword) =>
  BiliIndex.apiFetch("/api/auth/reset-password", {
    method: "POST",
    body: JSON.stringify({
      username: String(username || "").trim(),
      reset_key: String(resetKey || "").replace(/\D/g, ""),
      new_password: newPassword,
    }),
  });

BiliIndex.login = async (username, password) => {
  const fingerprint_hash = await BiliIndex.collectFingerprint();
  const data = await BiliIndex.apiFetch("/api/auth/login", {
    method: "POST",
    body: JSON.stringify({ username, password, fingerprint_hash }),
  });
  const me = await BiliIndex.apiFetch("/api/user/me", {
    headers: { Authorization: `Bearer ${data.access_token}` },
  });
  await BiliIndex.setSession(data.access_token, me);
  return me;
};

BiliIndex.fetchMe = async () => {
  const me = await BiliIndex.apiFetch("/api/user/me");
  await BiliIndex.storageSet({ user: me });
  return me;
};

BiliIndex.fetchMembership = () => BiliIndex.apiFetch("/api/user/membership");

BiliIndex.exchangeMembership = () =>
  BiliIndex.apiFetch("/api/user/points/exchange", { method: "POST" });

BiliIndex.invalidateTagCache = async () => {
  await BiliIndex.storageSet({
    tagTree: null,
    quickTags: null,
    tagsCachedAt: 0,
    tagsRevision: "",
  });
};

/** 从 API 拉取完整标签树 + 快捷打标列表并缓存 */
BiliIndex.refreshTagCaches = async (force = false) => {
  const { tagTree, quickTags, tagsCachedAt, tagsRevision } = await BiliIndex.storageGet([
    "tagTree",
    "quickTags",
    "tagsCachedAt",
    "tagsRevision",
  ]);
  let revision = tagsRevision || "";
  if (!force && BiliIndex.cacheValid(tagsCachedAt, tagTree) && BiliIndex.cacheValid(tagsCachedAt, quickTags)) {
    try {
      const revRes = await BiliIndex.fetchPublic("/api/tag/revision");
      revision = revRes.revision || revision;
      if (revision && revision === tagsRevision) {
        return { tree: tagTree, quick: quickTags };
      }
    } catch {
      return { tree: tagTree, quick: quickTags };
    }
  }

  const [treeRes, quickRes, emojiRes] = await Promise.all([
    BiliIndex.fetchPublic("/api/tag/tree"),
    BiliIndex.fetchPublic("/api/tag/quick-list"),
    BiliIndex.fetchPublic("/api/tag/quick-list?kind=emoji"),
  ]);
  revision = treeRes.revision || revision;
  let tree = treeRes.items || [];
  let quick = quickRes.items || [];
  const emojiItems = emojiRes.items || [];

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

  if (!quick.length && tree.length) {
    quick = tree.filter((t) => {
      const kind =
        t.tag_kind ||
        (t.level === 1 ? "category" : t.icon ? "emoji" : t.level === 3 ? "style" : "sub");
      return kind !== "category";
    });
  }

  if (!revision) {
    try {
      const revRes = await BiliIndex.fetchPublic("/api/tag/revision");
      revision = revRes.revision || "";
    } catch {
      revision = "";
    }
  }

  await BiliIndex.storageSet({
    tagTree: tree,
    quickTags: quick,
    tagsCachedAt: Date.now(),
    tagsRevision: revision,
  });
  return { tree, quick };
};

BiliIndex.getTagTree = async (force = false) => {
  const { tree } = await BiliIndex.refreshTagCaches(force);
  return tree;
};

BiliIndex.getQuickTags = async (force = false) => {
  const { quick } = await BiliIndex.refreshTagCaches(force);
  return quick;
};

/** 按类型分组，供弹窗打标界面使用 */
BiliIndex.groupTags = (items) => {
  const groups = {
    category: [],
    sub: [],
    style: [],
    emoji: [],
  };
  for (const t of items || []) {
    let kind =
      t.tag_kind ||
      (t.level === 1 ? "category" : t.icon ? "emoji" : t.level === 3 ? "style" : "sub");
    if (BiliIndex.isEmojiTag(t)) kind = "emoji";
    if (groups[kind]) groups[kind].push(t);
    else groups.sub.push(t);
  }
  const sortFn = (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || a.id - b.id;
  Object.keys(groups).forEach((k) => groups[k].sort(sortFn));
  return groups;
};

BiliIndex.checkTagFrequency = (up_mid, slot = "tag1") => {
  const q = new URLSearchParams({
    up_mid: String(up_mid || ""),
    slot: slot || "tag1",
  });
  return BiliIndex.apiFetch(`/api/tag/check-frequency?${q}`);
};

BiliIndex.quickTag = (up_mid, tag_code, meta = {}) =>
  BiliIndex.apiFetch("/api/tag/quick-submit", {
    method: "POST",
    body: JSON.stringify({
      up_mid,
      tag_code,
      slot: meta.slot || "tag1",
      nickname: meta.nickname || null,
      fan_count: meta.fan_count ?? null,
    }),
  });

BiliIndex.submitTag = (up_mid, tag_id) =>
  BiliIndex.apiFetch("/api/tag/submit", {
    method: "POST",
    body: JSON.stringify({ up_mid, tag_ids: [tag_id] }),
  });
