import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { AppDataSource, PlatformAccount, Work } from '../models/index.js';
import { logger } from '../utils/logger.js';
import { WorkDayStatisticsService } from './WorkDayStatisticsService.js';
import { AccountService } from './AccountService.js';
import type { ProxyConfig } from '@media-manager/shared';
import { BrowserManager } from '../automation/browser.js';
import { In } from 'typeorm';

/** 小红书笔记详情页跳转到登录时抛出，用于触发「先刷新登录、再决定是否账号失效」 */
export class XhsLoginExpiredError extends Error {
  constructor() {
    super('XHS_LOGIN_EXPIRED');
    this.name = 'XhsLoginExpiredError';
  }
}

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

interface NoteTrendItem {
  date?: number | string;
  count?: number | string;
  count_with_double?: number | string;
  /** API 返回的比率数值字段（用于 cover_click_rate、two_second_exit_rate、completion_rate 等，存时加 "%"） */
  coun?: number | string;
}

interface NoteDaySection {
  view_list?: NoteTrendItem[];
  like_list?: NoteTrendItem[];
  comment_list?: NoteTrendItem[];
  share_list?: NoteTrendItem[];
  collect_list?: NoteTrendItem[];
  rise_fans_list?: NoteTrendItem[];
  imp_list?: NoteTrendItem[];
  cover_click_rate_list?: NoteTrendItem[];
  exit_view2s_list?: NoteTrendItem[];
  view_time_list?: NoteTrendItem[];
  finish_list?: NoteTrendItem[];
}

interface NoteBaseData {
  day?: NoteDaySection;
  view_count?: number;
  like_count?: number;
  comment_count?: number;
  share_count?: number;
  collect_count?: number;
  rise_fans_count?: number;
  /** 展现量/曝光数 */
  impl_count?: number;
  /** 封面点击率（数值，非百分号字符串） */
  cover_click_rate?: number;
  /** 2s 退出率（数值，非百分号字符串） */
  exit_view2s_rate?: number;
  /** 完播率（full_view_rate） */
  full_view_rate?: number;
  /** 平均观看时长（秒） */
  view_time_avg_with_double?: number;
  view_time_avg?: number;
}

interface DailyWorkStatPatch {
  workId: number;
  recordDate: Date;
  playCount?: number;
  exposureCount?: number;
  likeCount?: number;
  commentCount?: number;
  shareCount?: number;
  collectCount?: number;
  fansIncrease?: number;
  coverClickRate?: string;
  avgWatchDuration?: string;
  totalWatchDuration?: string;
  completionRate?: string;
  twoSecondExitRate?: string;
}

/** Playwright 只接受 Strict | Lax | None，浏览器导出可能是小写或 no_restriction，需规范化 */
function normalizeSameSite(value: unknown): 'Strict' | 'Lax' | 'None' | undefined {
  if (value === undefined || value === null) return undefined;
  const s = String(value).trim().toLowerCase();
  if (s === 'strict') return 'Strict';
  if (s === 'lax') return 'Lax';
  if (s === 'none' || s === 'no_restriction') return 'None';
  return undefined;
}

/** 与进入后台/每日用户数据一致：每条 cookie 仅保留 url 或 domain 其一，且 domain 仅在有有效值时设置，避免 Playwright addCookies 报错 */
function parseCookiesFromAccount(cookieData: string | null): PlaywrightCookie[] {
  if (!cookieData) return [];
  const raw = cookieData.trim();
  if (!raw) return [];

  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: Record<string, unknown>) => {
          const name = String(c?.name ?? '').trim();
          const value = String(c?.value ?? '').trim();
          if (!name) return null;
          const domain = c?.domain ? String(c.domain).trim() : undefined;
          const pathVal = c?.path ? String(c.path) : '/';
          const url = !domain ? 'https://creator.xiaohongshu.com' : undefined;
          const sameSite = normalizeSameSite(c?.sameSite);
          return {
            name,
            value,
            ...(domain ? { domain } : { url }),
            path: pathVal,
            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 ? { sameSite } : {}),
          } as PlaywrightCookie;
        })
        .filter(Boolean) as PlaywrightCookie[];
    } catch {
      // fallthrough
    }
  }

  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 }> {
  const headless = true;
  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 };
}

function getChinaDateFromTimestamp(ts: number): Date {
  const d = new Date(ts);
  const formatter = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Asia/Shanghai',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  });
  const parts = formatter.formatToParts(d);
  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;
  const day = parseInt(get('day'), 10);
  return new Date(y, m, day, 0, 0, 0, 0);
}

function toNumber(val: unknown, defaultValue = 0): number {
  if (typeof val === 'number') return Number.isFinite(val) ? val : defaultValue;
  if (typeof val === 'string') {
    const n = Number(val);
    return Number.isFinite(n) ? n : defaultValue;
  }
  return defaultValue;
}

function toNonNegativeNumber(val: unknown): number | undefined {
  const n = toNumber(val, NaN);
  if (!Number.isFinite(n)) return undefined;
  return n < 0 ? 0 : n;
}

function toRateString(val: unknown): string | undefined {
  const n = toNumber(val, NaN);
  if (!Number.isFinite(n)) return undefined;
  return n.toString();
}

/** 比率类（非 0 加 %，0 保持为 "0"） */
function toRatePercentStringFromValue(val: unknown): string | undefined {
  const n = toNumber(val, NaN);
  if (!Number.isFinite(n)) return undefined;
  if (n <= 0) return '0';
  return `${n}%`;
}

/** 从 item 取 coun（或 count）转为字符串并追加 "%"，用于比率类字段 */
function toRatePercentString(item: NoteTrendItem): string | undefined {
  const raw = item?.coun ?? item?.count;
  const n = toNumber(raw, NaN);
  if (!Number.isFinite(n)) return undefined;
  if (n === 0) return '0';
  return `${n}%`;
}

/** 平均时长等：保留两位小数，四舍五入 */
function toFixed2String(val: unknown): string | undefined {
  const n = toNonNegativeNumber(val);
  if (n == null) return undefined;
  const rounded = Math.round(n * 100) / 100;
  return rounded.toFixed(2);
}

export class XiaohongshuWorkNoteStatisticsImportService {
  private accountRepository = AppDataSource.getRepository(PlatformAccount);
  private workRepository = AppDataSource.getRepository(Work);
  private workDayStatisticsService = new WorkDayStatisticsService();

  /**
   * 统一入口：定时任务调用，批量为所有小红书账号同步作品日统计
   */
  static async runDailyImport(): Promise<void> {
    const svc = new XiaohongshuWorkNoteStatisticsImportService();
    await svc.runDailyImportForAllXhsAccounts();
  }

  async runDailyImportForAllXhsAccounts(): Promise<void> {
    const accounts = await this.accountRepository.find({
      where: { platform: 'xiaohongshu' as any },
    });

    logger.info(`[XHS WorkStats] Start import for ${accounts.length} accounts`);

    for (const account of accounts) {
      try {
        await this.importAccountWorksStatistics(account);
      } catch (e) {
        logger.error(
          `[XHS WorkStats] Account failed. accountId=${account.id} name=${account.accountName || ''}`,
          e
        );
        // 单账号失败（含登录失效、刷新失败等）仅记录日志，不中断循环，其他账号照常同步
      }
    }

    logger.info('[XHS WorkStats] All accounts done');
  }

  /**
   * 按账号同步作品日统计。检测到登录失效时：先尝试刷新登录一次；刷新仍失效则执行账号失效，刷新成功则用新 cookie 重试一次。
   * @param isRetry 是否为「刷新登录后的重试」，避免无限递归
   */
  /**
   * 按账号同步作品日统计
   * @param options.workIdFilter 仅处理指定 workId（用于「同步作品后为新作品补首批日统计」）
   * @param options.ignorePublishTimeLimit 是否忽略「发布日期+14天」限制（用于首批补数据）
   * @param options.ignorePublishAgeLimit 是否忽略「发布超过 1 年跳过」限制（用于首次全量补数）
   */
  async importAccountWorksStatistics(
    account: PlatformAccount,
    isRetry = false,
    options?: {
      workIdFilter?: number[];
      ignorePublishTimeLimit?: boolean;
      ignorePublishAgeLimit?: boolean;
      onProgress?: (payload: { index: number; total: number; work: Work }) => void;
    }
  ): Promise<void> {
    const cookies = parseCookiesFromAccount(account.cookieData);
    if (!cookies.length) {
      logger.warn(`[XHS WorkStats] accountId=${account.id} cookieData 为空或无法解析，跳过`);
      return;
    }

    const where: any = {
      accountId: account.id,
      platform: 'xiaohongshu' as any,
    };
    if (options?.workIdFilter && options.workIdFilter.length > 0) {
      where.id = In(options.workIdFilter);
    }

    let works = await this.workRepository.find({ where });

    if (!works.length) {
      logger.info(`[XHS WorkStats] accountId=${account.id} 没有作品，跳过`);
      return;
    }

    // 用户更关心最近发布的作品：按发布时间从近到远处理；无发布时间的排最后
    works.sort((a, b) => {
      const ta = a.publishTime ? new Date(a.publishTime).getTime() : -Infinity;
      const tb = b.publishTime ? new Date(b.publishTime).getTime() : -Infinity;
      if (tb !== ta) return tb - ta;
      return (b.id || 0) - (a.id || 0);
    });

    // 定时任务优化：发布超过 1 年的作品，跳过 note/base 拉取（不更新 works.yesterday_*，也不写 work_day_statistics）
    // 仅在首次同步作品的“全量补数”场景（ignorePublishAgeLimit=true）放开此限制
    if (!options?.ignorePublishAgeLimit) {
      const now = new Date();
      const formatter = new Intl.DateTimeFormat('en-CA', {
        timeZone: 'Asia/Shanghai',
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
      });
      const parts = formatter.formatToParts(now);
      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;
      const d = parseInt(get('day'), 10);
      const todayChina = new Date(y, m, d, 0, 0, 0, 0);
      const cutoff = new Date(todayChina);
      cutoff.setFullYear(cutoff.getFullYear() - 1);

      const before = works.length;
      works = works.filter((w) => {
        if (!w.publishTime) return true; // 没有发布时间：保守起见不跳过
        const pub = new Date(w.publishTime);
        pub.setHours(0, 0, 0, 0);
        return pub.getTime() >= cutoff.getTime();
      });
      const skipped = before - works.length;
      if (skipped > 0) {
        logger.info(`[XHS WorkStats] accountId=${account.id} skip old works (>1y). skipped=${skipped} remain=${works.length}`);
      }
      if (!works.length) {
        logger.info(`[XHS WorkStats] accountId=${account.id} all works are older than 1y, skip.`);
        return;
      }
    }

    let browser: Browser | null = null;
    let shouldClose = false;
    let context: BrowserContext | null = null;
    let page: Page | null = null;
    let closedDueToLoginExpired = false;

    const ensureBrowser = async (): Promise<void> => {
      if (page) return;
      const x = await createBrowserForAccount(account.proxyConfig);
      browser = x.browser;
      shouldClose = x.shouldClose;
      context = await browser.newContext({
        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',
      });
      await context.addCookies(cookies as any);
      context.setDefaultTimeout(60_000);
      page = await context.newPage();
    };

    try {
      let totalInserted = 0;
      let totalUpdated = 0;
      const total = works.length;
      let skippedNoNoteId = 0;
      let skippedNoData = 0;
      let skippedNoPatches = 0;

      for (let i = 0; i < works.length; i++) {
        const work = works[i];
        if (options?.onProgress) {
          options.onProgress({ index: i + 1, total, work });
        }
        const noteId = (work.platformVideoId || '').trim();
        if (!noteId) {
          skippedNoNoteId++;
          continue;
        }

        try {
          // 直接走浏览器（无 Python 前置），保持与「进入后台 / 作品同步」完全一致的登录方式
          await ensureBrowser();
          const data: NoteBaseData | null = await this.fetchNoteBaseData(page!, noteId);
          if (!data) {
            skippedNoData++;
            logger.warn(
              `[XHS WorkStats] Python 与浏览器均未拿到 note/base 数据，跳过。 accountId=${account.id} workId=${work.id} noteId=${noteId}`
            );
            continue;
          }

          // 同步 base 顶层“汇总指标”到 works 表（用于作品列表/总览等按 work 累计口径展示）
          await this.applyWorkSnapshotFromBaseData(work.id, data).catch((e) => {
            logger.warn(
              `[XHS WorkStats] Failed to update works snapshot from base data. workId=${work.id} noteId=${noteId}`,
              e
            );
          });

          let patches = this.buildDailyStatisticsFromNoteData(work.id, data);
          // 默认：只保留「作品发布后 14 天内」的日统计；首批补数（ignorePublishTimeLimit=true）时不过滤
          if (!options?.ignorePublishTimeLimit && work.publishTime) {
            const publishDay = new Date(work.publishTime);
            publishDay.setHours(0, 0, 0, 0);
            const lastAllowed = new Date(publishDay);
            lastAllowed.setDate(lastAllowed.getDate() + 13); // 发布当日 + 13 天 = 共 14 天
            patches = patches.filter((p) => p.recordDate.getTime() <= lastAllowed.getTime());
          }
          if (!patches.length) {
            skippedNoPatches++;
            const dayKeys = data?.day && typeof data.day === 'object' ? Object.keys(data.day) : [];
            logger.warn(
              `[XHS WorkStats] note/base 无日维度数据，跳过入库。 accountId=${account.id} workId=${work.id} noteId=${noteId} data.day keys=[${dayKeys.join(',')}]`
            );
            continue;
          }

          const result = await this.workDayStatisticsService.saveStatisticsForDateBatch(
            patches.map((p) => ({
              workId: p.workId,
              recordDate: p.recordDate,
              playCount: p.playCount,
              exposureCount: p.exposureCount,
              likeCount: p.likeCount,
              commentCount: p.commentCount,
              shareCount: p.shareCount,
              collectCount: p.collectCount,
              fansIncrease: p.fansIncrease,
              coverClickRate: p.coverClickRate,
              avgWatchDuration: p.avgWatchDuration,
              totalWatchDuration: p.totalWatchDuration,
              completionRate: p.completionRate,
              twoSecondExitRate: p.twoSecondExitRate,
            }))
          );

          totalInserted += result.inserted;
          totalUpdated += result.updated;
        } catch (e) {
          if (e instanceof XhsLoginExpiredError) {
            closedDueToLoginExpired = true;
            if (context) {
              await context.close().catch(() => undefined);
              context = null;
            }
            if (shouldClose) {
              await browser.close().catch(() => undefined);
            }

            try {
              if (!isRetry) {
                logger.info(`[XHS WorkStats] accountId=${account.id} 登录失效，尝试刷新登录...`);
                try {
                  const accountService = new AccountService();
                  const refreshResult = await accountService.refreshAccount(account.userId, account.id);
                  if (refreshResult.needReLogin) {
                    await this.accountRepository.update(account.id, { status: 'expired' as any });
                    logger.warn(`[XHS WorkStats] accountId=${account.id} 刷新后仍需重新登录，已标记账号失效`);
                    return;
                  }
                  const refreshed = await this.accountRepository.findOne({ where: { id: account.id } });
                  if (refreshed) {
                    logger.info(`[XHS WorkStats] accountId=${account.id} 刷新成功，重新同步数据`);
                    return this.importAccountWorksStatistics(refreshed, true, options);
                  }
                } catch (refreshErr) {
                  logger.error(`[XHS WorkStats] accountId=${account.id} 刷新登录失败`, refreshErr);
                  await this.accountRepository.update(account.id, { status: 'expired' as any });
                  logger.warn(`[XHS WorkStats] accountId=${account.id} 已标记账号失效`);
                  return;
                }
              } else {
                await this.accountRepository.update(account.id, { status: 'expired' as any });
                logger.warn(`[XHS WorkStats] accountId=${account.id} 刷新后仍跳转登录，已标记账号失效`);
                return;
              }
            } catch (expireErr) {
              logger.error(
                `[XHS WorkStats] accountId=${account.id} 账号失效处理异常（不影响其他账号）`,
                expireErr
              );
              return;
            }
          }
          logger.error(
            `[XHS WorkStats] Failed to import note stats. accountId=${account.id} workId=${work.id} noteId=${noteId}`,
            e
          );
        }
      }

      if (skippedNoNoteId > 0 || skippedNoData > 0 || skippedNoPatches > 0) {
        logger.info(
          `[XHS WorkStats] accountId=${account.id} 跳过统计: 无 noteId=${skippedNoNoteId} 无数据=${skippedNoData} 无日维度=${skippedNoPatches}`
        );
      }
      logger.info(
        `[XHS WorkStats] accountId=${account.id} completed. inserted=${totalInserted}, updated=${totalUpdated}`
      );
    } finally {
      if (!closedDueToLoginExpired) {
        if (context) {
          await context.close().catch(() => undefined);
        }
        if (shouldClose && browser) {
          await browser.close().catch(() => undefined);
        }
      }
    }
  }

  /**
   * 通过 Python 调用 note/base（与进入后台/每日用户数据一致：仅用账号已存 Cookie，不启浏览器）。
   * 若返回 null，会触发浏览器兜底；通常原因：Python 服务未开/地址错、Cookie 失效（创作者端 401）、或响应结构变化。
   */
  private async fetchNoteBaseViaPython(
    account: PlatformAccount,
    noteId: string
  ): Promise<NoteBaseData | null> {
    const base = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
    const url = `${base}/xiaohongshu/note_base`;
    const cookie = String(account.cookieData || '').trim();
    if (!cookie) {
      logger.warn(`[XHS WorkStats] fetchNoteBaseViaPython: accountId=${account.id} 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, note_id: noteId }),
      });
      const text = await res.text();
      if (!res.ok) {
        logger.warn(
          `[XHS WorkStats] fetchNoteBaseViaPython: Python 返回 HTTP ${res.status} accountId=${account.id} noteId=${noteId} body=${text.slice(0, 200)}`
        );
        return null;
      }
      const body = text ? (JSON.parse(text) as { data?: unknown; code?: number; msg?: string }) : null;
      if (!body || typeof body !== 'object') {
        logger.warn(`[XHS WorkStats] fetchNoteBaseViaPython: 响应非 JSON accountId=${account.id} noteId=${noteId}`);
        return null;
      }
      const code = body.code;
      let data = body.data;
      if (!data || typeof data !== 'object') {
        logger.warn(
          `[XHS WorkStats] fetchNoteBaseViaPython: 无 data 或 data 非对象 accountId=${account.id} noteId=${noteId} code=${code} msg=${(body as any).msg ?? ''}`
        );
        return null;
      }
      // 兼容接口返回 data.data（如 { data: { data: { day, view_count, ... } } }）
      const inner = (data as Record<string, unknown>).data;
      if (inner && typeof inner === 'object' && (inner as Record<string, unknown>).day !== undefined) {
        data = inner;
      }
      return data as NoteBaseData;
    } catch (e) {
      logger.warn(
        `[XHS WorkStats] fetchNoteBaseViaPython: 请求异常 accountId=${account.id} noteId=${noteId}`,
        e instanceof Error ? e.message : e
      );
      return null;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private async fetchNoteBaseData(page: Page, noteId: string): Promise<NoteBaseData | null> {
    const noteUrl = `https://creator.xiaohongshu.com/statistics/note-detail?noteId=${encodeURIComponent(
      noteId
    )}`;
    const apiPattern = /\/api\/galaxy\/creator\/datacenter\/note\/base\?note_id=/i;

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

    await page.goto(noteUrl, { waitUntil: 'domcontentloaded' }).catch(() => undefined);
    await page.waitForTimeout(1500);

    if (page.url().includes('login')) {
      logger.warn(
        `[XHS WorkStats] note-detail 页面跳转到登录，可能 cookie 失效。noteId=${noteId}`
      );
      throw new XhsLoginExpiredError();
    }

    let res = await responsePromise;
    if (!res) {
      // 兜底再等一轮
      try {
        res = await page.waitForResponse(
          (r) => r.url().match(apiPattern) != null && r.request().method() === 'GET',
          { timeout: 15_000 }
        );
      } catch {
        logger.warn(
          `[XHS WorkStats] 未捕获到 note/base 接口响应，跳过该笔记。noteId=${noteId}`
        );
        return null;
      }
    }

    const body = await res.json().catch(() => null);
    if (!body || typeof body !== 'object') {
      logger.warn(`[XHS WorkStats] note/base 响应不是 JSON，跳过。noteId=${noteId}`);
      return null;
    }

    const data = (body as any).data as NoteBaseData | undefined;
    if (!data || typeof data !== 'object') {
      logger.warn(`[XHS WorkStats] note/base data 为空，跳过。noteId=${noteId}`);
      return null;
    }

    return data;
  }

  private async applyWorkSnapshotFromBaseData(workId: number, data: NoteBaseData): Promise<void> {
    // base 接口字段：以 note/base 的 data 顶层为准
    const patch: Partial<Work> = {};

    const viewCount = toNonNegativeNumber((data as any).view_count);
    if (viewCount != null) (patch as any).yesterdayPlayCount = Math.trunc(viewCount);

    const likeCount = toNonNegativeNumber((data as any).like_count);
    if (likeCount != null) (patch as any).yesterdayLikeCount = Math.trunc(likeCount);

    const commentCount = toNonNegativeNumber((data as any).comment_count);
    if (commentCount != null) (patch as any).yesterdayCommentCount = Math.trunc(commentCount);

    const shareCount = toNonNegativeNumber((data as any).share_count);
    if (shareCount != null) (patch as any).yesterdayShareCount = Math.trunc(shareCount);

    const collectCount = toNonNegativeNumber((data as any).collect_count);
    if (collectCount != null) (patch as any).yesterdayCollectCount = Math.trunc(collectCount);

    const fansIncrease = toNonNegativeNumber((data as any).rise_fans_count);
    if (fansIncrease != null) (patch as any).yesterdayFansIncrease = Math.trunc(fansIncrease);

    const exposureCount = toNonNegativeNumber((data as any).impl_count);
    if (exposureCount != null) (patch as any).yesterdayExposureCount = Math.trunc(exposureCount);

    const coverClickRateStr = toRatePercentStringFromValue((data as any).cover_click_rate);
    if (coverClickRateStr != null) (patch as any).yesterdayCoverClickRate = coverClickRateStr;

    const avgWatch = (data as any).view_time_avg_with_double ?? (data as any).view_time_avg;
    const avgWatchStr = toFixed2String(avgWatch);
    if (avgWatchStr != null) (patch as any).yesterdayAvgWatchDuration = avgWatchStr;

    const completionRateStr = toRatePercentStringFromValue((data as any).full_view_rate);
    if (completionRateStr != null) (patch as any).yesterdayCompletionRate = completionRateStr;

    const twoSecondExitRateStr = toRatePercentStringFromValue((data as any).exit_view2s_rate);
    if (twoSecondExitRateStr != null) (patch as any).yesterdayTwoSecondExitRate = twoSecondExitRateStr;

    // 没有任何字段可更新就跳过
    if (Object.keys(patch).length === 0) return;
    await this.workRepository.update(workId, patch as any);
  }

  private buildDailyStatisticsFromNoteData(workId: number, data: NoteBaseData): DailyWorkStatPatch[] {
    const day = data.day;
    if (!day) return [];

    const map = new Map<number, DailyWorkStatPatch>();

    const addIntMetric = (items: NoteTrendItem[] | undefined, field: keyof DailyWorkStatPatch) => {
      if (!Array.isArray(items)) return;
      for (const item of items) {
        const tsRaw = item?.date;
        if (tsRaw == null) continue;
        const ts = toNumber(tsRaw, NaN);
        if (!Number.isFinite(ts)) continue;
        const d = getChinaDateFromTimestamp(ts);
        const key = d.getTime();
        let entry = map.get(key);
        if (!entry) {
          entry = { workId, recordDate: d };
          map.set(key, entry);
        }
        const prev = (entry as any)[field] ?? 0;
        const inc = toNumber(item.count, 0);
        (entry as any)[field] = prev + inc;
      }
    };

    /** 比率类：使用 item.coun，转为字符串并加 "%" */
    const addRatePercentMetric = (items: NoteTrendItem[] | undefined, field: keyof DailyWorkStatPatch) => {
      if (!Array.isArray(items)) return;
      for (const item of items) {
        const tsRaw = item?.date;
        if (tsRaw == null) continue;
        const ts = toNumber(tsRaw, NaN);
        if (!Number.isFinite(ts)) continue;
        const d = getChinaDateFromTimestamp(ts);
        const key = d.getTime();
        let entry = map.get(key);
        if (!entry) {
          entry = { workId, recordDate: d };
          map.set(key, entry);
        }
        const s = toRatePercentString(item);
        if (s != null) {
          (entry as any)[field] = s;
        }
      }
    };

    /** 时长/数值字符串（不加 %） */
    const addRateMetric = (items: NoteTrendItem[] | undefined, field: keyof DailyWorkStatPatch) => {
      if (!Array.isArray(items)) return;
      for (const item of items) {
        const tsRaw = item?.date;
        if (tsRaw == null) continue;
        const ts = toNumber(tsRaw, NaN);
        if (!Number.isFinite(ts)) continue;
        const d = getChinaDateFromTimestamp(ts);
        const key = d.getTime();
        let entry = map.get(key);
        if (!entry) {
          entry = { workId, recordDate: d };
          map.set(key, entry);
        }
        const s = toRateString(item.count_with_double ?? item.count ?? item.coun);
        if (s != null) {
          (entry as any)[field] = s;
        }
      }
    };

    // 数值型：每日增量
    addIntMetric(day.view_list, 'playCount');
    addIntMetric(day.like_list, 'likeCount');
    addIntMetric(day.comment_list, 'commentCount');
    addIntMetric(day.share_list, 'shareCount');
    addIntMetric(day.collect_list, 'collectCount');
    addIntMetric(day.imp_list, 'exposureCount');
    addIntMetric(day.rise_fans_list, 'fansIncrease');

    // 比率类：使用 coun 字段，入库时带 "%"
    addRatePercentMetric(day.cover_click_rate_list, 'coverClickRate');
    addRatePercentMetric(day.exit_view2s_list, 'twoSecondExitRate');
    addRatePercentMetric(day.finish_list, 'completionRate');

    // 平均观看时长：view_time_list → avg_watch_duration（数值字符串，不加 %）
    addRateMetric(day.view_time_list, 'avgWatchDuration');

    return Array.from(map.values()).sort(
      (a, b) => a.recordDate.getTime() - b.recordDate.getTime()
    );
  }
}

