import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

import {
  GIFT_SOURCE_ROLES,
  NdjsonRawGiftBackupStore,
  TiktokBrowserObserverCollector,
  TiktokLiveConnectorGiftCollector,
  TiktokWebcastGiftCollector,
  createTiktokGiftBackfillPlan,
  createGiftSourceDescriptor,
  decodeTiktokWebcastFrame,
  evaluateTiktokGiftRouteResilience,
  hashRawGiftPayload,
  mergeRealtimeSourceEvents,
  shouldUseTiktokLiveConnector,
  validateRawGiftBackupManifest
} from "../../gift-source-adapters/src/index.js";
import {
  NdjsonRawP1EvidenceStore,
  P1_EVIDENCE_EVENT_TYPES,
  P1_EVIDENCE_SOURCE_ROLES,
  createP1EvidenceHealthReport,
  createP1EvidenceSourceDescriptor,
  replayRawP1EvidenceRecords,
  validateRawP1EvidenceManifest,
  withP1RawReference
} from "../../live-analytics-p1-evidence/src/index.js";
import {
  EVENT_TYPES,
  createId,
  nowIso
} from "../../shared-types/src/index.js";

export const LIVE_DATA_COLLECTOR_PLUGIN_MODES = Object.freeze({
  DATA_ONLY: "data_only"
});

export const LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES = Object.freeze({
  GIFT: "gift",
  PUBLIC_CHAT: "public_chat",
  MEMBER_JOIN: "member_join",
  FOLLOW: "follow",
  SHARE: "share",
  LIKE: "like",
  ROOM_METRIC: "room_metric",
  USER_LABELS: "user_labels",
  LIVE_STATE: "live_state",
  SYSTEM: "system",
  SOURCE_HEALTH: "source_health",
  OTHER_P1: "other_p1"
});

export const LIVE_DATA_COLLECTOR_PLUGIN_FILES = Object.freeze({
  FACTS: "facts.ndjson",
  STATUS: "status.ndjson",
  SUMMARY: "summary.json",
  COMPARISON_JSON: "comparison-report.json",
  COMPARISON_CSV: "comparison-report.csv",
  SETTLEMENT_JSON: "settlement-report.json",
  SETTLEMENT_CSV: "settlement-corrections.csv",
  DIAGNOSTICS_JSON: "collection-diagnostics.json"
});

export const LIVE_DATA_COLLECTOR_REPORT_SOURCE_TYPES = Object.freeze({
  PRODUCTION_AFTER_LIVE: "production_after_live",
  FIELD_TEST_COMPARISON: "field_test_comparison",
  UNKNOWN_EXTERNAL: "unknown_external"
});

export const LIVE_DATA_COLLECTOR_SETTLEMENT_MODES = Object.freeze({
  LINE_ITEM_BACKFILL: "line_item_backfill",
  AGGREGATE_CONTROL_TOTAL: "aggregate_control_total",
  MISSING_FORMAL_EVIDENCE: "missing_formal_evidence"
});

export const LIVE_DATA_COLLECTOR_ROUTE_ROLES = Object.freeze({
  PRIMARY: GIFT_SOURCE_ROLES.PRIMARY_REALTIME,
  BACKUP: GIFT_SOURCE_ROLES.BACKUP_REALTIME
});

export const LIVE_DATA_COLLECTOR_RECOVERY_ALERT_DEFAULT_TTL_MS = 15 * 60 * 1000;

export const LIVE_DATA_COLLECTOR_RECOVERY_ALERT_DEFAULT_THROTTLE_MS = 10 * 60 * 1000;

export function createLiveDataCollectorRecoveryHandoff({
  recovery = {},
  roomInput = "",
  alertEmail = "",
  recoveryLink = "",
  generatedAt = nowIso(),
  ttlMs = LIVE_DATA_COLLECTOR_RECOVERY_ALERT_DEFAULT_TTL_MS
} = {}) {
  const recipient = normalizeRecoveryAlertEmail(alertEmail);
  const targetUrl = stringOrNull(recovery?.target_url);
  const link = stringOrNull(recoveryLink);
  const normalizedTtlMs = positiveNumberOrDefault(ttlMs, LIVE_DATA_COLLECTOR_RECOVERY_ALERT_DEFAULT_TTL_MS);
  const generatedTime = Date.parse(generatedAt);
  const expiresAt = Number.isFinite(generatedTime)
    ? new Date(generatedTime + normalizedTtlMs).toISOString()
    : null;
  const roomLabel = createRecoveryRoomLabel(roomInput, targetUrl);
  const reasonCodes = [];
  if (recovery?.required !== true) {
    reasonCodes.push("browser_recovery_not_required");
  }
  if (recovery?.available === false) {
    reasonCodes.push("browser_recovery_route_unavailable");
  }
  if (!targetUrl) {
    reasonCodes.push("browser_recovery_target_url_required");
  }
  if (!recipient) {
    reasonCodes.push("browser_recovery_alert_email_required");
  }
  if (!link) {
    reasonCodes.push("browser_recovery_link_required");
  }

  const subject = `[Live Rush] TikTok 需要人工验证：${roomLabel}`;
  const text = [
    `直播间：${roomLabel}`,
    "TikTok 备用采集页需要登录或验证码处理。",
    link ? `处理入口：${link}` : "处理入口：暂未生成",
    targetUrl ? `TikTok 页面：${targetUrl}` : null,
    expiresAt ? `链接有效期至：${expiresAt}` : null,
    "处理方法：在采集电脑或远程桌面打开入口，点“打开 TikTok 验证页”，完成后把浏览器最小化，不要关闭。",
    "安全说明：这封邮件不包含 TikTok 密码、Cookie 或本机浏览器资料目录。"
  ].filter(Boolean).join("\n");
  const html = [
    "<div style=\"font-family:Arial,'Microsoft YaHei',sans-serif;line-height:1.6;color:#111827\">",
    `<h2 style="margin:0 0 12px">TikTok 需要人工验证：${escapeRecoveryHtml(roomLabel)}</h2>`,
    "<p>TikTok 备用采集页需要登录或验证码处理。处理完成后，采集器会自动恢复。</p>",
    link ? `<p><a href="${escapeRecoveryHtml(link)}" style="display:inline-block;background:#111827;color:#fff;padding:10px 14px;border-radius:6px;text-decoration:none">打开处理入口</a></p>` : "",
    targetUrl ? `<p>直播页：<span style="font-family:monospace">${escapeRecoveryHtml(targetUrl)}</span></p>` : "",
    expiresAt ? `<p>链接有效期至：${escapeRecoveryHtml(expiresAt)}</p>` : "",
    "<p>请在采集电脑或远程桌面打开入口，点“打开 TikTok 验证页”，完成后把浏览器最小化，不要关闭。</p>",
    "<p style=\"color:#6b7280\">安全说明：这封邮件不包含 TikTok 密码、Cookie 或本机浏览器资料目录。</p>",
    "</div>"
  ].join("");

  return {
    ok: reasonCodes.length === 0,
    reason_code: reasonCodes[0] ?? null,
    reason_codes: reasonCodes,
    to: recipient,
    subject,
    text,
    html,
    recovery_link: link,
    target_url: targetUrl,
    room_input: stringOrNull(roomInput),
    room_label: roomLabel,
    generated_at: generatedAt,
    expires_at: expiresAt,
    ttl_ms: normalizedTtlMs,
    safety: {
      contains_tiktok_cookie: false,
      contains_tiktok_password: false,
      contains_browser_profile_path: false,
      local_browser_session_stays_on_collector_machine: true
    },
    email: {
      to: recipient,
      subject,
      text,
      html
    }
  };
}

export function shouldSendLiveDataCollectorRecoveryAlert({
  lastAlert = null,
  recovery = {},
  alertEmail = "",
  nowMs = Date.now(),
  throttleMs = LIVE_DATA_COLLECTOR_RECOVERY_ALERT_DEFAULT_THROTTLE_MS
} = {}) {
  const recipient = normalizeRecoveryAlertEmail(alertEmail);
  const targetUrl = stringOrNull(recovery?.target_url);
  if (recovery?.required !== true) {
    return { should_send: false, reason_code: "browser_recovery_not_required" };
  }
  if (recovery?.available === false) {
    return { should_send: false, reason_code: "browser_recovery_route_unavailable" };
  }
  if (!targetUrl) {
    return { should_send: false, reason_code: "browser_recovery_target_url_required" };
  }
  if (!recipient) {
    return { should_send: false, reason_code: "browser_recovery_alert_email_required" };
  }
  if (!lastAlert) {
    return { should_send: true, reason_code: "browser_recovery_alert_first_send" };
  }
  if (normalizeRecoveryAlertEmail(lastAlert.to ?? lastAlert.alert_email) !== recipient) {
    return { should_send: true, reason_code: "browser_recovery_alert_recipient_changed" };
  }
  if (stringOrNull(lastAlert.target_url) !== targetUrl) {
    return { should_send: true, reason_code: "browser_recovery_alert_target_changed" };
  }
  const lastSentAt = Date.parse(lastAlert.sent_at ?? lastAlert.generated_at ?? lastAlert.created_at ?? "");
  if (!Number.isFinite(lastSentAt)) {
    return { should_send: true, reason_code: "browser_recovery_alert_last_send_unknown" };
  }
  const normalizedThrottleMs = positiveNumberOrDefault(throttleMs, LIVE_DATA_COLLECTOR_RECOVERY_ALERT_DEFAULT_THROTTLE_MS);
  const ageMs = nowMs - lastSentAt;
  if (ageMs >= normalizedThrottleMs) {
    return {
      should_send: true,
      reason_code: "browser_recovery_alert_throttle_elapsed",
      age_ms: ageMs
    };
  }
  return {
    should_send: false,
    reason_code: "browser_recovery_alert_throttled",
    age_ms: ageMs,
    retry_after_ms: normalizedThrottleMs - ageMs
  };
}

export function createLiveDataCollectorPluginConfig(input = {}) {
  const tenantId = stringOrDefault(input.tenant_id ?? input.tenantId, "tenant_field_test");
  const sessionId = stringOrDefault(input.session_id ?? input.sessionId, createId("collector_session"));
  const liveRoomId = stringOrDefault(input.live_room_id ?? input.liveRoomId, null);
  const dataDir = stringOrDefault(input.data_dir ?? input.dataDir, ".runtime/live-data-collector");
  const collectorId = stringOrDefault(input.collector_id ?? input.collectorId, createId("live_data_collector"));
  const sessionDir = join(dataDir, tenantId, sessionId);
  const provider = input.provider ?? "tiktok_webcast";
  const routes = normalizeCollectorRoutes({
    routes: input.routes,
    webcast: input.webcast,
    primaryWebcast: input.primary_webcast ?? input.primaryWebcast,
    backupWebcast: input.backup_webcast ?? input.backupWebcast,
    tenantId,
    sessionId,
    liveRoomId,
    collectorId,
    provider
  });
  return {
    mode: LIVE_DATA_COLLECTOR_PLUGIN_MODES.DATA_ONLY,
    platform: input.platform ?? "tiktok",
    tenant_id: tenantId,
    session_id: sessionId,
    live_room_id: liveRoomId,
    collector_id: collectorId,
    data_dir: dataDir,
    session_dir: sessionDir,
    enable_gifts: input.enable_gifts !== false,
    enable_p1_evidence: input.enable_p1_evidence !== false,
    require_live_room_match: input.require_live_room_match !== false,
    provider,
    routes,
    webcast: routes[0]?.webcast ?? null
  };
}

export class LiveDataCollectorPlugin {
  constructor({
    config = {},
    WebSocketImpl = globalThis.WebSocket,
    fetchImpl = globalThis.fetch,
    messageDecoder = decodeTiktokWebcastFrame,
    TikTokLiveConnectorImpl = null,
    tiktokLiveConnectorModuleLoader = null,
    CdpClientImpl = null,
    browserLauncher = null,
    now = nowIso
  } = {}) {
    this.config = createLiveDataCollectorPluginConfig(config);
    this.WebSocketImpl = WebSocketImpl;
    this.fetchImpl = fetchImpl;
    this.messageDecoder = messageDecoder;
    this.TikTokLiveConnectorImpl = TikTokLiveConnectorImpl;
    this.tiktokLiveConnectorModuleLoader = tiktokLiveConnectorModuleLoader;
    this.CdpClientImpl = CdpClientImpl;
    this.browserLauncher = browserLauncher;
    this.now = now;
    this.sessionContext = {
      tenant_id: this.config.tenant_id,
      live_room_id: this.config.live_room_id,
      session_id: this.config.session_id
    };
    this.stats = {
      started_at: null,
      stopped_at: null,
      first_frame_at: null,
      last_frame_at: null,
      first_event_at: null,
      frame_count: 0,
      gift_count: 0,
      first_gift_at: null,
      last_gift_at: null,
      p1_event_count: 0,
      rejected_count: 0,
      issue_count: 0,
      last_event_at: null,
      last_issue_code: null
    };
    this.issues = [];
    mkdirSync(this.config.session_dir, { recursive: true });
    this.factFilePath = join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.FACTS);
    this.statusFilePath = join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.STATUS);
    ensureFile(this.factFilePath);
    ensureFile(this.statusFilePath);
    this.routeStates = this.config.routes.map((route) => this.#createRouteState(route));
    this.routesByRole = new Map(this.routeStates.map((routeState) => [routeState.role, routeState]));
    this.primaryRoute = this.routesByRole.get(GIFT_SOURCE_ROLES.PRIMARY_REALTIME) ?? this.routeStates[0];
    this.giftStore = this.primaryRoute?.giftStore ?? null;
    this.p1Store = this.primaryRoute?.p1Store ?? null;
    this.collector = null;
    this.lastComparison = null;
    this.lastSettlement = null;
    this.lastDiagnostics = null;
    this.recentGiftEvents = [];
    this.recentGiftEventsByRole = new Map(this.routeStates.map((routeState) => [routeState.role, []]));
  }

  async start() {
    this.stats.started_at = this.stats.started_at ?? this.now();
    this.#appendStatus({
      event_type: "collector_plugin_started",
      state: "running"
    });
    const startTasks = this.routeStates.filter((route) => route.enabled).map(async (routeState) => {
      const collector = this.#createCollector(routeState);
      routeState.collector = collector;
      if (routeState.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME) {
        this.collector = collector;
      }
      return {
        role: routeState.role,
        result: await collector.start()
      };
    });
    const results = await Promise.all(startTasks);
    return {
      ok: results.every((item) => item.result.ok === true),
      collector_id: this.config.collector_id,
      mode: this.config.mode,
      route_count: results.length,
      routes: results,
      snapshot: this.snapshot()
    };
  }

  stop(input = {}) {
    this.stats.stopped_at = this.now();
    const stoppedRoutes = this.routeStates.map((routeState) => ({
      role: routeState.role,
      result: routeState.collector?.stop(input) ?? { ok: true, state: "not_started" }
    }));
    this.#appendStatus({
      event_type: "collector_plugin_stopped",
      state: "stopped",
      status: stoppedRoutes
    });
    this.#writeSummary();
    return {
      ok: stoppedRoutes.every((item) => item.result.ok !== false),
      routes: stoppedRoutes,
      snapshot: this.snapshot()
    };
  }

  async openBrowserRecovery({ role = GIFT_SOURCE_ROLES.BACKUP_REALTIME } = {}) {
    const routeState = this.routeStates.find((route) => route.role === role && isTiktokBrowserObserverRoute(route.webcast))
      ?? this.routeStates.find((route) => isTiktokBrowserObserverRoute(route.webcast));
    if (!routeState) {
      return {
        ok: false,
        reason_code: "browser_recovery_route_not_found"
      };
    }
    if (!routeState.collector) {
      return {
        ok: false,
        reason_code: "browser_recovery_route_not_started",
        target_url: routeTargetUrl(routeState.webcast) ?? null,
        browser_user_data_dir: routeState.webcast?.browser_user_data_dir ?? null
      };
    }
    if (typeof routeState.collector.openRecoveryPage !== "function") {
      return {
        ok: false,
        reason_code: "browser_recovery_not_supported",
        target_url: routeTargetUrl(routeState.webcast) ?? null,
        browser_user_data_dir: routeState.webcast?.browser_user_data_dir ?? null
      };
    }
    return routeState.collector.openRecoveryPage();
  }

  ingestWebcastFrame(frame, { role = GIFT_SOURCE_ROLES.PRIMARY_REALTIME, receivedAt = this.now() } = {}) {
    const routeState = this.#routeForRole(role);
    this.stats.started_at = this.stats.started_at ?? receivedAt;
    let decoded;
    try {
      decoded = this.messageDecoder(frame, {
        sourceDescriptor: routeState.giftSourceDescriptor,
        tenantId: this.config.tenant_id,
        sessionId: this.config.session_id,
        liveRoomId: this.config.live_room_id,
        provider: this.config.provider,
        receivedAt,
        includeP1RawItems: true
      });
    } catch (error) {
      this.#recordIssue("collector_frame_decode_failed", { role, message: error?.message ?? null });
      return {
        ok: false,
        reason_code: "collector_frame_decode_failed"
      };
    }
    if (decoded.ok === true) {
      this.#recordRouteFrame(routeState, { decoded, frame, receivedAt });
    }
    for (const gift of decoded.events ?? []) {
      this.#recordGift(routeState, gift, { receivedAt });
    }
    for (const p1Event of decoded.p1_events ?? []) {
      this.#recordP1WithRawBackup(routeState, p1Event, decoded.p1_raw_items ?? [], { receivedAt });
    }
    for (const issue of decoded.issues ?? []) {
      this.#recordIssue(issue.reason_code ?? "collector_frame_issue", { role, issue });
    }
    const rejectedCount = (decoded.rejected?.length ?? 0) + (decoded.p1_rejected?.length ?? 0);
    this.stats.rejected_count += rejectedCount;
    routeState.stats.rejected_count += rejectedCount;
    return {
      ok: decoded.ok === true && (decoded.p1_rejected?.length ?? 0) === 0,
      decoded,
      snapshot: this.snapshot()
    };
  }

  snapshot() {
    return {
      ok: this.issues.length === 0,
      mode: this.config.mode,
      platform: this.config.platform,
      collector_id: this.config.collector_id,
      tenant_id: this.config.tenant_id,
      live_room_id: this.config.live_room_id,
      session_id: this.config.session_id,
      session_dir: this.config.session_dir,
      gameplay_enabled: false,
      route_count: this.routeStates.length,
      output_files: this.outputFiles(),
      stats: { ...this.stats },
      routes: this.routeStates.map((routeState) => this.#routeSnapshot(routeState)),
      collector: this.primaryRoute?.collector?.snapshot?.() ?? null,
      realtime: this.createRealtimeStatus({ generatedAt: this.now() }),
      diagnostics: this.createCollectionDiagnostics({ write: false, includeRouteDetails: false }),
      settlement: this.lastSettlement?.report ? summarizeSettlementReport(this.lastSettlement.report) : null,
      issues: this.issues.slice(-20)
    };
  }

  outputFiles() {
    const routeFiles = Object.fromEntries(this.routeStates.map((routeState) => [routeState.role, {
      gifts: routeState.giftStore.filePath,
      p1: routeState.p1Store.filePath,
      frames: routeState.rawFrameFilePath
    }]));
    return {
      facts: this.factFilePath,
      status: this.statusFilePath,
      summary: join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.SUMMARY),
      comparison_json: join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.COMPARISON_JSON),
      comparison_csv: join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.COMPARISON_CSV),
      settlement_json: join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.SETTLEMENT_JSON),
      settlement_csv: join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.SETTLEMENT_CSV),
      diagnostics_json: join(this.config.session_dir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.DIAGNOSTICS_JSON),
      routes: routeFiles,
      gifts: this.primaryRoute?.giftStore.filePath ?? null,
      p1: this.primaryRoute?.p1Store.filePath ?? null
    };
  }

  createManifests() {
    const routes = this.routeStates.map((routeState) => {
      const giftManifest = routeState.giftStore.createManifest();
      const p1Records = routeState.p1Store.listRecords();
      const p1Manifest = routeState.p1Store.createManifest({ records: p1Records });
      const frameArchive = readRawFrameArchive(routeState.rawFrameFilePath);
      const frameManifest = createRawFrameArchiveManifest({
        filePath: routeState.rawFrameFilePath,
        records: frameArchive.records,
        parseIssues: frameArchive.issues,
        sourceDescriptor: routeState.giftSourceDescriptor,
        sessionContext: this.sessionContext,
        createdAt: this.now()
      });
      const giftValidation = validateRawGiftBackupManifest(giftManifest);
      const p1Validation = validateRawP1EvidenceManifest(p1Manifest, {
        records: p1Records,
        allowPending: p1Records.length === 0
      });
      const frameValidation = validateRawFrameArchiveManifest(frameManifest);
      return {
        role: routeState.role,
        ok: giftValidation.ok && p1Validation.ok && frameValidation.ok,
        gift_manifest: giftManifest,
        p1_manifest: p1Manifest,
        frame_manifest: frameManifest,
        gift_validation: giftValidation,
        p1_validation: p1Validation,
        frame_validation: frameValidation
      };
    });
    return {
      ok: routes.every((route) => route.ok),
      routes,
      gift_manifest: routes.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME)?.gift_manifest ?? routes[0]?.gift_manifest ?? null,
      p1_manifest: routes.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME)?.p1_manifest ?? routes[0]?.p1_manifest ?? null,
      frame_manifest: routes.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME)?.frame_manifest ?? routes[0]?.frame_manifest ?? null
    };
  }

  replay() {
    const routeReplays = this.routeStates.map((routeState) => {
      const p1Records = routeState.p1Store.listRecords();
      const p1Replay = replayRawP1EvidenceRecords({
        records: p1Records,
        manifest: routeState.p1Store.createManifest({ records: p1Records }),
        sourceDescriptor: routeState.p1SourceDescriptor,
        sessionContext: this.sessionContext
      });
      const giftRecords = routeState.giftStore.listRecords();
      return {
        role: routeState.role,
        ok: p1Replay.ok,
        gift_raw_count: giftRecords.length,
        p1_replay: p1Replay
      };
    });
    const facts = readNdjson(this.factFilePath);
    const routeComparison = compareRouteFacts({
      facts,
      primaryRole: GIFT_SOURCE_ROLES.PRIMARY_REALTIME,
      backupRole: GIFT_SOURCE_ROLES.BACKUP_REALTIME,
      backupConfigured: this.routeStates.some((routeState) => routeState.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME && routeState.enabled !== false)
    });
    return {
      ok: routeReplays.every((route) => route.ok) && routeComparison.ok,
      routes: routeReplays,
      route_comparison: routeComparison,
      gift_raw_count: routeReplays.reduce((sum, route) => sum + route.gift_raw_count, 0),
      p1_replay: routeReplays.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME)?.p1_replay ?? routeReplays[0]?.p1_replay ?? null,
      normalized_fact_count: facts.length
    };
  }

  createHealthReport({
    requiredCategories = ["gift", "public_chat", "member_join", "source_health"],
    sourceHealth = {}
  } = {}) {
    const facts = readNdjson(this.factFilePath);
    const statuses = readNdjson(this.statusFilePath);
    const availableCategories = [...new Set(facts.map((fact) => fact.category).filter(Boolean))];
    if (this.stats.gift_count > 0 && !availableCategories.includes("gift")) {
      availableCategories.push("gift");
    }
    if (statuses.length > 0 && !availableCategories.includes("source_health")) {
      availableCategories.push("source_health");
    }
    const replay = this.replay();
    const sourceHealthByRole = Object.fromEntries(this.routeStates.map((routeState) => [routeState.role, {
      connected: routeState.collector?.snapshot?.().connected ?? routeState.stats.frame_count > 0,
      health: routeState.issues.length === 0 ? "healthy" : "attention",
      frame_count: routeState.stats.frame_count,
      gift_count: routeState.stats.gift_count,
      p1_event_count: routeState.stats.p1_event_count
    }]));
    return createP1EvidenceHealthReport({
      sessionContext: this.sessionContext,
      rawCoverageByRole: Object.fromEntries(this.routeStates.map((routeState) => [
        routeState.role,
        routeState.p1Store.listRecords().length
      ])),
      sourceHealth: {
        ...sourceHealthByRole,
        ...sourceHealth
      },
      replaySummary: replay.p1_replay,
      requiredCategories,
      availableCategories
    });
  }

  createRealtimeStatus({ generatedAt = this.now() } = {}) {
    const facts = readNdjson(this.factFilePath);
    const statuses = readNdjson(this.statusFilePath);
    const replay = this.replay();
    const diagnostics = this.createCollectionDiagnostics({
      generatedAt,
      write: false,
      includeRouteDetails: false
    });
    return createCollectorRealtimeStatus({
      plugin: this,
      facts,
      statuses,
      replay,
      diagnostics,
      generatedAt
    });
  }

  compareWithExternalReport(input = {}) {
    const externalRows = parseExternalReportRows(input);
    const report = createLiveDataCollectorComparisonReport({
      facts: readNdjson(this.factFilePath),
      externalRows,
      sourceName: input.sourceName ?? input.source_name ?? input.source ?? "external",
      generatedAt: input.generatedAt ?? this.now()
    });
    const output = writeLiveDataCollectorComparisonReport({
      report,
      outputDir: this.config.session_dir
    });
    this.lastComparison = {
      ok: report.ok,
      report,
      output
    };
    return this.lastComparison;
  }

  createSettlementReport(input = {}) {
    const generatedAt = input.generatedAt ?? this.now();
    const sourceName = input.sourceName ?? input.source_name ?? input.source ?? "guild_backend";
    const sourcePolicy = classifyExternalReportSource(sourceName);
    const externalRows = parseExternalReportRows(input);
    const aggregateSummary = parseExternalAggregateSummary(input);
    if (!sourcePolicy.production_allowed) {
      const report = {
        report_id: createId("collector_settlement_rejected"),
        generated_at: generatedAt,
        platform: this.config.platform,
        collector_id: this.config.collector_id,
        tenant_id: this.config.tenant_id,
        session_id: this.config.session_id,
        live_room_id: this.config.live_room_id,
        source_name: sourceName,
        source_policy: sourcePolicy,
        ok: false,
        ready_for_production_settlement: false,
        requires_mca: false,
        production_dependency_on_mca: false,
        reason_code: "settlement_source_not_allowed",
        summary: {
          after_live_gift_count: externalRows.filter((row) => row.category === "gift").length,
          aggregate_metric_count: aggregateSummary.metrics_present.length,
          unresolved_count: externalRows.length,
          accuracy_score: null
        },
        aggregate_reconciliation: {
          present: aggregateSummary.present,
          ok: false,
          official: aggregateSummary
        },
        corrections: [],
        next_actions: createSettlementNextActions({
          sourcePolicy,
          backfill: { official_count: 0, after_live_recovered_count: 0, adjustment_count: 0, review: { realtime_only_count: 0 } },
          comparison: { summary: {} },
          settlementMode: LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.MISSING_FORMAL_EVIDENCE,
          aggregateReconciliation: { present: aggregateSummary.present, ok: false, official: aggregateSummary, issues: [] }
        })
      };
      this.lastSettlement = {
        ok: false,
        reason_code: "settlement_source_not_allowed",
        report,
        output: null
      };
      return this.lastSettlement;
    }
    const facts = readNdjson(this.factFilePath);
    const diagnostics = this.createCollectionDiagnostics({ write: false, includeRouteDetails: false });
    let primaryEvents = facts
      .map(normalizeCollectorFactForComparison)
      .filter((row) => row?.category === "gift" && row.route_role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME)
      .map((row) => comparisonRowToBackfillGiftEvent(row, {
        liveRoomId: row.live_room_id ?? this.config.live_room_id,
        sourceRole: GIFT_SOURCE_ROLES.PRIMARY_REALTIME,
        sourceName: "live_rush_primary"
      }))
      .filter(Boolean);
    let backupEvents = facts
      .map(normalizeCollectorFactForComparison)
      .filter((row) => row?.category === "gift" && row.route_role === GIFT_SOURCE_ROLES.BACKUP_REALTIME)
      .map((row) => comparisonRowToBackfillGiftEvent(row, {
        liveRoomId: row.live_room_id ?? this.config.live_room_id,
        sourceRole: GIFT_SOURCE_ROLES.BACKUP_REALTIME,
        sourceName: "live_rush_backup"
      }))
      .filter(Boolean);
    let officialEvents = externalRows
      .filter((row) => row.category === "gift")
      .map((row) => comparisonRowToBackfillGiftEvent(row, {
        liveRoomId: row.live_room_id ?? this.config.live_room_id,
        sourceRole: GIFT_SOURCE_ROLES.OFFICIAL_BACKEND,
        sourceName
      }))
      .filter(Boolean);
    ({ primaryEvents, backupEvents, officialEvents } = applySettlementMatchKeys({
      primaryEvents,
      backupEvents,
      officialEvents
    }));
    primaryEvents = compactFinalizedComboEvents(primaryEvents);
    backupEvents = compactFinalizedComboEvents(backupEvents);
    officialEvents = compactFinalizedComboEvents(officialEvents);
    const ledger = createSettlementLedgerEvents({ primaryEvents, backupEvents });
    const requestedAggregateTolerances = input.aggregate_tolerances ?? input.aggregateTolerances ?? {};
    const requestedRequireRoomBinding = input.require_aggregate_room_binding ?? input.requireAggregateRoomBinding;
    const requestedRequireValueUnit = input.require_aggregate_value_unit ?? input.requireAggregateValueUnit;
    const aggregateOverrideIssues = createAggregateHardGateOverrideIssues({
      sourcePolicy,
      tolerances: requestedAggregateTolerances,
      requireRoomBinding: requestedRequireRoomBinding,
      requireValueUnit: requestedRequireValueUnit
    });
    let aggregateReconciliation = createAggregateControlReconciliation({
      collectorEvents: ledger.events,
      aggregateSummary,
      expectedLiveRoomId: this.config.live_room_id,
      tolerances: sourcePolicy.production_allowed ? {} : requestedAggregateTolerances,
      requireRoomBinding: sourcePolicy.production_allowed ? true : requestedRequireRoomBinding ?? true,
      requireValueUnit: sourcePolicy.production_allowed ? true : requestedRequireValueUnit ?? true
    });
    aggregateReconciliation = appendAggregateReconciliationIssues(aggregateReconciliation, aggregateOverrideIssues);
    const settlementReadinessGate = createSettlementReadinessGate({
      diagnostics,
      ledger,
      facts
    });
    const settlementMode = officialEvents.length > 0
      ? LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.LINE_ITEM_BACKFILL
      : aggregateReconciliation.present
        ? LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.AGGREGATE_CONTROL_TOTAL
        : LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.MISSING_FORMAL_EVIDENCE;
    const backfill = settlementMode === LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.AGGREGATE_CONTROL_TOTAL
      ? createAggregateControlBackfillPlan({
          primaryEvents,
          backupEvents,
          ledger,
          aggregateReconciliation,
          expectedLiveRoomId: this.config.live_room_id,
          generatedAt
        })
      : createTiktokGiftBackfillPlan({
          primaryEvents,
          backupEvents,
          officialEvents,
          expectedLiveRoomId: this.config.live_room_id,
          generatedAt
        });
    const comparison = createLiveDataCollectorComparisonReport({
      facts,
      externalRows,
      sourceName,
      generatedAt
    });
    const aggregateReady = !aggregateReconciliation.present || aggregateReconciliation.ok === true;
    const productionReady = sourcePolicy.production_allowed === true
      && backfill.ready_for_settlement === true
      && aggregateReady === true
      && settlementReadinessGate.ok === true;
    const accuracyScore = aggregateReconciliation.present
      ? createAggregateAccuracyScore(aggregateReconciliation)
      : comparison.accuracy_score;
    const aggregateUnresolvedCount = aggregateReconciliation.present && !aggregateReconciliation.ok
      ? Math.max(aggregateReconciliation.issues.length, 1)
      : 0;
    const summaryUnresolvedCount = settlementMode === LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.AGGREGATE_CONTROL_TOTAL
      ? backfill.unresolved_count + settlementReadinessGate.issue_count
      : backfill.unresolved_count + aggregateUnresolvedCount + settlementReadinessGate.issue_count;
    const report = {
      report_id: createId("collector_settlement"),
      generated_at: generatedAt,
      platform: this.config.platform,
      collector_id: this.config.collector_id,
      tenant_id: this.config.tenant_id,
      session_id: this.config.session_id,
      live_room_id: this.config.live_room_id,
      source_name: sourceName,
      source_policy: sourcePolicy,
      requires_mca: false,
      production_dependency_on_mca: false,
      settlement_mode: settlementMode,
      ready_for_production_settlement: productionReady,
      ok: sourcePolicy.production_allowed ? productionReady : comparison.ok,
      summary: {
        settlement_mode: settlementMode,
        primary_gift_count: primaryEvents.length,
        backup_gift_count: backupEvents.length,
        collector_ledger_gift_count: ledger.events.length,
        collector_paid_user_count: aggregateReconciliation.collector?.paid_user_count ?? null,
        collector_gift_value: aggregateReconciliation.collector?.total_gift_value ?? null,
        after_live_gift_count: officialEvents.length,
        matched_gift_count: comparison.summary.matched_gift_count,
        missing_realtime_gift_count: backfill.after_live_recovered_count,
        realtime_only_gift_count: backfill.review.realtime_only_count,
        adjustment_count: backfill.adjustment_count,
        aggregate_checked_metrics: aggregateReconciliation.checked_metrics ?? [],
        aggregate_issue_count: aggregateReconciliation.issues?.length ?? 0,
        settlement_gate_issue_count: settlementReadinessGate.issue_count,
        aggregate_deltas: aggregateReconciliation.deltas ?? {},
        official_total_gift_value: aggregateReconciliation.official?.total_gift_value ?? null,
        official_paid_user_count: aggregateReconciliation.official?.paid_user_count ?? null,
        unresolved_count: summaryUnresolvedCount,
        accuracy_score: accuracyScore
      },
      backfill,
      aggregate_reconciliation: aggregateReconciliation,
      settlement_readiness_gate: settlementReadinessGate,
      diagnostics,
      comparison,
      corrections: backfill.corrections,
      next_actions: createSettlementNextActions({
        sourcePolicy,
        backfill,
        comparison,
        settlementMode,
        aggregateReconciliation,
        settlementReadinessGate
      })
    };
    const output = writeLiveDataCollectorSettlementReport({
      report,
      outputDir: this.config.session_dir
    });
    this.lastSettlement = {
      ok: true,
      report,
      output
    };
    return this.lastSettlement;
  }

  createCollectionDiagnostics({
    generatedAt = this.now(),
    write = true,
    includeRouteDetails = true
  } = {}) {
    const facts = readNdjson(this.factFilePath);
    const statuses = readNdjson(this.statusFilePath);
    const manifests = this.createManifests();
    const replay = this.replay();
    const routeIndependence = evaluateCollectorRouteIndependence(this.routeStates);
    const primary = this.routeStates.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME);
    const backup = this.routeStates.find((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME);
    const giftFacts = facts.filter((fact) => fact.category === LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT);
    const p1Facts = facts.filter((fact) => fact.category && fact.category !== LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT);
    const issueList = createCollectionDiagnosticIssues({
      plugin: this,
      primary,
      backup,
      manifests,
      replay,
      routeIndependence,
      facts,
      statuses
    });
    const score = Math.max(0, 100 - issueList.reduce((sum, issue) => {
      if (issue.severity === "blocker") {
        return sum + 35;
      }
      if (issue.severity === "warning") {
        return sum + 15;
      }
      return sum + 5;
    }, 0));
    const report = {
      report_id: createId("collector_diagnostics"),
      generated_at: generatedAt,
      platform: this.config.platform,
      collector_id: this.config.collector_id,
      tenant_id: this.config.tenant_id,
      session_id: this.config.session_id,
      live_room_id: this.config.live_room_id,
      ok: issueList.every((issue) => issue.severity !== "blocker"),
      readiness_score: score,
      formal_use_ready: score >= 85 && routeIndependence.production_ready && manifests.ok && giftFacts.length > 0,
      mca_policy: {
        production_dependency_allowed: false,
        field_test_comparison_allowed: true,
        message: "MCA 只能用于测试期横向对比，正式采集和补数不能依赖 MCA。"
      },
      route_independence: routeIndependence,
      data_capture: {
        fact_count: facts.length,
        gift_fact_count: giftFacts.length,
        p1_fact_count: p1Facts.length,
        status_count: statuses.length,
        primary_gift_count: primary?.stats.gift_count ?? 0,
        backup_gift_count: backup?.stats.gift_count ?? 0,
        has_real_gift_sample: giftFacts.length > 0,
        last_event_at: this.stats.last_event_at
      },
      raw_backup: {
        ok: manifests.ok,
        route_count: manifests.routes.length,
        routes: manifests.routes.map((route) => ({
          role: route.role,
          ok: route.ok,
          gift_raw_count: route.gift_manifest?.record_count ?? 0,
          p1_raw_count: route.p1_manifest?.record_count ?? 0,
          raw_frame_archive_count: route.frame_manifest?.record_count ?? 0,
          raw_frame_archive_parse_issue_count: route.frame_manifest?.parse_issue_count ?? 0
        }))
      },
      route_comparison: replay.route_comparison,
      issues: issueList,
      next_actions: createCollectionDiagnosticActions(issueList, routeIndependence)
    };
    if (includeRouteDetails) {
      report.routes = this.routeStates.map((routeState) => this.#routeSnapshot(routeState));
    }
    if (write) {
      writeFileSync(this.outputFiles().diagnostics_json, JSON.stringify(report, null, 2), "utf8");
      this.lastDiagnostics = report;
    }
    return report;
  }

  #createRouteState(route) {
    const role = route.role ?? GIFT_SOURCE_ROLES.PRIMARY_REALTIME;
    const suffix = role === GIFT_SOURCE_ROLES.BACKUP_REALTIME ? "backup" : "primary";
    const rawFrameFilePath = join(this.config.session_dir, `${suffix}.frames.ndjson`);
    ensureFile(rawFrameFilePath);
    const giftSourceDescriptor = createGiftSourceDescriptor({
      role,
      source_id: `${this.config.collector_id}_gift_${suffix}`,
      source: routeSourceName(route.webcast)
    });
    const p1SourceDescriptor = createP1EvidenceSourceDescriptor({
      role: p1RoleForGiftRole(role),
      source_id: `${this.config.collector_id}_p1_${suffix}`,
      platform: this.config.platform,
      source: routeSourceName(route.webcast)
    });
    const routeSessionContext = {
      ...this.sessionContext
    };
    return {
      ...route,
      enabled: route.enabled !== false,
      role,
      suffix,
      giftSourceDescriptor,
      p1SourceDescriptor,
      giftStore: new NdjsonRawGiftBackupStore({
        filePath: join(this.config.session_dir, `${suffix}.gifts.ndjson`),
        sourceDescriptor: giftSourceDescriptor,
        sessionContext: routeSessionContext
      }),
      p1Store: new NdjsonRawP1EvidenceStore({
        filePath: join(this.config.session_dir, `${suffix}.p1.ndjson`),
        sourceDescriptor: p1SourceDescriptor,
        sessionContext: routeSessionContext
      }),
      rawFrameFilePath,
      rawFrameSequenceNo: null,
      collector: null,
      issues: [],
      stats: {
        first_frame_at: null,
        last_frame_at: null,
        first_event_at: null,
        frame_count: 0,
        gift_count: 0,
        first_gift_at: null,
        last_gift_at: null,
        p1_event_count: 0,
        raw_frame_count: 0,
        decoded_frame_count: 0,
        raw_frame_archive_count: 0,
        rejected_count: 0,
        last_event_at: null
      }
    };
  }

  #createCollector(routeState) {
    const useBrowserObserver = isTiktokBrowserObserverRoute(routeState.webcast);
    const useLiveConnector = !useBrowserObserver && shouldUseTiktokLiveConnector(routeState.webcast);
    const Collector = useBrowserObserver
      ? TiktokBrowserObserverCollector
      : useLiveConnector ? TiktokLiveConnectorGiftCollector : TiktokWebcastGiftCollector;
    const collectorOptions = {
      config: routeState.webcast,
      sourceDescriptor: routeState.giftSourceDescriptor,
      fetchImpl: this.fetchImpl,
      WebSocketImpl: this.WebSocketImpl,
      ConnectorImpl: this.TikTokLiveConnectorImpl,
      moduleLoader: this.tiktokLiveConnectorModuleLoader ?? undefined,
      CdpClientImpl: this.CdpClientImpl,
      browserLauncher: this.browserLauncher ?? undefined,
      messageDecoder: this.messageDecoder,
      p1RawStore: this.config.enable_p1_evidence ? routeState.p1Store : null,
      onDecodedFrame: (decoded, { receivedAt, frame = null, semanticFrame = true } = {}) => this.#recordRouteFrame(routeState, {
        decoded,
        frame,
        receivedAt,
        semanticFrame
      }),
      onGift: ({ event, receivedAt }) => this.#recordGift(routeState, event, { receivedAt }),
      onP1Evidence: ({ event, receivedAt }) => this.#recordP1Event(routeState, event, { receivedAt }),
      onStatus: (status) => this.#appendStatus({
        event_type: "collector_status",
        state: status.connected ? "connected" : "attention",
        role: routeState.role,
        status
      }),
      onIssue: (issue) => this.#recordIssue(issue.reason_code ?? "collector_issue", {
        role: routeState.role,
        issue
      })
    };
    return new Collector(collectorOptions);
  }

  #recordRouteFrame(routeState, { decoded = null, frame = null, receivedAt = this.now(), semanticFrame = true } = {}) {
    this.stats.first_frame_at = this.stats.first_frame_at ?? receivedAt;
    this.stats.last_frame_at = receivedAt;
    this.stats.frame_count += 1;
    routeState.stats.first_frame_at = routeState.stats.first_frame_at ?? receivedAt;
    routeState.stats.last_frame_at = receivedAt;
    routeState.stats.frame_count += 1;
    routeState.stats.raw_frame_count += 1;
    routeState.stats.decoded_frame_count += decoded ? 1 : 0;
    if (semanticFrame) {
      this.#learnLiveRoomIdFromRoute(routeState, liveRoomIdFromDecodedFrame(decoded), {
        receivedAt,
        reasonCode: "decoded_frame"
      });
    }
    try {
      if (!routeState.rawFrameSequenceNo) {
        routeState.rawFrameSequenceNo = countNdjsonLines(routeState.rawFrameFilePath) + 1;
      }
      const rawFrameRecord = createRawFrameArchiveRecord({
        frame,
        decoded,
        routeState,
        sessionContext: this.sessionContext,
        sequenceNo: routeState.rawFrameSequenceNo,
        receivedAt,
        semanticFrame
      });
      appendNdjson(routeState.rawFrameFilePath, rawFrameRecord);
      routeState.rawFrameSequenceNo += 1;
      routeState.stats.raw_frame_archive_count += 1;
    } catch (error) {
      this.#recordIssue("raw_frame_archive_write_failed", {
        role: routeState.role,
        file_path: routeState.rawFrameFilePath,
        message: error?.message ?? null
      });
    }
  }

  #learnLiveRoomIdFromRoute(routeState, liveRoomId, { receivedAt = this.now(), reasonCode = "route_signal" } = {}) {
    const normalizedLiveRoomId = stringOrNull(liveRoomId);
    if (!normalizedLiveRoomId) {
      return;
    }
    const previousLiveRoomId = stringOrNull(this.config.live_room_id);
    if (previousLiveRoomId && previousLiveRoomId !== normalizedLiveRoomId) {
      this.#recordIssue("live_room_id_rebound_from_route_signal", {
        role: routeState.role,
        previous_live_room_id: previousLiveRoomId,
        live_room_id: normalizedLiveRoomId,
        reason_code: reasonCode
      });
    }
    this.config.live_room_id = normalizedLiveRoomId;
    this.sessionContext.live_room_id = normalizedLiveRoomId;
    for (const candidate of this.routeStates) {
      candidate.webcast.live_room_id = normalizedLiveRoomId;
      candidate.giftStore.sessionContext.live_room_id = normalizedLiveRoomId;
      candidate.p1Store.sessionContext.live_room_id = normalizedLiveRoomId;
      const updateLiveRoomId = candidate.collector?.updateLiveRoomId;
      if (typeof updateLiveRoomId === "function") {
        const result = updateLiveRoomId.call(candidate.collector, normalizedLiveRoomId, {
          reconnectIfStandby: candidate.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME
        });
        if (result?.reconnecting) {
          this.#appendStatus({
            event_type: "primary_route_wake_up_from_backup_room_id",
            state: "running",
            role: candidate.role,
            observed_at: receivedAt,
            status: {
              live_room_id: normalizedLiveRoomId,
              learned_from_role: routeState.role,
              reason_code: reasonCode
            }
          });
        }
      }
    }
  }

  #routeForRole(role) {
    const normalizedRole = role === "backup" ? GIFT_SOURCE_ROLES.BACKUP_REALTIME
      : role === "primary" ? GIFT_SOURCE_ROLES.PRIMARY_REALTIME
        : role;
    const routeState = this.routesByRole.get(normalizedRole) ?? this.primaryRoute;
    if (!routeState) {
      throw new Error(`No collector route configured for role: ${String(role)}`);
    }
    return routeState;
  }

  #routeSnapshot(routeState) {
    return {
      role: routeState.role,
      enabled: routeState.enabled,
      output_files: {
        gifts: routeState.giftStore.filePath,
        p1: routeState.p1Store.filePath,
        frames: routeState.rawFrameFilePath
      },
      stats: { ...routeState.stats },
      collector: routeState.collector?.snapshot?.() ?? null,
      issues: routeState.issues.slice(-10)
    };
  }

  #recordGift(routeState, gift, { receivedAt = this.now() } = {}) {
    if (!this.config.enable_gifts) {
      return null;
    }
    const giftWithResilience = this.#giftWithResilience(routeState, gift);
    const rawRecord = routeState.giftStore.append(giftWithResilience, {
      normalizedGift: giftWithResilience,
      receivedAt
    });
    this.stats.gift_count += 1;
    routeState.stats.gift_count += 1;
    this.stats.first_event_at = this.stats.first_event_at ?? giftWithResilience.event_time ?? receivedAt;
    this.stats.first_gift_at = this.stats.first_gift_at ?? giftWithResilience.event_time ?? receivedAt;
    this.stats.last_gift_at = giftWithResilience.event_time ?? receivedAt;
    routeState.stats.first_event_at = routeState.stats.first_event_at ?? giftWithResilience.event_time ?? receivedAt;
    routeState.stats.first_gift_at = routeState.stats.first_gift_at ?? giftWithResilience.event_time ?? receivedAt;
    routeState.stats.last_gift_at = giftWithResilience.event_time ?? receivedAt;
    routeState.stats.last_event_at = giftWithResilience.event_time ?? receivedAt;
    this.stats.last_event_at = giftWithResilience.event_time ?? receivedAt;
    this.#learnLiveRoomIdFromRoute(routeState, giftWithResilience.live_room_id, {
      receivedAt,
      reasonCode: "gift_event"
    });
    const fact = {
      event_type: "gift_received",
      category: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT,
      route_role: routeState.role,
      tenant_id: this.config.tenant_id,
      live_room_id: giftWithResilience.live_room_id ?? this.config.live_room_id,
      session_id: this.config.session_id,
      source_event_id: giftWithResilience.source_event_id ?? null,
      event_time: giftWithResilience.event_time ?? receivedAt,
      received_at: receivedAt,
      normalized: giftWithResilience,
      needs_review: giftWithResilience.tiktok_resilience?.needs_after_live_review === true,
      resilience_decision: giftWithResilience.tiktok_resilience?.decision ?? null,
      risk_level: giftWithResilience.tiktok_resilience?.risk_level ?? null,
      risk_reasons: giftWithResilience.tiktok_resilience?.risk_reasons ?? [],
      raw_ref: {
        raw_record_id: rawRecord.raw_record_id,
        source_id: rawRecord.source_id,
        sequence_no: rawRecord.sequence_no,
        raw_payload_hash: rawRecord.raw_payload_hash
      },
      gameplay_score_applied: false
    };
    appendNdjson(this.factFilePath, fact);
    this.#rememberGiftForResilience(routeState, giftWithResilience);
    for (const issue of giftWithResilience.tiktok_resilience?.issues ?? []) {
      this.#recordIssue(issue.reason_code ?? "tiktok_gift_resilience_review_required", {
        role: routeState.role,
        source_event_id: giftWithResilience.source_event_id ?? null,
        issue
      });
    }
    return fact;
  }

  #giftWithResilience(routeState, gift = {}) {
    if (gift.tiktok_resilience) {
      return gift;
    }
    const primaryEvents = [
      ...(this.recentGiftEventsByRole.get(GIFT_SOURCE_ROLES.PRIMARY_REALTIME) ?? []),
      ...(routeState.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME ? [gift] : [])
    ];
    const backupEvents = [
      ...(this.recentGiftEventsByRole.get(GIFT_SOURCE_ROLES.BACKUP_REALTIME) ?? []),
      ...(routeState.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME ? [gift] : [])
    ];
    const report = evaluateTiktokGiftRouteResilience({
      sources: this.#routeSourcesForResilience(),
      primaryEvents,
      backupEvents,
      event: gift,
      recentEvents: this.recentGiftEvents,
      expectedLiveRoomId: this.config.live_room_id,
      bigGiftValueThreshold: routeState.webcast?.big_gift_value_threshold ?? routeState.webcast?.bigGiftValueThreshold ?? 100000,
      highFrequencyEventsPerSecondThreshold: routeState.webcast?.high_frequency_events_per_second_threshold ?? routeState.webcast?.highFrequencyEventsPerSecondThreshold ?? 20
    });
    return {
      ...gift,
      tiktok_resilience: {
        decision: report.decision,
        can_drive_live_score: report.can_drive_live_score,
        active_role: report.active_role,
        risk_level: report.risk.risk_level,
        risk_reasons: report.risk.risk_reasons,
        needs_after_live_review: report.needs_after_live_review,
        requires_backup_confirmation: report.requires_backup_confirmation,
        backup_confirmed: report.backup_confirmed,
        issues: report.issues,
        next_actions: report.next_actions
      }
    };
  }

  #routeSourcesForResilience() {
    return this.routeStates.filter((routeState) => routeState.enabled !== false).map((routeState) => ({
      ...routeState.giftSourceDescriptor,
      connected: routeState.collector?.snapshot?.().connected ?? true,
      health: routeState.issues.length > 0 ? "needs_review" : "healthy",
      event_lag_ms_p95: 0,
      event_count: routeState.stats.gift_count,
      last_event_at: routeState.stats.last_event_at
    }));
  }

  #rememberGiftForResilience(routeState, gift) {
    const byRole = this.recentGiftEventsByRole.get(routeState.role) ?? [];
    byRole.push(gift);
    this.recentGiftEventsByRole.set(routeState.role, byRole.slice(-500));
    this.recentGiftEvents.push(gift);
    this.recentGiftEvents = this.recentGiftEvents.slice(-1000);
  }

  #recordP1WithRawBackup(routeState, p1Event, rawItems, { receivedAt = this.now() } = {}) {
    let eventToRecord = p1Event;
    if (this.config.enable_p1_evidence) {
      const rawItem = rawP1ItemForEvent(p1Event, rawItems);
      const rawRecord = routeState.p1Store.append(rawItem ?? createMinimalP1Raw(p1Event), {
        normalizedEvent: p1Event,
        receivedAt
      });
      eventToRecord = withP1RawReference(p1Event, { rawRecord });
    }
    return this.#recordP1Event(routeState, eventToRecord, { receivedAt });
  }

  #recordP1Event(routeState, event, { receivedAt = this.now() } = {}) {
    this.stats.p1_event_count += 1;
    routeState.stats.p1_event_count += 1;
    this.stats.first_event_at = this.stats.first_event_at ?? event.event_time ?? receivedAt;
    routeState.stats.first_event_at = routeState.stats.first_event_at ?? event.event_time ?? receivedAt;
    routeState.stats.last_event_at = event.event_time ?? receivedAt;
    this.stats.last_event_at = event.event_time ?? receivedAt;
    this.#learnLiveRoomIdFromRoute(routeState, event.live_room_id, {
      receivedAt,
      reasonCode: "p1_event"
    });
    const fact = {
      event_type: event.event_type,
      category: categoryForP1Event(event.event_type),
      route_role: routeState.role,
      tenant_id: event.tenant_id ?? this.config.tenant_id,
      live_room_id: event.live_room_id ?? this.config.live_room_id,
      session_id: event.session_id ?? this.config.session_id,
      actor_user_id: event.actor_user_id ?? null,
      source_event_id: event.payload?.source_event_id ?? null,
      event_time: event.event_time ?? receivedAt,
      received_at: receivedAt,
      normalized: event,
      gameplay_score_applied: false
    };
    appendNdjson(this.factFilePath, fact);
    return fact;
  }

  #recordIssue(reasonCode, detail = {}) {
    const issue = {
      reason_code: reasonCode,
      observed_at: this.now(),
      detail
    };
    this.stats.issue_count += 1;
    this.stats.last_issue_code = reasonCode;
    this.issues.push(issue);
    const routeState = detail.role ? this.routesByRole.get(detail.role) : null;
    if (routeState) {
      routeState.issues.push(issue);
    }
    this.#appendStatus({
      event_type: "collector_issue",
      state: "attention",
      issue
    });
    return issue;
  }

  #appendStatus(status) {
    appendNdjson(this.statusFilePath, {
      observed_at: this.now(),
      collector_id: this.config.collector_id,
      tenant_id: this.config.tenant_id,
      live_room_id: this.config.live_room_id,
      session_id: this.config.session_id,
      ...status
    });
  }

  #writeSummary() {
    writeFileSync(this.outputFiles().summary, JSON.stringify({
      snapshot: this.snapshot(),
      manifests: this.createManifests(),
      replay: this.replay(),
      health: this.createHealthReport(),
      diagnostics: this.createCollectionDiagnostics({ write: true }),
      settlement: this.lastSettlement?.report ?? null
    }, null, 2), "utf8");
  }
}

export function readLiveDataCollectorFacts(filePath) {
  return readNdjson(filePath);
}

export function parseExternalReportRows({
  content = null,
  filePath = null,
  format = null,
  rows = null
} = {}) {
  if (Array.isArray(rows)) {
    return rows.map(normalizeExternalReportRow).filter(Boolean);
  }
  const text = content ?? (filePath ? readFileSync(filePath, "utf8") : "");
  const finalFormat = format ?? inferReportFormat(filePath, text);
  if (finalFormat === "json" || finalFormat === "jsonl" || finalFormat === "ndjson") {
    return parseJsonLikeRows(text, finalFormat).map(normalizeExternalReportRow).filter(Boolean);
  }
  return parseCsvRows(text).map(normalizeExternalReportRow).filter(Boolean);
}

export function parseExternalAggregateSummary(input = {}) {
  const rawRows = parseExternalRawReportRows(input);
  const candidates = [];
  for (const key of ["aggregate", "summary", "totals", "controlTotals", "control_totals"]) {
    if (input[key] && typeof input[key] === "object") {
      candidates.push(input[key]);
    }
  }
  for (const row of rawRows) {
    if (!row || typeof row !== "object") {
      continue;
    }
    candidates.push(row);
    for (const key of ["aggregate", "summary", "totals", "controlTotals", "control_totals"]) {
      if (row[key] && typeof row[key] === "object") {
        candidates.push(row[key]);
      }
    }
  }

  const aggregateRows = candidates.filter(isAggregateControlTotalRow);
  const summary = {
    present: false,
    live_room_id: null,
    total_gift_value: null,
    paid_user_count: null,
    gift_event_count: null,
    gift_count_total: null,
    value_unit: null,
    unsafe_metrics: [],
    metrics_present: [],
    source_row_count: aggregateRows.length
  };

  for (const row of aggregateRows) {
    const keyValueMetric = parseAggregateKeyValueRow(row);
    if (keyValueMetric) {
      if (keyValueMetric.metric === "value_unit") {
        summary.value_unit ??= normalizeAggregateValueUnit(keyValueMetric.value);
        continue;
      }
      assignAggregateMetric(summary, keyValueMetric.metric, keyValueMetric.value);
      continue;
    }
    summary.live_room_id ??= stringOrNull(pickMetricValue(row, AGGREGATE_ROOM_ALIASES));
    summary.value_unit ??= normalizeAggregateValueUnit(pickMetricValue(row, AGGREGATE_VALUE_UNIT_ALIASES)) ?? inferAggregateValueUnitFromKeys(row);
    assignAggregateMetric(summary, "total_gift_value", pickMetricValue(row, AGGREGATE_VALUE_ALIASES));
    assignAggregateMetric(summary, "paid_user_count", pickMetricValue(row, AGGREGATE_PAID_USER_ALIASES));
    assignAggregateMetric(summary, "gift_event_count", pickMetricValue(row, AGGREGATE_EVENT_COUNT_ALIASES));
    assignAggregateMetric(summary, "gift_count_total", pickMetricValue(row, AGGREGATE_GIFT_COUNT_TOTAL_ALIASES));
  }

  summary.metrics_present = [
    "total_gift_value",
    "paid_user_count",
    "gift_event_count",
    "gift_count_total"
  ].filter((metric) => summary[metric] !== null);
  summary.present = summary.metrics_present.length > 0;
  return summary;
}

function parseExternalRawReportRows({
  content = null,
  filePath = null,
  format = null,
  rows = null
} = {}) {
  if (Array.isArray(rows)) {
    return rows.filter((row) => row && typeof row === "object");
  }
  const text = content ?? (filePath ? readFileSync(filePath, "utf8") : "");
  const finalFormat = format ?? inferReportFormat(filePath, text);
  if (finalFormat === "json" || finalFormat === "jsonl" || finalFormat === "ndjson") {
    const parsedRows = parseJsonLikeRows(text, finalFormat).filter((row) => row && typeof row === "object");
    if (finalFormat === "json" && text.trim().startsWith("{")) {
      const parsed = JSON.parse(text);
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && !parsedRows.includes(parsed)) {
        return [parsed, ...parsedRows];
      }
    }
    return parsedRows;
  }
  return parseCsvRows(text).filter((row) => row && typeof row === "object");
}

export function createLiveDataCollectorComparisonReport({
  facts = [],
  externalRows = [],
  sourceName = "external",
  toleranceMs = 3000,
  generatedAt = nowIso()
} = {}) {
  const collectorRows = dedupeComparisonRows(facts.map(normalizeCollectorFactForComparison).filter(Boolean));
  const normalizedExternalRows = dedupeComparisonRows(externalRows);
  const collectorGifts = collectorRows.filter((row) => row.category === "gift");
  const externalGifts = normalizedExternalRows.filter((row) => row.category === "gift");
  const usedExternal = new Set();
  const matched = [];
  const collectorOnly = [];
  for (const collectorGift of collectorGifts) {
    const match = findBestGiftMatch(collectorGift, externalGifts, usedExternal, toleranceMs);
    if (!match) {
      collectorOnly.push(collectorGift);
      continue;
    }
    usedExternal.add(match.index);
    matched.push({
      collector: collectorGift,
      external: match.row,
      match_type: match.match_type,
      count_delta: Number(collectorGift.gift_count ?? 0) - Number(match.row.gift_count ?? 0),
      value_delta: Number(collectorGift.gift_total_value ?? 0) - Number(match.row.gift_total_value ?? 0)
    });
  }
  const externalOnly = externalGifts.filter((_, index) => !usedExternal.has(index));
  const mismatched = matched.filter((item) => item.count_delta !== 0 || item.value_delta !== 0);
  const categoryComparison = compareCategoryCounts({ collectorRows, externalRows: normalizedExternalRows });
  const collectorValue = sumBy(collectorGifts, "gift_total_value");
  const externalValue = sumBy(externalGifts, "gift_total_value");
  const score = createAccuracyScore({
    collectorCount: collectorGifts.length,
    externalCount: externalGifts.length,
    matchedCount: matched.length,
    mismatchCount: mismatched.length,
    valueDelta: Math.abs(collectorValue - externalValue),
    externalValue
  });
  return {
    report_id: createId("collector_compare"),
    generated_at: generatedAt,
    source_name: sourceName,
    source_policy: classifyExternalReportSource(sourceName),
    ok: collectorOnly.length === 0 && externalOnly.length === 0 && mismatched.length === 0,
    accuracy_score: score,
    summary: {
      collector_gift_count: collectorGifts.length,
      external_gift_count: externalGifts.length,
      matched_gift_count: matched.length,
      collector_only_gift_count: collectorOnly.length,
      external_only_gift_count: externalOnly.length,
      mismatched_gift_count: mismatched.length,
      collector_gift_value: collectorValue,
      external_gift_value: externalValue,
      gift_value_delta: collectorValue - externalValue
    },
    category_comparison: categoryComparison,
    matched,
    mismatched,
    collector_only: collectorOnly,
    external_only: externalOnly
  };
}

export function writeLiveDataCollectorComparisonReport({
  report,
  outputDir
} = {}) {
  if (!report || !outputDir) {
    throw new Error("report and outputDir are required");
  }
  mkdirSync(outputDir, { recursive: true });
  const jsonPath = join(outputDir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.COMPARISON_JSON);
  const csvPath = join(outputDir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.COMPARISON_CSV);
  writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf8");
  writeFileSync(csvPath, exportComparisonCsv(report), "utf8");
  return {
    json: jsonPath,
    csv: csvPath
  };
}

export function exportComparisonCsv(report = {}) {
  const rows = [
    ["section", "source_event_id", "user_id", "gift_id", "gift_name", "collector_count", "external_count", "collector_value", "external_value", "count_delta", "value_delta", "match_type"]
  ];
  for (const item of report.matched ?? []) {
    rows.push([
      item.count_delta === 0 && item.value_delta === 0 ? "matched" : "mismatched",
      item.collector.source_event_id ?? item.external.source_event_id ?? "",
      item.collector.user_id ?? item.external.user_id ?? "",
      item.collector.gift_id ?? item.external.gift_id ?? "",
      item.collector.gift_name ?? item.external.gift_name ?? "",
      item.collector.gift_count ?? "",
      item.external.gift_count ?? "",
      item.collector.gift_total_value ?? "",
      item.external.gift_total_value ?? "",
      item.count_delta,
      item.value_delta,
      item.match_type
    ]);
  }
  for (const item of report.collector_only ?? []) {
    rows.push(["collector_only", item.source_event_id ?? "", item.user_id ?? "", item.gift_id ?? "", item.gift_name ?? "", item.gift_count ?? "", "", item.gift_total_value ?? "", "", "", "", ""]);
  }
  for (const item of report.external_only ?? []) {
    rows.push(["external_only", item.source_event_id ?? "", item.user_id ?? "", item.gift_id ?? "", item.gift_name ?? "", "", item.gift_count ?? "", "", item.gift_total_value ?? "", "", "", ""]);
  }
  return rows.map((row) => row.map(csvCell).join(",")).join("\n");
}

export function writeLiveDataCollectorSettlementReport({
  report,
  outputDir
} = {}) {
  if (!report || !outputDir) {
    throw new Error("report and outputDir are required");
  }
  mkdirSync(outputDir, { recursive: true });
  const jsonPath = join(outputDir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.SETTLEMENT_JSON);
  const csvPath = join(outputDir, LIVE_DATA_COLLECTOR_PLUGIN_FILES.SETTLEMENT_CSV);
  writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf8");
  writeFileSync(csvPath, exportSettlementCorrectionsCsv(report), "utf8");
  return {
    json: jsonPath,
    csv: csvPath
  };
}

export function exportSettlementCorrectionsCsv(report = {}) {
  const rows = [
    ["section", "action", "reason_code", "match_key", "source_event_id", "user_id", "gift_id", "gift_name", "gift_count", "gift_total_value", "event_time", "production_allowed", "ready_for_production_settlement", "collector_value", "official_value", "delta", "tolerance"]
  ];
  for (const correction of report.corrections ?? report.backfill?.corrections ?? []) {
    const event = correction.event ?? {};
    rows.push([
      "correction",
      correction.action ?? "",
      correction.reason_code ?? "",
      correction.match_key ?? "",
      event.source_event_id ?? event.platform_event_id ?? "",
      event.gifter_user_id ?? "",
      event.gift_id ?? "",
      event.gift_name ?? "",
      event.gift_count ?? "",
      event.gift_total_value ?? "",
      event.event_time ?? "",
      report.source_policy?.production_allowed === true ? "true" : "false",
      report.ready_for_production_settlement === true ? "true" : "false",
      "",
      "",
      "",
      ""
    ]);
  }
  for (const row of aggregateReconciliationCsvRows(report)) {
    rows.push(row);
  }
  for (const row of settlementReadinessGateCsvRows(report)) {
    rows.push(row);
  }
  rows.push([
    "summary",
    report.ready_for_production_settlement ? "ready" : "review",
    report.ready_for_production_settlement ? "settlement_ready" : "settlement_not_ready",
    "",
    "",
    "",
    "",
    report.settlement_mode ?? "",
    report.summary?.after_live_gift_count ?? "",
    report.summary?.collector_gift_value ?? "",
    report.generated_at ?? "",
    report.source_policy?.production_allowed === true ? "true" : "false",
    report.ready_for_production_settlement === true ? "true" : "false",
    report.summary?.collector_gift_value ?? "",
    report.summary?.official_total_gift_value ?? "",
    report.summary?.aggregate_deltas?.total_gift_value?.delta ?? "",
    report.aggregate_reconciliation?.tolerances?.total_gift_value ?? ""
  ]);
  return rows.map((row) => row.map(csvCell).join(",")).join("\n");
}

function aggregateReconciliationCsvRows(report = {}) {
  const reconciliation = report.aggregate_reconciliation;
  if (!reconciliation?.present) {
    return [];
  }
  return (reconciliation.checked_metrics ?? []).map((metric) => {
    const delta = reconciliation.deltas?.[metric] ?? {};
    const issue = reconciliation.issues?.find((item) => item.metric === metric);
    const matched = !issue && Math.abs(numberOrZero(delta.delta)) <= numberOrZero(delta.tolerance);
    return [
      "aggregate",
      matched ? "matched" : "review",
      issue?.reason_code ?? `${metric}_matched`,
      "",
      "",
      "",
      "",
      metric,
      "",
      "",
      report.generated_at ?? "",
      report.source_policy?.production_allowed === true ? "true" : "false",
      report.ready_for_production_settlement === true ? "true" : "false",
      delta.collector_value ?? "",
      delta.official_value ?? "",
      delta.delta ?? "",
      delta.tolerance ?? ""
    ];
  });
}

function settlementReadinessGateCsvRows(report = {}) {
  const gate = report.settlement_readiness_gate;
  if (!gate?.issues?.length) {
    return [];
  }
  return gate.issues.map((issue) => [
    "readiness_gate",
    issue.severity === "blocker" ? "review" : "note",
    issue.reason_code ?? "collector_readiness_issue",
    "",
    "",
    "",
    "",
    issue.metric ?? "",
    "",
    "",
    report.generated_at ?? "",
    report.source_policy?.production_allowed === true ? "true" : "false",
    report.ready_for_production_settlement === true ? "true" : "false",
    issue.collector_value ?? "",
    issue.official_value ?? "",
    issue.delta ?? "",
    issue.tolerance ?? ""
  ]);
}

export function classifyExternalReportSource(sourceName = "external") {
  const normalized = String(sourceName ?? "external").trim().toLowerCase();
  const compact = normalized.replace(/[\s_-]+/gu, "");
  const productionSources = new Set([
    "guildbackend",
    "tiktokofficialexport",
    "creatorcenterexport",
    "liverushafterliveimport"
  ]);
  const fieldTestSources = new Set([
    "mca",
    "mcaexport",
    "fieldtestmca",
    "thirdpartytest",
    "thirdpartycomparison",
    "testcomparison"
  ]);
  if (productionSources.has(compact)) {
    return {
      source_name: sourceName,
      source_type: LIVE_DATA_COLLECTOR_REPORT_SOURCE_TYPES.PRODUCTION_AFTER_LIVE,
      production_allowed: true,
      can_drive_accuracy_backfill: true,
      can_drive_field_test_score: true,
      message: "可作为正式下播补数来源。"
    };
  }
  if (fieldTestSources.has(compact) || compact.includes("mca")) {
    return {
      source_name: sourceName,
      source_type: LIVE_DATA_COLLECTOR_REPORT_SOURCE_TYPES.FIELD_TEST_COMPARISON,
      production_allowed: false,
      can_drive_accuracy_backfill: false,
      can_drive_field_test_score: true,
      message: "仅用于测试期横向对比，不能作为正式采集或补数依赖。"
    };
  }
  return {
    source_name: sourceName,
    source_type: LIVE_DATA_COLLECTOR_REPORT_SOURCE_TYPES.UNKNOWN_EXTERNAL,
    production_allowed: false,
    can_drive_accuracy_backfill: false,
    can_drive_field_test_score: true,
    message: "未知来源只能用于临时对照，正式结算前请改用公会后台、TikTok 官方导出、达人后台或 Live Rush 下播导入。"
  };
}

function summarizeSettlementReport(report = {}) {
  return {
    source_name: report.source_name ?? null,
    source_type: report.source_policy?.source_type ?? null,
    production_allowed: report.source_policy?.production_allowed === true,
    settlement_mode: report.settlement_mode ?? report.summary?.settlement_mode ?? null,
    ready_for_production_settlement: report.ready_for_production_settlement === true,
    accuracy_score: report.summary?.accuracy_score ?? null,
    unresolved_count: report.summary?.unresolved_count ?? null,
    aggregate_ok: report.aggregate_reconciliation?.present ? report.aggregate_reconciliation.ok === true : null,
    aggregate_checked_metrics: report.aggregate_reconciliation?.checked_metrics ?? [],
    aggregate_issue_count: report.aggregate_reconciliation?.issues?.length ?? 0,
    aggregate_deltas: report.aggregate_reconciliation?.deltas ?? {},
    settlement_gate_ok: report.settlement_readiness_gate?.ok ?? null,
    settlement_gate_issue_count: report.settlement_readiness_gate?.issue_count ?? 0,
    correction_count: report.corrections?.length ?? report.backfill?.corrections?.length ?? 0,
    requires_mca: false
  };
}

function createCollectorRealtimeStatus({
  plugin,
  facts = [],
  statuses = [],
  replay = {},
  diagnostics = {},
  generatedAt = nowIso()
} = {}) {
  const giftRows = facts
    .map(normalizeCollectorFactForComparison)
    .filter((row) => row?.category === "gift");
  const ledgerGiftRows = dedupeComparisonRows(giftRows);
  const p1Facts = facts.filter((fact) => fact.category && fact.category !== LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT);
  const visibleP1Facts = p1Facts.filter(isOperatorVisibleP1Fact);
  const routeStatuses = annotateRealtimeRouteStatuses(
    plugin.routeStates.map((routeState) => createRealtimeRouteStatus(routeState, {
      generatedAt,
      p1Facts: visibleP1Facts
    }))
  );
  const routeComparison = replay.route_comparison ?? {};
  const routeGapCount = numberOrZero(routeComparison.primary_only_count)
    + numberOrZero(routeComparison.backup_only_count)
    + numberOrZero(routeComparison.mismatched_count);
  const firstGiftAt = earliestTime(ledgerGiftRows.map((row) => row.event_time ?? row.received_at));
  const firstFrameAt = earliestTime([
    plugin.stats.first_frame_at,
    ...routeStatuses.map((route) => route.first_frame_at)
  ]);
  const firstEventAt = earliestTime([
    firstGiftAt,
    ...visibleP1Facts.map((fact) => fact.event_time ?? fact.received_at),
    ...routeStatuses.map((route) => route.first_event_at)
  ]);
  const lastSignalAt = latestTime([
    ...routeStatuses.flatMap((route) => [route.last_signal_at, route.last_event_at])
  ]);
  const standbyStartedAt = earliestTime(routeStatuses.map((route) => route.standby_started_at));
  const liveDetectedAt = earliestTime(routeStatuses.map((route) => route.live_detected_at));
  const firstAttachAt = earliestTime([firstFrameAt, firstEventAt]);
  const attachDelaySeconds = liveDetectedAt && firstAttachAt
    ? Math.max(0, secondsBetween(liveDetectedAt, firstAttachAt))
    : null;
  const hasSignal = Boolean(lastSignalAt)
    || ledgerGiftRows.length > 0
    || visibleP1Facts.length > 0
    || routeStatuses.some((route) => routeHasRealtimeData(route));
  const gapRisk = createRealtimeGapRisk({
    diagnostics,
    routeComparison,
    routeStatuses,
    routeGapCount,
    hasSignal
  });
  const dataVisibility = createRealtimeDataVisibility({
    giftRows: ledgerGiftRows,
    rawGiftRows: giftRows,
    p1Facts: visibleP1Facts,
    routeStatuses,
    diagnostics,
    hasSignal
  });
  const state = decideRealtimeState({
    plugin,
    routeStatuses,
    gapRisk,
    hasSignal,
    dataVisibility
  });
  const level = decideRealtimeLevel({ state, gapRisk, diagnostics, dataVisibility });
  const nextActions = createRealtimeNextActions({
    state,
    gapRisk,
    diagnostics,
    routeStatuses,
    hasSignal,
    dataVisibility
  });
  const totals = {
    gift_event_count: ledgerGiftRows.length,
    raw_gift_fact_count: giftRows.length,
    gift_count_total: roundMetric(sumBy(ledgerGiftRows, "gift_count")),
    gift_value_total: roundMetric(sumBy(ledgerGiftRows, "gift_total_value")),
    paid_user_count: new Set(ledgerGiftRows.map((row) => row.user_id).filter(Boolean)).size,
    p1_event_count: visibleP1Facts.length,
    frame_count: Math.max(
      numberOrZero(plugin.stats.frame_count),
      routeStatuses.reduce((sum, route) => sum + numberOrZero(route.frame_count), 0)
    ),
    issue_count: numberOrZero(plugin.stats.issue_count),
    route_gap_count: routeGapCount,
    primary_gift_count: numberOrZero(routeComparison.primary_gift_count ?? routeStatuses.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME)?.gift_count),
    backup_gift_count: numberOrZero(routeComparison.backup_gift_count ?? routeStatuses.find((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME)?.gift_count)
  };
  const pipeline = createRealtimeCapturePipeline({
    plugin,
    routeStatuses,
    diagnostics,
    replay,
    routeComparison,
    gapRisk,
    hasSignal,
    dataVisibility
  });
  const guardrails = createRealtimeGuardrails({
    state,
    routeStatuses,
    gapRisk,
    dataVisibility,
    diagnostics
  });
  return {
    generated_at: generatedAt,
    state,
    level,
    headline: realtimeHeadlineForState(state, level),
    operator_message: realtimeOperatorMessage({ state, gapRisk, totals, routeStatuses, dataVisibility }),
    next_actions: nextActions,
    data_visibility: dataVisibility,
    pipeline,
    guardrails,
    data_freshness: {
      last_signal_at: lastSignalAt,
      age_seconds: secondsSince(lastSignalAt, generatedAt),
      level: freshnessLevel(secondsSince(lastSignalAt, generatedAt), hasSignal),
      message: freshnessMessage(secondsSince(lastSignalAt, generatedAt), hasSignal)
    },
    live_start: {
      pre_live_standby_enabled: routeStatuses.some((route) => route.standby_enabled),
      standby_active: state === "standby" && !hasSignal && routeStatuses.some((route) => route.state === "standby_waiting_for_live"),
      standby_started_at: standbyStartedAt,
      live_detected_at: liveDetectedAt,
      first_frame_at: firstFrameAt,
      first_event_at: firstEventAt,
      first_gift_at: firstGiftAt,
      first_attach_delay_seconds: attachDelaySeconds,
      message: liveStartMessage({ state, standbyStartedAt, liveDetectedAt, attachDelaySeconds })
    },
    coverage: {
      formal_readiness_score: diagnostics.readiness_score ?? null,
      formal_use_ready: diagnostics.formal_use_ready === true,
      backup_configured: routeStatuses.some((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME && route.enabled !== false),
      backup_independent: diagnostics.route_independence?.production_ready === true,
      backup_confirmed: routeComparison.checked === true && routeComparison.ok === true,
      mca_required_for_production: false
    },
    gap_risk: gapRisk,
    totals,
    routes: routeStatuses,
    recent_gifts: createRealtimeRecentGiftRows(ledgerGiftRows),
    recent_activity: createRealtimeRecentActivityRows({ giftRows: ledgerGiftRows, p1Facts: visibleP1Facts }),
    diagnostics_summary: {
      ok: diagnostics.ok === true,
      readiness_score: diagnostics.readiness_score ?? null,
      issue_count: diagnostics.issues?.length ?? 0,
      blocker_count: diagnostics.issues?.filter((issue) => issue.severity === "blocker").length ?? 0,
      warning_count: diagnostics.issues?.filter((issue) => issue.severity === "warning").length ?? 0
    }
  };
}

function createRealtimeRouteStatus(routeState, { generatedAt = nowIso(), p1Facts = null } = {}) {
  const collector = routeState.collector?.snapshot?.() ?? null;
  const stats = routeState.stats ?? {};
  const collectorStats = collector?.stats ?? {};
  const scopedP1Facts = Array.isArray(p1Facts)
    ? p1Facts.filter((fact) => fact.route_role === routeState.role)
    : null;
  const hasScopedP1Facts = Array.isArray(scopedP1Facts);
  const scopedP1EventTimes = hasScopedP1Facts
    ? scopedP1Facts.map((fact) => fact.event_time ?? fact.received_at)
    : [];
  const state = collector?.state
    ?? ((numberOrZero(stats.gift_count) + numberOrZero(stats.p1_event_count)) > 0
      ? "collecting_from_feed"
      : "not_started");
  const lastFrameAt = latestTime([stats.last_frame_at, collectorStats.last_frame_at]);
  const lastEventAt = latestTime([
    stats.last_gift_at,
    ...(hasScopedP1Facts ? [] : [stats.last_event_at, collectorStats.last_event_at]),
    ...scopedP1EventTimes
  ]);
  const lastSignalAt = latestTime([lastEventAt, lastFrameAt]);
  const firstFrameAt = earliestTime([stats.first_frame_at, collectorStats.first_frame_at]);
  const firstEventAt = earliestTime([
    stats.first_gift_at,
    ...(hasScopedP1Facts ? [] : [stats.first_event_at]),
    ...scopedP1EventTimes
  ]);
  const frameCount = Math.max(
    numberOrZero(stats.frame_count),
    numberOrZero(collectorStats.frame_count),
    numberOrZero(collectorStats.raw_frame_count)
  );
  const rawFrameCount = Math.max(numberOrZero(stats.raw_frame_count), numberOrZero(collectorStats.raw_frame_count));
  const decodedFrameCount = Math.max(numberOrZero(stats.decoded_frame_count), numberOrZero(collectorStats.decoded_frame_count));
  const rawFrameArchiveCount = numberOrZero(stats.raw_frame_archive_count);
  const giftCount = numberOrZero(stats.gift_count || collectorStats.accepted_count || collectorStats.event_count);
  const p1EventCount = hasScopedP1Facts
    ? scopedP1Facts.length
    : numberOrZero(stats.p1_event_count || collectorStats.p1_event_count);
  const semanticEventCount = giftCount + p1EventCount;
  const targetSource = collector?.target_source ?? routeTargetLabel(routeState.webcast);
  const browserEventTransportFrameCount = numberOrZero(collectorStats.browser_event_transport_frame_count);
  const browserLastEventTransportFrameAt = normalizeTime(collectorStats.browser_last_event_transport_frame_at);
  const browserRawFrameCount = targetSource === "tiktok_browser_observer"
    ? Math.max(numberOrZero(collectorStats.frame_count), numberOrZero(collectorStats.raw_frame_count))
    : 0;
  const browserPeripheralFrameCount = targetSource === "tiktok_browser_observer"
    ? Math.max(0, browserRawFrameCount - browserEventTransportFrameCount)
    : 0;
  const routeSignalFrameCount = targetSource === "tiktok_browser_observer"
    ? browserEventTransportFrameCount
    : frameCount;
  const routeLastSignalAt = targetSource === "tiktok_browser_observer"
    ? latestTime([lastEventAt, browserLastEventTransportFrameAt])
    : latestTime([lastEventAt, lastFrameAt]);
  const browserAttached = targetSource === "tiktok_browser_observer"
    ? collector?.attached === true || collector?.connected === true
    : null;
  const browserEventStreamConnected = targetSource === "tiktok_browser_observer"
    ? collector?.event_stream_connected === true || browserEventTransportFrameCount > 0 || semanticEventCount > 0
    : null;
  const browserNotLoggedInCount = numberOrZero(collectorStats.browser_not_logged_in_count);
  const browserLoginRequired = targetSource === "tiktok_browser_observer"
    && (collector?.browser_login_required === true
      || (browserNotLoggedInCount > 0
        && browserEventStreamConnected !== true
        && semanticEventCount === 0));
  const browserHeadless = targetSource === "tiktok_browser_observer"
    ? collector?.browser_headless === true || isHeadlessBrowserConfig(routeState.webcast)
    : null;
  const browserInteractiveRecoveryAvailable = targetSource === "tiktok_browser_observer"
    ? collector?.browser_interactive_recovery_available !== false && browserHeadless !== true
    : null;
  const connectorRuntime = targetSource === "tiktok_live_connector"
    ? createTiktokConnectorRuntimeStatus({
        collector,
        webcast: routeState.webcast
      })
    : null;
  const routeConnected = targetSource === "tiktok_browser_observer"
    ? browserEventStreamConnected === true
    : collector?.connected === true || numberOrZero(stats.gift_count) > 0 || p1EventCount > 0;
  const lastIssueCode = selectRealtimeRouteIssueCode({
    targetSource,
    routeState,
    collectorStats,
    browserLoginRequired
  });
  const status = {
    role: routeState.role,
    label: routeState.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME ? "备用线路" : "主线路",
    enabled: routeState.enabled !== false,
    state,
    connected: routeConnected,
    target_source: targetSource,
    target_url: collector?.target_url ?? routeTargetUrl(routeState.webcast),
    browser_manual_recovery_url: collector?.browser_manual_recovery_url ?? (isTiktokBrowserObserverRoute(routeState.webcast) ? routeTargetUrl(routeState.webcast) : null),
    browser_user_data_dir: collector?.browser_user_data_dir ?? routeState.webcast?.browser_user_data_dir ?? null,
    browser_recovery_required: collector?.browser_recovery_required === true || browserLoginRequired,
    browser_login_required: browserLoginRequired,
    browser_headless: browserHeadless,
    browser_interactive_recovery_available: browserInteractiveRecoveryAvailable,
    browser_can_minimize: collector?.browser_can_minimize ?? (targetSource === "tiktok_browser_observer" ? true : null),
    browser_can_close: collector?.browser_can_close ?? (targetSource === "tiktok_browser_observer" ? false : null),
    browser_open_as_app: collector?.browser_open_as_app ?? routeState.webcast?.browser_open_as_app ?? null,
    browser_keep_visible: collector?.browser_keep_visible ?? routeState.webcast?.browser_keep_visible ?? null,
    transport: collector?.transport ?? null,
    standby_enabled: routeState.webcast?.standby?.enabled === true,
    standby_started_at: normalizeTime(collectorStats.standby_started_at),
    live_detected_at: normalizeTime(collectorStats.live_detected_at),
    standby_poll_count: numberOrZero(collectorStats.standby_poll_count),
    standby_last_reason_code: collectorStats.standby_last_reason_code ?? null,
    standby_last_http_status: collectorStats.standby_last_http_status ?? null,
    reconnect_count: numberOrZero(collectorStats.reconnect_count),
    browser_websocket_count: numberOrZero(collectorStats.browser_websocket_count),
    browser_detached_count: numberOrZero(collectorStats.browser_detached_count),
    browser_relaunch_count: numberOrZero(collectorStats.browser_relaunch_count),
    browser_auto_minimized_count: numberOrZero(collectorStats.browser_auto_minimized_count),
    browser_event_transport_frame_count: browserEventTransportFrameCount,
    browser_peripheral_frame_count: browserPeripheralFrameCount,
    browser_polling_response_count: numberOrZero(collectorStats.browser_polling_response_count),
    browser_polling_response_body_unavailable_count: numberOrZero(collectorStats.browser_polling_response_body_unavailable_count),
    browser_runtime_capture_count: numberOrZero(collectorStats.browser_runtime_capture_count),
    browser_runtime_capture_failed_count: numberOrZero(collectorStats.browser_runtime_capture_failed_count),
    browser_runtime_capture_target_gone_count: numberOrZero(collectorStats.browser_runtime_capture_target_gone_count),
    browser_auxiliary_target_gone_count: numberOrZero(collectorStats.browser_auxiliary_target_gone_count),
    browser_attached: browserAttached,
    browser_event_stream_connected: browserEventStreamConnected,
    browser_page_health_check_count: numberOrZero(collectorStats.browser_page_health_check_count),
    browser_page_blocked_count: numberOrZero(collectorStats.browser_page_blocked_count),
    browser_not_logged_in_count: browserNotLoggedInCount,
    browser_last_detached_at: normalizeTime(collectorStats.browser_last_detached_at),
    browser_last_relaunch_at: normalizeTime(collectorStats.browser_last_relaunch_at),
    browser_last_minimized_at: normalizeTime(collectorStats.browser_last_minimized_at),
    browser_last_page_blocked_at: normalizeTime(collectorStats.browser_last_page_blocked_at),
    browser_last_not_logged_in_at: normalizeTime(collectorStats.browser_last_not_logged_in_at),
    browser_last_event_transport_frame_at: browserLastEventTransportFrameAt,
    connector_signing_configured: connectorRuntime?.connector_signing_configured ?? null,
    connector_uses_default_signing: connectorRuntime?.connector_uses_default_signing ?? null,
    connector_sign_api_key_configured: connectorRuntime?.connector_sign_api_key_configured ?? null,
    connector_sign_api_url_configured: connectorRuntime?.connector_sign_api_url_configured ?? null,
    connector_signed_websocket_provider_configured: connectorRuntime?.connector_signed_websocket_provider_configured ?? null,
    connector_request_polling_enabled: connectorRuntime?.connector_request_polling_enabled ?? null,
    connector_connect_with_unique_id: connectorRuntime?.connector_connect_with_unique_id ?? null,
    connector_fetch_room_info_on_connect: connectorRuntime?.connector_fetch_room_info_on_connect ?? null,
    connector_authenticate_ws: connectorRuntime?.connector_authenticate_ws ?? null,
    connector_session_cookie_configured: connectorRuntime?.connector_session_cookie_configured ?? null,
    first_frame_at: firstFrameAt,
    last_frame_at: lastFrameAt,
    first_event_at: firstEventAt,
    last_event_at: lastEventAt,
    last_signal_at: routeLastSignalAt,
    freshness_seconds: secondsSince(routeLastSignalAt, generatedAt),
    frame_count: frameCount,
    raw_frame_count: rawFrameCount,
    decoded_frame_count: decodedFrameCount,
    raw_frame_archive_count: rawFrameArchiveCount,
    semantic_event_count: semanticEventCount,
    signal_only: routeSignalFrameCount > 0 && semanticEventCount === 0,
    gift_count: giftCount,
    p1_event_count: p1EventCount,
    rejected_count: numberOrZero(stats.rejected_count || collectorStats.rejected_count),
    issue_count: routeState.issues.length,
    last_issue_code: lastIssueCode
  };
  const healthLevel = routeHealthLevel(status);
  return {
    ...status,
    health_level: healthLevel,
    message: routeHealthMessage({ ...status, health_level: healthLevel })
  };
}

function selectRealtimeRouteIssueCode({
  targetSource = null,
  routeState = {},
  collectorStats = {},
  browserLoginRequired = false
} = {}) {
  const latestIssueCode = routeState.issues?.at(-1)?.reason_code ?? collectorStats.last_issue_code ?? null;
  if (targetSource !== "tiktok_browser_observer") {
    return latestIssueCode;
  }
  if (numberOrZero(collectorStats.browser_page_blocked_count) > 0) {
    return "tiktok_browser_observer_page_verification_blocked";
  }
  if (browserLoginRequired) {
    return "tiktok_browser_observer_not_logged_in";
  }
  return latestIssueCode;
}

function annotateRealtimeRouteStatuses(routeStatuses = []) {
  const backupHasSignal = routeStatuses.some((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME && routeHasRealtimeData(route));
  const backupHasUsableFacts = routeStatuses.some((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME && routeHasUsableRealtimeFacts(route));
  return routeStatuses.map((route) => {
    if (route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME
      && route.state === "standby_waiting_for_live"
      && backupHasSignal) {
      const patched = {
        ...route,
        cross_route_live_confirmed: backupHasUsableFacts,
        cross_route_signal_seen: !backupHasUsableFacts,
        health_level: "warning"
      };
      return {
        ...patched,
        message: routeHealthMessage(patched)
      };
    }
    return route;
  });
}

function createRealtimeGapRisk({ diagnostics = {}, routeComparison = {}, routeStatuses = [], routeGapCount = 0, hasSignal = false } = {}) {
  const issues = [];
  if (routeComparison.checked === true && routeComparison.ok !== true) {
    issues.push({
      severity: "blocker",
      reason_code: routeComparison.reason_code ?? "primary_backup_mismatch",
      message: "主备线路的礼物底稿不一致，直播结束后必须导入正式后台数据补数。"
    });
  }
  for (const route of routeStatuses) {
    if (!route.enabled) {
      continue;
    }
    if (route.health_level === "blocked") {
      const blockedMessage = isBrowserPageVerificationBlocked(route) || isBrowserLoginRecoveryRequired(route)
        ? isBrowserInteractiveRecoveryUnavailable(route)
          ? `${route.label}运行在云端无界面浏览器，TikTok 登录/验证码不能自动完成；需要远程桌面可视化处理，或把采集器装到运营电脑。`
          : `${route.label}被 TikTok 验证/超时页挡住，当前电脑看不到礼物和公屏；请点击“打开 TikTok 验证页”，完成登录或验证后保持页面最小化。`
        : `${route.label}已经进入异常状态，需要检查网络、账号或采集入口。`;
      issues.push({
        severity: "blocker",
        reason_code: `${route.role}_blocked`,
        message: blockedMessage
      });
    } else if (route.health_level === "warning") {
      issues.push({
        severity: "warning",
        reason_code: `${route.role}_attention`,
        message: route.message
      });
    }
  }
  if (hasSignal && routeStatuses.some((route) => route.enabled && route.connected && route.freshness_seconds !== null && route.freshness_seconds > 60)) {
    issues.push({
      severity: "warning",
      reason_code: "realtime_signal_stale",
      message: "采集器还在运行，但最近一分钟没有收到新事件。"
    });
  }
  if (routeStatuses.some((route) => route.enabled && route.target_source === "tiktok_browser_observer" && route.signal_only)) {
    issues.push({
      severity: "warning",
      reason_code: "browser_route_signal_only",
      message: "备用线路已收到 TikTok 数据包，但还没有解出礼物或评论；如果送测试礼物后仍为 0，需要检查解码兼容性或浏览器登录状态。"
    });
  }
  if (routeStatuses.some((route) => route.enabled
    && route.target_source === "tiktok_browser_observer"
    && numberOrZero(route.browser_peripheral_frame_count) > 0
    && !routeHasRealtimeData(route))) {
    issues.push({
      severity: "warning",
      reason_code: "browser_route_peripheral_only",
      message: "备用浏览器只看到房间外围请求，还没有进入礼物/评论事件流；这不能作为可用明细，需要继续驻守或处理登录/验证。"
    });
  }
  if (routeStatuses.some((route) => route.enabled && route.cross_route_live_confirmed)) {
    issues.push({
      severity: "warning",
      reason_code: "primary_route_standby_backup_signal_active",
      message: "备用线路已解出可用明细，主线路仍在重试 TikTok 事件流；当前由备用线路顶上，下播后重点对账。"
    });
  }
  if (routeStatuses.some((route) => route.enabled && route.cross_route_signal_seen)) {
    issues.push({
      severity: "warning",
      reason_code: "primary_route_standby_backup_signal_only",
      message: "备用线路只收到 TikTok 原始包，主线路仍在重试；当前还不能认为备用线路已经顶上礼物/评论明细。"
    });
  }
  for (const issue of diagnostics.issues ?? []) {
    if ([
      "raw_backup_manifest_invalid",
      "primary_backup_mismatch",
      "backup_route_no_gifts",
      "backup_route_missing",
      "backup_route_not_independent"
    ].includes(issue.reason_code)) {
      issues.push(issue);
    }
  }
  const uniqueIssues = dedupeRealtimeIssues(issues);
  const hasBlocker = uniqueIssues.some((issue) => issue.severity === "blocker");
  const hasWarning = uniqueIssues.some((issue) => issue.severity === "warning");
  return {
    level: hasBlocker ? "blocker" : hasWarning ? "warning" : "none",
    gap_count: routeGapCount,
    issue_count: uniqueIssues.length,
    issues: uniqueIssues,
    message: hasBlocker
      ? "存在可能影响结算底稿的缺口风险。"
      : hasWarning
        ? "采集可继续，但建议盯住线路状态和下播对账。"
        : "当前没有发现明显缺口风险。"
  };
}

function decideRealtimeState({ plugin, routeStatuses = [], gapRisk = {}, hasSignal = false, dataVisibility = {} } = {}) {
  if (!plugin?.stats?.started_at) {
    return "not_started";
  }
  if (plugin.stats.stopped_at) {
    return "stopped";
  }
  if (hasSignal) {
    return gapRisk.level === "blocker" || gapRisk.level === "warning" ? "degraded" : "collecting";
  }
  if (routeStatuses.some((route) => route.health_level === "blocked")) {
    return "needs_review";
  }
  if (dataVisibility.state === "peripheral_signal_only") {
    return "connecting";
  }
  if (routeStatuses.some((route) => route.state === "standby_waiting_for_live")) {
    return "standby";
  }
  if (routeStatuses.some((route) => ["resolving_room", "connecting", "reconnecting"].includes(route.state))) {
    return "connecting";
  }
  if (routeStatuses.some((route) => route.health_level === "blocked") || (plugin.stats.issue_count ?? 0) > 0) {
    return "needs_review";
  }
  return "connecting";
}

function decideRealtimeLevel({ state, gapRisk = {}, diagnostics = {}, dataVisibility = {} } = {}) {
  if (state === "not_started") {
    return "idle";
  }
  if (state === "stopped") {
    if (gapRisk.level === "blocker" || dataVisibility.level === "blocked" || diagnostics.raw_backup?.ok === false) {
      return "blocked";
    }
    if (gapRisk.level === "warning" || dataVisibility.level === "warning") {
      return "warning";
    }
    return "idle";
  }
  if (state === "needs_review" || gapRisk.level === "blocker") {
    return "blocked";
  }
  if (state === "standby" || state === "connecting" || state === "degraded" || gapRisk.level === "warning") {
    return "warning";
  }
  if (diagnostics.ok === false) {
    return "warning";
  }
  return "good";
}

function createRealtimeNextActions({ state, gapRisk = {}, diagnostics = {}, routeStatuses = [], hasSignal = false, dataVisibility = {} } = {}) {
  const actions = [];
  if (state === "not_started") {
    actions.push("开播前先点开始采集，让插件进入待机驻守。");
  }
  if (state === "standby") {
    actions.push("保持采集器运行，不要退出托盘程序；开播后会自动接入直播事件流。");
  }
  if (state === "connecting") {
    if (dataVisibility.state === "peripheral_signal_only") {
      actions.push("备用浏览器已看到 TikTok 房间外围请求，但还没进入礼物/评论事件流；保持页面打开，检查是否需要登录、验证或切换网络。");
    } else {
      actions.push("如果超过 1 分钟还没有数据，检查主播账号是否填对、网络是否能访问 TikTok。");
    }
  }
  if (gapRisk.level === "blocker") {
    if (routeStatuses.some((route) => isBrowserInteractiveRecoveryUnavailable(route)
      && (isBrowserPageVerificationBlocked(route) || isBrowserLoginRecoveryRequired(route)))) {
      actions.push("当前采集机是云端无界面浏览器，TikTok 登录/验证码不能自动完成；请用远程桌面打开可视浏览器处理，或把采集器装到运营电脑。");
    } else if (routeStatuses.some((route) => isBrowserPageVerificationBlocked(route) || isBrowserLoginRecoveryRequired(route))) {
      actions.push("点击“打开 TikTok 验证页”，在自动打开的浏览器里完成登录/验证；验证通过后采集器会自动恢复，不要关闭这个 TikTok 页面。");
    }
    actions.push("本场结束后导入公会后台或 TikTok 官方导出的正式数据，生成补数报告后再归档。");
  } else if (gapRisk.level === "warning" && hasSignal) {
    actions.push("继续采集，同时关注最近礼物和线路延迟；下播后用后台大数做总账校验。");
  }
  if (dataVisibility.state === "raw_signal_only") {
    actions.push("已经收到 TikTok 原始数据包，但还没解出礼物/评论；系统会继续保存原始包，下播后可回放补解。");
  } else if (dataVisibility.state === "peripheral_signal_only") {
    actions.push("当前只看到房间外围信号，还不能证明礼物/评论已采到；测试时等主播开播后送小礼物，并观察是否进入明细。");
  } else if (dataVisibility.state === "semantic_no_gift") {
    actions.push("已经拿到公屏/互动数据，礼物明细还是 0；测试时先送一个小礼物，确认礼物链路能实时出现。");
  } else if (dataVisibility.state === "no_signal" && !["not_started", "standby"].includes(state)) {
    actions.push("如果直播确实已开始，请保持自动打开的 TikTok 备用页，不要关闭；同时检查浏览器是否需要登录、验证码或网络放行。");
  }
  const primaryConnector = routeStatuses.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME && isTiktokConnectorRoute(route));
  if (primaryConnector && isTiktokConnectorSigningBlocked(primaryConnector)) {
    actions.push(primaryConnector.connector_uses_default_signing === true
      ? "主线路正在被 TikTok 默认公开签名入口限流；生产测试请接入稳定签名通道，同时确认备线已经拿到礼物/评论明细。"
      : "主线路签名通道正在限流或异常；检查签名服务健康、限额和网络出口，当前必须让备线顶上。");
  } else if (primaryConnector && isTiktokConnectorFetchBlocked(primaryConnector)) {
    actions.push("主线路访问 TikTok 事件入口被拦截；检查网络出口/代理质量，优先让本机浏览器备线保持登录和最小化。");
  }
  if (primaryConnector && primaryConnector.connector_request_polling_enabled === false) {
    actions.push("打开主线路请求轮询兜底，降低 WebSocket 建连失败时完全无数据的风险。");
  }
  if (!routeStatuses.some((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME && route.enabled !== false)) {
    actions.push("补一个独立备用线路，避免主线路被 TikTok 限制时完全无数据。");
  }
  const browserBackup = routeStatuses.find((route) => route.target_source === "tiktok_browser_observer");
  if (browserBackup) {
    if (isBrowserPageVerificationBlocked(browserBackup) || isBrowserLoginRecoveryRequired(browserBackup)) {
      if (isBrowserInteractiveRecoveryUnavailable(browserBackup)) {
        actions.push("当前备线运行在云端无界面浏览器，登录/验证码不能靠后台自动完成；请用远程桌面打开可视浏览器处理，或把采集器装到运营电脑。");
      } else {
        actions.push("TikTok 备用页出现验证或超时，采集器看不到公屏和礼物；打开自动弹出的 TikTok 页面，登录/完成验证后保持最小化，不要关闭。");
      }
    } else if (browserBackup.last_issue_code === "tiktok_browser_observer_reconnect_exhausted"
      || (browserBackup.last_issue_code === "tiktok_browser_observer_detached" && !browserBackup.connected)
      || browserBackup.state === "reconnecting"
      || browserBackup.health_level === "blocked") {
      actions.push("备用 TikTok 页面被关闭或连接失败时，系统会自动重开；如果 1 分钟内没有恢复，请打开托盘菜单里的采集页面并检查浏览器登录状态。");
    } else if (browserBackup.browser_detached_count > 0) {
      actions.push("备用 TikTok 页面发生过误关并已恢复；继续采集，下播后重点核对误关时间附近的数据。");
    } else {
      actions.push("系统自动打开的 TikTok 备用直播页可以最小化，不要关闭；关闭会短暂中断备线证据，系统会重开并记录空窗。");
    }
  }
  for (const action of diagnostics.next_actions ?? []) {
    actions.push(action);
  }
  return [...new Set(actions)].slice(0, 6);
}

function createRealtimeDataVisibility({
  giftRows = [],
  rawGiftRows = [],
  p1Facts = [],
  routeStatuses = [],
  diagnostics = {},
  hasSignal = false
} = {}) {
  const rawFrameCount = routeStatuses.reduce((sum, route) => sum + numberOrZero(route.frame_count), 0);
  const rawSignalFrameCount = routeStatuses.reduce((sum, route) => {
    if (route.target_source === "tiktok_browser_observer") {
      return sum + numberOrZero(route.browser_event_transport_frame_count);
    }
    return sum + numberOrZero(route.frame_count);
  }, 0);
  const peripheralFrameCount = routeStatuses.reduce((sum, route) => sum + numberOrZero(route.browser_peripheral_frame_count), 0);
  const rawFrameArchiveCount = Math.max(
    routeStatuses.reduce((sum, route) => sum + numberOrZero(route.raw_frame_archive_count), 0),
    (diagnostics.raw_backup?.routes ?? []).reduce((sum, route) => sum + numberOrZero(route.raw_frame_archive_count), 0)
  );
  const decodedFrameCount = routeStatuses.reduce((sum, route) => sum + numberOrZero(route.decoded_frame_count), 0);
  const routeSemanticEventCount = routeStatuses.reduce((sum, route) => sum + numberOrZero(route.semantic_event_count), 0);
  const giftDetailCount = giftRows.length;
  const rawGiftFactCount = rawGiftRows.length;
  const p1DetailCount = p1Facts.length;
  const semanticEventCount = Math.max(giftDetailCount + p1DetailCount, routeSemanticEventCount);
  const rawBackupOk = diagnostics.raw_backup?.ok !== false;
  const signalOnlyRoutes = routeStatuses
    .filter((route) => route.enabled !== false && route.signal_only)
    .map((route) => route.role);
  const blockedRoute = routeStatuses.find((route) => route.enabled !== false && route.health_level === "blocked");
  const browserLoginSignalRoute = routeStatuses.find((route) => route.enabled !== false
    && numberOrZero(route.browser_not_logged_in_count) > 0
    && !routeHasRealtimeData(route));
  let state = "no_signal";
  let level = "idle";
  let title = "还没有直播信号";
  let message = "系统还没有收到 TikTok 直播数据。开播前这是正常待机状态。";
  if (giftDetailCount > 0) {
    state = "gift_detail_live";
    level = rawBackupOk ? "good" : "warning";
    title = "礼物明细正在写入";
    message = rawBackupOk
      ? "礼物明细、互动数据和本地原始证据都在写入，可以进入现场验收。"
      : "礼物明细已经写入，但本地原始证据有异常，下播后需要优先复核。";
  } else if (semanticEventCount > 0) {
    state = "semantic_no_gift";
    level = "warning";
    title = "已收到互动，暂未收到礼物";
    message = "公屏、进房、点赞或人气已经进入底稿；如果现场有人送礼但这里仍是 0，需要马上关注礼物解码链路。";
  } else if (hasSignal && rawSignalFrameCount > 0) {
    state = "raw_signal_only";
    level = "warning";
    title = "已收到原始数据包，正在解码";
    message = "TikTok 数据包已经到本机，但还没有解出礼物/评论明细；原始包会保存在本地，便于下播后回放补解。";
  } else if (blockedRoute && isBrowserLoginRecoveryRequired(blockedRoute) && peripheralFrameCount > 0) {
    state = "peripheral_signal_only";
    level = "blocked";
    title = "只看到房间外围信号";
    message = isBrowserInteractiveRecoveryUnavailable(blockedRoute)
      ? "备用浏览器运行在云端无界面模式，TikTok 登录或验证码不能自动处理；当前只看到外围请求，不能算采集成功。"
      : "备用浏览器还没有完成 TikTok 登录或验证，只看到房间外围请求；这不能算采集成功，需要打开验证页处理。";
  } else if (blockedRoute) {
    level = "blocked";
    title = "还没有拿到可用直播数据";
    message = isBrowserPageVerificationBlocked(blockedRoute) || isBrowserLoginRecoveryRequired(blockedRoute)
      ? isBrowserInteractiveRecoveryUnavailable(blockedRoute)
        ? "TikTok 备用页在云端无界面浏览器里被登录或验证挡住，后台无法自动点验证码；需要远程桌面可视化处理或迁到运营电脑。"
        : "TikTok 备用页被验证或超时页挡住，当前电脑看不到礼物、公屏和人气；需要先完成浏览器登录或验证。"
      : "采集线路已经进入异常状态，当前不是正常待机；需要检查网络、账号、浏览器验证或采集入口。";
  } else if (peripheralFrameCount > 0) {
    state = "peripheral_signal_only";
    level = "warning";
    title = "只看到房间外围信号";
    message = browserLoginSignalRoute
      ? isBrowserInteractiveRecoveryUnavailable(browserLoginSignalRoute)
        ? "备用浏览器运行在云端无界面模式，TikTok 登录或验证码不能自动处理；当前只看到外围请求，不能算采集成功。"
        : "备用浏览器还没有完成 TikTok 登录或验证，只看到房间外围请求；这不能算采集成功，需要打开验证页处理。"
      : "浏览器已访问 TikTok 直播页并看到外围请求，但还没有进入礼物/评论事件流；这不能算采集成功。";
  } else if (hasSignal) {
    state = "signal_seen";
    level = "warning";
    title = "看到直播信号，等待明细";
    message = "采集线路已经有信号，但面板还没有可展示的礼物或互动明细。";
  }
  return {
    state,
    level,
    title,
    message,
    panel_detail_state: state,
    can_use_for_live_score: giftDetailCount > 0,
    requires_after_live_review: level !== "good",
    raw_signal: {
      present: rawSignalFrameCount > 0,
      frame_count: rawSignalFrameCount,
      archived_frame_count: rawFrameArchiveCount,
      archived_total_frame_count: rawFrameArchiveCount,
      signal_only_routes: signalOnlyRoutes
    },
    peripheral_signal: {
      present: peripheralFrameCount > 0,
      frame_count: peripheralFrameCount,
      browser_not_logged_in_count: routeStatuses.reduce((sum, route) => sum + numberOrZero(route.browser_not_logged_in_count), 0),
      routes: routeStatuses
        .filter((route) => numberOrZero(route.browser_peripheral_frame_count) > 0)
        .map((route) => route.role)
    },
    browser_interactive_recovery_available: !routeStatuses.some((route) => isBrowserInteractiveRecoveryUnavailable(route)),
    decoder: {
      decoded_frame_count: decodedFrameCount,
      semantic_event_count: semanticEventCount,
      needs_decoder_review: state === "raw_signal_only"
    },
    semantic_events: {
      count: semanticEventCount,
      p1_count: p1DetailCount,
      categories: [...new Set(p1Facts.map((fact) => fact.category).filter(Boolean))]
    },
    gift_details: {
      count: giftDetailCount,
      raw_fact_count: rawGiftFactCount,
      has_paid_user: giftRows.some((row) => row.user_id)
    },
    local_ledger: {
      raw_backup_ok: rawBackupOk,
      can_replay_after_live: rawBackupOk && (rawSignalFrameCount > 0 || rawGiftFactCount > 0 || p1DetailCount > 0),
      has_unarchived_raw_signal: rawSignalFrameCount > 0 && rawFrameArchiveCount === 0 && rawGiftFactCount === 0 && p1DetailCount === 0
    }
  };
}

function isOperatorVisibleP1Fact(fact = {}) {
  return Boolean(fact?.category)
    && fact.category !== LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT
    && fact.category !== LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SOURCE_HEALTH;
}

function createRealtimeCapturePipeline({
  plugin,
  routeStatuses = [],
  diagnostics = {},
  replay = {},
  routeComparison = {},
  gapRisk = {},
  hasSignal = false,
  dataVisibility = {}
} = {}) {
  const primary = routeStatuses.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME);
  const backup = routeStatuses.find((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME);
  const stages = [
    createPipelineStage({
      code: "standby",
      label: "开播前驻守",
      level: plugin?.stats?.started_at ? "good" : "idle",
      state: plugin?.stats?.started_at ? "running" : "not_started",
      message: plugin?.stats?.started_at
        ? "采集器已启动，会在开播前持续找直播入口。"
        : "还没有开始驻守。",
      action: plugin?.stats?.started_at ? "保持托盘程序运行。" : "填写主播账号后点击开始驻守。"
    }),
    routePipelineStage("primary_route", "主线路", primary, {
      fallbackActive: backup && routeHasUsableRealtimeFacts(backup) && !routeHasUsableRealtimeFacts(primary ?? {})
    }),
    routePipelineStage("backup_route", "备用线路", backup, {
      isBackup: true
    }),
    createPipelineStage({
      code: "decoder",
      label: "解码成明细",
      level: dataVisibility.state === "gift_detail_live"
        ? "good"
        : ["raw_signal_only", "peripheral_signal_only", "semantic_no_gift", "signal_seen"].includes(dataVisibility.state) ? "warning" : hasSignal ? "warning" : "idle",
      state: dataVisibility.state ?? "no_signal",
      message: dataVisibility.message ?? "等待直播数据。",
      action: routeActionForPipeline({ dataVisibility })
    }),
    createPipelineStage({
      code: "local_ledger",
      label: "本地底稿",
      level: diagnostics.raw_backup?.ok === false
        ? "blocked"
        : dataVisibility.local_ledger?.can_replay_after_live ? "good" : hasSignal ? "warning" : "idle",
      state: diagnostics.raw_backup?.ok === false ? "raw_backup_error" : dataVisibility.local_ledger?.can_replay_after_live ? "replayable" : "waiting",
      message: diagnostics.raw_backup?.ok === false
        ? "本地原始证据保存异常，需要处理后再正式验收。"
        : dataVisibility.local_ledger?.can_replay_after_live
          ? "原始包/明细已落盘，下播后可以回放、对账、补数。"
          : dataVisibility.local_ledger?.has_unarchived_raw_signal
            ? "已看到直播信号，但本地原始帧落盘还未确认，继续观察并保留日志。"
            : "等待第一批可保存的数据。",
      action: diagnostics.raw_backup?.ok === false
        ? "打开本地目录检查磁盘权限和日志。"
        : dataVisibility.local_ledger?.has_unarchived_raw_signal
          ? "不要退出程序；如果持续出现，重启采集器并保留日志。"
          : "保持程序运行。"
    }),
    createPipelineStage({
      code: "reconciliation",
      label: "主备核对",
      level: routeComparison.checked === true
        ? routeComparison.ok === true ? "good" : "blocked"
        : gapRisk.level === "blocker" ? "blocked" : gapRisk.level === "warning" ? "warning" : dataVisibility.gift_details?.count > 0 ? "warning" : "idle",
      state: routeComparison.checked === true ? routeComparison.ok === true ? "matched" : "mismatch" : "pending",
      message: routeComparison.checked === true
        ? routeComparison.ok === true
          ? "主备逐笔礼物已对齐。"
          : "主备礼物底稿不一致，需要下播后导入正式后台数据补数。"
        : "礼物出现后会持续做主备核对；下播后用后台大数验收。",
      action: routeComparison.checked === true && routeComparison.ok !== true
        ? "不要直接结算，导入公会后台/TikTok 官方导出做补数。"
        : "测试期可用 MCA 陪跑校准，正式使用不依赖 MCA。"
    })
  ];
  return {
    level: aggregatePipelineLevel(stages),
    summary: pipelineSummary(stages),
    stages,
    replay: {
      route_comparison_checked: routeComparison.checked === true,
      raw_replay_ready: dataVisibility.local_ledger?.can_replay_after_live === true,
      gift_raw_count: numberOrZero(replay.gift_raw_count),
      p1_raw_count: numberOrZero(replay.p1_replay?.raw_count),
      raw_frame_archive_count: numberOrZero(dataVisibility.raw_signal?.archived_frame_count)
    }
  };
}

function routePipelineStage(code, label, route, { fallbackActive = false, isBackup = false } = {}) {
  if (!route) {
    return createPipelineStage({
      code,
      label,
      level: isBackup ? "blocked" : "idle",
      state: "missing",
      message: isBackup ? "没有可用备用线路，主线异常时会缺少独立兜底。" : "线路未配置。",
      action: isBackup ? "启用本机浏览器备用线路。" : "填写主播账号或技术通道。"
    });
  }
  let level = route.health_level === "healthy" ? "good" : route.health_level;
  if (route.health_level === "standby") {
    level = "warning";
  }
  if (fallbackActive && route.health_level !== "healthy") {
    level = "warning";
  }
  return createPipelineStage({
    code,
    label,
    level,
    state: route.state,
    message: fallbackActive
      ? "备线已解出可用明细，主线仍在重试 TikTok 事件流。"
      : route.message,
    action: routeActionForPipeline({ route, isBackup, fallbackActive }),
    role: route.role,
    target_source: route.target_source,
    counts: {
      frames: numberOrZero(route.frame_count),
      decoded_frames: numberOrZero(route.decoded_frame_count),
      gifts: numberOrZero(route.gift_count),
      p1: numberOrZero(route.p1_event_count),
      reconnects: numberOrZero(route.reconnect_count)
    }
  });
}

function routeActionForPipeline({ route = null, isBackup = false, fallbackActive = false, dataVisibility = null } = {}) {
  if (dataVisibility) {
    if (dataVisibility.state === "gift_detail_live") {
      return "继续采集，下播后导入后台大数验收。";
    }
    if (dataVisibility.state === "semantic_no_gift") {
      return "现场送一个小礼物做验收；如果仍没有礼物明细，转技术复核礼物解码。";
    }
    if (dataVisibility.state === "raw_signal_only") {
      return "不要退出程序，保留原始包；技术同学下播后可回放补解。";
    }
    if (dataVisibility.state === "peripheral_signal_only") {
      return dataVisibility.browser_interactive_recovery_available === false
        ? "云端无界面浏览器不能自动完成登录/验证码；需要远程桌面可视化处理或迁到运营电脑。"
        : "保持 TikTok 备用页打开，先确认是否登录/验证通过；外围请求不能当作礼物底稿。";
    }
    return "等待直播信号或检查浏览器登录状态。";
  }
  if (!route) {
    return isBackup ? "启用本机浏览器备用线路。" : "检查主播账号或采集通道。";
  }
  if (fallbackActive) {
    return "备线先顶上，主线自动重试；下播后重点核对开播前几分钟。";
  }
  if (isTiktokConnectorSigningBlocked(route)) {
    return route.connector_uses_default_signing === true
      ? "接入稳定签名通道；在主线恢复前，确认备线已登录并能拿到礼物/评论明细。"
      : "检查签名服务健康、限额和网络出口；主线恢复前由备线顶上。";
  }
  if (isTiktokConnectorFetchBlocked(route)) {
    return "检查 TikTok 网络出口/代理质量；优先让本机浏览器备线保持可用。";
  }
  if (route.health_level === "blocked") {
    if (isBrowserPageVerificationBlocked(route) || isBrowserLoginRecoveryRequired(route)) {
      return isBrowserInteractiveRecoveryUnavailable(route)
        ? "用远程桌面打开可视浏览器完成 TikTok 登录/验证，或把采集器装到运营电脑。"
        : "打开自动弹出的 TikTok 备用页，完成登录/验证后保持最小化；不要关闭浏览器。";
    }
    return "检查网络、浏览器登录、验证码或 TikTok 限制。";
  }
  if (route.target_source === "tiktok_browser_observer" && route.signal_only) {
    return "保持 TikTok 备用页打开，等待解码；送礼仍无明细时转技术复核。";
  }
  if (route.state === "standby_waiting_for_live") {
    return "保持驻守，不要退出托盘程序。";
  }
  if (route.health_level === "healthy") {
    return "继续运行。";
  }
  return "继续观察，超过 1 分钟无数据再处理。";
}

function createPipelineStage({
  code,
  label,
  level = "idle",
  state = null,
  message = "",
  action = "",
  role = null,
  target_source = null,
  counts = null
} = {}) {
  return {
    code,
    label,
    level,
    state,
    message,
    action,
    ...(role ? { role } : {}),
    ...(target_source ? { target_source } : {}),
    ...(counts ? { counts } : {})
  };
}

function aggregatePipelineLevel(stages = []) {
  if (stages.some((stage) => stage.level === "blocked")) {
    return "blocked";
  }
  if (stages.some((stage) => stage.level === "warning")) {
    return "warning";
  }
  if (stages.some((stage) => stage.level === "good")) {
    return "good";
  }
  return "idle";
}

function pipelineSummary(stages = []) {
  const blocked = stages.filter((stage) => stage.level === "blocked");
  if (blocked.length > 0) {
    return `${blocked[0].label}需要处理：${blocked[0].message}`;
  }
  const warning = stages.filter((stage) => stage.level === "warning");
  if (warning.length > 0) {
    return `${warning[0].label}需要关注：${warning[0].message}`;
  }
  if (stages.some((stage) => stage.level === "good")) {
    return "采集链路正在运行。";
  }
  return "等待开始采集。";
}

function createRealtimeGuardrails({ state, routeStatuses = [], gapRisk = {}, dataVisibility = {}, diagnostics = {} } = {}) {
  const browserRoute = routeStatuses.find((route) => route.target_source === "tiktok_browser_observer");
  const primaryConnectorRoute = routeStatuses.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME && isTiktokConnectorRoute(route));
  const browserActive = Boolean(browserRoute);
  const browserInteractiveUnavailable = isBrowserInteractiveRecoveryUnavailable(browserRoute);
  const browserClosed = browserRoute?.last_issue_code === "tiktok_browser_observer_detached"
    || browserRoute?.last_issue_code === "tiktok_browser_observer_reconnect_exhausted";
  const canCloseControlPanel = true;
  const canCloseTiktokPage = !browserActive;
  const autoRecoveryActive = browserActive && browserRoute?.last_issue_code !== "tiktok_browser_observer_reconnect_exhausted";
  return {
    level: browserClosed || gapRisk.level === "blocker" || diagnostics.raw_backup?.ok === false
      ? "blocked"
      : gapRisk.level === "warning" || dataVisibility.level === "warning" ? "warning" : state === "not_started" ? "idle" : "good",
    control_panel: {
      can_close: canCloseControlPanel,
      message: "这个控制面板可以关闭，采集服务会继续在托盘运行。"
    },
    tiktok_browser_page: {
      present: browserActive,
      can_minimize: browserActive,
      can_close: canCloseTiktokPage,
      message: browserActive
        ? browserInteractiveUnavailable
          ? "当前备线是云端无界面浏览器；遇到 TikTok 登录/验证码时，需要远程桌面可视化处理，后台无法自动点击。"
          : "系统自动打开的 TikTok 备用直播页可以最小化，但不要关闭；关闭会造成备线短暂空窗，系统会自动重开并记录缺口。"
        : "当前没有启用本机浏览器备线。"
    },
    primary_connector: {
      present: Boolean(primaryConnectorRoute),
      signing_configured: primaryConnectorRoute?.connector_signing_configured ?? null,
      uses_default_signing: primaryConnectorRoute?.connector_uses_default_signing ?? null,
      request_polling_enabled: primaryConnectorRoute?.connector_request_polling_enabled ?? null,
      message: primaryConnectorRoute
        ? isTiktokConnectorSigningBlocked(primaryConnectorRoute)
          ? primaryConnectorRoute.connector_uses_default_signing === true
            ? "主线路正在使用默认公开签名入口，遇到 TikTok 限流会自动退避；正式压测需要稳定签名通道和独立备线同时可用。"
            : "主线路签名通道正在限流或异常；系统会重试，但现场必须依靠备线兜底。"
          : isTiktokConnectorFetchBlocked(primaryConnectorRoute)
            ? "主线路访问 TikTok 事件入口被拦截；需要检查网络出口，同时保持备线可用。"
            : "主线路会自动连接 TikTok 事件流；如果入口异常，会退避重试并由备线兜底。"
        : "当前没有启用 TikTok 主线路事件流。"
    },
    tray_app: {
      can_exit_during_live: false,
      message: "直播期间不要退出托盘采集器；退出会停止主线和备线。"
    },
    auto_recovery: {
      browser_relaunch: browserActive,
      reconnect_enabled: routeStatuses.some((route) => route.reconnect_count >= 0),
      raw_replay_enabled: dataVisibility.local_ledger?.can_replay_after_live === true,
      message: autoRecoveryActive
        ? browserInteractiveUnavailable
          ? "线路异常会自动重连，原始包会落盘；但云端无界面浏览器无法自动完成 TikTok 登录/验证码。"
          : "线路异常会自动重连；浏览器备线误关会自动重开；原始包会保存在本地用于回放补数。"
        : "自动恢复能力受限，请检查浏览器备线或本地落盘。"
    }
  };
}

function createRealtimeRecentGiftRows(giftRows = []) {
  return giftRows
    .slice(-20)
    .reverse()
    .map((row) => ({
      event_time: row.event_time ?? row.received_at ?? null,
      received_at: row.received_at ?? null,
      route_role: row.route_role ?? null,
      source_event_id: row.source_event_id ?? null,
      user_id: row.user_id ?? null,
      user_name: row.user_name ?? null,
      gift_id: row.gift_id ?? null,
      gift_name: row.gift_name ?? null,
      gift_count: row.gift_count ?? 0,
      gift_total_value: row.gift_total_value ?? 0
    }));
}

function createRealtimeRecentActivityRows({ giftRows = [], p1Facts = [] } = {}) {
  const giftActivities = giftRows.map((row) => ({
    category: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT,
    event_time: row.event_time ?? row.received_at ?? null,
    received_at: row.received_at ?? null,
    route_role: row.route_role ?? null,
    title: row.gift_name ?? "未知礼物",
    actor: row.user_name ?? row.user_id ?? "未知用户",
    value: `${numberOrZero(row.gift_count)} 个 / ${numberOrZero(row.gift_total_value)} 钻`,
    source_event_id: row.source_event_id ?? null,
    gift_id: row.gift_id ?? null,
    gift_name: row.gift_name ?? null,
    gift_count: row.gift_count ?? 0,
    gift_total_value: row.gift_total_value ?? 0
  }));
  const p1Activities = p1Facts.map((fact) => ({
    category: fact.category ?? LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.OTHER_P1,
    event_time: fact.event_time ?? fact.received_at ?? null,
    received_at: fact.received_at ?? null,
    route_role: fact.route_role ?? null,
    title: labelForP1Activity(fact.category, fact),
    actor: fact.normalized?.actor_display_name
      ?? fact.normalized?.actor_user_id
      ?? fact.actor_user_id
      ?? "直播间",
    value: p1ActivityValue(fact),
    source_event_id: fact.source_event_id ?? fact.normalized?.payload?.source_event_id ?? null
  }));
  return [...giftActivities, ...p1Activities]
    .sort((a, b) => toTimestampOrZero(b.event_time ?? b.received_at) - toTimestampOrZero(a.event_time ?? a.received_at))
    .slice(0, 30);
}

function labelForP1Activity(category, fact = {}) {
  const map = {
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.PUBLIC_CHAT]: "公屏评论",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.MEMBER_JOIN]: "进房",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.FOLLOW]: "关注",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SHARE]: "分享",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.LIKE]: "点赞",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.ROOM_METRIC]: "人气",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.USER_LABELS]: "用户标签",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.LIVE_STATE]: "直播状态",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SYSTEM]: "系统消息",
    [LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SOURCE_HEALTH]: "采集健康"
  };
  if (category === LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.PUBLIC_CHAT) {
    return fact.normalized?.text ?? fact.normalized?.comment ?? map[category] ?? "互动";
  }
  return map[category] ?? "互动";
}

function p1ActivityValue(fact = {}) {
  const payload = fact.normalized?.payload ?? fact.normalized ?? {};
  if (fact.category === LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.ROOM_METRIC) {
    return `在线 ${payload.viewer_count ?? payload.viewerCount ?? "-"} / 点赞 ${payload.like_count ?? payload.likeCount ?? "-"}`;
  }
  if (fact.category === LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.LIKE) {
    return `${payload.like_count ?? payload.likeCount ?? payload.count ?? 1} 次`;
  }
  return fact.source_event_id ?? "";
}

function realtimeHeadlineForState(state, level) {
  const map = {
    not_started: "还没开始采集",
    standby: "开播前驻守中",
    connecting: "正在连接 TikTok",
    collecting: "正在实时采集",
    degraded: "采集中，有缺口风险",
    needs_review: "采集异常，需要处理",
    stopped: "本场已结束"
  };
  if (level === "blocked" && state !== "stopped") {
    return "需要马上复核";
  }
  return map[state] ?? "正在采集";
}

function realtimeOperatorMessage({ state, gapRisk = {}, totals = {}, routeStatuses = [], dataVisibility = {} } = {}) {
  if (state === "standby") {
    const standbyRoute = routeStatuses.find((route) => route.state === "standby_waiting_for_live");
    const reason = standbyRoute?.standby_last_reason_code;
    if (reason === "tiktok_live_room_fetch_blocked") {
      return "TikTok 暂时没有把直播入口返回给当前电脑，插件会继续轮询，并优先依靠本机浏览器驻守备线。";
    }
    if (reason === "tiktok_live_connector_signer_rate_limited") {
      return standbyRoute?.connector_uses_default_signing === true
        ? "主线路正在用默认公开签名入口重试，当前被 TikTok 限流；如果直播已经开始，请优先确认本机浏览器备线已登录、未关闭，并能看到礼物/公屏。"
        : "TikTok 事件流签名通道暂时限流，插件会自动退避重试；如果直播已经开始，请优先检查本机浏览器备线是否登录并保持最小化。";
    }
    return "插件已经在开播前待命，只要主播开始直播并能拿到入口，就会自动采集礼物、公屏、人气和互动。";
  }
  if (state === "connecting") {
    if (dataVisibility.state === "peripheral_signal_only") {
      return "TikTok 备用页已经打开并看到房间外围信号，但还没有进入礼物/评论事件流；这时不能算采集成功，系统会继续重试。";
    }
    return "正在寻找直播入口或建立事件流连接，通常需要几十秒；超过 1 分钟请检查账号、网络和通道配置。";
  }
  if (state === "collecting") {
    if (dataVisibility.state === "raw_signal_only") {
      return "已接入 TikTok 原始数据包，但暂时还没解出礼物/评论明细；系统会继续保存原始包，便于下播后回放补解。";
    }
    if (dataVisibility.state === "peripheral_signal_only") {
      return "浏览器只看到 TikTok 房间外围请求，还没有拿到礼物/评论事件流；这不能作为正式底稿。";
    }
    if (dataVisibility.state === "semantic_no_gift") {
      return "已接入直播互动数据，暂未收到礼物明细。测试时先送一个小礼物，确认礼物链路能实时出现。";
    }
    return `采集正常，已记录 ${totals.gift_event_count ?? 0} 笔礼物、${totals.paid_user_count ?? 0} 个付费用户。`;
  }
  if (state === "degraded") {
    if (dataVisibility.state === "raw_signal_only") {
      return "已收到 TikTok 原始数据包，但暂时还没解出礼物/评论明细；系统会继续保存原始包，便于下播后回放补解。";
    }
    if (dataVisibility.state === "peripheral_signal_only") {
      return "当前只看到 TikTok 房间外围请求，还没进入礼物/评论事件流；不要把这场数据用于正式结算。";
    }
    if (dataVisibility.state === "semantic_no_gift") {
      return "互动数据已经写入，礼物明细仍为 0；如果现场已送礼，需要马上关注礼物解码链路。";
    }
    return gapRisk.message ?? "采集还在继续，但存在需要下播对账确认的风险。";
  }
  if (state === "needs_review") {
    if (routeStatuses.some((route) => isBrowserPageVerificationBlocked(route) || isBrowserLoginRecoveryRequired(route))) {
      return "TikTok 备用页被验证/超时页挡住，当前电脑看不到真实礼物和公屏；先完成浏览器登录或验证，再继续验收这场数据。";
    }
    return "采集入口或本地落盘出现异常，先不要用这场数据做正式结算。";
  }
  if (state === "stopped") {
    return "本场已结束，可以导出底稿、诊断、对账和补数报告。";
  }
  return "填写 TikTok 主播账号或直播间链接后，开播前即可开始驻守。";
}

function liveStartMessage({ state, standbyStartedAt, liveDetectedAt, attachDelaySeconds } = {}) {
  if (state === "standby") {
    return standbyStartedAt ? "已提前驻守，等待 TikTok 返回直播入口。" : "正在准备进入待机驻守。";
  }
  if (liveDetectedAt && attachDelaySeconds !== null) {
    return `检测到开播后 ${attachDelaySeconds} 秒拿到第一条数据。`;
  }
  if (liveDetectedAt) {
    return "已检测到开播，正在等待第一条可落盘数据。";
  }
  if (state === "collecting" || state === "degraded") {
    return "已接入直播数据流。";
  }
  return "尚未确认直播开始。";
}

function freshnessLevel(ageSeconds, hasSignal) {
  if (!hasSignal) {
    return "idle";
  }
  if (ageSeconds === null) {
    return "unknown";
  }
  if (ageSeconds <= 15) {
    return "fresh";
  }
  if (ageSeconds <= 60) {
    return "late";
  }
  return "stale";
}

function freshnessMessage(ageSeconds, hasSignal) {
  if (!hasSignal) {
    return "还没有收到直播数据。";
  }
  if (ageSeconds === null) {
    return "已收到数据，但暂时无法计算延迟。";
  }
  if (ageSeconds <= 15) {
    return "实时数据很新。";
  }
  if (ageSeconds <= 60) {
    return "最近数据有一点延迟，请继续观察。";
  }
  return "超过一分钟没有新数据，可能断流、下播或被限制。";
}

function routeHealthLevel(route = {}) {
  if (!route.enabled) {
    return "idle";
  }
  if (route.state === "needs_review") {
    return "blocked";
  }
  if (route.cross_route_live_confirmed) {
    return "warning";
  }
  if (route.cross_route_signal_seen) {
    return "warning";
  }
  if (route.target_source === "tiktok_browser_observer"
    && (isBrowserPageVerificationBlocked(route) || isBrowserLoginRecoveryRequired(route))) {
    return "blocked";
  }
  if (route.target_source === "tiktok_browser_observer"
    && numberOrZero(route.browser_peripheral_frame_count) > 0
    && !routeHasRealtimeData(route)) {
    return "warning";
  }
  if (isTiktokConnectorSigningBlocked(route) || isTiktokConnectorFetchBlocked(route)) {
    return "warning";
  }
  if (route.state === "standby_waiting_for_live") {
    return "standby";
  }
  if (route.target_source === "tiktok_browser_observer" && route.browser_attached && !routeHasRealtimeData(route)) {
    return "standby";
  }
  if (route.state === "reconnecting") {
    return "warning";
  }
  if (route.target_source === "tiktok_browser_observer" && route.signal_only) {
    return "warning";
  }
  if (route.connected && route.freshness_seconds !== null && route.freshness_seconds > 60) {
    return "warning";
  }
  if (route.connected || route.gift_count > 0 || route.p1_event_count > 0) {
    return "healthy";
  }
  if (route.issue_count > 0) {
    return "warning";
  }
  if (["resolving_room", "connecting"].includes(route.state)) {
    return "warning";
  }
  return "idle";
}

function routeHealthMessage(route = {}) {
  if (!route.enabled) {
    return `${route.label}未启用。`;
  }
  if (route.cross_route_live_confirmed) {
    return `${route.label}仍在重试 TikTok 事件流；备线已解出可用明细，当前由备线顶上。`;
  }
  if (route.cross_route_signal_seen) {
    return `${route.label}仍在重试 TikTok 事件流；备线只看到原始包，还不能顶上礼物/评论明细。`;
  }
  if (route.target_source === "tiktok_browser_observer") {
    if (isBrowserPageVerificationBlocked(route)) {
      if (isBrowserInteractiveRecoveryUnavailable(route)) {
        return `${route.label}运行在云端无界面浏览器，TikTok 登录/验证码不能自动完成；需要远程桌面打开可视浏览器，或把采集器装到运营电脑。`;
      }
      return `${route.label}被 TikTok 验证/超时页挡住，当前电脑看不到礼物和公屏；点击“打开 TikTok 验证页”完成登录或验证。`;
    }
    if (isBrowserLoginRecoveryRequired(route)) {
      if (isBrowserInteractiveRecoveryUnavailable(route)) {
        return `${route.label}运行在云端无界面浏览器，TikTok 登录/验证码不能自动完成；需要远程桌面打开可视浏览器，或把采集器装到运营电脑。`;
      }
      return `${route.label}浏览器还没有完成 TikTok 登录或验证，只能看到外围请求；打开验证页处理后继续驻守。`;
    }
    if (route.last_issue_code === "tiktok_browser_observer_browser_not_found") {
      return `${route.label}找不到 Chrome 或 Edge，浏览器备线无法启动。`;
    }
    if (route.state === "reconnecting" || (route.last_issue_code === "tiktok_browser_observer_detached" && !route.connected)) {
      return `${route.label}的 TikTok 备用页面被关闭，正在自动重开。`;
    }
    if (route.last_issue_code === "tiktok_browser_observer_reconnect_exhausted") {
      return `${route.label}多次重开失败，需要检查浏览器、登录状态或网络。`;
    }
    if (route.last_issue_code === "tiktok_browser_observer_cdp_target_not_found") {
      return `${route.label}没有找到 TikTok 监控页；127.0.0.1 是本机控制台，不是采集页。`;
    }
    if (route.last_issue_code === "tiktok_browser_observer_cdp_attach_failed") {
      return `${route.label}浏览器监听没有接上，需要检查本机浏览器是否正常启动。`;
    }
    if (route.browser_detached_count > 0 && route.health_level === "warning") {
      return `${route.label}发生过误关或重连，当前继续观察。`;
    }
    if (numberOrZero(route.browser_not_logged_in_count) > 0 && !routeHasRealtimeData(route)) {
      if (isBrowserInteractiveRecoveryUnavailable(route)) {
        return `${route.label}运行在云端无界面浏览器，TikTok 登录/验证码不能自动完成；需要远程桌面打开可视浏览器，或把采集器装到运营电脑。`;
      }
      return `${route.label}浏览器还没有完成 TikTok 登录或验证，只能看到外围请求；打开验证页处理后继续驻守。`;
    }
    if (numberOrZero(route.browser_peripheral_frame_count) > 0 && !routeHasRealtimeData(route)) {
      return `${route.label}只看到 TikTok 房间外围请求，还没有进入礼物/评论事件流；保持页面打开并检查登录、验证或网络。`;
    }
    if (route.signal_only) {
      return `${route.label}已收到 TikTok 数据包，正在等待礼物/评论；送小礼物后仍为 0 时，需要检查解码兼容性或浏览器登录状态。`;
    }
    if (route.state === "standby_waiting_for_live" || (route.browser_attached && !routeHasRealtimeData(route))) {
      if (isBrowserInteractiveRecoveryUnavailable(route)) {
        return `${route.label}已在云端无界面浏览器后台驻守；能自动发现公开页面信号，但遇到 TikTok 登录/验证码时需要远程桌面处理，或把采集器装到运营电脑。`;
      }
      return `${route.label}浏览器已驻守。可以最小化，不要关闭自动打开的 TikTok 页面。`;
    }
  }
  if (route.last_issue_code === "tiktok_live_connector_signer_rate_limited"
    || route.standby_last_reason_code === "tiktok_live_connector_signer_rate_limited") {
    return route.connector_uses_default_signing === true
      ? `${route.label}正在使用默认公开签名入口，被限流时会自动退避重试；生产测试需要稳定签名通道，不能只依赖这一路。`
      : `${route.label}的 TikTok 事件流签名通道被限流，正在自动退避重试；备线必须保持可用来顶上。`;
  }
  if (route.last_issue_code === "tiktok_live_connector_signer_failed"
    || route.standby_last_reason_code === "tiktok_live_connector_signer_failed") {
    return route.connector_signing_configured === true
      ? `${route.label}的签名通道返回异常，正在自动重试；需要检查签名服务健康和限额。`
      : `${route.label}缺少稳定签名通道，默认入口返回异常；需要让备线先顶上并检查主线通道配置。`;
  }
  if (isTiktokConnectorFetchBlocked(route)) {
    return `${route.label}访问 TikTok 事件入口被拦截或限流，正在自动退避重试；需要检查网络出口、代理质量或切换到可视浏览器备线。`;
  }
  if (route.state === "standby_waiting_for_live") {
    if (route.standby_last_reason_code === "tiktok_live_room_fetch_blocked") {
      return `${route.label}待机中，TikTok 暂时没有返回直播入口。`;
    }
    if (route.standby_last_reason_code === "tiktok_live_connector_signer_rate_limited") {
      return route.connector_uses_default_signing === true
        ? `${route.label}正在用默认公开签名入口重试，当前被 TikTok 限流。`
        : `${route.label}的签名通道被 TikTok 限流，正在自动退避重试。`;
    }
    return `${route.label}待机中，等待主播开播。`;
  }
  if (route.health_level === "blocked") {
    return `${route.label}需要处理：${route.last_issue_code ?? "采集异常"}。`;
  }
  if (route.health_level === "warning") {
    if (route.freshness_seconds !== null && route.freshness_seconds > 60) {
      return `${route.label}超过一分钟没有新数据。`;
    }
    if (route.reconnect_count > 0) {
      return `${route.label}发生过重连，当前继续观察。`;
    }
    return `${route.label}正在连接或需要关注。`;
  }
  if (route.health_level === "healthy") {
    return `${route.label}已收到数据。`;
  }
  return `${route.label}还没有开始写入数据。`;
}

function isBrowserPageVerificationBlocked(route = {}) {
  return route.target_source === "tiktok_browser_observer"
    && (route.last_issue_code === "tiktok_browser_observer_page_verification_blocked"
      || numberOrZero(route.browser_page_blocked_count) > 0);
}

function isBrowserLoginRecoveryRequired(route = {}) {
  return route.target_source === "tiktok_browser_observer"
    && (route.browser_login_required === true
      || (numberOrZero(route.browser_not_logged_in_count) > 0 && !routeHasRealtimeData(route)));
}

function isBrowserInteractiveRecoveryUnavailable(route = {}) {
  return route.target_source === "tiktok_browser_observer"
    && (route.browser_headless === true || route.browser_interactive_recovery_available === false);
}

function createTiktokConnectorRuntimeStatus({ collector = null, webcast = {} } = {}) {
  const signApiKeyConfigured = collector?.connector_sign_api_key_configured
    ?? Boolean(stringOrNull(webcast.connector_sign_api_key ?? webcast.sign_api_key));
  const signApiUrlConfigured = collector?.connector_sign_api_url_configured
    ?? Boolean(stringOrNull(webcast.connector_sign_api_url ?? webcast.sign_api_url));
  const signedWebsocketProviderConfigured = collector?.connector_signed_websocket_provider_configured
    ?? typeof webcast.connector_signed_websocket_provider === "function";
  const signingConfigured = collector?.connector_signing_configured
    ?? (signApiKeyConfigured || signApiUrlConfigured || signedWebsocketProviderConfigured);
  return {
    connector_signing_configured: signingConfigured,
    connector_uses_default_signing: collector?.connector_uses_default_signing ?? !signingConfigured,
    connector_sign_api_key_configured: signApiKeyConfigured,
    connector_sign_api_url_configured: signApiUrlConfigured,
    connector_signed_websocket_provider_configured: signedWebsocketProviderConfigured,
    connector_request_polling_enabled: collector?.connector_request_polling_enabled
      ?? webcast.connector_enable_request_polling !== false,
    connector_connect_with_unique_id: collector?.connector_connect_with_unique_id
      ?? webcast.connector_connect_with_unique_id === true,
    connector_fetch_room_info_on_connect: collector?.connector_fetch_room_info_on_connect
      ?? webcast.connector_fetch_room_info_on_connect === true,
    connector_authenticate_ws: collector?.connector_authenticate_ws
      ?? Boolean(webcast.authenticate_ws && webcast.session_cookie),
    connector_session_cookie_configured: collector?.connector_session_cookie_configured
      ?? Boolean(stringOrNull(webcast.session_cookie))
  };
}

function isTiktokConnectorRoute(route = {}) {
  return route.target_source === "tiktok_live_connector";
}

function isTiktokConnectorSigningBlocked(route = {}) {
  if (!isTiktokConnectorRoute(route)) {
    return false;
  }
  return [
    route.last_issue_code,
    route.standby_last_reason_code
  ].includes("tiktok_live_connector_signer_rate_limited")
    || [
      route.last_issue_code,
      route.standby_last_reason_code
    ].includes("tiktok_live_connector_signer_failed");
}

function isTiktokConnectorFetchBlocked(route = {}) {
  if (!isTiktokConnectorRoute(route)) {
    return false;
  }
  return [
    route.last_issue_code,
    route.standby_last_reason_code
  ].includes("tiktok_live_connector_fetch_blocked");
}

function isHeadlessBrowserConfig(webcast = {}) {
  if (webcast?.browser_headless === true || webcast?.browserHeadless === true) {
    return true;
  }
  return hasHeadlessBrowserLaunchArg(webcast?.browser_launch_args ?? webcast?.browserLaunchArgs ?? []);
}

function hasHeadlessBrowserLaunchArg(args = []) {
  const values = Array.isArray(args)
    ? args
    : String(args ?? "").split(/\s+/u);
  return values.some((value) => /^--headless(?:=|$)/u.test(String(value ?? "").trim()));
}

function routeHasRealtimeData(route = {}) {
  if (route.target_source === "tiktok_browser_observer") {
    return Boolean(route.first_event_at || route.last_event_at || route.last_signal_at)
      || numberOrZero(route.browser_event_transport_frame_count) > 0
      || numberOrZero(route.gift_count) > 0
      || numberOrZero(route.p1_event_count) > 0;
  }
  return Boolean(route.first_frame_at || route.last_frame_at || route.first_event_at || route.last_event_at)
    || numberOrZero(route.frame_count) > 0
    || numberOrZero(route.gift_count) > 0
    || numberOrZero(route.p1_event_count) > 0;
}

function routeHasUsableRealtimeFacts(route = {}) {
  return Boolean(route.first_event_at || route.last_event_at)
    || numberOrZero(route.gift_count) > 0
    || numberOrZero(route.p1_event_count) > 0
    || numberOrZero(route.semantic_event_count) > 0;
}

function routeTargetLabel(webcast = {}) {
  if (isTiktokBrowserObserverRoute(webcast)) {
    return "tiktok_browser_observer";
  }
  if (webcast?.websocket_url) {
    return "websocket_url";
  }
  if (webcast?.signer_url) {
    return "managed_channel";
  }
  if (webcast?.creator_handle || webcast?.live_room_url) {
    return "tiktok_live_connector";
  }
  if (webcast?.live_room_id) {
    return "room_id_only";
  }
  return null;
}

function routeTargetUrl(webcast = {}) {
  const liveRoomUrl = stringOrNull(webcast?.live_room_url ?? webcast?.liveRoomUrl);
  if (liveRoomUrl && isTiktokLiveRoomUrl(liveRoomUrl)) {
    return liveRoomUrl;
  }
  const creatorHandle = stringOrNull(webcast?.creator_handle ?? webcast?.creatorHandle);
  if (creatorHandle) {
    return `https://www.tiktok.com/@${creatorHandle.replace(/^@/u, "")}/live`;
  }
  return null;
}

function isTiktokLiveRoomUrl(value) {
  try {
    const hostname = new URL(String(value ?? "")).hostname.toLowerCase();
    return hostname === "tiktok.com" || hostname.endsWith(".tiktok.com");
  } catch {
    return false;
  }
}

function dedupeRealtimeIssues(issues = []) {
  const seen = new Set();
  const output = [];
  for (const issue of issues) {
    const key = `${issue.severity ?? ""}:${issue.reason_code ?? ""}:${issue.message ?? ""}`;
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    output.push(issue);
  }
  return output;
}

function earliestTime(values = []) {
  const timestamps = validTimestamps(values);
  if (timestamps.length === 0) {
    return null;
  }
  return new Date(Math.min(...timestamps)).toISOString();
}

function latestTime(values = []) {
  const timestamps = validTimestamps(values);
  if (timestamps.length === 0) {
    return null;
  }
  return new Date(Math.max(...timestamps)).toISOString();
}

function validTimestamps(values = []) {
  return values
    .map((value) => normalizeTime(value))
    .map((value) => value ? new Date(value).getTime() : null)
    .filter((value) => Number.isFinite(value));
}

function secondsSince(timestamp, generatedAt = nowIso()) {
  if (!timestamp) {
    return null;
  }
  const seconds = secondsBetween(timestamp, generatedAt);
  return seconds === null ? null : Math.max(0, seconds);
}

function secondsBetween(start, end) {
  if (!start || !end) {
    return null;
  }
  const startTime = new Date(start).getTime();
  const endTime = new Date(end).getTime();
  if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) {
    return null;
  }
  return Math.round((endTime - startTime) / 1000);
}

function toTimestampOrZero(value) {
  const timestamp = new Date(value).getTime();
  return Number.isFinite(timestamp) ? timestamp : 0;
}

const AGGREGATE_VALUE_ALIASES = [
  "total_gift_value",
  "totalGiftValue",
  "gift_total_value",
  "giftTotalValue",
  "total_value",
  "totalValue",
  "total_revenue",
  "totalRevenue",
  "gross_revenue",
  "grossRevenue",
  "revenue",
  "total_diamonds",
  "totalDiamonds",
  "diamond_count",
  "diamondCount",
  "diamonds",
  "amount",
  "value",
  "总流水",
  "总礼物流水",
  "礼物流水",
  "后台总流水",
  "本场流水",
  "总钻石",
  "钻石",
  "总金额",
  "金额",
  "礼物金额"
];

const AGGREGATE_STRONG_VALUE_ALIASES = [
  "total_gift_value",
  "totalGiftValue",
  "total_value",
  "totalValue",
  "total_revenue",
  "totalRevenue",
  "gross_revenue",
  "grossRevenue",
  "total_diamonds",
  "totalDiamonds",
  "diamond_count",
  "diamondCount",
  "总流水",
  "总礼物流水",
  "礼物流水",
  "后台总流水",
  "本场流水",
  "总钻石",
  "总金额",
  "礼物金额"
];

const AGGREGATE_PAID_USER_ALIASES = [
  "paid_user_count",
  "paidUserCount",
  "paying_user_count",
  "payingUserCount",
  "payer_count",
  "payerCount",
  "paying_users",
  "payingUsers",
  "paid_users",
  "paidUsers",
  "unique_payers",
  "uniquePayers",
  "unique_paid_users",
  "uniquePaidUsers",
  "pay_user_count",
  "payUserCount",
  "付费人数",
  "付费用户数",
  "付费粉丝数",
  "送礼人数",
  "打赏人数"
];

const AGGREGATE_EVENT_COUNT_ALIASES = [
  "gift_event_count",
  "giftEventCount",
  "gift_order_count",
  "giftOrderCount",
  "gift_record_count",
  "giftRecordCount",
  "order_count",
  "orderCount",
  "record_count",
  "recordCount",
  "line_item_count",
  "lineItemCount",
  "礼物笔数",
  "礼物订单数",
  "订单数",
  "记录数"
];

const AGGREGATE_GIFT_COUNT_TOTAL_ALIASES = [
  "gift_count_total",
  "giftCountTotal",
  "total_gift_count",
  "totalGiftCount",
  "gift_count",
  "giftCount",
  "gift_num_total",
  "giftNumTotal",
  "gift_num",
  "giftNum",
  "礼物数量",
  "礼物总数",
  "总礼物数"
];

const AGGREGATE_ROOM_ALIASES = [
  "live_room_id",
  "liveRoomId",
  "room_id",
  "roomId",
  "room",
  "直播间ID",
  "房间ID"
];

const AGGREGATE_VALUE_UNIT_ALIASES = [
  "value_unit",
  "valueUnit",
  "gift_value_unit",
  "giftValueUnit",
  "revenue_unit",
  "revenueUnit",
  "diamond_unit",
  "diamondUnit",
  "unit",
  "currency",
  "价值单位",
  "流水单位",
  "礼物单位",
  "钻石单位",
  "单位",
  "币种"
];

const AGGREGATE_SUPPORTED_VALUE_UNITS = new Set([
  "diamond",
  "diamonds",
  "point",
  "points",
  "coin",
  "coins",
  "gift_point",
  "gift_points",
  "钻石",
  "积分",
  "礼物积分",
  "音浪",
  "金币"
]);

const AGGREGATE_CURRENCY_VALUE_UNITS = new Set([
  "usd",
  "eur",
  "gbp",
  "cny",
  "rmb",
  "myr",
  "sgd",
  "currency",
  "money",
  "金额",
  "人民币",
  "美元",
  "$",
  "￥"
]);

const AGGREGATE_KEY_ALIASES = [
  "metric",
  "metric_name",
  "name",
  "item",
  "key",
  "指标",
  "项目",
  "名称"
];

const AGGREGATE_KEY_VALUE_ALIASES = [
  "metric_value",
  "metricValue",
  "number",
  "count",
  "amount",
  "value",
  "total",
  "数值",
  "数量",
  "金额",
  "总计"
];

const AGGREGATE_TOLERANCE_DEFAULTS = Object.freeze({
  total_gift_value: 0,
  paid_user_count: 0,
  gift_event_count: 0,
  gift_count_total: 0
});

function createSettlementLedgerEvents({ primaryEvents = [], backupEvents = [] } = {}) {
  const merged = mergeRealtimeSourceEvents({ primaryEvents, backupEvents });
  return {
    ...merged,
    events: merged.accepted
  };
}

function compactFinalizedComboEvents(events = []) {
  const output = [];
  const indexByKey = new Map();
  for (const event of events) {
    const key = settlementComboEventKey(event);
    if (!key) {
      output.push(event);
      continue;
    }
    if (!indexByKey.has(key)) {
      indexByKey.set(key, output.length);
      output.push(event);
      continue;
    }
    const index = indexByKey.get(key);
    output[index] = preferFinalizedGiftEvent(output[index], event);
  }
  return output;
}

function settlementComboEventKey(event = {}) {
  const hasComboEvidence = Boolean(event.gift_combo_id)
    || (event.combo_count !== null && event.combo_count !== undefined)
    || event.is_combo_final === false;
  if (!hasComboEvidence) {
    return null;
  }
  const eventKey = event.gift_combo_id
    ?? event.platform_event_id
    ?? event.source_event_id
    ?? event.match_key
    ?? "";
  return [
    event.source_role ?? event.source ?? "",
    event.live_room_id ?? "",
    event.gifter_user_id ?? "",
    event.gift_id ?? event.gift_name ?? "",
    eventKey
  ].join(":");
}

function preferFinalizedGiftEvent(left = {}, right = {}) {
  return compareFinalizedGiftPreference(right, left) > 0 ? right : left;
}

function compareFinalizedGiftPreference(left = {}, right = {}) {
  const leftScore = finalizedGiftPreferenceScore(left);
  const rightScore = finalizedGiftPreferenceScore(right);
  for (let index = 0; index < leftScore.length; index += 1) {
    if (leftScore[index] !== rightScore[index]) {
      return leftScore[index] - rightScore[index];
    }
  }
  return 0;
}

function finalizedGiftPreferenceScore(event = {}) {
  return [
    event.is_combo_final === true ? 1 : 0,
    numberOrZero(event.combo_count),
    numberOrZero(event.gift_count),
    numberOrZero(event.gift_total_value),
    toTimestampOrZero(event.received_at ?? event.event_time)
  ];
}

function createCollectorLedgerControlTotals(events = []) {
  const paidUsers = new Set();
  let missingUserIdCount = 0;
  let giftCountTotal = 0;
  let totalGiftValue = 0;
  for (const event of events) {
    const userId = stringOrNull(event.gifter_user_id);
    if (userId) {
      paidUsers.add(userId);
    } else {
      missingUserIdCount += 1;
    }
    giftCountTotal += numberOrZero(event.gift_count);
    totalGiftValue += numberOrZero(event.gift_total_value);
  }
  return {
    gift_event_count: events.length,
    gift_count_total: roundMetric(giftCountTotal),
    total_gift_value: roundMetric(totalGiftValue),
    paid_user_count: paidUsers.size,
    missing_user_id_count: missingUserIdCount
  };
}

function createAggregateControlReconciliation({
  collectorEvents = [],
  aggregateSummary = {},
  expectedLiveRoomId = null,
  tolerances = {},
  requireRoomBinding = true,
  requireValueUnit = true
} = {}) {
  const collector = createCollectorLedgerControlTotals(collectorEvents);
  const official = {
    present: aggregateSummary.present === true,
    live_room_id: aggregateSummary.live_room_id ?? null,
    total_gift_value: numberOrNull(aggregateSummary.total_gift_value),
    paid_user_count: numberOrNull(aggregateSummary.paid_user_count),
    gift_event_count: numberOrNull(aggregateSummary.gift_event_count),
    gift_count_total: numberOrNull(aggregateSummary.gift_count_total),
    value_unit: normalizeAggregateValueUnit(aggregateSummary.value_unit),
    unsafe_metrics: Array.isArray(aggregateSummary.unsafe_metrics) ? aggregateSummary.unsafe_metrics : [],
    metrics_present: aggregateSummary.metrics_present ?? [],
    source_row_count: aggregateSummary.source_row_count ?? 0
  };
  const effectiveTolerances = {
    ...AGGREGATE_TOLERANCE_DEFAULTS,
    ...tolerances
  };
  const checkedMetrics = [];
  const deltas = {};
  const issues = [];

  if (!official.present) {
    return {
      present: false,
      ok: false,
      checked_metrics: [],
      collector,
      official,
      deltas,
      tolerances: effectiveTolerances,
      issues: [{
        severity: "blocker",
        reason_code: "no_aggregate_metrics",
        message: "没有导入后台总流水、付费人数或其他大数字段。"
      }]
    };
  }

  const hasRequiredValueMetric = official.total_gift_value !== null;
  const hasRequiredSecondaryMetric = official.paid_user_count !== null
    || official.gift_event_count !== null
    || official.gift_count_total !== null;
  if (!hasRequiredValueMetric || !hasRequiredSecondaryMetric) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_missing_required_metrics",
      metric: !hasRequiredValueMetric ? "total_gift_value" : "secondary_count",
      message: "后台大数至少要包含总流水，以及付费人数、礼物笔数或礼物数量中的一个。"
    });
  }
  if (requireRoomBinding && expectedLiveRoomId && !official.live_room_id) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_live_room_missing",
      metric: "live_room_id",
      collector_value: expectedLiveRoomId,
      official_value: null,
      delta: null,
      message: "后台大数缺少直播间或场次标识，不能确认它属于当前采集底稿。"
    });
  }
  if (expectedLiveRoomId && official.live_room_id && String(expectedLiveRoomId) !== String(official.live_room_id)) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_live_room_mismatch",
      metric: "live_room_id",
      collector_value: expectedLiveRoomId,
      official_value: official.live_room_id,
      delta: null,
      message: "后台大数所属直播间与当前采集底稿不一致。"
    });
  }
  if (requireValueUnit && official.total_gift_value !== null && !official.value_unit) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_value_unit_missing",
      metric: "value_unit",
      collector_value: "gift_value_points",
      official_value: null,
      delta: null,
      message: "后台总流水缺少单位，必须确认与本地底稿的礼物价值口径一致。"
    });
  } else if (official.value_unit && !isSupportedAggregateValueUnit(official.value_unit)) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_value_unit_unsupported",
      metric: "value_unit",
      collector_value: "gift_value_points",
      official_value: official.value_unit,
      delta: null,
      message: "后台流水单位不是礼物钻石/积分等可核对口径，不能直接和本地底稿结算。"
    });
  }
  for (const unsafeMetric of official.unsafe_metrics) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_metric_unsafe_precision",
      metric: unsafeMetric.metric,
      collector_value: null,
      official_value: unsafeMetric.raw_value,
      delta: null,
      message: "后台大数超过安全整数范围或精度不可确认，请改用字符串原始导出或拆分明细复核。"
    });
  }

  for (const metric of ["total_gift_value", "paid_user_count", "gift_event_count", "gift_count_total"]) {
    if (official[metric] === null) {
      continue;
    }
    checkedMetrics.push(metric);
    const collectorValue = numberOrZero(collector[metric]);
    const officialValue = numberOrZero(official[metric]);
    const delta = roundMetric(collectorValue - officialValue);
    const tolerance = numberOrZero(effectiveTolerances[metric]);
    deltas[metric] = {
      collector_value: collectorValue,
      official_value: officialValue,
      delta,
      tolerance
    };
    if (Math.abs(delta) > tolerance) {
      issues.push({
        severity: "blocker",
        reason_code: `${metric}_mismatch`,
        metric,
        collector_value: collectorValue,
        official_value: officialValue,
        delta,
        tolerance,
        message: aggregateMetricMismatchMessage(metric)
      });
    }
  }

  if (checkedMetrics.length === 0) {
    issues.push({
      severity: "blocker",
      reason_code: "no_aggregate_metrics",
      message: "后台大数字段无法识别，请至少提供总流水或付费人数。"
    });
  }
  if (collectorEvents.length === 0 && checkedMetrics.some((metric) => numberOrZero(official[metric]) > 0)) {
    issues.push({
      severity: "blocker",
      reason_code: "collector_ledger_empty",
      message: "后台显示本场有礼物或付费用户，但 Live Rush 本地逐笔底稿为空。"
    });
  }
  if (official.paid_user_count !== null && collector.missing_user_id_count > 0) {
    issues.push({
      severity: "blocker",
      reason_code: "collector_paid_user_count_unverifiable",
      metric: "paid_user_count",
      collector_value: collector.paid_user_count,
      official_value: official.paid_user_count,
      delta: roundMetric(collector.paid_user_count - official.paid_user_count),
      message: "本地底稿里存在缺用户 ID 的礼物，付费人数需要人工复核。"
    });
  }

  return {
    present: true,
    ok: !issues.some((issue) => issue.severity === "blocker") && checkedMetrics.length > 0,
    checked_metrics: checkedMetrics,
    collector,
    official,
    deltas,
    tolerances: effectiveTolerances,
    issues
  };
}

function createAggregateHardGateOverrideIssues({
  sourcePolicy = {},
  tolerances = {},
  requireRoomBinding,
  requireValueUnit
} = {}) {
  if (sourcePolicy.production_allowed !== true) {
    return [];
  }
  const issues = [];
  const relaxedToleranceMetrics = Object.entries(tolerances ?? {})
    .filter(([metric, value]) => numberOrZero(value) !== numberOrZero(AGGREGATE_TOLERANCE_DEFAULTS[metric]));
  if (relaxedToleranceMetrics.length > 0) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_hard_gate_override_not_allowed",
      metrics: relaxedToleranceMetrics.map(([metric]) => metric),
      message: "正式结算不能放宽后台大数差异容忍度，必须逐项严格对齐。"
    });
  }
  if (requireRoomBinding === false) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_room_binding_override_not_allowed",
      message: "正式结算不能关闭直播间或场次绑定校验。"
    });
  }
  if (requireValueUnit === false) {
    issues.push({
      severity: "blocker",
      reason_code: "aggregate_value_unit_override_not_allowed",
      message: "正式结算不能关闭流水单位校验。"
    });
  }
  return issues;
}

function appendAggregateReconciliationIssues(reconciliation = {}, extraIssues = []) {
  if (!extraIssues.length) {
    return reconciliation;
  }
  const issues = [
    ...(Array.isArray(reconciliation.issues) ? reconciliation.issues : []),
    ...extraIssues
  ];
  return {
    ...reconciliation,
    ok: false,
    issues
  };
}

function createSettlementReadinessGate({
  diagnostics = {},
  ledger = {},
  facts = []
} = {}) {
  const issues = [];
  const diagnosticsIssues = Array.isArray(diagnostics.issues) ? diagnostics.issues : [];
  for (const issue of diagnosticsIssues) {
    if (issue.severity !== "blocker") {
      continue;
    }
    issues.push({
      severity: "blocker",
      reason_code: issue.reason_code ?? "collector_diagnostic_blocker",
      message: issue.message ?? "本地采集诊断未通过，不能用于正式结算。"
    });
  }
  if (diagnostics.formal_use_ready === false && !issues.some((issue) => issue.reason_code === "collector_formal_readiness_failed")) {
    issues.push({
      severity: "blocker",
      reason_code: "collector_formal_readiness_failed",
      message: "本地采集健康门禁未通过，不能用后台大数覆盖采集异常。"
    });
  }
  const mismatches = Array.isArray(ledger.mismatches) ? ledger.mismatches : [];
  if (mismatches.length > 0) {
    issues.push({
      severity: "blocker",
      reason_code: "collector_ledger_conflict",
      mismatch_count: mismatches.length,
      message: "本地主备逐笔底稿存在金额或数量冲突，不能只靠后台大数判定准确。"
    });
  }
  const reviewFacts = facts.filter((fact) => fact.category === LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT && fact.needs_review === true);
  if (reviewFacts.length > 0) {
    issues.push({
      severity: "warning",
      reason_code: "collector_review_flags_present",
      review_count: reviewFacts.length,
      message: "本场存在大礼物、连击或高频波动等复核标记，归档时应保留原始证据和录屏。"
    });
  }
  const unresolvedComboCount = countUnresolvedComboGifts(facts);
  if (unresolvedComboCount > 0) {
    issues.push({
      severity: "blocker",
      reason_code: "collector_combo_not_finalized",
      combo_count: unresolvedComboCount,
      message: "本地底稿存在未收到最终帧的 TikTok 连击礼物，只能下播复核，不能直接正式结算。"
    });
  }
  const blockerCount = issues.filter((issue) => issue.severity === "blocker").length;
  return {
    ok: blockerCount === 0,
    issue_count: issues.length,
    blocker_count: blockerCount,
    diagnostics_ready: diagnostics.formal_use_ready === true,
    route_comparison: diagnostics.route_comparison ?? null,
    ledger_mismatch_count: mismatches.length,
    issues
  };
}

function countUnresolvedComboGifts(facts = []) {
  const comboRows = facts
    .map(normalizeCollectorFactForComparison)
    .filter((row) => row?.category === "gift" && row.gift_combo_id);
  const finalizedKeys = new Set(
    comboRows
      .filter((row) => row.is_combo_final === true)
      .map(comboFactSettlementKey)
  );
  const unresolvedKeys = new Set();
  for (const row of comboRows) {
    if (row.is_combo_final !== false) {
      continue;
    }
    const key = comboFactSettlementKey(row);
    if (!finalizedKeys.has(key)) {
      unresolvedKeys.add(key);
    }
  }
  return unresolvedKeys.size;
}

function comboFactSettlementKey(row = {}) {
  return [
    row.live_room_id ?? "",
    row.user_id ?? "",
    row.gift_id ?? row.gift_name ?? "",
    row.gift_combo_id ?? ""
  ].join(":");
}

function createAggregateControlBackfillPlan({
  primaryEvents = [],
  backupEvents = [],
  ledger = {},
  aggregateReconciliation = {},
  expectedLiveRoomId = null,
  generatedAt = nowIso()
} = {}) {
  const unresolvedCount = aggregateReconciliation.ok ? 0 : Math.max(aggregateReconciliation.issues?.length ?? 0, 1);
  const roomMismatchCount = aggregateReconciliation.issues?.some((issue) => issue.reason_code === "aggregate_live_room_mismatch") ? 1 : 0;
  return {
    ok: aggregateReconciliation.ok === true,
    platform: "tiktok",
    generated_at: generatedAt,
    expected_live_room_id: expectedLiveRoomId,
    primary_count: primaryEvents.length,
    backup_count: backupEvents.length,
    merged_realtime_count: ledger.events?.length ?? 0,
    official_count: 0,
    official_control_total_count: aggregateReconciliation.checked_metrics?.length ?? 0,
    primary_only_count: 0,
    backup_only_count: 0,
    confirmed_realtime_count: ledger.confirmations?.length ?? 0,
    mismatch_count: aggregateReconciliation.issues?.length ?? 0,
    after_live_recovered_count: 0,
    adjustment_count: 0,
    unresolved_count: unresolvedCount,
    ready_for_settlement: aggregateReconciliation.ok === true,
    review: {
      realtime_only_count: 0,
      official_only_count: 0,
      adjustment_count: 0,
      room_mismatch_count: roomMismatchCount
    },
    corrections: [],
    next_actions: []
  };
}

function createAggregateAccuracyScore(reconciliation = {}) {
  if (!reconciliation.present || reconciliation.checked_metrics?.length === 0) {
    return null;
  }
  const weights = {
    total_gift_value: 60,
    paid_user_count: 25,
    gift_event_count: 10,
    gift_count_total: 5
  };
  const activeWeight = reconciliation.checked_metrics.reduce((sum, metric) => sum + (weights[metric] ?? 0), 0) || 1;
  let penalty = 0;
  for (const metric of reconciliation.checked_metrics) {
    const delta = reconciliation.deltas?.[metric];
    if (!delta) {
      continue;
    }
    const denominator = Math.max(Math.abs(numberOrZero(delta.official_value)), Math.abs(numberOrZero(delta.collector_value)), 1);
    const ratio = Math.min(Math.abs(numberOrZero(delta.delta)) / denominator, 1);
    penalty += ratio * ((weights[metric] ?? 0) / activeWeight) * 100;
  }
  if (reconciliation.issues?.some((issue) => issue.reason_code === "aggregate_live_room_mismatch")) {
    penalty += 20;
  }
  if (reconciliation.issues?.some((issue) => issue.reason_code === "collector_ledger_empty")) {
    penalty += 30;
  }
  return Number(Math.max(0, 100 - penalty).toFixed(2));
}

function isAggregateControlTotalRow(row = {}) {
  if (!row || typeof row !== "object") {
    return false;
  }
  if (parseAggregateKeyValueRow(row)) {
    return true;
  }
  if (isLikelyGiftLineItemRaw(row)) {
    return false;
  }
  const hasStrongValue = pickMetricValue(row, AGGREGATE_STRONG_VALUE_ALIASES) !== null;
  const hasControlMetric = pickMetricValue(row, AGGREGATE_PAID_USER_ALIASES) !== null
    || pickMetricValue(row, AGGREGATE_EVENT_COUNT_ALIASES) !== null
    || pickMetricValue(row, AGGREGATE_GIFT_COUNT_TOTAL_ALIASES) !== null;
  if (hasStrongValue || hasControlMetric) {
    return true;
  }
  return !isLikelyGiftLineItemRaw(row) && pickMetricValue(row, AGGREGATE_VALUE_ALIASES) !== null;
}

function isLikelyGiftLineItemRaw(row = {}) {
  return firstDefined(
    row.source_event_id,
    row.msg_id,
    row.msgId,
    row.message_id,
    row.event_id,
    row.order_id,
    row.gift_id,
    row.giftId,
    row.gift_name,
    row.giftName,
    row.user_id,
    row.userId,
    row.gifter_user_id,
    row.open_id,
    row.uid,
    row["消息ID"],
    row["订单ID"],
    row["礼物ID"],
    row["礼物名称"],
    row["用户ID"],
    row["送礼用户ID"]
  ) !== undefined;
}

function parseAggregateKeyValueRow(row = {}) {
  const metricName = stringOrNull(pickMetricValue(row, AGGREGATE_KEY_ALIASES));
  if (!metricName) {
    return null;
  }
  const metric = metricNameToAggregateMetric(metricName);
  if (!metric) {
    return null;
  }
  return {
    metric,
    value: pickMetricValue(row, AGGREGATE_KEY_VALUE_ALIASES)
  };
}

function metricNameToAggregateMetric(value) {
  const compact = compactMetricKey(value);
  if (AGGREGATE_VALUE_UNIT_ALIASES.map(compactMetricKey).includes(compact) || /单位|币种|currency|unit/iu.test(value)) {
    return "value_unit";
  }
  if (AGGREGATE_VALUE_ALIASES.map(compactMetricKey).includes(compact) || /流水|钻石|金额|revenue|diamond|value/iu.test(value)) {
    return "total_gift_value";
  }
  if (AGGREGATE_PAID_USER_ALIASES.map(compactMetricKey).includes(compact) || /付费|送礼人数|打赏人数|payer|paiduser|payinguser/iu.test(value)) {
    return "paid_user_count";
  }
  if (AGGREGATE_EVENT_COUNT_ALIASES.map(compactMetricKey).includes(compact) || /笔数|订单数|记录数|order|record|event/iu.test(value)) {
    return "gift_event_count";
  }
  if (AGGREGATE_GIFT_COUNT_TOTAL_ALIASES.map(compactMetricKey).includes(compact) || /礼物数量|礼物总数|giftcount|giftnum/iu.test(value)) {
    return "gift_count_total";
  }
  return null;
}

function assignAggregateMetric(summary, metric, value) {
  if (summary[metric] !== null) {
    return;
  }
  const parsed = parseAggregateNumber(value);
  if (parsed.value !== null) {
    summary[metric] = parsed.value;
  }
  if (parsed.unsafe) {
    summary.unsafe_metrics.push({
      metric,
      raw_value: stringOrNull(value),
      parsed_value: parsed.value
    });
  }
}

function parseAggregateNumber(value) {
  const parsed = numberOrNull(value);
  if (parsed === null) {
    return {
      value: null,
      unsafe: false
    };
  }
  const raw = String(value ?? "").trim().replaceAll(",", "");
  const integerLike = /^[-+]?\d+$/u.test(raw);
  return {
    value: parsed,
    unsafe: integerLike && Math.abs(parsed) > Number.MAX_SAFE_INTEGER
  };
}

function normalizeAggregateValueUnit(value) {
  const unit = stringOrNull(value);
  if (!unit) {
    return null;
  }
  const compact = compactMetricKey(unit);
  const canonical = {
    diamond: "diamonds",
    diamonds: "diamonds",
    totaldiamonds: "diamonds",
    point: "points",
    points: "points",
    giftpoint: "gift_points",
    giftpoints: "gift_points",
    coin: "coins",
    coins: "coins",
    钻石: "diamonds",
    总钻石: "diamonds",
    积分: "points",
    礼物积分: "gift_points",
    音浪: "coins",
    金币: "coins",
    usd: "usd",
    eur: "eur",
    gbp: "gbp",
    cny: "cny",
    rmb: "rmb",
    myr: "myr",
    sgd: "sgd",
    currency: "currency",
    money: "money",
    金额: "money",
    人民币: "rmb",
    美元: "usd",
    $: "usd",
    "￥": "rmb"
  };
  return canonical[compact] ?? unit.trim();
}

function inferAggregateValueUnitFromKeys(row = {}) {
  for (const key of Object.keys(row)) {
    const compact = compactMetricKey(key);
    if (/diamond|diamonds|钻石/iu.test(compact)) {
      return "diamonds";
    }
    if (/point|points|积分/iu.test(compact)) {
      return "points";
    }
    if (/coin|coins|音浪|金币/iu.test(compact)) {
      return "coins";
    }
    if (/金额|revenue|currency|usd|eur|gbp|cny|rmb|myr|sgd/iu.test(compact)) {
      return "money";
    }
  }
  return null;
}

function isSupportedAggregateValueUnit(unit) {
  const normalized = normalizeAggregateValueUnit(unit);
  if (!normalized) {
    return false;
  }
  return AGGREGATE_SUPPORTED_VALUE_UNITS.has(normalized)
    && !AGGREGATE_CURRENCY_VALUE_UNITS.has(normalized);
}

function pickMetricValue(row = {}, aliases = []) {
  const aliasesByCompactKey = new Set(aliases.map(compactMetricKey));
  for (const [key, value] of Object.entries(row)) {
    if (aliasesByCompactKey.has(compactMetricKey(key)) && value !== undefined && value !== null && value !== "") {
      return value;
    }
  }
  return null;
}

function aggregateMetricMismatchMessage(metric) {
  const labels = {
    total_gift_value: "后台总流水与 Live Rush 本地逐笔底稿不一致。",
    paid_user_count: "后台付费人数与 Live Rush 本地逐笔底稿不一致。",
    gift_event_count: "后台礼物笔数与 Live Rush 本地逐笔底稿不一致。",
    gift_count_total: "后台礼物数量与 Live Rush 本地逐笔底稿不一致。"
  };
  return labels[metric] ?? "后台大数与 Live Rush 本地逐笔底稿不一致。";
}

function comparisonRowToBackfillGiftEvent(row = {}, {
  liveRoomId = null,
  sourceRole = null,
  sourceName = "external"
} = {}) {
  if (!row || row.category !== "gift") {
    return null;
  }
  const eventTime = normalizeTime(row.event_time) ?? normalizeTime(row.received_at) ?? nowIso();
  const giftCount = numberOrZero(row.gift_count || 1) || 1;
  const giftTotalValue = numberOrZero(row.gift_total_value);
  return {
    source: sourceName,
    source_role: sourceRole ?? row.route_role ?? null,
    source_event_id: row.source_event_id ?? null,
    platform_event_id: row.platform_event_id ?? row.source_event_id ?? null,
    live_room_id: row.live_room_id ?? liveRoomId ?? null,
    gifter_user_id: row.user_id ?? null,
    gifter_display_name: row.user_name ?? null,
    gift_id: row.gift_id ?? null,
    gift_name: row.gift_name ?? row.gift_id ?? null,
    gift_unit_value: giftCount > 0 ? giftTotalValue / giftCount : giftTotalValue,
    gift_count: giftCount,
    gift_total_value: giftTotalValue,
    gift_combo_id: row.gift_combo_id ?? null,
    combo_count: row.combo_count ?? null,
    is_combo_final: row.is_combo_final ?? true,
    event_time: eventTime,
    received_at: normalizeTime(row.received_at) ?? eventTime
  };
}

function applySettlementMatchKeys({
  primaryEvents = [],
  backupEvents = [],
  officialEvents = []
} = {}) {
  const roles = [
    [GIFT_SOURCE_ROLES.PRIMARY_REALTIME, primaryEvents],
    [GIFT_SOURCE_ROLES.BACKUP_REALTIME, backupEvents],
    [GIFT_SOURCE_ROLES.OFFICIAL_BACKEND, officialEvents]
  ];
  const outputByRole = new Map();
  for (const [role, events] of roles) {
    const exactKeys = createSettlementOccurrenceKeys(events, createSettlementSemanticMatchBase);
    const reviewKeys = createSettlementOccurrenceKeys(events, createSettlementReviewMatchBase);
    outputByRole.set(role, events.map((event) => ({
      ...event,
      match_key: exactKeys.get(event),
      review_match_key: reviewKeys.get(event)
    })));
  }
  return {
    primaryEvents: outputByRole.get(GIFT_SOURCE_ROLES.PRIMARY_REALTIME) ?? [],
    backupEvents: outputByRole.get(GIFT_SOURCE_ROLES.BACKUP_REALTIME) ?? [],
    officialEvents: outputByRole.get(GIFT_SOURCE_ROLES.OFFICIAL_BACKEND) ?? []
  };
}

function createSettlementOccurrenceKeys(events = [], baseKeyFactory) {
  const groups = new Map();
  for (const event of events) {
    const baseKey = baseKeyFactory(event);
    if (!groups.has(baseKey)) {
      groups.set(baseKey, []);
    }
    groups.get(baseKey).push(event);
  }
  const keys = new Map();
  for (const [baseKey, group] of groups) {
    const sorted = [...group].sort((left, right) => {
      const delta = new Date(left.event_time).getTime() - new Date(right.event_time).getTime();
      if (Number.isFinite(delta) && delta !== 0) {
        return delta;
      }
      return String(left.source_event_id ?? "").localeCompare(String(right.source_event_id ?? ""));
    });
    sorted.forEach((event, index) => {
      keys.set(event, `${baseKey}:occurrence:${index + 1}`);
    });
  }
  return keys;
}

function createSettlementSemanticMatchBase(event = {}) {
  const bucket = Math.floor(new Date(event.event_time).getTime() / 1000);
  return [
    event.live_room_id ?? "",
    event.gifter_user_id ?? "",
    event.gift_id ?? event.gift_name ?? "",
    event.gift_count ?? 0,
    event.gift_total_value ?? 0,
    Number.isFinite(bucket) ? bucket : event.event_time ?? ""
  ].join(":");
}

function createSettlementReviewMatchBase(event = {}) {
  const bucket = Math.floor(new Date(event.event_time).getTime() / 1000);
  return [
    event.live_room_id ?? "",
    event.gifter_user_id ?? "",
    event.gift_id ?? event.gift_name ?? "",
    event.gift_combo_id ?? "single",
    Number.isFinite(bucket) ? bucket : event.event_time ?? ""
  ].join(":");
}

function createSettlementNextActions({
  sourcePolicy,
  backfill,
  comparison,
  settlementMode = LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.LINE_ITEM_BACKFILL,
  aggregateReconciliation = { present: false, ok: false, issues: [] },
  settlementReadinessGate = { ok: true, issues: [] }
}) {
  const actions = [];
  if (!sourcePolicy.production_allowed) {
    actions.push("正式环境请导入公会后台、TikTok 官方导出、达人后台或 Live Rush 下播导入；MCA 只保留做测试期对照。");
  }
  if (!settlementReadinessGate.ok) {
    actions.push("先修复本地采集健康门禁；后台大数只能校验总账，不能替代 Live Rush 逐笔底稿。");
  }
  if (settlementReadinessGate.issues?.some((issue) => issue.reason_code === "backup_route_no_gifts")) {
    actions.push("备用线路已配置但没有采到礼物，本场不能作为正式双路线样本归档。");
  }
  if (settlementReadinessGate.issues?.some((issue) => issue.reason_code === "collector_ledger_conflict" || issue.reason_code === "primary_backup_mismatch")) {
    actions.push("主备逐笔底稿冲突时，以本地原始证据、录屏和正式下播报表复核差异，不能只看后台总数。");
  }
  if (settlementMode === LIVE_DATA_COLLECTOR_SETTLEMENT_MODES.AGGREGATE_CONTROL_TOTAL) {
    if (aggregateReconciliation.ok && settlementReadinessGate.ok) {
      actions.push("后台总流水、付费人数等大数已与 Live Rush 本地逐笔底稿对齐，可以把本地底稿和后台截图一起归档。");
    } else {
      actions.push("后台只有大数时无法自动定位是哪一笔礼物差异；请用本地逐笔底稿、原始证据、录屏，测试期可加 MCA 做差额定位。");
    }
    if (aggregateReconciliation.issues?.some((issue) => issue.reason_code === "aggregate_missing_required_metrics")) {
      actions.push("后台大数至少要提供总流水，以及付费人数、礼物笔数或礼物数量中的一个。");
    }
    if (aggregateReconciliation.issues?.some((issue) => issue.reason_code === "aggregate_live_room_missing" || issue.reason_code === "aggregate_live_room_mismatch")) {
      actions.push("后台大数必须能绑定到当前直播间或本场场次，避免把别的直播间总数对进来。");
    }
    if (aggregateReconciliation.issues?.some((issue) => issue.reason_code === "aggregate_value_unit_missing" || issue.reason_code === "aggregate_value_unit_unsupported")) {
      actions.push("后台总流水必须使用礼物钻石/积分等同一口径；如果只有金额展示，先换成后台原始礼物价值口径。");
    }
    if (aggregateReconciliation.issues?.some((issue) => issue.reason_code === "aggregate_metric_unsafe_precision")) {
      actions.push("后台大数存在精度风险，请改用原始导出文件或拆分明细后再核对。");
    }
    if (aggregateReconciliation.issues?.some((issue) => issue.reason_code === "collector_ledger_empty")) {
      actions.push("后台显示有流水但本地底稿为空，本场不能直接结算，需要排查采集入口、网络拦截和待机时间。");
    }
    if (aggregateReconciliation.issues?.some((issue) => issue.reason_code.endsWith("_mismatch"))) {
      actions.push("先按总流水、付费人数、礼物笔数的差额判断漏采窗口，再回看本地原始包和直播录屏补证。");
    }
    return [...new Set(actions)];
  }
  if (aggregateReconciliation.present && !aggregateReconciliation.ok) {
    actions.push("逐笔明细和后台大数不一致，正式归档前先复核后台导出口径、跨房间记录和大礼物连击。");
  }
  if ((backfill.official_count ?? 0) === 0 && !aggregateReconciliation.present) {
    actions.push("直播结束后导入后台总流水/付费人数；如果能拿到逐笔明细，也可以导入逐笔礼物明细做自动补数。");
  }
  if ((backfill.after_live_recovered_count ?? 0) > 0) {
    actions.push("把下播报表中实时漏采的礼物写入补数修正表，再进入人工复核。");
  }
  if ((backfill.adjustment_count ?? 0) > 0) {
    actions.push("以正式下播报表为准生成金额或数量调整记录。");
  }
  if ((backfill.review?.realtime_only_count ?? 0) > 0) {
    actions.push("检查实时多出的礼物是否为重复、撤回、跨房间或报表延迟。");
  }
  if ((comparison.summary?.collector_only_gift_count ?? 0) > 0 || (comparison.summary?.external_only_gift_count ?? 0) > 0) {
    actions.push("打开对账 CSV，按缺失礼物、额外礼物、金额差异三类逐项复核。");
  }
  if (actions.length === 0) {
    actions.push("本场正式下播报表已对齐，可以进入结算归档。");
  }
  return [...new Set(actions)];
}

function evaluateCollectorRouteIndependence(routeStates = []) {
  const primary = routeStates.find((route) => route.role === GIFT_SOURCE_ROLES.PRIMARY_REALTIME && route.enabled !== false);
  const backup = routeStates.find((route) => route.role === GIFT_SOURCE_ROLES.BACKUP_REALTIME && route.enabled !== false);
  if (!primary) {
    return {
      level: "primary_missing",
      production_ready: false,
      independent_statistics_ready: false,
      independent_accounting_ready: false,
      redundancy_class: "missing_primary",
      primary: null,
      backup: null,
      message: "没有主采集线路。"
    };
  }
  const primaryTarget = routeTargetDescriptor(primary.webcast);
  if (!backup) {
    return {
      level: "backup_missing",
      production_ready: false,
      independent_statistics_ready: false,
      independent_accounting_ready: false,
      redundancy_class: "single_realtime_route",
      primary: primaryTarget,
      backup: null,
      message: "没有备用线路；可以采集，但不满足正式多链路容灾要求。"
    };
  }
  const backupTarget = routeTargetDescriptor(backup.webcast);
  if (isLocalHeterogeneousRealtimePair(primaryTarget, backupTarget)) {
    return {
      level: "local_heterogeneous_browser_backup",
      production_ready: true,
      independent_statistics_ready: true,
      independent_accounting_ready: true,
      redundancy_class: "local_heterogeneous_realtime_ledgers",
      primary: primaryTarget,
      backup: backupTarget,
      message: "主备都在用户本机运行，但采集技术不同：一条走程序化 Webcast/SDK，一条走真实浏览器页面监听，可独立统计和独立落账。"
    };
  }
  if (primaryTarget.explicit && backupTarget.explicit && primaryTarget.host && backupTarget.host && primaryTarget.host !== backupTarget.host) {
    return {
      level: "independent_managed_channel",
      production_ready: true,
      independent_statistics_ready: true,
      independent_accounting_ready: true,
      redundancy_class: "independent_realtime_ledgers",
      primary: primaryTarget,
      backup: backupTarget,
      message: "主备线路位于不同托管入口或不同域名，可以独立统计并独立落账。"
    };
  }
  if (backupTarget.explicit && (!primaryTarget.explicit || primaryTarget.host !== backupTarget.host)) {
    return {
      level: "managed_backup_channel",
      production_ready: true,
      independent_statistics_ready: true,
      independent_accounting_ready: true,
      redundancy_class: "managed_backup_realtime_ledger",
      primary: primaryTarget,
      backup: backupTarget,
      message: "备用线路使用独立托管入口，可独立统计和落账，能作为主线路被拦时的容灾。"
    };
  }
  if (primaryTarget.explicit && !backupTarget.explicit) {
    return {
      level: "managed_primary_auto_backup",
      production_ready: true,
      independent_statistics_ready: true,
      independent_accounting_ready: true,
      redundancy_class: "managed_primary_auto_backup_ledgers",
      primary: primaryTarget,
      backup: backupTarget,
      message: "主线路使用托管入口，备线路使用本机自动入口，可以形成不同故障域的独立统计。"
    };
  }
  if (primaryTarget.identity === backupTarget.identity) {
    return {
      level: "same_provider_dual_instance",
      production_ready: false,
      independent_statistics_ready: false,
      independent_accounting_ready: false,
      redundancy_class: "hot_dual_instance_only",
      primary: primaryTarget,
      backup: backupTarget,
      message: "主备是同一 TikTok 入口的双实例，只能提高连接稳定性，不能算独立统计或独立核算。"
    };
  }
  if (primaryTarget.provider !== backupTarget.provider) {
    return {
      level: "different_provider_unproven",
      production_ready: false,
      independent_statistics_ready: false,
      independent_accounting_ready: false,
      redundancy_class: "unproven_provider_split",
      primary: primaryTarget,
      backup: backupTarget,
      message: "主备显示为不同采集提供方，但缺少独立托管入口或独立节点证据，不能按正式容灾认定。"
    };
  }
  return {
    level: "partially_independent",
    production_ready: false,
    independent_statistics_ready: false,
    independent_accounting_ready: false,
    redundancy_class: "unproven_split",
    primary: primaryTarget,
    backup: backupTarget,
    message: "主备线路有差异，但独立性证据不足；正式使用前必须配置能独立统计和落账的备用入口。"
  };
}

function routeTargetDescriptor(webcast = {}) {
  const browserObserver = isTiktokBrowserObserverRoute(webcast);
  const websocketUrl = stringOrNull(webcast.websocket_url ?? webcast.websocketUrl);
  const signerUrl = stringOrNull(webcast.signer_url ?? webcast.signerUrl);
  const liveRoomUrl = stringOrNull(webcast.live_room_url ?? webcast.liveRoomUrl);
  const creatorHandle = stringOrNull(webcast.creator_handle ?? webcast.creatorHandle);
  const browserProfile = stringOrNull(webcast.browser_user_data_dir ?? webcast.browserUserDataDir ?? webcast.browser?.user_data_dir ?? webcast.browser?.userDataDir);
  const explicitUrl = signerUrl ?? websocketUrl;
  const source = browserObserver ? "local_browser_cdp"
    : signerUrl ? "signer_url"
    : websocketUrl ? "websocket_url"
      : liveRoomUrl ? "live_room_url"
        : creatorHandle ? "creator_handle"
          : "missing";
  const host = explicitUrl ? safeUrlHost(explicitUrl) : liveRoomUrl ? safeUrlHost(liveRoomUrl) : null;
  const provider = browserObserver ? "tiktok_browser_observer" : webcast.provider ?? "tiktok_webcast";
  const captureFamily = browserObserver ? "local_browser_observer"
    : explicitUrl ? "managed_webcast"
      : "tiktok_live_connector";
  return {
    provider,
    source,
    explicit: Boolean(explicitUrl),
    host,
    capture_family: captureFamily,
    local_only: browserObserver || !explicitUrl,
    has_auto_connector_target: Boolean(creatorHandle || liveRoomUrl),
    statistics_line: browserObserver ? "local_browser_observer_line" : explicitUrl ? "managed_webcast_line" : "tiktok_auto_connector_line",
    accounting_line: browserObserver ? `local_browser_observer_ledger:${browserProfile ?? "default"}` : explicitUrl ? `managed_ledger:${host ?? "unknown"}` : "same_tiktok_auto_ledger",
    failure_domain: browserObserver ? `${provider}|cdp|${browserProfile ?? "default"}` : explicitUrl ? `${provider}|${source}|${host ?? "unknown"}` : `${provider}|auto_connector`,
    identity: [
      provider,
      source,
      host ?? "",
      browserProfile ? `browser_profile:${browserProfile}` : "",
      creatorHandle ? `creator:${creatorHandle}` : "",
      webcast.live_room_id ? `room:${webcast.live_room_id}` : ""
    ].filter(Boolean).join("|")
  };
}

function isLocalHeterogeneousRealtimePair(primaryTarget = {}, backupTarget = {}) {
  const families = new Set([primaryTarget.capture_family, backupTarget.capture_family]);
  return families.has("tiktok_live_connector") && families.has("local_browser_observer");
}

function isTiktokBrowserObserverRoute(webcast = {}) {
  const provider = String(webcast?.provider ?? "").toLowerCase();
  return provider === "tiktok_browser_observer"
    || provider === "local_browser_observer"
    || webcast?.browser_observer_enabled === true
    || webcast?.local_browser_observer_enabled === true;
}

function routeSourceName(webcast = {}) {
  return isTiktokBrowserObserverRoute(webcast) ? "tiktok_browser_observer" : "tiktok_webcast";
}

function safeUrlHost(value) {
  try {
    return new URL(value).host;
  } catch {
    return null;
  }
}

function createCollectionDiagnosticIssues({
  plugin,
  primary,
  backup,
  manifests,
  replay,
  routeIndependence,
  facts,
  statuses
}) {
  const issues = [];
  if (!primary) {
    issues.push({
      severity: "blocker",
      reason_code: "primary_route_missing",
      message: "没有主采集线路。"
    });
  }
  if (!backup) {
    issues.push({
      severity: "blocker",
      reason_code: "backup_route_missing",
      message: "没有独立备用采集线路；可以单线测试，但不能按正式容灾样本归档。"
    });
  } else if (!routeIndependence.production_ready) {
    issues.push({
      severity: "blocker",
      reason_code: "backup_route_not_independent",
      message: routeIndependence.message
    });
  }
  if (!manifests.ok) {
    issues.push({
      severity: "blocker",
      reason_code: "raw_backup_manifest_invalid",
      message: "原始证据文件校验未通过，不能用于正式追溯。"
    });
  }
  if (replay.route_comparison?.checked && !replay.route_comparison.ok) {
    issues.push({
      severity: "blocker",
      reason_code: replay.route_comparison.reason_code === "backup_route_no_gifts" ? "backup_route_no_gifts" : "primary_backup_mismatch",
      message: replay.route_comparison.reason_code === "backup_route_no_gifts"
        ? "备用线路已配置但没有采到礼物，不能视为多链路容灾已生效。"
        : "主备礼物数据不一致，需要导入正式下播报表补数。"
    });
  }
  const giftFacts = facts.filter((fact) => fact.category === LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.GIFT);
  if (giftFacts.length === 0) {
    issues.push({
      severity: "warning",
      reason_code: "no_real_gift_samples_observed",
      message: "还没有真实礼物样本，不能判断正式采集准确性。"
    });
  }
  if ((plugin.stats.issue_count ?? 0) > 0) {
    issues.push({
      severity: "warning",
      reason_code: plugin.stats.last_issue_code ?? "collector_issue_observed",
      message: "采集期间出现过异常，建议查看技术日志和诊断 JSON。"
    });
  }
  if (facts.length === 0 && statuses.length === 0 && plugin.stats.started_at) {
    issues.push({
      severity: "warning",
      reason_code: "no_runtime_records_observed",
      message: "采集器已启动但还没有写入数据包，可能仍在待机或入口被 TikTok 暂时拦截。"
    });
  }
  return issues;
}

function createCollectionDiagnosticActions(issues, routeIndependence) {
  const actions = [];
  if (issues.some((issue) => issue.reason_code === "backup_route_missing" || issue.reason_code === "backup_route_not_independent")) {
    actions.push("启用本机浏览器驻守备线，让备用线路通过真实 TikTok 页面监听，和主线路形成不同采集技术。");
  }
  if (issues.some((issue) => issue.reason_code === "primary_backup_mismatch" || issue.reason_code === "backup_route_no_gifts")) {
    actions.push("直播结束后导入公会后台或 TikTok 官方导出，生成正式补数修正表。");
  }
  if (issues.some((issue) => issue.reason_code === "no_real_gift_samples_observed")) {
    actions.push("先在真实直播间跑小额礼物样本，再判断是否可以替代 MCA。");
  }
  if (issues.some((issue) => issue.reason_code === "raw_backup_manifest_invalid")) {
    actions.push("先修复本地数据目录或磁盘权限，确保原始礼物和 P1 证据能落盘。");
  }
  if (!routeIndependence.production_ready) {
    actions.push("当前可以做测试采集，但正式替代 MCA 前需要把备线独立性跑通。");
  }
  if (actions.length === 0) {
    actions.push("继续真实直播间压测，重点覆盖大礼物、连击礼物和高频刷屏场景。");
  }
  return [...new Set(actions)];
}

function normalizeCollectorRoutes({ routes, webcast, primaryWebcast, backupWebcast, tenantId, sessionId, liveRoomId, collectorId, provider }) {
  const routeInputs = Array.isArray(routes) && routes.length > 0
    ? routes
    : [
        { role: GIFT_SOURCE_ROLES.PRIMARY_REALTIME, webcast: primaryWebcast ?? webcast ?? {} },
        ...(backupWebcast ? [{ role: GIFT_SOURCE_ROLES.BACKUP_REALTIME, webcast: backupWebcast }] : [])
      ];
  return routeInputs.map((route, index) => {
    const role = route.role ?? (index === 0 ? GIFT_SOURCE_ROLES.PRIMARY_REALTIME : GIFT_SOURCE_ROLES.BACKUP_REALTIME);
    return {
      role,
      enabled: route.enabled !== false,
      webcast: {
        ...(route.webcast ?? route),
        tenant_id: tenantId,
        session_id: sessionId,
        live_room_id: route.webcast?.live_room_id ?? route.live_room_id ?? liveRoomId,
        collector_id: `${collectorId}_${role}`,
        provider: route.webcast?.provider ?? route.provider ?? provider,
        role
      }
    };
  });
}

function p1RoleForGiftRole(role) {
  return role === GIFT_SOURCE_ROLES.BACKUP_REALTIME
    ? P1_EVIDENCE_SOURCE_ROLES.BACKUP_REALTIME
    : P1_EVIDENCE_SOURCE_ROLES.PRIMARY_REALTIME;
}

function compareRouteFacts({ facts, primaryRole, backupRole, backupConfigured = false }) {
  const primaryGifts = facts
    .map(normalizeCollectorFactForComparison)
    .filter((row) => row?.category === "gift" && row.route_role === primaryRole);
  const backupGifts = facts
    .map(normalizeCollectorFactForComparison)
    .filter((row) => row?.category === "gift" && row.route_role === backupRole);
  if (backupGifts.length === 0) {
    if (backupConfigured && primaryGifts.length > 0) {
      return {
        ok: false,
        checked: true,
        reason_code: "backup_route_no_gifts",
        primary_gift_count: primaryGifts.length,
        backup_gift_count: 0,
        matched_count: 0,
        primary_only_count: primaryGifts.length,
        backup_only_count: 0,
        mismatched_count: 0,
        accuracy_score: 0
      };
    }
    return {
      ok: true,
      checked: false,
      reason_code: backupConfigured ? "no_gift_samples_observed" : "backup_route_not_configured",
      primary_gift_count: primaryGifts.length,
      backup_gift_count: 0,
      matched_count: 0,
      primary_only_count: primaryGifts.length,
      backup_only_count: 0,
      mismatched_count: 0
    };
  }
  const report = createLiveDataCollectorComparisonReport({
    facts: primaryGifts.map((row) => ({
      category: "gift",
      route_role: primaryRole,
      normalized: row,
      source_event_id: row.source_event_id,
      live_room_id: row.live_room_id,
      event_time: row.event_time
    })),
    externalRows: backupGifts,
    sourceName: "backup_route"
  });
  return {
    ok: report.ok,
    checked: true,
    primary_gift_count: primaryGifts.length,
    backup_gift_count: backupGifts.length,
    matched_count: report.summary.matched_gift_count,
    primary_only_count: report.summary.collector_only_gift_count,
    backup_only_count: report.summary.external_only_gift_count,
    mismatched_count: report.summary.mismatched_gift_count,
    accuracy_score: report.accuracy_score
  };
}

function normalizeCollectorFactForComparison(fact = {}) {
  const normalized = fact.normalized ?? fact;
  if (fact.category === "gift" || fact.event_type === "gift_received" || normalized.gift_id || normalized.gift_name) {
    return {
      category: "gift",
      route_role: fact.route_role ?? normalized.source_role ?? normalized.role ?? null,
      live_room_id: stringOrNull(fact.live_room_id ?? normalized.live_room_id ?? normalized.room_id ?? normalized.roomId),
      source_event_id: stringOrNull(fact.source_event_id ?? normalized.source_event_id ?? normalized.msg_id ?? normalized.msgId),
      platform_event_id: stringOrNull(normalized.platform_event_id ?? normalized.platformEventId ?? normalized.platform_payload?.msg_id ?? normalized.platform_payload?.message_id),
      user_id: stringOrNull(normalized.gifter_user_id ?? normalized.actor_user_id ?? normalized.user_id ?? fact.actor_user_id),
      user_name: stringOrNull(normalized.gifter_display_name ?? normalized.nickname ?? normalized.user_name),
      gift_id: stringOrNull(normalized.gift_id ?? normalized.giftId),
      gift_name: stringOrNull(normalized.gift_name ?? normalized.giftName),
      gift_count: numberOrZero(normalized.gift_count ?? normalized.giftCount ?? normalized.repeat_count ?? normalized.repeatCount ?? 1),
      gift_total_value: numberOrZero(normalized.gift_total_value ?? normalized.giftTotalValue ?? normalized.value ?? normalized.amount),
      gift_combo_id: stringOrNull(normalized.gift_combo_id ?? normalized.combo_id ?? normalized.comboId),
      combo_count: numberOrNull(normalized.combo_count ?? normalized.comboCount ?? normalized.repeat_count ?? normalized.repeatCount),
      is_combo_final: booleanOrNull(normalized.is_combo_final ?? normalized.repeat_end ?? normalized.repeatEnd ?? normalized.platform_payload?.repeat_end),
      event_time: normalizeTime(fact.event_time ?? normalized.event_time ?? normalized.timestamp),
      received_at: normalizeTime(fact.received_at ?? normalized.received_at)
    };
  }
  return {
    category: fact.category ?? categoryForExternalRow(normalized),
    route_role: fact.route_role ?? null,
    live_room_id: stringOrNull(fact.live_room_id ?? normalized.live_room_id ?? normalized.room_id ?? normalized.roomId),
    source_event_id: stringOrNull(fact.source_event_id ?? normalized.source_event_id ?? normalized.msg_id ?? normalized.msgId),
    user_id: stringOrNull(fact.actor_user_id ?? normalized.actor_user_id ?? normalized.user_id),
    event_time: normalizeTime(fact.event_time ?? normalized.event_time ?? normalized.timestamp)
  };
}

function normalizeExternalReportRow(row = {}) {
  if (!row || typeof row !== "object") {
    return null;
  }
  const category = categoryForExternalRow(row);
  if (category === "gift") {
    return {
      category,
      live_room_id: stringOrNull(firstDefined(row.live_room_id, row.liveRoomId, row.room_id, row.roomId, row.room, row["直播间ID"], row["房间ID"])),
      source_event_id: stringOrNull(firstDefined(row.source_event_id, row.msg_id, row.msgId, row.message_id, row.event_id, row.order_id, row["消息ID"], row["订单ID"], row["记录ID"])),
      platform_event_id: stringOrNull(firstDefined(row.platform_event_id, row.platformEventId, row.platform_msg_id, row.platformMsgId, row.message_id, row.msg_id, row.msgId, row["平台消息ID"], row["消息ID"])),
      user_id: stringOrNull(firstDefined(row.user_id, row.userId, row.gifter_user_id, row.open_id, row.uid, row.unique_id, row.username, row["用户ID"], row["送礼用户ID"], row["主播粉丝ID"])),
      user_name: stringOrNull(firstDefined(row.nickname, row.user_name, row.gifter_display_name, row.display_name, row["用户昵称"], row["昵称"], row["送礼用户"])),
      gift_id: stringOrNull(firstDefined(row.gift_id, row.giftId, row.sec_gift_id, row.gift_code, row["礼物ID"], row["礼物编号"])),
      gift_name: stringOrNull(firstDefined(row.gift_name, row.giftName, row.gift, row.item_name, row["礼物名称"], row["礼物"])),
      gift_count: numberOrZero(firstDefined(row.gift_count, row.giftCount, row.repeat_count, row.repeatCount, row.gift_num, row.count, row["礼物数量"], row["数量"], 1)),
      gift_total_value: numberOrZero(firstDefined(row.gift_total_value, row.giftTotalValue, row.total_value, row.gift_value, row.amount, row.diamonds, row.value, row["钻石"], row["礼物价值"], row["金额"])),
      gift_combo_id: stringOrNull(firstDefined(row.gift_combo_id, row.giftComboId, row.combo_id, row.comboId, row["连击ID"], row["组合ID"])),
      combo_count: numberOrNull(firstDefined(row.combo_count, row.comboCount, row.repeat_count, row.repeatCount, row["连击数量"])),
      is_combo_final: booleanOrNull(firstDefined(row.is_combo_final, row.isComboFinal, row.repeat_end, row.repeatEnd, row["连击结束"], row["是否最终帧"])),
      event_time: normalizeTime(firstDefined(row.event_time, row.timestamp, row.time, row.create_time, row.created_at, row.date, row["送礼时间"], row["时间"], row["创建时间"]))
    };
  }
  return {
    category,
    live_room_id: stringOrNull(firstDefined(row.live_room_id, row.liveRoomId, row.room_id, row.roomId, row.room, row["直播间ID"], row["房间ID"])),
    source_event_id: stringOrNull(firstDefined(row.source_event_id, row.msg_id, row.msgId, row.message_id, row.event_id)),
    user_id: stringOrNull(firstDefined(row.user_id, row.userId, row.open_id, row.uid, row.unique_id, row.username)),
    event_time: normalizeTime(firstDefined(row.event_time, row.timestamp, row.time, row.create_time, row.created_at, row.date)),
    raw: row
  };
}

function categoryForExternalRow(row = {}) {
  if (isAggregateControlTotalRow(row)) {
    return "aggregate_control_total";
  }
  const type = String(firstDefined(row.category, row.event_type, row.type, row.message_type, row.event, "") ?? "").toLowerCase();
  if (type.includes("gift") || row.gift_id || row.gift_name || row.giftName || row.sec_gift_id || row.gift_num) {
    return "gift";
  }
  if (type.includes("chat") || type.includes("comment") || row.comment || row.text) {
    return "public_chat";
  }
  if (type.includes("member") || type.includes("join")) {
    return "member_join";
  }
  if (type.includes("like")) {
    return "like";
  }
  if (type.includes("follow")) {
    return "follow";
  }
  if (type.includes("share")) {
    return "share";
  }
  if (type.includes("room") || type.includes("viewer") || row.viewer_count || row.online_count) {
    return "room_metric";
  }
  return "other_p1";
}

function findBestGiftMatch(collectorGift, externalGifts, usedExternal, toleranceMs) {
  if (collectorGift.source_event_id) {
    const exactIndex = externalGifts.findIndex((row, index) => !usedExternal.has(index) && row.source_event_id && row.source_event_id === collectorGift.source_event_id);
    if (exactIndex >= 0) {
      return {
        index: exactIndex,
        row: externalGifts[exactIndex],
        match_type: "source_event_id"
      };
    }
  }
  let best = null;
  externalGifts.forEach((row, index) => {
    if (usedExternal.has(index)) {
      return;
    }
    const userScore = collectorGift.user_id && row.user_id && collectorGift.user_id === row.user_id ? 2 : 0;
    const giftScore = collectorGift.gift_id && row.gift_id && collectorGift.gift_id === row.gift_id ? 2
      : collectorGift.gift_name && row.gift_name && collectorGift.gift_name === row.gift_name ? 1 : 0;
    const countScore = Number(collectorGift.gift_count) === Number(row.gift_count) ? 1 : 0;
    const timeDelta = timeDeltaMs(collectorGift.event_time, row.event_time);
    const timeScore = timeDelta === null ? 0 : timeDelta <= toleranceMs ? 2 : 0;
    const score = userScore + giftScore + countScore + timeScore;
    if (score < 4) {
      return;
    }
    if (!best || score > best.score || (score === best.score && (timeDelta ?? Number.MAX_SAFE_INTEGER) < (best.timeDelta ?? Number.MAX_SAFE_INTEGER))) {
      best = {
        index,
        row,
        score,
        timeDelta,
        match_type: "semantic_time_window"
      };
    }
  });
  return best;
}

function compareCategoryCounts({ collectorRows, externalRows }) {
  const categories = [...new Set([
    ...collectorRows.map((row) => row.category),
    ...externalRows.map((row) => row.category)
  ].filter(Boolean))].sort();
  return categories.map((category) => {
    const collectorCount = collectorRows.filter((row) => row.category === category).length;
    const externalCount = externalRows.filter((row) => row.category === category).length;
    return {
      category,
      collector_count: collectorCount,
      external_count: externalCount,
      delta: collectorCount - externalCount
    };
  });
}

function dedupeComparisonRows(rows = []) {
  const output = [];
  const indexByKey = new Map();
  for (const row of rows) {
    if (row.category !== "gift") {
      output.push(row);
      continue;
    }
    const key = comparisonGiftDedupeKey(row);
    if (!key) {
      output.push(row);
      continue;
    }
    if (indexByKey.has(key)) {
      const index = indexByKey.get(key);
      output[index] = preferFinalizedGiftRow(output[index], row);
      continue;
    }
    indexByKey.set(key, output.length);
    output.push(row);
  }
  return output;
}

function comparisonGiftDedupeKey(row = {}) {
  if (row.gift_combo_id) {
    return `combo:${row.live_room_id ?? ""}:${row.user_id ?? ""}:${row.gift_id ?? row.gift_name ?? ""}:${row.gift_combo_id}`;
  }
  const hasComboEvidence = row.combo_count !== null && row.combo_count !== undefined
    || row.is_combo_final !== null && row.is_combo_final !== undefined;
  if (hasComboEvidence && row.platform_event_id) {
    return `combo-platform:${row.live_room_id ?? ""}:${row.user_id ?? ""}:${row.gift_id ?? row.gift_name ?? ""}:${row.platform_event_id}`;
  }
  if (row.source_event_id) {
    return `id:${row.source_event_id}`;
  }
  return `semantic:${row.user_id ?? ""}:${row.gift_id ?? row.gift_name ?? ""}:${row.gift_count ?? ""}:${row.event_time ?? ""}`;
}

function preferFinalizedGiftRow(left = {}, right = {}) {
  return compareFinalizedGiftPreference(right, left) > 0 ? right : left;
}

function createAccuracyScore({ collectorCount, externalCount, matchedCount, mismatchCount, valueDelta, externalValue }) {
  const denominator = Math.max(collectorCount, externalCount, 1);
  const missingRatio = (denominator - matchedCount) / denominator;
  const mismatchRatio = mismatchCount / denominator;
  const valueRatio = externalValue > 0 ? valueDelta / externalValue : valueDelta > 0 ? 1 : 0;
  return Number(Math.max(0, 100 - (missingRatio * 55) - (mismatchRatio * 30) - (Math.min(valueRatio, 1) * 15)).toFixed(2));
}

function parseJsonLikeRows(text, format) {
  if (!text.trim()) {
    return [];
  }
  if (format === "jsonl" || format === "ndjson") {
    return text.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
  }
  const parsed = JSON.parse(text);
  if (Array.isArray(parsed)) {
    return parsed;
  }
  if (Array.isArray(parsed.rows)) {
    return parsed.rows;
  }
  if (Array.isArray(parsed.events)) {
    return parsed.events;
  }
  if (Array.isArray(parsed.gifts)) {
    return parsed.gifts;
  }
  return [parsed];
}

function parseCsvRows(text) {
  const lines = text.split(/\r?\n/u).filter((line) => line.trim());
  if (lines.length === 0) {
    return [];
  }
  const headers = splitCsvLine(lines[0]).map((header) => header.trim());
  return lines.slice(1).map((line) => {
    const cells = splitCsvLine(line);
    return Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? ""]));
  });
}

function splitCsvLine(line) {
  const cells = [];
  let current = "";
  let quoted = false;
  for (let index = 0; index < line.length; index += 1) {
    const char = line[index];
    if (char === "\"" && line[index + 1] === "\"") {
      current += "\"";
      index += 1;
      continue;
    }
    if (char === "\"") {
      quoted = !quoted;
      continue;
    }
    if (char === "," && !quoted) {
      cells.push(current);
      current = "";
      continue;
    }
    current += char;
  }
  cells.push(current);
  return cells;
}

function inferReportFormat(filePath, text) {
  const lower = String(filePath ?? "").toLowerCase();
  if (lower.endsWith(".jsonl") || lower.endsWith(".ndjson")) {
    return "ndjson";
  }
  if (lower.endsWith(".json")) {
    return "json";
  }
  const trimmed = text.trim();
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
    return "json";
  }
  return "csv";
}

function createRawFrameArchiveRecord({
  frame = null,
  decoded = null,
  routeState,
  sessionContext = {},
  sequenceNo = 1,
  receivedAt = nowIso(),
  semanticFrame = true
} = {}) {
  const serialized = serializeRawFrameForArchive(frame);
  const liveRoomId = (semanticFrame ? liveRoomIdFromDecodedFrame(decoded) : null)
    ?? sessionContext.live_room_id
    ?? routeState?.webcast?.live_room_id
    ?? null;
  return {
    raw_frame_id: createId("raw_frame"),
    schema_version: "live_data_collector_raw_frame_v1",
    tenant_id: sessionContext.tenant_id ?? null,
    live_room_id: liveRoomId,
    session_id: sessionContext.session_id ?? null,
    route_role: routeState?.role ?? null,
    source_id: routeState?.giftSourceDescriptor?.source_id ?? null,
    source: routeTargetLabel(routeState?.webcast),
    received_at: receivedAt,
    sequence_no: Number(sequenceNo),
    raw_payload_format: serialized.format,
    raw_payload_encoding: serialized.encoding,
    raw_payload_byte_length: serialized.byte_length,
    raw_payload_hash: hashRawGiftPayload(serialized.raw_payload),
    raw_payload: serialized.raw_payload,
    decoded_summary: summarizeDecodedFrame(decoded)
  };
}

function serializeRawFrameForArchive(frame) {
  if (frame === null || frame === undefined) {
    return {
      format: "empty",
      encoding: "none",
      byte_length: 0,
      raw_payload: null
    };
  }
  if (typeof frame === "string") {
    return {
      format: "text",
      encoding: "utf8",
      byte_length: Buffer.byteLength(frame, "utf8"),
      raw_payload: frame
    };
  }
  if (typeof Buffer !== "undefined" && Buffer.isBuffer(frame)) {
    return {
      format: "binary",
      encoding: "base64",
      byte_length: frame.byteLength,
      raw_payload: frame.toString("base64")
    };
  }
  if (frame instanceof ArrayBuffer) {
    const buffer = Buffer.from(frame);
    return {
      format: "binary",
      encoding: "base64",
      byte_length: buffer.byteLength,
      raw_payload: buffer.toString("base64")
    };
  }
  if (ArrayBuffer.isView(frame)) {
    const buffer = Buffer.from(frame.buffer, frame.byteOffset, frame.byteLength);
    return {
      format: "binary",
      encoding: "base64",
      byte_length: buffer.byteLength,
      raw_payload: buffer.toString("base64")
    };
  }
  const jsonSafeFrame = toJsonSafeValue(frame);
  const jsonText = JSON.stringify(jsonSafeFrame);
  return {
    format: "json",
    encoding: "json",
    byte_length: Buffer.byteLength(jsonText, "utf8"),
    raw_payload: jsonSafeFrame
  };
}

function summarizeDecodedFrame(decoded = null) {
  if (!decoded || typeof decoded !== "object") {
    return {
      ok: null,
      event_count: 0,
      p1_event_count: 0,
      rejected_count: 0,
      issue_reason_codes: []
    };
  }
  return {
    ok: decoded.ok === true,
    event_count: decoded.events?.length ?? 0,
    p1_event_count: decoded.p1_events?.length ?? 0,
    rejected_count: (decoded.rejected?.length ?? 0) + (decoded.p1_rejected?.length ?? 0),
    issue_reason_codes: (decoded.issues ?? []).map((issue) => issue.reason_code).filter(Boolean)
  };
}

function toJsonSafeValue(value, seen = new WeakSet()) {
  if (value === null || value === undefined) {
    return value ?? null;
  }
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
    return value;
  }
  if (typeof value === "bigint") {
    return value.toString();
  }
  if (typeof value === "function" || typeof value === "symbol") {
    return String(value);
  }
  if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
    return {
      __type: "Buffer",
      base64: value.toString("base64")
    };
  }
  if (value instanceof ArrayBuffer) {
    return {
      __type: "ArrayBuffer",
      base64: Buffer.from(value).toString("base64")
    };
  }
  if (ArrayBuffer.isView(value)) {
    return {
      __type: value.constructor?.name ?? "TypedArray",
      base64: Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64")
    };
  }
  if (seen.has(value)) {
    return "[Circular]";
  }
  seen.add(value);
  if (Array.isArray(value)) {
    return value.map((item) => toJsonSafeValue(item, seen));
  }
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJsonSafeValue(item, seen)]));
}

function readRawFrameArchive(filePath) {
  if (!filePath || !existsSync(filePath)) {
    return {
      records: [],
      issues: []
    };
  }
  const records = [];
  const issues = [];
  readFileSync(filePath, "utf8")
    .split("\n")
    .forEach((line, index) => {
      const trimmed = line.trim();
      if (!trimmed) {
        return;
      }
      try {
        records.push(JSON.parse(trimmed));
      } catch (error) {
        issues.push({
          field: "line",
          reason_code: "raw_frame_record_parse_failed",
          line_no: index + 1,
          message: error?.message ?? null
        });
      }
    });
  return {
    records,
    issues
  };
}

function createRawFrameArchiveManifest({
  filePath = null,
  records = [],
  parseIssues = [],
  sourceDescriptor = {},
  sessionContext = {},
  createdAt = nowIso()
} = {}) {
  const receivedTimes = records.map((record) => record.received_at).filter(Boolean).sort();
  const firstWith = (field) => records.find((record) => record[field])?.[field] ?? null;
  return {
    manifest_id: createId("raw_frame_manifest"),
    created_at: createdAt,
    file_path: filePath,
    tenant_id: sessionContext.tenant_id ?? firstWith("tenant_id"),
    live_room_id: sessionContext.live_room_id ?? firstWith("live_room_id"),
    session_id: sessionContext.session_id ?? firstWith("session_id"),
    source_role: sourceDescriptor.role ?? null,
    source_id: sourceDescriptor.source_id ?? null,
    record_count: records.length,
    first_sequence_no: records[0]?.sequence_no ?? null,
    last_sequence_no: records.at(-1)?.sequence_no ?? null,
    first_received_at: receivedTimes[0] ?? null,
    last_received_at: receivedTimes.at(-1) ?? null,
    raw_payload_hashes: records.map((record) => record.raw_payload_hash).filter(Boolean),
    raw_payload_included: false,
    archive_records_include_raw_payload: true,
    upload_raw_default: false,
    local_only: true,
    parse_issue_count: parseIssues.length,
    parse_issues: parseIssues.slice(0, 10)
  };
}

function validateRawFrameArchiveManifest(manifest = {}) {
  const issues = [];
  if (!manifest.file_path) {
    issues.push({ field: "file_path", reason_code: "raw_frame_archive_path_required" });
  }
  if (manifest.raw_payload_included !== false) {
    issues.push({ field: "raw_payload_included", reason_code: "manifest_must_not_include_raw_payload" });
  }
  if (manifest.archive_records_include_raw_payload !== true) {
    issues.push({ field: "archive_records_include_raw_payload", reason_code: "raw_frame_archive_must_keep_payload_locally" });
  }
  if (manifest.upload_raw_default !== false) {
    issues.push({ field: "upload_raw_default", reason_code: "raw_upload_disabled_by_default" });
  }
  if (manifest.local_only !== true) {
    issues.push({ field: "local_only", reason_code: "raw_frame_archive_local_only" });
  }
  if (!Array.isArray(manifest.raw_payload_hashes)) {
    issues.push({ field: "raw_payload_hashes", reason_code: "raw_payload_hashes_required" });
  }
  if (numberOrZero(manifest.parse_issue_count) > 0) {
    issues.push({ field: "parse_issues", reason_code: "raw_frame_archive_parse_failed" });
  }
  return {
    ok: issues.length === 0,
    issues
  };
}

function countNdjsonLines(filePath) {
  if (!filePath || !existsSync(filePath)) {
    return 0;
  }
  return readFileSync(filePath, "utf8")
    .split("\n")
    .filter((line) => line.trim())
    .length;
}

function ensureFile(filePath) {
  if (!existsSync(filePath)) {
    writeFileSync(filePath, "", "utf8");
  }
}

function appendNdjson(filePath, value) {
  appendFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
}

function readNdjson(filePath) {
  if (!existsSync(filePath)) {
    return [];
  }
  return readFileSync(filePath, "utf8")
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => JSON.parse(line));
}

function stringOrDefault(value, fallback) {
  if (value === null || value === undefined || value === "") {
    return fallback;
  }
  return String(value);
}

function stringOrNull(value) {
  if (value === null || value === undefined || value === "") {
    return null;
  }
  return String(value);
}

function normalizeRecoveryAlertEmail(value) {
  const text = stringOrNull(value)?.trim();
  if (!text) {
    return null;
  }
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(text) ? text : null;
}

function positiveNumberOrDefault(value, fallback) {
  const number = Number(value);
  return Number.isFinite(number) && number > 0 ? number : fallback;
}

function createRecoveryRoomLabel(roomInput, targetUrl) {
  const room = stringOrNull(roomInput)?.trim();
  if (room) {
    return room;
  }
  const target = stringOrNull(targetUrl);
  if (!target) {
    return "未填写直播间";
  }
  try {
    const parsed = new URL(target);
    const handle = parsed.pathname.split("/").find((part) => part.startsWith("@"));
    return handle || target;
  } catch {
    return target;
  }
}

function escapeRecoveryHtml(value) {
  return stringOrDefault(value, "").replace(/[&<>"']/gu, (char) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    "\"": "&quot;",
    "'": "&#39;"
  })[char]);
}

function liveRoomIdFromDecodedFrame(decoded = null) {
  if (!decoded || typeof decoded !== "object") {
    return null;
  }
  const candidates = [
    decoded,
    ...(Array.isArray(decoded.events) ? decoded.events : []),
    ...(Array.isArray(decoded.p1_events) ? decoded.p1_events : []),
    ...(Array.isArray(decoded.p1_raw_items) ? decoded.p1_raw_items : [])
  ];
  for (const item of candidates) {
    const shape = item?.payload && typeof item.payload === "object" ? item.payload : item;
    const liveRoomId = stringOrNull(firstDefined(
      shape?.live_room_id,
      shape?.liveRoomId,
      shape?.room_id,
      shape?.roomId,
      shape?.room?.id,
      shape?.expected_live_room_id,
      shape?.platform_payload?.room_id
    ));
    if (liveRoomId) {
      return liveRoomId;
    }
  }
  return null;
}

function numberOrZero(value) {
  const number = Number(value);
  return Number.isFinite(number) ? number : 0;
}

function numberOrNull(value) {
  if (value === null || value === undefined || value === "") {
    return null;
  }
  if (typeof value === "number") {
    return Number.isFinite(value) ? value : null;
  }
  const cleaned = String(value)
    .trim()
    .replaceAll(",", "")
    .replace(/[^\d.+-]/gu, "");
  if (!cleaned) {
    return null;
  }
  const number = Number(cleaned);
  return Number.isFinite(number) ? number : null;
}

function booleanOrNull(value) {
  if (value === null || value === undefined || value === "") {
    return null;
  }
  if (typeof value === "boolean") {
    return value;
  }
  const normalized = String(value).trim().toLowerCase();
  if (["true", "1", "yes", "y"].includes(normalized)) {
    return true;
  }
  if (["false", "0", "no", "n"].includes(normalized)) {
    return false;
  }
  return null;
}

function roundMetric(value) {
  const number = Number(value);
  return Number.isFinite(number) ? Number(number.toFixed(6)) : 0;
}

function compactMetricKey(value) {
  return String(value ?? "").trim().toLowerCase().replace(/[\s_\-.:：/\\()（）]+/gu, "");
}

function firstDefined(...values) {
  return values.find((value) => value !== undefined && value !== null && value !== "");
}

function normalizeTime(value) {
  if (!value) {
    return null;
  }
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
}

function timeDeltaMs(left, right) {
  if (!left || !right) {
    return null;
  }
  const leftTime = new Date(left).getTime();
  const rightTime = new Date(right).getTime();
  if (!Number.isFinite(leftTime) || !Number.isFinite(rightTime)) {
    return null;
  }
  return Math.abs(leftTime - rightTime);
}

function sumBy(rows, field) {
  return rows.reduce((sum, row) => sum + numberOrZero(row[field]), 0);
}

function csvCell(value) {
  const text = String(value ?? "");
  if (!/[",\n\r]/u.test(text)) {
    return text;
  }
  return `"${text.replaceAll("\"", "\"\"")}"`;
}

function categoryForP1Event(eventType) {
  const map = {
    [EVENT_TYPES.PUBLIC_CHAT_RECEIVED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.PUBLIC_CHAT,
    [EVENT_TYPES.VIEWER_MEMBER_JOINED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.MEMBER_JOIN,
    [EVENT_TYPES.VIEWER_FOLLOWED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.FOLLOW,
    [EVENT_TYPES.VIEWER_SHARED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SHARE,
    [EVENT_TYPES.LIKE_BURST_RECORDED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.LIKE,
    [EVENT_TYPES.ROOM_METRIC_SAMPLED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.ROOM_METRIC,
    [EVENT_TYPES.VIEWER_VISIBLE_LABELS_OBSERVED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.USER_LABELS,
    [EVENT_TYPES.STREAM_LIFECYCLE_RECORDED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.LIVE_STATE,
    [EVENT_TYPES.SYSTEM_NOTICE_RECEIVED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SYSTEM,
    [EVENT_TYPES.LIVE_SOURCE_HEALTH_REPORTED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SOURCE_HEALTH,
    [EVENT_TYPES.P1_CAPTURE_DEGRADED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SOURCE_HEALTH,
    [EVENT_TYPES.P1_CAPTURE_RECOVERED]: LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.SOURCE_HEALTH
  };
  return map[eventType] ?? (Object.values(P1_EVIDENCE_EVENT_TYPES).includes(eventType)
    ? LIVE_DATA_COLLECTOR_PLUGIN_EVENT_CATEGORIES.OTHER_P1
    : "unknown");
}

function rawP1ItemForEvent(event = {}, rawItems = []) {
  const sourceEventId = event.payload?.source_event_id ?? null;
  if (!sourceEventId) {
    return rawItems[0] ?? null;
  }
  return rawItems.find((item) => {
    const shape = item?.payload && typeof item.payload === "object" ? item.payload : item;
    const id = shape?.source_event_id ?? shape?.msg_id ?? shape?.msgId ?? shape?.message_id ?? shape?.messageId ?? shape?.id ?? shape?.event_id ?? shape?.eventId;
    return String(id ?? "") === String(sourceEventId);
  }) ?? rawItems[0] ?? null;
}

function createMinimalP1Raw(event = {}) {
  return {
    type: event.event_type,
    source_event_id: event.payload?.source_event_id ?? null,
    event_time: event.event_time ?? nowIso(),
    live_room_id: event.live_room_id ?? null,
    actor_user_id: event.actor_user_id ?? null
  };
}
