import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium, type Browser, type Page, type BrowserContext } from 'playwright';
import * as XLSXNS from 'xlsx';
import { AppDataSource, PlatformAccount } from '../models/index.js';
import { BrowserManager } from '../automation/browser.js';
import { logger } from '../utils/logger.js';
import { UserDayStatisticsService } from './UserDayStatisticsService.js';
import { AccountService } from './AccountService.js';
import { getPythonServiceBaseUrl } from './PythonServiceConfigService.js';
import type { ProxyConfig } from '@media-manager/shared';
import { WS_EVENTS } from '@media-manager/shared';
import { wsManager } from '../websocket/index.js';

// xlsx 在 ESM 下可能挂在 default 上；这里做一次兼容兜底
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const XLSX: any = (XLSXNS as any).default ?? (XLSXNS as any);

type PlaywrightCookie = {
  name: string;
  value: string;
  domain?: string;
  path?: string;
  url?: string;
  expires?: number;
  httpOnly?: boolean;
  secure?: boolean;
  sameSite?: 'Lax' | 'None' | 'Strict';
};

type MetricKind =
  | 'playCount'
  | 'exposureCount'
  | 'likeCount'
  | 'commentCount'
  | 'shareCount'
  | 'collectCount'
  | 'fansIncrease'
  | 'worksCount'
  | 'coverClickRate'
  | 'avgWatchDuration'
  | 'totalWatchDuration'
  | 'completionRate';

type ExportMode = 'watch' | 'interaction' | 'fans' | 'publish';

function ensureDir(p: string) {
  return fs.mkdir(p, { recursive: true });
}

function normalizeDateText(input: unknown): Date | null {
  if (!input) return null;
  if (input instanceof Date && !Number.isNaN(input.getTime())) {
    const d = new Date(input);
    d.setHours(0, 0, 0, 0);
    return d;
  }
  const s = String(input).trim();
  // 2026年01月27日
  const m1 = s.match(/(\d{4})\D(\d{1,2})\D(\d{1,2})\D?/);
  if (m1) {
    const yyyy = Number(m1[1]);
    const mm = Number(m1[2]);
    const dd = Number(m1[3]);
    if (!yyyy || !mm || !dd) return null;
    const d = new Date(yyyy, mm - 1, dd);
    d.setHours(0, 0, 0, 0);
    return d;
  }
  // 01-27（兜底：用当前年份）
  const m2 = s.match(/^(\d{1,2})[-/](\d{1,2})$/);
  if (m2) {
    const yyyy = new Date().getFullYear();
    const mm = Number(m2[1]);
    const dd = Number(m2[2]);
    const d = new Date(yyyy, mm - 1, dd);
    d.setHours(0, 0, 0, 0);
    return d;
  }
  return null;
}

function parseChineseNumberLike(input: unknown): number | null {
  if (input === null || input === undefined) return null;
  const s = String(input).trim();
  if (!s) return null;
  // 8,077
  const plain = s.replace(/,/g, '');
  // 4.8万
  const wan = plain.match(/^(\d+(\.\d+)?)\s*万$/);
  if (wan) return Math.round(Number(wan[1]) * 10000);
  const yi = plain.match(/^(\d+(\.\d+)?)\s*亿$/);
  if (yi) return Math.round(Number(yi[1]) * 100000000);
  const n = Number(plain.replace(/[^\d.-]/g, ''));
  if (Number.isFinite(n)) return Math.round(n);
  return null;
}

function detectMetricKind(sheetName: string): MetricKind | null {
  const n = sheetName.trim();
  // 观看数据：子表命名可能是「观看趋势」或「观看数趋势」
  if (n.includes('观看趋势') || n.includes('观看数')) return 'playCount';
  if (n.includes('曝光趋势')) return 'exposureCount';
  if (n.includes('封面点击率')) return 'coverClickRate';
  if (n.includes('平均观看时长')) return 'avgWatchDuration';
  if (n.includes('观看总时长')) return 'totalWatchDuration';
  if (n.includes('完播率')) return 'completionRate';

  // 互动数据
  if (n.includes('点赞') && n.includes('趋势')) return 'likeCount';
  if (n.includes('评论') && n.includes('趋势')) return 'commentCount';
  if (n.includes('分享') && n.includes('趋势')) return 'shareCount';
  if (n.includes('收藏') && n.includes('趋势')) return 'collectCount';

  // 涨粉数据（只取净涨粉趋势）
  if (n.includes('净涨粉') && n.includes('趋势')) return 'fansIncrease';

  // 发布数据：总发布趋势 → 每日发布数，入库 works_count
  if (n.includes('总发布趋势')) return 'worksCount';
  return null;
}

function parseCookiesFromAccount(cookieData: string | null): PlaywrightCookie[] {
  if (!cookieData) return [];
  const raw = cookieData.trim();
  if (!raw) return [];

  // 1) JSON array（最常见：浏览器插件导出/前端保存）
  if (raw.startsWith('[') || raw.startsWith('{')) {
    try {
      const parsed = JSON.parse(raw);
      const arr = Array.isArray(parsed) ? parsed : (parsed?.cookies ? parsed.cookies : []);
      if (!Array.isArray(arr)) return [];
      return arr
        .map((c: any) => {
          const name = String(c?.name ?? '').trim();
          const value = String(c?.value ?? '').trim();
          if (!name) return null;
          const domain = c?.domain ? String(c.domain) : undefined;
          const pathVal = c?.path ? String(c.path) : '/';
          const url = !domain ? 'https://creator.xiaohongshu.com' : undefined;
          const sameSiteRaw = c?.sameSite;
          const sameSite =
            sameSiteRaw === 'Lax' || sameSiteRaw === 'None' || sameSiteRaw === 'Strict'
              ? sameSiteRaw
              : undefined;

          return {
            name,
            value,
            domain,
            path: pathVal,
            url,
            expires: typeof c?.expires === 'number' ? c.expires : undefined,
            httpOnly: typeof c?.httpOnly === 'boolean' ? c.httpOnly : undefined,
            secure: typeof c?.secure === 'boolean' ? c.secure : undefined,
            sameSite,
          } satisfies PlaywrightCookie;
        })
        .filter(Boolean) as PlaywrightCookie[];
    } catch {
      // fallthrough
    }
  }

  // 2) "a=b; c=d" 拼接格式
  const pairs = raw.split(';').map((p) => p.trim()).filter(Boolean);
  const cookies: PlaywrightCookie[] = [];
  for (const p of pairs) {
    const idx = p.indexOf('=');
    if (idx <= 0) continue;
    const name = p.slice(0, idx).trim();
    const value = p.slice(idx + 1).trim();
    if (!name) continue;
    cookies.push({ name, value, url: 'https://creator.xiaohongshu.com' });
  }
  return cookies;
}

async function createBrowserForAccount(proxy: ProxyConfig | null): Promise<{ browser: Browser; shouldClose: boolean }> {
  // 静默同步：默认一律 headless，不弹窗
  // 只有在“引导登录/验证”时（XHS_STORAGE_STATE_BOOTSTRAP=1 且 XHS_IMPORT_HEADLESS=0）才允许 headful
  const allowHeadfulForBootstrap = process.env.XHS_STORAGE_STATE_BOOTSTRAP === '1' && process.env.XHS_IMPORT_HEADLESS === '0';
  const headless = !allowHeadfulForBootstrap;
  if (proxy?.enabled) {
    const server = `${proxy.type}://${proxy.host}:${proxy.port}`;
    const browser = await chromium.launch({
      headless,
      proxy: {
        server,
        username: proxy.username,
        password: proxy.password,
      },
      args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--window-size=1920,1080'],
    });
    return { browser, shouldClose: true };
  }
  const browser = await BrowserManager.getBrowser({ headless });
  return { browser, shouldClose: false };
}

export function parseXhsExcel(
  filePath: string,
  mode: ExportMode
): Map<string, { recordDate: Date } & Record<string, any>> {
  const wb = XLSX.readFile(filePath);
  const result = new Map<string, { recordDate: Date } & Record<string, any>>();

  logger.info(
    `[XHS Import] Excel loaded. mode=${mode} file=${path.basename(filePath)} sheets=${wb.SheetNames.join(' | ')}`
  );

  for (const sheetName of wb.SheetNames) {
    const kind = detectMetricKind(sheetName);
    if (!kind) continue;

    // 按导出类型过滤不相关子表，避免误写字段
    if (
      (mode === 'watch' &&
        !['playCount', 'exposureCount', 'coverClickRate', 'avgWatchDuration', 'totalWatchDuration', 'completionRate'].includes(kind)) ||
      (mode === 'interaction' && !['likeCount', 'commentCount', 'shareCount', 'collectCount'].includes(kind)) ||
      (mode === 'fans' && kind !== 'fansIncrease') ||
      (mode === 'publish' && kind !== 'worksCount')
    ) {
      continue;
    }

    const sheet = wb.Sheets[sheetName];
    const rows = (XLSX.utils.sheet_to_json(sheet, { defval: '' }) as Record<string, any>[]);

    if (rows.length) {
      const keys = Object.keys(rows[0] || {});
      logger.info(`[XHS Import] Sheet parsed. name=${sheetName} kind=${kind} rows=${rows.length} keys=${keys.join(',')}`);
    } else {
      logger.warn(`[XHS Import] Sheet empty. name=${sheetName} kind=${kind}`);
    }

    for (const row of rows) {
      const dateVal = row['日期'] ?? row['date'] ?? row['Date'] ?? row[Object.keys(row)[0] ?? ''];
      const valueVal = row['数值'] ?? row['value'] ?? row['Value'] ?? row[Object.keys(row)[1] ?? ''];
      const d = normalizeDateText(dateVal);
      if (!d) continue;
      const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
      if (!result.has(key)) result.set(key, { recordDate: d });
      const obj = result.get(key)!;

      if (kind === 'playCount' || kind === 'exposureCount' || kind === 'likeCount' || kind === 'commentCount' || kind === 'shareCount' || kind === 'collectCount' || kind === 'fansIncrease' || kind === 'worksCount') {
        const n = parseChineseNumberLike(valueVal);
        if (typeof n === 'number') {
          if (kind === 'playCount') obj.playCount = n;
          if (kind === 'exposureCount') obj.exposureCount = n;
          if (kind === 'likeCount') obj.likeCount = n;
          if (kind === 'worksCount') obj.worksCount = n;
          if (kind === 'commentCount') obj.commentCount = n;
          if (kind === 'shareCount') obj.shareCount = n;
          if (kind === 'collectCount') obj.collectCount = n;
          if (kind === 'fansIncrease') obj.fansIncrease = n; // 允许负数
        }
      } else {
        const s = String(valueVal ?? '').trim();
        if (kind === 'coverClickRate') obj.coverClickRate = s || '0';
        if (kind === 'avgWatchDuration') obj.avgWatchDuration = s || '0';
        if (kind === 'totalWatchDuration') obj.totalWatchDuration = s || '0';
        if (kind === 'completionRate') obj.completionRate = s || '0';
      }
    }
  }

  return result;
}

export { parseCookiesFromAccount, createBrowserForAccount };

export class XiaohongshuAccountOverviewImportService {
  private accountRepository = AppDataSource.getRepository(PlatformAccount);
  private userDayStatisticsService = new UserDayStatisticsService();

  private downloadDir = path.resolve(process.cwd(), 'tmp', 'xhs-account-overview');
  private stateDir = path.resolve(process.cwd(), 'tmp', 'xhs-storage-state');

  private getStatePath(accountId: number) {
    return path.join(this.stateDir, `${accountId}.json`);
  }

  private async ensureStorageState(account: PlatformAccount, cookies: PlaywrightCookie[]): Promise<string | null> {
    const statePath = this.getStatePath(account.id);
    try {
      await fs.access(statePath);
      return statePath;
    } catch {
      // no state
    }

    // 需要你在弹出的浏览器里完成一次登录/验证，然后脚本会自动保存 storageState
    // 启用方式：XHS_IMPORT_HEADLESS=0 且 XHS_STORAGE_STATE_BOOTSTRAP=1
    if (!(process.env.XHS_IMPORT_HEADLESS === '0' && process.env.XHS_STORAGE_STATE_BOOTSTRAP === '1')) {
      return null;
    }

    await ensureDir(this.stateDir);
    logger.warn(`[XHS Import] No storageState for accountId=${account.id}. Bootstrapping... 请在弹出的浏览器中完成登录/验证。`);

    const { browser, shouldClose } = await createBrowserForAccount(account.proxyConfig);
    try {
      const context = await browser.newContext({
        viewport: { width: 1920, height: 1080 },
        locale: 'zh-CN',
        timezoneId: 'Asia/Shanghai',
      });
      await context.addCookies(cookies as any);
      const page = await context.newPage();
      await page.goto('https://creator.xiaohongshu.com/statistics/account/v2', { waitUntil: 'domcontentloaded' });

      // 最长等 5 分钟：让你手动完成登录/滑块/短信等
      await page
        .waitForFunction(() => {
          const t = document.body?.innerText || '';
          return t.includes('账号概览') || t.includes('数据总览') || t.includes('观看数据');
        }, { timeout: 5 * 60_000 })
        .catch(() => undefined);

      await context.storageState({ path: statePath });
      logger.info(`[XHS Import] storageState saved: ${statePath}`);
      await context.close();
      return statePath;
    } finally {
      if (shouldClose) await browser.close().catch(() => undefined);
    }
  }

  /**
   * 统一入口：定时任务与添加账号均调用此方法，执行“账号概览-观看/互动/涨粉-近30日 + 粉丝 overall_new”
   */
  static async runDailyImport(): Promise<void> {
    const svc = new XiaohongshuAccountOverviewImportService();
    await svc.runDailyImportForAllXhsAccounts();
  }

  /**
   * 单账号入口：仅为指定账号执行近30日账号概览导入（用于账号从失效恢复为 active 时补数）
   */
  static async runDailyImportForAccount(accountId: number): Promise<void> {
    const svc = new XiaohongshuAccountOverviewImportService();
    const account = await svc.accountRepository.findOne({
      where: { id: accountId, platform: 'xiaohongshu' as any },
    });
    if (!account) {
      throw new Error(`未找到小红书账号 id=${accountId}`);
    }
    await svc.importAccountLast30Days(account);
  }

  /**
   * 单账号入口：仅为指定账号执行近30日账号概览导入（用于账号从失效恢复为 active 时补数）
   */
  static async runDailyImportForAccount(accountId: number): Promise<void> {
    const svc = new XiaohongshuAccountOverviewImportService();
    const account = await svc.accountRepository.findOne({
      where: { id: accountId, platform: 'xiaohongshu' as any },
    });
    if (!account) {
      throw new Error(`未找到小红书账号 id=${accountId}`);
    }
    await svc.importAccountLast30Days(account);
  }

  /**
   * 为所有小红书账号导出“观看数据-近30日”并导入 user_day_statistics
   */
  async runDailyImportForAllXhsAccounts(): Promise<void> {
    await ensureDir(this.downloadDir);

    const accounts = await this.accountRepository.find({
      where: { platform: 'xiaohongshu' as any },
    });

    logger.info(`[XHS Import] Start. total_accounts=${accounts.length}`);

    await Promise.all(
      accounts.map((account) =>
        this.importAccountLast30Days(account).catch((e) => {
          logger.error(`[XHS Import] Account failed. accountId=${account.id} name=${account.accountName || ''}`, e);
        })
      )
    );

    logger.info('[XHS Import] Done.');
  }

  /** 通过 Python 调用 account/base（登录与打开后台一致：使用账号已存 Cookie） */
  private async fetchAccountBaseViaPython(account: PlatformAccount): Promise<Record<string, unknown> | null> {
    const base = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
    const url = `${base}/xiaohongshu/account_base`;
    const cookie = String(account.cookieData || '').trim();
    if (!cookie) return null;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 35_000);
    try {
      const res = await fetch(url, {
        method: 'POST',
        signal: controller.signal,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ cookie }),
      });
      const text = await res.text();
      const body = text ? (JSON.parse(text) as Record<string, unknown>) : null;
      if (!body || typeof body !== 'object') return null;
      return body;
    } catch {
      return null;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /** 通过 Python 调用 fans/overall_new（登录与打开后台一致） */
  private async fetchFansOverallNewViaPython(account: PlatformAccount): Promise<Record<string, unknown> | null> {
    const base = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
    const url = `${base}/xiaohongshu/fans_overall_new`;
    const cookie = String(account.cookieData || '').trim();
    if (!cookie) return null;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 35_000);
    try {
      const res = await fetch(url, {
        method: 'POST',
        signal: controller.signal,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ cookie }),
      });
      const text = await res.text();
      const body = text ? (JSON.parse(text) as Record<string, unknown>) : null;
      if (!body || typeof body !== 'object') return null;
      return body;
    } catch {
      return null;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /** 通过 Python 合并接口一次拉取 account_base + fans_overall_new（内部并行，减少耗时） */
  private async fetchAccountOverviewViaPython(
    account: PlatformAccount
  ): Promise<{ account_base: Record<string, unknown> | null; fans_overall_new: Record<string, unknown> | null } | null> {
    const base = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
    const url = `${base}/xiaohongshu/account_overview`;
    const cookie = String(account.cookieData || '').trim();
    if (!cookie) return null;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 50_000);
    try {
      const res = await fetch(url, {
        method: 'POST',
        signal: controller.signal,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ cookie }),
      });
      const text = await res.text();
      const body = text ? (JSON.parse(text) as Record<string, unknown>) : null;
      if (!body || typeof body !== 'object') return null;
      const accountBase = body.account_base as Record<string, unknown> | undefined;
      const fansOverallNew = body.fans_overall_new as Record<string, unknown> | undefined;
      return {
        account_base: accountBase && typeof accountBase === 'object' ? accountBase : null,
        fans_overall_new: fansOverallNew && typeof fansOverallNew === 'object' ? fansOverallNew : null,
      };
    } catch {
      return null;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /**
   * 单账号：优先 Python（合并接口 account_overview，失败则拆成两次调用），失败则刷新重试一次，再失败则浏览器兜底
   */
  async importAccountLast30Days(account: PlatformAccount, isRetry = false): Promise<void> {
    const cookies = parseCookiesFromAccount(account.cookieData);
    if (!cookies.length) {
      throw new Error('cookieData 为空或无法解析');
    }

    const processAccountBaseAndFans = async (
      accountBaseBody: Record<string, unknown> | null,
      fansBody: Record<string, unknown> | null
    ): Promise<boolean> => {
      const data = accountBaseBody?.data as Record<string, unknown> | undefined;
      const thirty = data?.thirty as Record<string, unknown> | undefined;
      let pythonAccountBaseOk = false;
      if (thirty && typeof thirty === 'object') {
        const perDay = this.parseAccountBaseThirty(thirty);
        if (perDay.size > 0) {
          let inserted = 0;
          let updated = 0;
          const today = new Date();
          today.setHours(0, 0, 0, 0);
          for (const v of perDay.values()) {
            const { recordDate, ...patch } = v;
            if (
              recordDate.getTime() === today.getTime() &&
              patch.fansCount === undefined &&
              account.fansCount != null &&
              account.fansCount > 0
            ) {
              (patch as Record<string, unknown>).fansCount = account.fansCount;
            }
            const r = await this.userDayStatisticsService.saveStatisticsForDate(account.id, recordDate, patch);
            inserted += r.inserted;
            updated += r.updated;
          }
          logger.info(
            `[XHS Import] account/base (via Python). accountId=${account.id} days=${perDay.size} inserted=${inserted} updated=${updated}`
          );
          pythonAccountBaseOk = true;
        }
      }
      if (fansBody && typeof fansBody === 'object') {
        const list = this.parseFansOverallNewResponse(fansBody);
        if (list.length > 0) {
          let fansUpdated = 0;
          for (const { recordDate, fansCount } of list) {
            const r = await this.userDayStatisticsService.saveStatisticsForDate(account.id, recordDate, {
              fansCount,
            });
            fansUpdated += r.inserted + r.updated;
          }
          logger.info(`[XHS Import] Fans trend (via Python). accountId=${account.id} days=${list.length} updated=${fansUpdated}`);
        }
      }
      return pythonAccountBaseOk;
    };

    // 优先 Python：先试合并接口，失败再拆成两次调用
    try {
      let accountBaseBody: Record<string, unknown> | null = null;
      let fansBody: Record<string, unknown> | null = null;
      const overview = await this.fetchAccountOverviewViaPython(account);
      if (overview?.account_base != null || overview?.fans_overall_new != null) {
        accountBaseBody = overview.account_base ?? null;
        fansBody = overview.fans_overall_new ?? null;
      }
      if (accountBaseBody == null && fansBody == null) {
        accountBaseBody = await this.fetchAccountBaseViaPython(account);
        if (accountBaseBody != null) {
          try {
            fansBody = await this.fetchFansOverallNewViaPython(account);
          } catch (e) {
            logger.warn(`[XHS Import] Fans via Python failed (non-fatal). accountId=${account.id}`, e instanceof Error ? e.message : e);
          }
        }
      }
      if (accountBaseBody != null) {
        const ok = await processAccountBaseAndFans(accountBaseBody, fansBody);
        if (ok) {
          logger.info(`[XHS Import] Account all tabs done (via Python). accountId=${account.id}`);
          return;
        }
      }
    } catch (pythonError) {
      logger.warn(
        '[XHS Import] Python path failed, fallback to browser. accountId=' + account.id,
        pythonError instanceof Error ? pythonError.message : pythonError
      );
    }

    if (!isRetry) {
      try {
        const accountService = new AccountService();
        const refreshResult = await accountService.refreshAccount(account.userId, account.id);
        if (!refreshResult.needReLogin) {
          const refreshedAccount = await this.accountRepository.findOne({ where: { id: account.id } });
          if (refreshedAccount) {
            logger.info(`[XHS Import] Account ${account.id} refreshed, retrying import...`);
            return await this.importAccountLast30Days(refreshedAccount, true);
          }
        }
      } catch (refreshError) {
        logger.error(`[XHS Import] Account ${account.id} refresh failed:`, refreshError);
      }
    }

    // 浏览器兜底
    const { browser, shouldClose } = await createBrowserForAccount(account.proxyConfig);
    try {
      const statePath = await this.ensureStorageState(account, cookies);
      const context = await browser.newContext({
        acceptDownloads: true,
        viewport: { width: 1920, height: 1080 },
        locale: 'zh-CN',
        timezoneId: 'Asia/Shanghai',
        userAgent:
          'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        ...(statePath ? { storageState: statePath } : {}),
      });
      context.setDefaultTimeout(60_000);
      // 如果没 state，就退回 cookie-only（可能导出为 0）
      if (!statePath) {
        await context.addCookies(cookies as any);
      }

      const page = await context.newPage();

      // account/base 在页面加载时自动请求，先挂监听再访问
      const accountBasePattern = /\/api\/galaxy\/v2\/creator\/datacenter\/account\/base/i;
      const responsePromise = page.waitForResponse(
        (r) => r.url().match(accountBasePattern) != null && r.request().method() === 'GET',
        { timeout: 30_000 }
      );

      await page.goto('https://creator.xiaohongshu.com/statistics/account/v2', { waitUntil: 'domcontentloaded' });
      await page.waitForTimeout(3000); // 等几秒，让页面发起 account/base 请求

      if (page.url().includes('login')) {
        // 第一次检测到登录失效时，尝试刷新账号；刷新失败则仅跳过本账号，不影响其他账号
        if (!isRetry) {
          logger.info(`[XHS Import] Login expired detected for account ${account.id}, attempting to refresh...`);
          responsePromise.catch(() => {}); // 关闭前吸收 rejection，避免 context 关闭导致进程崩溃
          await context.close();
          if (shouldClose) await browser.close();
          try {
            const accountService = new AccountService();
            const refreshResult = await accountService.refreshAccount(account.userId, account.id);
            if (refreshResult.needReLogin) {
              logger.warn(`[XHS Import] Account ${account.id} refresh failed, still needs re-login, skip this account`);
              return;
            }
            const refreshedAccount = await this.accountRepository.findOne({ where: { id: account.id } });
            if (!refreshedAccount) {
              logger.warn(`[XHS Import] Account ${account.id} refreshed but not found, skip`);
              return;
            }
            logger.info(`[XHS Import] Account ${account.id} refreshed successfully, retrying import...`);
            return await this.importAccountLast30Days(refreshedAccount, true);
          } catch (refreshError) {
            logger.error(`[XHS Import] Account ${account.id} refresh failed, skip this account:`, refreshError);
            return;
          }
        } else {
          responsePromise.catch(() => {});
          logger.warn(`[XHS Import] Account ${account.id} still login expired after retry, skip this account`);
          return;
        }
      }

      // 检测“暂无访问权限 / 权限申请中”提示：仅推送提示，不修改账号状态（避免误判或用户不想自动变更）
      const bodyText = (await page.textContent('body').catch(() => '')) || '';
      if (bodyText.includes('暂无访问权限') || bodyText.includes('数据权限申请中') || bodyText.includes('次日再来查看')) {
        // await this.accountRepository.update(account.id, { status: 'expired' as any });
        wsManager.sendToUser(account.userId, WS_EVENTS.SYSTEM_MESSAGE, {
          level: 'warning',
          message: `小红书账号「${account.accountName || account.accountId || account.id}」暂无数据看板访问权限，请到小红书创作服务平台申请数据权限（通过后一般次日生效）。`,
          platform: 'xiaohongshu',
          accountId: account.id,
        });
        throw new Error('小红书数据看板暂无访问权限/申请中，已通知用户');
      }

      // 直接监听 account/base，无需点击 账号概览/笔记数据
      await this.importFromAccountBaseApi(responsePromise, page, account);

      // 粉丝数据页：打开粉丝数据、点击近30天，解析 overall_new 接口，将每日粉丝总数写入 user_day_statistics.fans_count
      await this.importFansDataTrendFromPage(context, page, account);

      logger.info(`[XHS Import] Account all tabs done. accountId=${account.id}`);

      await context.close();
    } finally {
      if (shouldClose) {
        await browser.close().catch(() => undefined);
      }
    }
  }

  /**
   * 等待 account/base 接口响应，解析 data.thirty 各 *_list 按 date 合并为按日数据并写入 user_day_statistics
   * 字段映射：view_list→playCount, impl_count_list→exposureCount, comment_list→commentCount,
   * like_list→likeCount, share_list→shareCount, collect_list→collectCount,
   * net_rise_fans_count_list→fansIncrease, cover_click_rate_list→coverClickRate(格式化为"14%"),
   * avg_view_time_list→avgWatchDuration("12秒"), view_time_list→totalWatchDuration("1866秒"),
   * video_full_view_rate_list→completionRate("15%"), publish_note_num_list→worksCount
   */
  private async importFromAccountBaseApi(
    responsePromise: Promise<import('playwright').Response>,
    _page: Page,
    account: PlatformAccount
  ): Promise<void> {
    let res: import('playwright').Response;
    try {
      res = await responsePromise;
    } catch {
      logger.warn(`[XHS Import] account/base response not captured, skip. accountId=${account.id}`);
      return;
    }

    const body = await res.json().catch(() => null);
    if (!body || typeof body !== 'object') {
      logger.warn(`[XHS Import] account/base not valid JSON. accountId=${account.id}`);
      return;
    }

    const data = (body as Record<string, unknown>).data as Record<string, unknown> | undefined;
    const thirty = data?.thirty as Record<string, unknown> | undefined;
    if (!thirty || typeof thirty !== 'object') {
      logger.warn(`[XHS Import] account/base data.thirty missing. accountId=${account.id}`);
      return;
    }

    const perDay = this.parseAccountBaseThirty(thirty);
    if (perDay.size === 0) {
      logger.info(`[XHS Import] account/base no days parsed. accountId=${account.id}`);
      return;
    }

    let inserted = 0;
    let updated = 0;
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    for (const v of perDay.values()) {
      const { recordDate, ...patch } = v;
      if (recordDate.getTime() === today.getTime() && patch.fansCount === undefined && account.fansCount != null && account.fansCount > 0) {
        (patch as Record<string, unknown>).fansCount = account.fansCount;
      }
      const r = await this.userDayStatisticsService.saveStatisticsForDate(account.id, recordDate, patch);
      inserted += r.inserted;
      updated += r.updated;
    }

    logger.info(
      `[XHS Import] account/base imported. accountId=${account.id} days=${perDay.size} inserted=${inserted} updated=${updated}`
    );
  }

  /**
   * 解析 data.thirty：各 *_list 每项 { date: 毫秒, count[, count_with_double] }，按 date 合并为按日一条
   * 注意：接口返回的 date 是「中国时区（Asia/Shanghai）该日 0 点」的 UTC 时间戳，需按中国时区解析日期
   */
  private parseAccountBaseThirty(thirty: Record<string, unknown>): Map<string, { recordDate: Date } & Record<string, unknown>> {
    const map = new Map<string, { recordDate: Date } & Record<string, unknown>>();

    // 使用 Intl.DateTimeFormat 获取中国时区的年月日
    const cstFormatter = new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    });

    const toKey = (ms: number): string => {
      // 将 UTC 时间戳转成中国时区的日期字符串 YYYY-MM-DD
      return cstFormatter.format(new Date(ms));
    };

    const toRecordDate = (ms: number): Date => {
      // 获取中国时区的年月日
      const parts = cstFormatter.formatToParts(new Date(ms));
      const get = (type: string) => parts.find((p) => p.type === type)?.value ?? '0';
      const y = parseInt(get('year'), 10);
      const m = parseInt(get('month'), 10) - 1; // month 是 1-12，Date 构造函数需要 0-11
      const d = parseInt(get('day'), 10);
      // 构造本地时区的该日 0 点（如果服务器在中国时区，就是中国时区的 0 点）
      return new Date(y, m, d, 0, 0, 0, 0);
    };

    const setFromList = (
      listKey: string,
      field: string,
      formatter?: (n: number) => string | number
    ) => {
      const arr = thirty[listKey];
      if (!Array.isArray(arr)) return;
      for (const item of arr) {
        if (!item || typeof item !== 'object') continue;
        const o = item as Record<string, unknown>;
        const dateMs = o.date;
        const countRaw = o.count;
        if (dateMs == null || countRaw == null) continue;
        const ts = typeof dateMs === 'number' ? dateMs : Number(dateMs);
        if (!Number.isFinite(ts)) continue;
        const key = toKey(ts);
        if (!map.has(key)) {
          map.set(key, { recordDate: toRecordDate(ts) });
        } else {
          (map.get(key)!.recordDate as Date) = toRecordDate(ts);
        }
        const rec = map.get(key)!;
        const n = typeof countRaw === 'number' ? countRaw : Number(countRaw);
        if (!Number.isFinite(n)) continue;
        const val = formatter ? formatter(n) : n;
        (rec as Record<string, unknown>)[field] = val;
      }
    };

    setFromList('view_list', 'playCount');
    setFromList('impl_count_list', 'exposureCount');
    setFromList('comment_list', 'commentCount');
    setFromList('like_list', 'likeCount');
    setFromList('share_list', 'shareCount');
    setFromList('collect_list', 'collectCount');
    setFromList('net_rise_fans_count_list', 'fansIncrease');
    setFromList('cover_click_rate_list', 'coverClickRate', (n) => `${Math.round(n)}%`);
    setFromList('avg_view_time_list', 'avgWatchDuration', (n) => `${Math.round(n)}秒`);
    setFromList('view_time_list', 'totalWatchDuration', (n) => `${Math.round(n)}秒`);
    setFromList('video_full_view_rate_list', 'completionRate', (n) => `${typeof n === 'number' ? Math.round(n) : n}%`);
    setFromList('publish_note_num_list', 'worksCount');

    return map;
  }

  /**
   * 粉丝数据页：打开粉丝数据、点击「粉丝数据概览」近30天，监听 overall_new 接口响应，解析每日粉丝总数并写入 user_day_statistics.fans_count
   */
  private async importFansDataTrendFromPage(
    _context: BrowserContext,
    page: Page,
    account: PlatformAccount
  ): Promise<void> {
    const fansDataUrl = 'https://creator.xiaohongshu.com/statistics/fans-data';
    const overallNewPattern = /\/api\/galaxy\/creator\/data\/fans\/overall_new/i;
    const near30ButtonSelector =
      '#content-area > main > div:nth-child(3) > div > div.content > div.css-12s9z8c.fans-data-container > div.title-container > div.extra-box > div > label:nth-child(2)';

    await page.goto(fansDataUrl, { waitUntil: 'domcontentloaded' });
    await page.waitForTimeout(2000);

    if (page.url().includes('login')) {
      logger.warn(`[XHS Import] Fans data page redirected to login, skip fans trend. accountId=${account.id}`);
      return;
    }

    const responsePromise = page.waitForResponse(
      (res) => res.url().match(overallNewPattern) != null && res.request().method() === 'GET',
      { timeout: 30_000 }
    );

    const btn = page.locator(near30ButtonSelector).or(page.locator('.fans-data-container').getByText('近30天').first());
    await btn.click().catch(() => undefined);
    await page.waitForTimeout(1500);

    let res;
    try {
      res = await responsePromise;
    } catch {
      try {
        res = await page.waitForResponse(
          (r) => r.url().match(overallNewPattern) != null && r.request().method() === 'GET',
          { timeout: 15_000 }
        );
      } catch {
        logger.warn(`[XHS Import] No overall_new response captured, skip fans trend. accountId=${account.id}`);
        return;
      }
    }

    const body = await res.json().catch(() => null);
    if (!body || typeof body !== 'object') {
      logger.warn(`[XHS Import] overall_new response not valid JSON, skip. accountId=${account.id}`);
      return;
    }

    const list = this.parseFansOverallNewResponse(body);
    if (!list.length) {
      logger.info(`[XHS Import] No fans trend items from overall_new. accountId=${account.id}`);
      return;
    }

    let updated = 0;
    for (const { recordDate, fansCount } of list) {
      const r = await this.userDayStatisticsService.saveStatisticsForDate(account.id, recordDate, { fansCount });
      updated += r.inserted + r.updated;
    }
    logger.info(`[XHS Import] Fans trend imported. accountId=${account.id} days=${list.length} updated=${updated}`);
  }

  /**
   * 解析 overall_new 接口返回的 JSON，提取 (recordDate, fansCount) 列表
   * 接口格式：data.thirty.fans_list（或 fans_list_iterator），每项 { date: 毫秒时间戳, count: 粉丝数 }
   */
  private parseFansOverallNewResponse(body: Record<string, unknown>): Array<{ recordDate: Date; fansCount: number }> {
    const list: Array<{ recordDate: Date; fansCount: number }> = [];
    const data = body.data as Record<string, unknown> | undefined;
    if (!data || typeof data !== 'object') return list;

    const thirty = data.thirty as Record<string, unknown> | undefined;
    if (!thirty || typeof thirty !== 'object') return list;

    const arr = (thirty.fans_list as unknown[]) ?? (thirty.fans_list_iterator as unknown[]) ?? [];
    if (!Array.isArray(arr)) return list;

    for (const item of arr) {
      if (!item || typeof item !== 'object') continue;
      const o = item as Record<string, unknown>;
      const dateMs = o.date;
      const countRaw = o.count;
      if (dateMs == null || countRaw == null) continue;
      const ts = typeof dateMs === 'number' ? dateMs : Number(dateMs);
      if (!Number.isFinite(ts)) continue;
      const d = new Date(ts);
      d.setHours(0, 0, 0, 0);
      const n = typeof countRaw === 'number' ? countRaw : Number(countRaw);
      if (!Number.isFinite(n) || n < 0) continue;
      list.push({ recordDate: d, fansCount: Math.round(n) });
    }
    return list;
  }
}

