// bridges/common.js
// ---------------------------------------------------------------------------
// 本文件是纯浏览器代码，不要被 Node require。
// 每个 bridge 文件顶部包含一行：
//   // @@include ./common.js
// session.js 在注入 bridge 前会把这一行替换为本文件全部内容（lib/session.js::expandBridgeSource）。
//
// 设计取舍：
// - READ 数据 auto = DOM 优先 + API 兜底（与 X 取反，因为小红书 DOM 覆盖广，
//   同源 API 仅在评论 (edith) 等少数路径稳定）。
// - 反爬识别基于 og:xhs:note_* meta 三件齐全；连续 3 次 risk hit → 暂停 5 分钟。
// - navigateLocation 严格限制 *.xiaohongshu.com / *.xhslink.com 同源，绝不跨站跳转。
// - 顶部一行 @@include 由 kit 的 makeBridgeExpander 展开：装入 window.__jse_visual。
// ---------------------------------------------------------------------------

// @@include @js-eyes/visual-bridge-kit/bridge/visual.common.js
// @@include ./_visual-xhs.js

const __jseXhsCache = {
  loginCache: null,
  loginCacheHref: null,
  // 软限流状态机（与 X v3.0 同形态，连续 3 次 risk → 暂停 5 分钟）
  antiCrawl: {
    paused: false,
    pauseUntil: 0,
    consecutiveRiskHits: 0,
    // v3.1 PR-B2：四档分类（soft/captcha/hard）
    kind: null,            // 当前档位
    lastReason: null,      // 最近一次原因字符串
    lastSeenAt: 0,         // 最近一次时间戳
    softCount: 0,
    captchaCount: 0,
    hardCount: 0,
  },
};

const __JSE_XHS_PAUSE_MS = 5 * 60 * 1000;
const __JSE_XHS_MAX_CONSECUTIVE_RISK = 3;

const XHS_HOST_RE = /(?:^|\.)(?:xiaohongshu\.com|xhscdn\.com)$/i;
const XHS_SHORT_HOST_RE = /(?:^|\.)xhslink\.com$/i;

function clampLimit(value, defaultValue, maxValue) {
  const n = Number(value);
  if (!Number.isFinite(n) || n <= 0) return defaultValue;
  return Math.min(Math.floor(n), maxValue);
}

function shortText(value, maxLen) {
  const text = String(value == null ? '' : value);
  const limit = clampLimit(maxLen, 2000, 20000);
  if (text.length <= limit) return { text, truncated: false, length: text.length };
  return { text: text.slice(0, limit), truncated: true, length: text.length };
}

function okResult(data) { return { ok: true, data, antiCrawlState: snapshotAntiCrawl() }; }
function errResult(error, extra) {
  return Object.assign({ ok: false, error: String(error) }, extra || {}, { antiCrawlState: snapshotAntiCrawl() });
}

function snapshotAntiCrawl() {
  var s = __jseXhsCache.antiCrawl;
  return {
    paused: s.paused,
    pauseUntil: s.pauseUntil,
    consecutiveRiskHits: s.consecutiveRiskHits,
    kind: s.kind || null,
    lastReason: s.lastReason || null,
    lastSeenAt: s.lastSeenAt || 0,
    softCount: s.softCount | 0,
    captchaCount: s.captchaCount | 0,
    hardCount: s.hardCount | 0,
  };
}

function delay(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }

function isOnXhs() {
  try {
    return XHS_HOST_RE.test(location.hostname) || XHS_SHORT_HOST_RE.test(location.hostname);
  } catch (_) { return false; }
}

function getCookieValue(name) {
  try {
    var re = new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()\[\]\\\/+^]/g, '\\$&') + '=([^;]*)');
    var m = document.cookie.match(re);
    return m ? decodeURIComponent(m[1]) : null;
  } catch (_) { return null; }
}

function getXhsAuthCookies() {
  return {
    a1: getCookieValue('a1'),
    web_session: getCookieValue('web_session'),
    webId: getCookieValue('webId'),
  };
}

// ---------------------------------------------------------------------------
// 反爬：软限流 / 状态机
// ---------------------------------------------------------------------------

function isAntiCrawlPaused() {
  var s = __jseXhsCache.antiCrawl;
  if (!s.paused) return false;
  if (Date.now() >= s.pauseUntil) {
    s.paused = false;
    s.pauseUntil = 0;
    s.consecutiveRiskHits = 0;
    return false;
  }
  return true;
}

function recordRiskHit(reason, kind) {
  var s = __jseXhsCache.antiCrawl;
  var k = kind === 'hard' || kind === 'captcha' || kind === 'soft' ? kind : 'soft';
  s.consecutiveRiskHits = (s.consecutiveRiskHits | 0) + 1;
  s.kind = k;
  s.lastReason = reason || null;
  s.lastSeenAt = Date.now();
  if (k === 'hard') s.hardCount = (s.hardCount | 0) + 1;
  else if (k === 'captcha') s.captchaCount = (s.captchaCount | 0) + 1;
  else s.softCount = (s.softCount | 0) + 1;
  // hard 立即暂停；captcha 2 次或 soft 3 次连击暂停 5 分钟。
  if (k === 'hard' || s.consecutiveRiskHits >= __JSE_XHS_MAX_CONSECUTIVE_RISK ||
      (k === 'captcha' && s.captchaCount >= 2)) {
    s.paused = true;
    s.pauseUntil = Date.now() + __JSE_XHS_PAUSE_MS;
  }
  return { paused: s.paused, pauseUntil: s.pauseUntil, consecutiveRiskHits: s.consecutiveRiskHits, reason: reason || null, kind: k };
}

function recordSuccess() {
  __jseXhsCache.antiCrawl.consecutiveRiskHits = 0;
}

// ---------------------------------------------------------------------------
// HTTP / API
// ---------------------------------------------------------------------------

/**
 * fetchXhsApi - bridge 内同源 fetch（自动带 cookie）。
 * 用于评论 API（edith）等。
 *
 * @param {string} apiUrl 完整 URL（必须 *.xiaohongshu.com）
 * @param {Object} [options] - { method, headers, body, timeoutMs }
 * @returns {Promise<{ ok, status, data?, error?, contentType?, snippet? }>}
 */
async function fetchXhsApi(apiUrl, options) {
  options = options || {};
  if (isAntiCrawlPaused()) {
    return errResult('anti_crawl_paused', { pauseUntil: __jseXhsCache.antiCrawl.pauseUntil });
  }
  var u;
  try { u = new URL(apiUrl, location.href); } catch (_) {
    return errResult('bad_url', { url: apiUrl });
  }
  if (!XHS_HOST_RE.test(u.hostname)) {
    return errResult('cross_origin_forbidden', { hostname: u.hostname });
  }

  var headers = Object.assign({
    'accept': 'application/json, text/plain, */*',
  }, options.headers || {});

  var ctrl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
  var timeoutMs = options.timeoutMs || 25000;
  var timer = ctrl ? setTimeout(function () { try { ctrl.abort(); } catch (_) {} }, timeoutMs) : null;

  try {
    var resp = await fetch(u.toString(), {
      method: options.method || 'GET',
      headers: headers,
      body: options.body,
      credentials: 'include',
      signal: ctrl ? ctrl.signal : undefined,
    });
    var contentType = resp.headers.get('content-type') || '';
    if (!resp.ok) {
      if (resp.status === 461) {
        recordRiskHit('http_461', 'captcha');
      } else if (resp.status === 429) {
        recordRiskHit('http_429', 'soft');
      } else if (resp.status === 403 || resp.status === 401) {
        recordRiskHit('http_' + resp.status, 'hard');
      }
      var snippet = '';
      try { snippet = (await resp.text()).slice(0, 500); } catch (_) {}
      return errResult('http_' + resp.status, { status: resp.status, contentType: contentType, snippet: snippet });
    }
    if (contentType.indexOf('application/json') === -1) {
      var raw = '';
      try { raw = await resp.text(); } catch (_) {}
      return errResult('non_json_response', { status: resp.status, contentType: contentType, snippet: shortText(raw, 500).text });
    }
    var json = await resp.json();
    if (json && json.success === false) {
      // 已知风控/账号异常 code（300011 = Account abnormal，缺登录态会触发）。
      var c = json.code;
      var msg = json.msg || '';
      // captcha：明确出现验证关键词或 461
      var captchaByMsg = /captcha|verify|滑块|验证码/i.test(msg);
      // hard：明显登录态丢失 / 账号封禁
      var hardByCode = (c === 10001 || c === 10002);
      var hardByMsg = /banned|封禁|frozen|登录后|请登录|未登录/i.test(msg);
      // soft：评论 / behavior abnormal / too frequent
      var softByCode = (c === -1 || c === 461 || (c >= 300010 && c <= 300099));
      var softByMsg = /risk|behavior|abnormal|too\s+frequent|频繁|风险/i.test(msg);
      var classifiedKind = null;
      if (captchaByMsg) classifiedKind = 'captcha';
      else if (hardByCode || hardByMsg) classifiedKind = 'hard';
      else if (softByCode || softByMsg) classifiedKind = 'soft';
      if (classifiedKind) {
        recordRiskHit('xhs_api_code_' + c, classifiedKind);
      }
      return errResult('xhs_api_error', { status: resp.status, code: c, msg: msg, payload: json });
    }
    recordSuccess();
    return { ok: true, data: json, status: resp.status, antiCrawlState: snapshotAntiCrawl() };
  } catch (err) {
    return errResult('fetch_failed', { message: String((err && err.message) || err) });
  } finally {
    if (timer) clearTimeout(timer);
  }
}

// ---------------------------------------------------------------------------
// Note meta 解析（OG / xhs meta，与 cheerio 路径对位）
// ---------------------------------------------------------------------------

function readMetaContent(name) {
  var el = document.querySelector('meta[name="' + name + '"]');
  if (el && el.getAttribute('content')) return el.getAttribute('content');
  el = document.querySelector('meta[property="' + name + '"]');
  if (el && el.getAttribute('content')) return el.getAttribute('content');
  return null;
}

function parseNoteMeta() {
  var ogTitle = readMetaContent('og:title') || (document.title || '');
  if (ogTitle && ogTitle.length > 6 && ogTitle.endsWith(' - 小红书')) {
    ogTitle = ogTitle.slice(0, -6);
  }
  var description = readMetaContent('description') || readMetaContent('og:description') || '';
  var imageUrls = [];
  document.querySelectorAll('meta[name="og:image"], meta[property="og:image"]').forEach(function (m) {
    var c = m.getAttribute('content');
    if (c) imageUrls.push(c);
  });
  return {
    title: ogTitle || '',
    description: description || '',
    image_urls: Array.from(new Set(imageUrls)),
    note_like: readMetaContent('og:xhs:note_like') || null,
    note_comment: readMetaContent('og:xhs:note_comment') || null,
    note_collect: readMetaContent('og:xhs:note_collect') || null,
  };
}

/**
 * detectAntiCrawl - meta 三件齐全才视为正常笔记页；缺失时记录 risk hit。
 */
function detectAntiCrawl(meta) {
  var hasLike = !!(meta && meta.note_like);
  var hasComment = !!(meta && meta.note_comment);
  var hasCollect = !!(meta && meta.note_collect);
  if (hasLike && hasComment && hasCollect) {
    return { ok: true, reason: 'meta_complete' };
  }
  // hard：当前 location 已被跳到登录页 → 强制判 hard
  var loc = '';
  try { loc = String(location.href || ''); } catch (_) { loc = ''; }
  if (/\/login(\?|$|\/)/.test(loc)) {
    recordRiskHit('redirect_to_login', 'hard');
    return { ok: false, reason: 'redirect_to_login', kind: 'hard', hasLike: hasLike, hasComment: hasComment, hasCollect: hasCollect };
  }
  // soft：meta 缺失，可能是反爬空壳页或加载未完成
  recordRiskHit('meta_incomplete', 'soft');
  return { ok: false, reason: 'meta_incomplete', kind: 'soft', hasLike: hasLike, hasComment: hasComment, hasCollect: hasCollect };
}

// ---------------------------------------------------------------------------
// 媒体白名单
// ---------------------------------------------------------------------------

function pickMediaFromNote(node) {
  if (!node) return [];
  var urls = [];
  node.querySelectorAll('img').forEach(function (img) {
    var src = img.src || img.getAttribute('data-src');
    if (!src) return;
    if (/sns-webpic|sns-img|ci\.xiaohongshu\.com|xhscdn\.com/i.test(src) && !/avatar/i.test(src)) {
      urls.push(src);
    }
  });
  return Array.from(new Set(urls));
}

// ---------------------------------------------------------------------------
// 用户信息（嵌在页面 HTML 里的 JSON）
// ---------------------------------------------------------------------------

function parseUserInfoFromHtml() {
  try {
    var html = document.documentElement.outerHTML;
    var userIdMatch = html.match(/"userId":\s*"([^"]+)"/);
    var nicknameMatch = html.match(/"nickname":\s*"([^"]+)"/);
    var userId = userIdMatch ? userIdMatch[1] : null;
    var nickname = nicknameMatch ? nicknameMatch[1] : null;
    return {
      userId: userId,
      nickname: nickname,
      userUrl: userId ? 'https://www.xiaohongshu.com/user/profile/' + userId : null,
    };
  } catch (_) {
    return { userId: null, nickname: null, userUrl: null };
  }
}

// ---------------------------------------------------------------------------
// 计数文案解析（"1.2万" 之类）
// ---------------------------------------------------------------------------

function parseCountText(value) {
  if (value == null) return null;
  var s = String(value).trim().replace(/\s+/g, '');
  if (!s) return null;
  if (/^[0-9]+$/.test(s)) return parseInt(s, 10);
  var m = s.match(/^([0-9]+(?:\.[0-9]+)?)([万w亿k千])/i);
  if (m) {
    var n = parseFloat(m[1]);
    var unit = m[2].toLowerCase();
    if (unit === '万' || unit === 'w') return Math.round(n * 10000);
    if (unit === '亿') return Math.round(n * 1e8);
    if (unit === 'k') return Math.round(n * 1000);
    if (unit === '千') return Math.round(n * 1000);
  }
  var m2 = s.match(/^([0-9]+(?:\.[0-9]+)?)$/);
  if (m2) return parseFloat(m2[1]);
  return null;
}

// ---------------------------------------------------------------------------
// 评论 normalize
// ---------------------------------------------------------------------------

function normalizeXhsApiComment(comment) {
  if (!comment || typeof comment !== 'object') return null;
  var userInfo = comment.user_info || comment.user || {};
  var replies = Array.isArray(comment.sub_comments)
    ? comment.sub_comments.map(normalizeXhsApiComment).filter(Boolean)
    : [];
  return {
    comment_id: comment.id || comment.comment_id || '',
    author_name: userInfo.nickname || userInfo.user_name || '',
    author_id: userInfo.user_id || userInfo.userId || '',
    author_avatar: userInfo.image || userInfo.avatar || '',
    content: comment.content || comment.note_comment || '',
    like_count: comment.like_count != null ? comment.like_count : (comment.liked_count != null ? comment.liked_count : 0),
    time: comment.create_time || comment.time || '',
    replies: replies,
  };
}

// ---------------------------------------------------------------------------
// session state（登录态）
// ---------------------------------------------------------------------------

function sessionStateCommon() {
  try {
    var cookies = getXhsAuthCookies();
    var loggedIn = !!(cookies.a1 && cookies.web_session);
    var userInfo = parseUserInfoFromHtml();
    return {
      ok: true,
      data: {
        loggedIn: loggedIn,
        url: location.href,
        hostname: location.hostname,
        username: userInfo.nickname || null,
        userId: userInfo.userId || null,
        cookieFlags: { hasA1: !!cookies.a1, hasWebSession: !!cookies.web_session, hasWebId: !!cookies.webId },
      },
      antiCrawlState: snapshotAntiCrawl(),
    };
  } catch (err) {
    return errResult('session_state_failed', { message: String((err && err.message) || err) });
  }
}

// ---------------------------------------------------------------------------
// navigateLocation（仅同源，仅 location.assign）
// ---------------------------------------------------------------------------

function navigateLocation(targetUrl) {
  try {
    var u = new URL(targetUrl, location.href);
    var same = (XHS_HOST_RE.test(u.hostname) || XHS_SHORT_HOST_RE.test(u.hostname));
    if (!same) {
      return { ok: false, error: 'cross_origin_navigation_forbidden', hostname: u.hostname };
    }
    var fromUrl = location.href;
    if (fromUrl === u.toString()) {
      return okResult({ noop: true, from: { url: fromUrl }, to: { url: u.toString() } });
    }
    location.assign(u.toString());
    return okResult({ from: { url: fromUrl }, to: { url: u.toString() } });
  } catch (err) {
    return errResult('navigate_failed', { message: String((err && err.message) || err) });
  }
}

// ---------------------------------------------------------------------------
// URL 解析
// ---------------------------------------------------------------------------

function parseNoteIdFromHref(href) {
  try {
    var u = new URL(href || location.href, location.href);
    // 标准笔记路径
    var m = u.pathname.match(/\/(?:explore|discovery\/item|search_result)\/([\w-]+)/i);
    if (m) {
      return {
        noteId: m[1],
        xsec_token: u.searchParams.get('xsec_token') || '',
        xsec_source: u.searchParams.get('xsec_source') || '',
      };
    }
    // 用户主页笔记卡片路径：/user/profile/<userId>/<noteId>?xsec_token=...
    var m2 = u.pathname.match(/\/user\/profile\/[\w-]+\/([\w-]+)/i);
    if (m2) {
      return {
        noteId: m2[1],
        xsec_token: u.searchParams.get('xsec_token') || '',
        xsec_source: u.searchParams.get('xsec_source') || '',
      };
    }
    return null;
  } catch (_) { return null; }
}

// ---------------------------------------------------------------------------
// React fiber helpers
//   小红书 SPA 把笔记 href（带 xsec_token）放在 React props 里，DOM attribute
//   只有 path。读 fiber __reactProps$ 才能拿到带 token 的真实链接。
// ---------------------------------------------------------------------------

function readReactProps(el) {
  if (!el) return null;
  try {
    var key = Object.keys(el).find(function (k) { return k.indexOf('__reactProps$') === 0; });
    return key ? el[key] : null;
  } catch (_) { return null; }
}

function readReactHref(el) {
  var props = readReactProps(el);
  if (props && typeof props.href === 'string' && props.href) return props.href;
  // 卡片层级的 onClick / state 也可能带 noteCard 引用
  var fiberKey = el ? Object.keys(el).find(function (k) { return k.indexOf('__reactFiber$') === 0; }) : null;
  if (!fiberKey) return null;
  try {
    var f = el[fiberKey];
    for (var depth = 0; depth < 6 && f; depth++) {
      var p = f.memoizedProps || (f.return && f.return.memoizedProps);
      if (p && typeof p.href === 'string' && p.href) return p.href;
      if (p && p.note && p.note.id && p.note.xsec_token) {
        return '/explore/' + p.note.id + '?xsec_token=' + encodeURIComponent(p.note.xsec_token)
          + (p.note.xsec_source ? ('&xsec_source=' + encodeURIComponent(p.note.xsec_source)) : '');
      }
      f = f.return;
    }
  } catch (_) {}
  return null;
}
