/* ===== Screens Part 3: Brief, Content, Monitor, Reports, Review ===== */

// ========== 05 · BRIEF COMPOSER ==========
const UNIVERSAL_CONTENT_BOUNDARIES = {
  hardBlocks: [
    '违法犯罪、武器制作、恐怖主义、组织仇恨、人口贩运、诈骗和禁限售交易',
    '儿童安全、性剥削、未成年人裸露、诱导未成年人互动',
    '仇恨言论、定向骚扰、威胁、网暴动员和人肉搜索',
    '自伤自杀、危险挑战、会诱导模仿的不安全行为',
    '隐私泄露、身份证件/住址/联系方式/黑客数据',
    '钓鱼、恶意软件、虚假抽奖、金字塔骗局、误导性外链',
    '未授权音乐/视频/图片/商标/达人素材复用',
  ],
  cautionClaims: [
    '医疗、金融、法律、安全、就业、公共事件、灾害、政策和危机信息必须有来源',
    '产品能力、续航、价格、交付、销量、排名、用户收益不得绝对化',
    'AIGC 或显著编辑的真实人物/真实场景需按平台要求标识',
    '商业合作、赠品、佣金、联盟链接、达人激励必须披露',
    '禁止刷量、买粉、互关互赞、评论农场、重复铺量和诱导互动',
  ],
};

const PLATFORM_CONTENT_BOUNDARIES = [
  {
    id: 'xhs',
    platform: '小红书',
    family: 'visual-search-commerce',
    source: 'GitHub 家族映射 + 国内平台官方源待接入',
    formatRules: ['真实体验优先，商业笔记需显性标注', '封面/标题不能制造虚假结果、虚假测评或虚假用户经历', '图文证据需能支撑前后对比、清单推荐和排名判断'],
    linkRules: ['私域引流、二维码、抽奖、外链购买和导流话术需单独审核', '不使用“评论区见”“私信发链接”规避平台规则'],
    creatorRules: ['达人必须披露合作关系、赠品或试用权益', '不能伪装素人自发体验或虚构购买记录'],
    reviewTriggers: ['全网最低、闭眼入、官方认证、必买、治疗/收益/安全承诺', '竞品拉踩、截图证据、用户评论截图、价格和库存承诺'],
  },
  {
    id: 'douyin',
    platform: '抖音 / TikTok-style',
    family: 'short-video-recommendation',
    source: 'OpenTermsArchive TikTok Community Guidelines + 抖音官方源待接入',
    formatRules: ['前 3 秒钩子不能误导能力、价格、结果、风险或公共事件', '危险动作、驾驶/设备演示和挑战类内容必须避免诱导模仿', '真实人物或真实场景 AIGC 需按平台要求标识'],
    linkRules: ['直播、私信、橱窗、外链和私域导流需检查交易和合规风险', '禁止以承诺福利、破解、返利诱导跳转'],
    creatorRules: ['品牌合作、付费口播、赠品试用、佣金和挂链必须披露', '不得使用刷量、互赞、评论脚本或虚假评价'],
    reviewTriggers: ['挑战、实测、秒杀、限时抢、很快到账、无人驾驶、真实事故/灾害画面', 'AI 换脸、仿声、公众人物表态或产品背书'],
  },
  {
    id: 'wechat-official',
    platform: '微信公众号',
    family: 'longform-editorial',
    source: 'OpenTermsArchive WeChat Acceptable Use Policy + 微信公众平台官方源待接入',
    formatRules: ['标题、封面和摘要不得标题党、恐慌化或假冒官方通知', '长文需区分事实、推断、观点和品牌立场', '技术、政策和行业判断需保留来源与边界'],
    linkRules: ['外链、小程序、二维码、表单收集、抽奖和诱导分享需审核', '不得导向诈骗、违规交易、侵权下载或隐私收集'],
    creatorRules: ['转载、引用、配图、数据图和第三方素材需保留授权或来源', '评论精选和回复不得泄露用户隐私或升级争议'],
    reviewTriggers: ['政策解读、行业排名、技术参数、价格交付、融资/收益、抽奖福利', '医疗/金融/法律/公共安全类表述'],
  },
  {
    id: 'wechat-channels',
    platform: '视频号',
    family: 'wechat-short-video-live',
    source: 'OpenTermsArchive WeChat Acceptable Use Policy + 视频号官方源待接入',
    formatRules: ['短视频钩子不能夸大承诺或制造未证实事实', '直播口播的价格、库存、交付、能力和福利需预审', '评论区承接需避免争吵、隐私泄露和未授权承诺'],
    linkRules: ['私域添加、企业微信、群聊、二维码、直播间转化链路需单独审查', '不以福利承诺诱导用户离开平台完成高风险交易'],
    creatorRules: ['官方账号、达人账号和员工账号身份要清楚', '品牌合作和达人权益不得隐藏'],
    reviewTriggers: ['直播优惠、限量、交付时间、配置确定、私信领取、群内报价', '用户投诉、事故、负面评论和售后承诺'],
  },
  {
    id: 'youtube',
    platform: 'YouTube',
    family: 'long-video-search',
    source: 'OpenTermsArchive YouTube Community Guidelines',
    formatRules: ['标题、缩略图、描述和置顶评论必须匹配实际内容', '长测/技术视频需展示测试条件、样本和限制', '避免危险行为、误导元数据、重复搬运和未授权第三方内容'],
    linkRules: ['描述、口播、画面 URL 和置顶评论都算外链风险', '不得导向成人、诈骗、恶意软件、侵权下载、仇恨/骚扰或禁限售交易'],
    creatorRules: ['赞助、联盟链接、赠品、借测和付费推广需披露', '不得买量、互订、诱导订阅交换或虚假互动'],
    reviewTriggers: ['缩略图夸张承诺、未授权音乐/素材、外链下载、枪械/危险操作、医疗误导、AI 公共事件'],
  },
  {
    id: 'instagram',
    platform: 'Instagram',
    family: 'visual-social',
    source: 'OpenTermsArchive Instagram / Facebook Community Guidelines',
    formatRules: ['账号身份、品牌资产和视觉素材不得造成冒充或虚假来源', 'Reels / Stories 不得使用震惊、血腥、性化或骚扰性创意', '付费内容标准比自然帖更严'],
    linkRules: ['bio link、贴纸、私信和购物标签需检查诈骗、侵权和敏感品类风险', '不得利用私人困难或敏感属性做定向诱导'],
    creatorRules: ['创作者关系、素材授权、音乐和 logo 使用需清楚', '不得伪装官方、伪装达人或规避封禁'],
    reviewTriggers: ['before/after、用户肖像、音乐授权、抽奖、敏感属性暗示、竞品 logo', '公共人物 AI 图像或仿声'],
  },
  {
    id: 'weibo-x',
    platform: '微博 / X-style',
    family: 'public-conversation',
    source: 'OpenTermsArchive X Community Guidelines + 微博官方源待接入',
    formatRules: ['热点参与必须与品牌事实相关，不借灾害、公共事件或争议蹭话题', '短文本声明需可被截图独立理解', '避免引战、网暴动员和与用户争夺解释权'],
    linkRules: ['话题、转评、抽奖、外链和私信承接需检查诱导互动和诈骗风险', '不发布隐私信息、截图曝光或未授权聊天记录'],
    creatorRules: ['达人转发和话题共创需披露合作关系', '不得组织水军、刷话题、控评或伪造声量'],
    reviewTriggers: ['热搜借势、声明回应、竞品事故、用户投诉、抽奖、价格战', '政治/公共安全/灾害相关词'],
  },
  {
    id: 'bilibili',
    platform: 'B站 / YouTube long-form style',
    family: 'community-long-video',
    source: 'OpenTermsArchive YouTube Community Guidelines + B站官方源待接入',
    formatRules: ['标题/封面不能承诺视频里没有交付的测评结论', '技术拆解和实测需标注条件、方法、限制和样本', '避免危险驾驶、设备误用和可模仿的高风险操作'],
    linkRules: ['简介、评论置顶、动态和充电/店铺跳转需检查侵权、诈骗和导流风险', '第三方下载、未授权素材包和敏感交易链接禁用'],
    creatorRules: ['恰饭、借测、送测、赞助、联合出品和素材来源需披露', '弹幕/评论引导不能煽动攻击竞品或用户'],
    reviewTriggers: ['首发爆料、实测排行、拆机、事故画面、竞品对比、未授权 BGM/影视素材', '无人驾驶/安全能力绝对化表达'],
  },
];

const AIGC_PLATFORM_VARIANT_RULES = [
  { id: 'xhs-seeding', platform: '小红书', label: '图文 / 短视频种草', len: '短视频 + 笔记', tone: '真实体验 · 搜索友好', hooks: ['标题承接统一主题，但更短更可搜', '前三行给场景或问题，不伪装素人体验', '封面和正文证据必须能支撑卖点'] },
  { id: 'wechat-channels-short-video', platform: '视频号', label: '短视频 / 直播承接', len: '15-90s', tone: '官方口播 · 场景解释', hooks: ['首行直接进入主题', '口播保留概念车边界', '评论区承接不做价格/交付承诺'] },
  { id: 'wechat-mp-longform', platform: '微信公众号', label: '长图文 / 深度解释', len: '800-1800 字', tone: '官方叙事 · 解释型', hooks: ['标题稳定承接统一主题', '正文区分事实、概念、愿景和活动信息', '适合放完整 CTA 与风险边界'] },
  { id: 'youtube-video-search', platform: 'YouTube', label: '视频搜索 / 海外解释', len: '30s-3min', tone: 'English · clear claim boundary', hooks: ['Title must match the actual footage', 'Description explains concept limits', 'Avoid unsupported performance or delivery claims'] },
  { id: 'instagram-visual-social', platform: 'Instagram', label: 'Reels / 视觉社交', len: 'Reel / Post', tone: 'Visual launch signal', hooks: ['Theme line stays concise', 'Visual identity and source must be clear', 'No implied endorsement or fake partnership'] },
];

const isBriefExternalSocialPost = (post = {}) => {
  const type = `${post.source_type || ''} ${post.asset_type || ''} ${post.source_post_type || ''} ${post.relationship_status || ''}`.toLowerCase();
  return Boolean(
    post.is_official_post === false ||
    type.includes('earned') ||
    type.includes('ugc') ||
    type.includes('kol') ||
    type.includes('external')
  );
};
const getBriefContentCount = (brief = {}, postLookup = null) => {
  const item = brief || {};
  if (postLookup) {
    const ownedAssetCount = (item.assetPostIds || []).filter(id => postLookup.has(id)).length;
    return ownedAssetCount || (item.platformVariants || []).length || 0;
  }
  return (item.assetPostIds || []).length || (item.platformVariants || []).length || 0;
};
const getBriefPublishState = (brief = {}) => {
  const raw = String(brief?.status || '').toLowerCase();
  if (['published', 'active', 'live', 'ready'].includes(raw)) return 'published';
  if (['draft', 'pending', 'unpublished'].includes(raw)) return 'draft';
  return getBriefContentCount(brief) ? 'published' : 'draft';
};
const getBriefPublishStatusMeta = (brief = {}) => {
  const state = getBriefPublishState(brief);
  return state === 'published'
    ? { state, label: '已发布', tagClass: 'accent' }
    : { state, label: '草稿', tagClass: '' };
};
const getBriefPostDate = (post = {}) => post.published_at || post.publish_time || post.created_at || '';
const makePostLookup = (posts = []) => {
  const map = new Map();
  posts.forEach(post => {
    [post.post_id, post.content_id, post.id].filter(Boolean).forEach(id => map.set(id, post));
  });
  return map;
};
const getBriefAssetPosts = (brief = {}, posts = []) => {
  const ids = new Set((brief?.assetPostIds || []).filter(Boolean));
  if (!ids.size) return [];
  return posts.filter(post => ids.has(post.post_id) || ids.has(post.content_id) || ids.has(post.id));
};
const getBriefSortTime = (brief = {}, postLookup = null) => {
  const item = brief || {};
  const assetDates = (item.assetPostIds || [])
    .map(id => postLookup?.get(id))
    .filter(Boolean)
    .map(getBriefPostDate);
  const contentDates = [
    ...assetDates,
    ...(item.platformVariants || []).map(variant => variant.publishedAt),
  ].filter(Boolean).map(value => {
    const t = new Date(String(value).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime();
    return Number.isFinite(t) ? t : 0;
  }).filter(Boolean);
  if (contentDates.length) return Math.max(...contentDates);
  const fallbackDates = [
    item.endDate,
    item.startDate,
    item.updatedAt,
    item.createdAt,
  ].filter(Boolean).map(value => {
    const t = new Date(String(value).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime();
    return Number.isFinite(t) ? t : 0;
  });
  return Math.max(0, ...fallbackDates);
};
const formatBriefDate = (brief = {}, postLookup = null) => {
  const t = getBriefSortTime(brief, postLookup);
  if (!t) return '未排期';
  return new Date(t).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
};
const briefTypeLabel = (brief = {}) => {
  const item = brief || {};
  return item.parentCampaignId ? '子 Brief' : item.type === 'campaign' ? 'Campaign' : item.type === 'series' ? '栏目' : 'Brief';
};
const getAssignedBriefId = (post = {}) => post.primaryMasterBriefId || post.briefMapping?.primaryMasterBriefId || '';
const getBriefOptionLabel = (brief = {}, postLookup = null) => {
  const count = getBriefContentCount(brief, postLookup);
  return `${brief.name || brief.id}${count ? ` · ${count}` : ''}`;
};
const cleanBriefText = (value = '') => {
  if (typeof value !== 'string') return '';
  return value
    .replace(/<[^>]+>/g, ' ')
    .replace(/&nbsp;|&#160;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/\s+/g, ' ')
    .trim();
};
const getBriefPostTitle = (post = {}) => cleanBriefText(post.title || post.caption?.title || post.raw?.title || '未命名内容');
const getBriefPostBody = (post = {}) => [
  post.body,
  post.caption?.text,
  post.caption?.content,
  post.caption,
  post.description,
  post.text,
  post.raw?.desc,
  post.raw?.Digest,
  post.raw?.digest,
  post.raw?.note_card?.desc,
  post.raw?.object_desc?.description,
  post.raw?.description,
  post.raw?.caption,
  post.raw?.edge_media_to_caption?.edges?.[0]?.node?.text,
].map(cleanBriefText).find(value => value) || '';
const getBriefPostLabel = (post = {}) => {
  const date = getBriefPostDate(post) || '未排期';
  const platform = post.platform || '平台';
  const title = getBriefPostTitle(post);
  return `${date} · ${platform} · ${title}`.slice(0, 120);
};

const Brief = () => {
  const [campaigns, setCampaigns] = React.useState([]);
  const [briefId, setBriefId] = React.useState('');
  const [marketHandoffs, setMarketHandoffs] = React.useState([]);
  const [socialPosts, setSocialPosts] = React.useState([]);
  const [strategyThesis, setStrategyThesis] = React.useState(null);
  const [briefView, setBriefView] = React.useState('briefs');
  const [mappingDrafts, setMappingDrafts] = React.useState({});
  const [mappingStatus, setMappingStatus] = React.useState({});
  const [briefCreateOpen, setBriefCreateOpen] = React.useState(false);
  const [newBriefTitle, setNewBriefTitle] = React.useState('');
  const [editingTitleId, setEditingTitleId] = React.useState('');
  const [titleDraft, setTitleDraft] = React.useState('');
  const [briefMutationStatus, setBriefMutationStatus] = React.useState('');

  React.useEffect(() => {
    fetch(socialApiUrl('/api/initiatives'))
      .then(response => response.json())
      .then(data => {
        const rows = data.initiatives || data.campaigns || [];
        setCampaigns(rows);
        setBriefId(current => current || [...rows].sort((a, b) => getBriefSortTime(b) - getBriefSortTime(a))[0]?.id || '');
      })
      .catch(() => {});
    fetch(socialApiUrl('/api/market/handoffs?target_module=strategy&limit=5'))
      .then(response => response.json())
      .then(data => setMarketHandoffs(data.handoffs || []))
      .catch(() => setMarketHandoffs([]));
    fetch(socialApiUrl('/api/social/sync/status'))
      .then(response => response.json())
      .then(data => setSocialPosts(data.posts || []))
      .catch(() => setSocialPosts([]));
    fetch(socialApiUrl('/api/strategy/thesis'))
      .then(response => response.json())
      .then(data => setStrategyThesis(data.strategyThesis || null))
      .catch(() => setStrategyThesis(null));
  }, []);

  const ownedSocialPosts = React.useMemo(() => socialPosts.filter(post => !isBriefExternalSocialPost(post)), [socialPosts]);
  const postLookup = React.useMemo(() => makePostLookup(ownedSocialPosts), [ownedSocialPosts]);
  const sortedBriefs = React.useMemo(
    () => [...campaigns].sort((a, b) => getBriefSortTime(b, postLookup) - getBriefSortTime(a, postLookup) || getBriefContentCount(b, postLookup) - getBriefContentCount(a, postLookup) || String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN')),
    [campaigns, postLookup]
  );
  const activeCampaign = sortedBriefs.find(item => item.id === briefId) || sortedBriefs[0] || null;
  const configurablePosts = React.useMemo(
    () => [...ownedSocialPosts].filter(post => post.post_id || post.content_id || post.id).sort((a, b) => getPostTimeValue(b) - getPostTimeValue(a)),
    [ownedSocialPosts]
  );
  const upsertBrief = React.useCallback((nextBrief) => {
    if (!nextBrief?.id) return;
    setCampaigns(prev => {
      const exists = prev.some(item => item.id === nextBrief.id);
      return exists ? prev.map(item => item.id === nextBrief.id ? nextBrief : item) : [nextBrief, ...prev];
    });
  }, []);
  const createBrief = async () => {
    const title = newBriefTitle.trim();
    if (!title) {
      setBriefMutationStatus('请先填写 Brief 标题');
      return;
    }
    setBriefMutationStatus('creating');
    try {
      const response = await fetch(socialApiUrl('/api/initiatives'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          type: 'standalone',
          name: title,
          status: 'draft',
          stage: 'Brief 编排',
          goals: [],
          platformFocus: [],
          assetPostIds: [],
          assetCampaignLabels: [title],
          masterBrief: { title: `${title} Master Brief` },
        }),
      });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || data.message || 'Brief 创建失败');
      upsertBrief(data.initiative);
      setBriefId(data.initiative.id);
      setBriefCreateOpen(false);
      setNewBriefTitle('');
      setBriefMutationStatus('created');
      window.setTimeout(() => setBriefMutationStatus(''), 1200);
    } catch (error) {
      setBriefMutationStatus(error.message || 'Brief 创建失败');
    }
  };
  const beginTitleEdit = () => {
    if (!activeCampaign?.id) return;
    setEditingTitleId(activeCampaign.id);
    setTitleDraft(activeCampaign.name || '');
    setBriefMutationStatus('');
  };
  const saveBriefTitle = async () => {
    if (!activeCampaign?.id) return;
    const nextTitle = titleDraft.trim();
    if (!nextTitle) {
      setBriefMutationStatus('标题不能为空');
      return;
    }
    setBriefMutationStatus('saving-title');
    try {
      const response = await fetch(socialApiUrl(`/api/initiatives/${encodeURIComponent(activeCampaign.id)}`), {
        method: 'PATCH',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          name: nextTitle,
          assetCampaignLabels: activeCampaign.assetCampaignLabels?.length ? activeCampaign.assetCampaignLabels : [nextTitle],
        }),
      });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '标题保存失败');
      upsertBrief(data.initiative);
      setEditingTitleId('');
      setBriefMutationStatus('saved-title');
      window.setTimeout(() => setBriefMutationStatus(''), 1200);
    } catch (error) {
      setBriefMutationStatus(error.message || '标题保存失败');
    }
  };
  const updateBriefPublishState = async (nextState) => {
    if (!activeCampaign?.id) return;
    setBriefMutationStatus('saving-status');
    try {
      const response = await fetch(socialApiUrl(`/api/initiatives/${encodeURIComponent(activeCampaign.id)}`), {
        method: 'PATCH',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ status: nextState === 'published' ? 'published' : 'draft' }),
      });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '状态保存失败');
      upsertBrief(data.initiative);
      setBriefMutationStatus('saved-status');
      window.setTimeout(() => setBriefMutationStatus(''), 1200);
    } catch (error) {
      setBriefMutationStatus(error.message || '状态保存失败');
    }
  };
  const assignPostBriefFromBrief = async (post, targetBriefId) => {
    const postId = post?.post_id || post?.content_id || post?.id || '';
    if (!postId) return;
    setMappingStatus(prev => ({ ...prev, [postId]: 'saving' }));
    try {
      const response = await fetch(socialApiUrl('/api/social/posts/brief'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ postId, briefId: targetBriefId }),
      });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || data.message || 'Brief 配置失败');
      setSocialPosts(data.posts || []);
      setCampaigns(data.initiatives || campaigns);
      setMappingDrafts(prev => ({ ...prev, [postId]: targetBriefId || '' }));
      setMappingStatus(prev => ({ ...prev, [postId]: 'saved' }));
      window.setTimeout(() => setMappingStatus(prev => ({ ...prev, [postId]: '' })), 1200);
    } catch (error) {
      setMappingStatus(prev => ({ ...prev, [postId]: error.message || 'error' }));
    }
  };

  return (
    <div className="page wide">
      <div className="page-header">
        <div className="heading">
          <h1>Brief <span className="hl">编排器</span> · 从策略对象到执行的翻译层</h1>
          <p>Brief 按内容发布日期倒序排列；平台变体规则进入 AIGC 内容生成目录。</p>
        </div>
        <div className="actions">
          <button className={`btn sm ${briefView === 'briefs' ? 'primary' : 'ghost'}`} onClick={() => setBriefView('briefs')}>Brief 列表</button>
          <button className={`btn sm ${briefView === 'content-map' ? 'primary' : 'ghost'}`} onClick={() => setBriefView('content-map')}>内容挂载</button>
          {briefView === 'briefs' && (
            <select className="input" value={activeCampaign?.id || ''} onChange={e => setBriefId(e.target.value)} style={{width:260}}>
              {sortedBriefs.map(item => <option key={item.id} value={item.id}>{formatBriefDate(item, postLookup)} · {item.name}</option>)}
            </select>
          )}
          <button className="btn"><Icon name="external" className="icon sm"/>导出全部</button>
          <button className="btn primary">同步到内容创作 <Icon name="arrow" className="icon sm"/></button>
        </div>
      </div>

      {briefView === 'content-map' ? (
        <BriefContentMappingView
          posts={configurablePosts}
          briefs={sortedBriefs}
          postLookup={postLookup}
          mappingDrafts={mappingDrafts}
          mappingStatus={mappingStatus}
          onDraftChange={(postId, briefId) => setMappingDrafts(prev => ({ ...prev, [postId]: briefId }))}
          onSave={assignPostBriefFromBrief}
        />
      ) : (
      <div className="grid" style={{gridTemplateColumns:'260px 1fr 300px'}}>
        <div className="card">
          <div className="card-header"><h3>Brief 列表</h3><span className="count">{sortedBriefs.length}</span></div>
          <div className="card-body flush">
            {sortedBriefs.map(item => {
              const statusMeta = getBriefPublishStatusMeta(item);
              return (
                <div
                  key={item.id}
                  onClick={() => setBriefId(item.id)}
                  style={{
                    padding:'11px 14px',
                    borderLeft: activeCampaign?.id === item.id ? '2px solid var(--accent)' : '2px solid transparent',
                    borderBottom:'1px solid var(--hairline)',
                    background: activeCampaign?.id === item.id ? 'var(--accent-tint)' : 'transparent',
                    cursor:'pointer',
                  }}
                >
                  <div style={{display:'flex', justifyContent:'space-between', gap:8, alignItems:'center', marginBottom:5}}>
                    <span style={{fontSize:12, fontWeight: activeCampaign?.id === item.id ? 650 : 520, lineHeight:1.35}}>{item.name}</span>
                    <span className="count">{getBriefContentCount(item, postLookup)}</span>
                  </div>
                  <div style={{display:'flex', justifyContent:'space-between', gap:8, alignItems:'center', fontSize:10, color:'var(--ink-4)', fontFamily:'var(--f-mono)'}}>
                    <span>{formatBriefDate(item, postLookup)} · {briefTypeLabel(item)}</span>
                    <span className={`tag mono ${statusMeta.tagClass}`} style={{fontSize:9}}>{statusMeta.label}</span>
                  </div>
                </div>
              );
            })}
            <div style={{padding:12}}>
              {briefCreateOpen ? (
                <div style={{display:'grid', gap:8}}>
                  <input
                    className="input"
                    value={newBriefTitle}
                    onChange={event => setNewBriefTitle(event.target.value)}
                    onKeyDown={event => {
                      if (event.key === 'Enter') createBrief();
                      if (event.key === 'Escape') { setBriefCreateOpen(false); setNewBriefTitle(''); }
                    }}
                    placeholder="输入 Brief 标题"
                    autoFocus
                  />
                  <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:6}}>
                    <button className="btn sm primary" onClick={createBrief} disabled={briefMutationStatus === 'creating'}>
                      <Icon name="save" className="icon sm"/>{briefMutationStatus === 'creating' ? '创建中' : '创建'}
                    </button>
                    <button className="btn sm ghost" onClick={() => { setBriefCreateOpen(false); setNewBriefTitle(''); }}>
                      <Icon name="x" className="icon sm"/>取消
                    </button>
                  </div>
                  {briefMutationStatus && briefMutationStatus !== 'creating' && <small className="mono" style={{fontSize:10, color: briefMutationStatus === 'created' ? 'var(--accent)' : 'var(--danger)'}}>{briefMutationStatus === 'created' ? '已创建' : briefMutationStatus}</small>}
                </div>
              ) : (
                <button className="btn sm ghost" style={{width:'100%'}} onClick={() => { setBriefCreateOpen(true); setBriefMutationStatus(''); }}>
                  <Icon name="plus" className="icon sm"/>新建 Brief
                </button>
              )}
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-header">
            {editingTitleId === activeCampaign?.id ? (
              <div style={{display:'flex', alignItems:'center', gap:6, flex:'1 1 auto', minWidth:0}}>
                <input
                  className="input"
                  value={titleDraft}
                  onChange={event => setTitleDraft(event.target.value)}
                  onKeyDown={event => {
                    if (event.key === 'Enter') saveBriefTitle();
                    if (event.key === 'Escape') setEditingTitleId('');
                  }}
                  style={{height:32, minWidth:0}}
                  autoFocus
                />
                <button className="btn sm primary" onClick={saveBriefTitle} disabled={briefMutationStatus === 'saving-title'}>
                  <Icon name="save" className="icon sm"/>{briefMutationStatus === 'saving-title' ? '保存中' : '保存'}
                </button>
                <button className="btn sm ghost" onClick={() => setEditingTitleId('')}><Icon name="x" className="icon sm"/>取消</button>
              </div>
            ) : (
              <h3 style={{minWidth:0}}>{activeCampaign?.name || 'Brief'}</h3>
            )}
            <span className="count">{getBriefContentCount(activeCampaign, postLookup)} 内容</span>
            <span className={`tag mono ${getBriefPublishStatusMeta(activeCampaign).tagClass}`} style={{fontSize:10}}>{getBriefPublishStatusMeta(activeCampaign).label}</span>
            <div className="actions">
              <span className="chip" style={{fontSize:10}}><span className="dot"></span>边界自检通过</span>
              {editingTitleId !== activeCampaign?.id && (
                <button className="btn sm ghost" onClick={beginTitleEdit}><Icon name="edit" className="icon sm"/>改标题</button>
              )}
              <select
                className="input"
                value={getBriefPublishState(activeCampaign)}
                onChange={event => updateBriefPublishState(event.target.value)}
                disabled={!activeCampaign || briefMutationStatus === 'saving-status'}
                style={{width:94, height:30, fontSize:12}}
              >
                <option value="published">已发布</option>
                <option value="draft">草稿</option>
              </select>
              <button className="btn sm ghost">重新生成</button>
            </div>
          </div>
          <div className="card-body" style={{display:'flex', flexDirection:'column', gap:14}}>
            <BriefMetaStrip brief={activeCampaign} postLookup={postLookup}/>
            <MasterBriefBody brief={activeCampaign?.masterBrief} campaign={activeCampaign} strategyThesis={strategyThesis}/>
            <BriefContainedContent brief={activeCampaign} socialPosts={ownedSocialPosts}/>
            <ContentBoundaryBriefBody brief={activeCampaign?.masterBrief} campaign={activeCampaign}/>
          </div>
        </div>

        <div className="col">
          <BriefMarketContextCard handoff={marketHandoffs[0]}/>

          <div className="card">
            <div className="card-header"><h3>品牌边界 · 自动注入</h3></div>
            <div className="card-body" style={{display:'flex', flexDirection:'column', gap:8}}>
              <BoundaryRow type="block" label="禁用语言" items={['秒杀', '碾压', '断崖式', '屠榜']}/>
              <BoundaryRow type="avoid" label="慎用方向" items={['赛车对比', '暴力加速', '硬核科技']}/>
              <BoundaryRow type="prefer" label="鼓励方向" items={['静奢', '克制', '家庭场景', '夜间美学']}/>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><h3>QA Checklist</h3><span className="count">8 / 8</span></div>
            <div className="card-body" style={{display:'flex', flexDirection:'column', gap:6}}>
              {['品牌定位一致','命题清晰','人群边界','CTA 明确','平台语法适配','禁用词扫描','法务风险','审核流程'].map((c, i) => (
                <div key={i} style={{display:'flex', alignItems:'center', gap:8, fontSize:12}}>
                  <Icon name="check" className="icon sm" style={{color:'var(--accent)'}}/>
                  <span>{c}</span>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
      )}
    </div>
  );
};

const BriefMarketContextCard = ({ handoff }) => {
  const pack = handoff?.contextPack || {};
  const snapshot = pack.market_snapshot || {};
  const risks = pack.risk_notes || [];
  const whiteSpaces = pack.competitive_benchmark?.white_spaces || [];
  return (
    <div className="card strategy-market-input">
      <div className="card-header">
        <h3>Market Context</h3>
        <span className="count">{handoff ? 'ready' : 'none'}</span>
      </div>
      <div className="card-body" style={{display:'grid', gap:8}}>
        {handoff ? (
          <>
            <p>{snapshot.summary}</p>
            <div className="market-signal-themes">
              {whiteSpaces.slice(0, 2).map(item => <span key={item} className="tag mono">{item}</span>)}
              {risks.slice(0, 2).flatMap(item => item.review_reason || []).map(item => <span key={item} className="tag mono">{item}</span>)}
            </div>
          </>
        ) : (
          <div className="market-empty-state">暂无市场感知 handoff。</div>
        )}
      </div>
    </div>
  );
};

const BriefContentMappingView = ({ posts = [], briefs = [], postLookup = null, mappingDrafts = {}, mappingStatus = {}, onDraftChange, onSave }) => {
  const briefById = React.useMemo(() => new Map(briefs.map(brief => [brief.id, brief])), [briefs]);
  return (
    <div className="card">
      <div className="card-header">
        <h3>内容挂载 · 根据内容主体选择 Brief</h3>
        <span className="count">{posts.length}</span>
      </div>
      <div className="card-body" style={{display:'grid', gap:12}}>
        <div className="callout" style={{fontSize:13, margin:0}}>
          这里只处理“已发内容 {'->'} Brief”的映射。先读发布日期、平台、标题和正文主体，再选择对应 Brief 保存。
        </div>
        <div style={{display:'grid', gap:10}}>
          <div className="mono" style={{display:'grid', gridTemplateColumns:'120px 100px minmax(0,1fr) 220px 96px', gap:10, padding:'0 12px', fontSize:10, color:'var(--ink-4)', textTransform:'uppercase'}}>
            <span>发布日期</span>
            <span>平台</span>
            <span>标题 / 正文</span>
            <span>目标 Brief</span>
            <span>操作</span>
          </div>
          {posts.map(post => {
            const postId = post.post_id || post.content_id || post.id;
            const currentBriefId = getAssignedBriefId(post);
            const draftBriefId = Object.prototype.hasOwnProperty.call(mappingDrafts, postId) ? mappingDrafts[postId] : currentBriefId;
            const currentBrief = briefById.get(currentBriefId);
            const status = mappingStatus[postId] || '';
            const body = getBriefPostBody(post);
            return (
              <article key={postId} style={{display:'grid', gridTemplateColumns:'120px 100px minmax(0,1fr) 220px 96px', gap:10, alignItems:'start', padding:12, background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8}}>
                <div className="mono" style={{fontSize:11, color:'var(--ink-4)'}}>{getBriefPostDate(post) || '-'}</div>
                <div><span className="tag">{post.platform || '-'}</span></div>
                <div style={{display:'grid', gap:5, minWidth:0}}>
                  <b style={{fontSize:13, lineHeight:1.4}}>{getBriefPostTitle(post)}</b>
                  <span style={{fontSize:12, color:'var(--ink-3)', lineHeight:1.55, maxHeight:74, overflow:'hidden'}}>{body || '正文为空；请根据标题、平台和原始内容链接判断。'}</span>
                  <small className="mono" style={{color:'var(--ink-4)'}}>当前 Brief：{currentBrief?.name || '未配置'}</small>
                </div>
                <select className="input" value={draftBriefId || ''} onChange={event => onDraftChange(postId, event.target.value)} style={{height:32, fontSize:12}}>
                  <option value="">未配置</option>
                  {briefs.map(brief => <option key={brief.id} value={brief.id}>{getBriefOptionLabel(brief, postLookup)}</option>)}
                </select>
                <div style={{display:'grid', gap:5}}>
                  <button className="btn sm primary" onClick={() => onSave(post, draftBriefId || '')} disabled={status === 'saving'}>
                    {status === 'saving' ? '保存中' : '保存'}
                  </button>
                  {status && status !== 'saving' && <small className="mono" style={{fontSize:10, color: status === 'saved' ? 'var(--accent)' : 'var(--danger)'}}>{status === 'saved' ? '已保存' : status}</small>}
                </div>
              </article>
            );
          })}
          {!posts.length && <div className="callout" style={{fontSize:13, margin:0}}>暂无可挂载的已发内容。</div>}
        </div>
      </div>
    </div>
  );
};

const MasterBriefBody = ({ brief, campaign, strategyThesis }) => (
  <>
    {strategyThesis ? (
      <Section label="策略母题约束">
        <div style={{display:'grid', gap:6}}>
          <span style={{fontFamily:'var(--f-serif)', fontSize:15}}>{strategyThesis.coreThesis}</span>
          <small style={{color:'var(--ink-3)', lineHeight:1.55}}>{strategyThesis.marketBelief}</small>
          <div style={{display:'flex', gap:5, flexWrap:'wrap'}}>
            {(strategyThesis.contentParadigm || []).slice(0, 9).map(item => <span key={item} className="tag mono">{item}</span>)}
          </div>
        </div>
      </Section>
    ) : null}
    <Section label="策略对象背景">
      {brief?.background || `${campaign?.name || '当前策略对象'} 的背景待补充。`}
    </Section>
    <Section label="本轮目标">
      {brief?.objective || '主目标待补充。'}
    </Section>
    <Section label="目标人群">
      {brief?.audienceSummary || '目标人群待补充。'}
    </Section>
    <Section label="核心命题">
      <span style={{fontFamily:'var(--f-serif)', fontStyle:'italic', fontSize:15}}>{brief?.coreMessage || campaign?.coreMessage || '核心命题待确认。'}</span>
    </Section>
    <Section label="卖点优先级">
      <ol style={{margin:'4px 0', paddingLeft:20}}>
        {(brief?.sellingPoints?.length ? brief.sellingPoints : campaign?.sellingPoints || []).map(item => <li key={item}>{item}</li>)}
      </ol>
    </Section>
    <Section label="CTA 要求">
      {brief?.cta || campaign?.cta || 'CTA 待补充。'}
    </Section>
  </>
);

const BriefMetaStrip = ({ brief, postLookup }) => (
  <div style={{display:'grid', gridTemplateColumns:'repeat(5,minmax(0,1fr))', gap:10}}>
    <KV label="日期" value={formatBriefDate(brief, postLookup)}/>
    <KV label="状态" value={getBriefPublishStatusMeta(brief).label}/>
    <KV label="内容数" value={String(getBriefContentCount(brief, postLookup))}/>
    <KV label="挂接内容" value={String(postLookup ? (brief?.assetPostIds || []).filter(id => postLookup.has(id)).length : (brief?.assetPostIds || []).length)}/>
    <KV label="类型" value={briefTypeLabel(brief)}/>
  </div>
);

const BriefContainedContent = ({ brief = {}, socialPosts = [] }) => {
  const item = brief || {};
  const variants = item.platformVariants || [];
  const assetPosts = getBriefAssetPosts(item, socialPosts).sort((a, b) => {
    const at = new Date(String(getBriefPostDate(a)).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime() || 0;
    const bt = new Date(String(getBriefPostDate(b)).replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/')).getTime() || 0;
    return bt - at;
  });
  const rows = assetPosts.length
    ? assetPosts.map(post => ({ platform: post.platform || '-', title: post.title || post.caption || post.description || '未命名内容', publishedAt: getBriefPostDate(post), id: post.post_id || post.content_id || post.id }))
    : variants.map((variant, index) => ({ ...variant, id: `${variant.platform}-${variant.title}-${index}` }));
  if (!rows.length) {
    return (
      <Section label="包含内容">
        <div className="callout" style={{fontSize:13, margin:0}}>暂无已挂接的平台内容。</div>
      </Section>
    );
  }
  return (
    <Section label="包含内容">
      <div style={{display:'grid', gap:7}}>
        {rows.map(row => (
          <div key={row.id} style={{display:'grid', gridTemplateColumns:'86px 1fr auto', gap:8, alignItems:'center', padding:'7px 8px', border:'1px solid var(--hairline)', borderRadius:6, background:'var(--bg-2)'}}>
            <span className="tag" style={{fontSize:10}}>{row.platform}</span>
            <span style={{fontSize:12, lineHeight:1.45, minWidth:0}}>{row.title || row.unifiedTopic}</span>
            <span className="mono" style={{fontSize:10, color:'var(--ink-4)'}}>{row.publishedAt || '-'}</span>
          </div>
        ))}
      </div>
    </Section>
  );
};

const ContentBoundaryBriefBody = ({ brief, campaign }) => {
  const campaignBoundaries = brief?.contentBoundaries?.length ? brief.contentBoundaries : campaign?.contentBoundaries || ['延续品牌长期定位和本轮核心命题', '不要把短期热点凌驾于品牌记忆之上', '所有内容必须保留清晰 CTA'];
  const riskBoundaries = brief?.riskBoundaries?.length ? brief.riskBoundaries : campaign?.riskBoundaries || ['禁用夸张促销表达', '避免无证据竞品拉踩', '能力表达需带条件和场景'];
  return (
    <>
      <Section label="项目内容边界">
        <BoundaryNoteList items={campaignBoundaries} tone="accent"/>
      </Section>
      <Section label="项目风险边界">
        <BoundaryNoteList items={riskBoundaries} tone="warn"/>
      </Section>
      <Section label="全平台 Hard Blocks">
        <BoundaryNoteList items={UNIVERSAL_CONTENT_BOUNDARIES.hardBlocks} tone="danger" columns={2}/>
      </Section>
      <Section label="披露 / 证据 / 谨慎声明">
        <BoundaryNoteList items={UNIVERSAL_CONTENT_BOUNDARIES.cautionClaims} tone="info"/>
      </Section>
      <Section label="达人内容创作 handoff">
        <div className="callout" style={{fontSize:13, margin:0}}>
          Brief 只输出内容边界和风险约束；平台变体规则、达人开场、场景脚本、多达人改写版由 AIGC 内容生成模块继续叠加。
        </div>
      </Section>
    </>
  );
};

const BoundaryNoteList = ({ items, tone = 'accent', columns = 1 }) => {
  const colors = {
    accent: ['var(--accent)', 'var(--accent-tint)'],
    warn: ['var(--warn)', 'var(--warn-tint)'],
    danger: ['var(--danger)', 'var(--danger-tint)'],
    info: ['var(--info)', 'var(--info-tint)'],
  };
  const [color, bg] = colors[tone] || colors.accent;
  return (
    <div style={{display:'grid', gridTemplateColumns:`repeat(${columns}, minmax(0, 1fr))`, gap:6}}>
      {items.map(item => (
        <div key={item} style={{display:'flex', gap:7, alignItems:'flex-start', padding:'7px 8px', border:`1px solid color-mix(in oklch, ${color} 24%, var(--hairline))`, borderRadius:6, background:bg}}>
          <span style={{width:6, height:6, borderRadius:'50%', background:color, marginTop:7, flex:'0 0 auto'}}></span>
          <span style={{fontSize:12, lineHeight:1.55, color:'var(--ink-2)'}}>{item}</span>
        </div>
      ))}
    </div>
  );
};

const PlatformBoundaryCard = ({ item }) => (
  <div style={{border:'1px solid var(--hairline)', borderRadius:8, padding:10, background:'var(--bg-2)', display:'flex', flexDirection:'column', gap:8}}>
    <div style={{display:'flex', justifyContent:'space-between', gap:8, alignItems:'flex-start'}}>
      <div>
        <div style={{fontSize:13, fontWeight:600}}>{item.platform}</div>
        <div style={{fontSize:10, color:'var(--ink-4)', fontFamily:'var(--f-mono)', marginTop:2}}>{item.family}</div>
      </div>
      <span className="tag" style={{fontSize:10}}>source</span>
    </div>
    <div style={{fontSize:10, color:'var(--ink-4)', lineHeight:1.45}}>{item.source}</div>
    <MiniBoundaryGroup label="Format" items={item.formatRules}/>
    <MiniBoundaryGroup label="Link" items={item.linkRules}/>
    <MiniBoundaryGroup label="Creator" items={item.creatorRules}/>
    <MiniBoundaryGroup label="Review Triggers" items={item.reviewTriggers} warn/>
  </div>
);

const MiniBoundaryGroup = ({ label, items, warn }) => (
  <div>
    <div style={{fontFamily:'var(--f-mono)', fontSize:9, color: warn ? 'var(--warn)' : 'var(--ink-4)', textTransform:'uppercase', marginBottom:3}}>{label}</div>
    <ul style={{margin:0, paddingLeft:15, color:'var(--ink-3)', fontSize:11, lineHeight:1.45}}>
      {items.slice(0, 2).map(item => <li key={item}>{item}</li>)}
    </ul>
  </div>
);

const CreatorContentVariantBody = ({ creator }) => (
  <>
    <div style={{display:'flex', gap:12, alignItems:'center', padding:12, background:'var(--bg-2)', borderRadius:8, border:'1px solid var(--hairline)'}}>
      <div className="avatar" style={{width:36, height:36, fontSize:14}}>{creator.avatar}</div>
      <div>
        <div style={{fontSize:14, fontWeight:500}}>{creator.handle}</div>
        <div style={{fontSize:11, color:'var(--ink-3)'}}>适配风格：{creator.style}</div>
      </div>
    </div>
    <Section label="必须出现">
      <ul style={{margin:'4px 0', paddingLeft:20}}>
        {creator.must.map((m, i) => <li key={i}>{m}</li>)}
      </ul>
    </Section>
    <Section label="避免">
      <ul style={{margin:'4px 0', paddingLeft:20, color:'var(--ink-3)'}}>
        {creator.avoid.map((a, i) => <li key={i}>{a}</li>)}
      </ul>
    </Section>
    <Section label="个性化开场建议">
      <div className="callout" style={{fontSize:13, margin:0}}>
        {creator.opening}
      </div>
    </Section>
  </>
);

const AigcPlatformRuleBody = ({ rule }) => {
  const boundary = PLATFORM_CONTENT_BOUNDARIES.find(item => rule?.platform && (item.platform === rule.platform || item.platform.includes(rule.platform) || rule.platform.includes(item.platform)));
  return (
    <Section label="平台变体规则">
      <div style={{display:'grid', gap:10}}>
        <div style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:10}}>
          <KV label="平台" value={rule.platform}/>
          <KV label="变体" value={rule.label}/>
          <KV label="篇幅/格式" value={rule.len}/>
        </div>
        <div style={{padding:10, background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8}}>
          <div style={{fontSize:12, fontWeight:600, marginBottom:6}}>{rule.tone}</div>
          <ul style={{margin:0, paddingLeft:18, fontSize:12, color:'var(--ink-2)', lineHeight:1.6}}>
            {rule.hooks.map(item => <li key={item}>{item}</li>)}
          </ul>
        </div>
        {boundary ? (
          <div style={{display:'grid', gridTemplateColumns:'repeat(2,minmax(0,1fr))', gap:8}}>
            <MiniBoundaryGroup label="Format" items={boundary.formatRules}/>
            <MiniBoundaryGroup label="Review Triggers" items={boundary.reviewTriggers} warn/>
          </div>
        ) : null}
      </div>
    </Section>
  );
};

const Section = ({ label, children }) => (
  <div>
    <div style={{fontFamily:'var(--f-mono)', fontSize:10, color:'var(--ink-4)', textTransform:'uppercase', letterSpacing:'0.08em', marginBottom:6}}>{label}</div>
    <div style={{fontSize:13, lineHeight:1.65, color:'var(--ink-2)'}}>{children}</div>
  </div>
);

const KV = ({ label, value }) => (
  <div style={{padding:8, background:'var(--bg-2)', borderRadius:6, border:'1px solid var(--hairline)'}}>
    <div style={{fontFamily:'var(--f-mono)', fontSize:9, color:'var(--ink-4)', textTransform:'uppercase'}}>{label}</div>
    <div style={{fontSize:13, marginTop:2}}>{value}</div>
  </div>
);

const BoundaryRow = ({ type, label, items }) => {
  const colors = { block: ['var(--danger)', 'var(--danger-tint)'], avoid: ['var(--warn)', 'var(--warn-tint)'], prefer: ['var(--accent)', 'var(--accent-tint)'] };
  const [c, bg] = colors[type];
  return (
    <div>
      <div style={{fontFamily:'var(--f-mono)', fontSize:10, color: c, textTransform:'uppercase', marginBottom:4}}>{label}</div>
      <div style={{display:'flex', gap:4, flexWrap:'wrap'}}>
        {items.map(i => <span key={i} style={{fontSize:11, padding:'2px 7px', background:bg, color:c, borderRadius:3, border:`1px solid color-mix(in oklch, ${c} 25%, var(--hairline))`}}>{i}</span>)}
      </div>
    </div>
  );
};

// ========== 06 · CONTENT GENERATION + QA ==========
const PR_REVIEW_SKILL_SUMMARY = {
  name: 'Chief Public Relations Officer',
  principles: [
    '危机中先处理情绪，再处理事实',
    '品牌不要和用户争夺解释权',
    '不要说“你误会了 / 本意不是 / 过度解读”',
    '能改文案就不要硬解释',
  ],
};

const runPrContentAudit = (draft = {}) => {
  const text = `${draft.title || ''}\n${draft.hook || ''}\n${draft.body || ''}`;
  const rules = [
    { level: 'S3', token: '玻璃心', note: '默认用户敏感，容易升级舆情' },
    { level: 'S2', token: '你误会了', note: '品牌与用户争夺解释权' },
    { level: 'S2', token: '本意不是', note: '先解释自己，未承接用户情绪' },
    { level: 'S2', token: '过度解读', note: '指责用户解读，容易被截图传播' },
    { level: 'S2', token: '牛马', note: '品牌替用户自嘲，存在冒犯风险' },
    { level: 'S2', token: '买不起', note: '可能被理解为品牌轻视用户' },
    { level: 'S2', token: '秒杀', note: '过强促销/对比表达，需改写' },
    { level: 'S1', token: '很快', note: '模糊能力表达，建议量化' },
    { level: 'S1', token: '绝了', note: '口语化夸张，需判断是否符合品牌语气' },
  ];
  const hits = rules.filter(rule => text.includes(rule.token));
  const rank = { S0: 0, S1: 1, S2: 2, S3: 3, S4: 4 };
  const level = hits.reduce((max, hit) => rank[hit.level] > rank[max] ? hit.level : max, 'S0');
  const action = {
    S0: '无明显风险，仅需监测',
    S1: '轻微争议，建议优化表达或温和回应',
    S2: '中度舆情，建议修改内容并准备公开回应',
    S3: '严重舆情，建议统一口径并正式说明',
    S4: '重大危机，需启动危机公关机制',
  }[level];
  return { level, hits, action };
};

const CONTENT_DRAFTS = [
  { title: '当城市开始静音，奥瑞利亚把"家"开到了夜里', hook: '前3行：雨后停车场一滴水声', body: '以静奢为锚。女儿在后排睡着。一公里路，像时间被装进抽屉。', platform: '小红书', score: 92, deviation: 0.04 },
  { title: '26dB · 比你家卧室还安静的车', hook: '前3行：数字对比 · 引发好奇', body: '26dB 意味着什么？我们请 4 位父亲在车里读睡前故事。', platform: '小红书', score: 88, deviation: 0.08 },
  { title: '第二台车，为什么越来越多人选静音？', hook: '前3s：静音开关声对比', body: '15 秒切片 · 3 个场景 · 0 参数。', platform: '抖音', score: 85, deviation: 0.12 },
  { title: '夜归的人，需要怎样的车位', hook: '图集 · 9 张夜间停车美学', body: '回家那一刻才是真正的驾驶。', platform: '微博', score: 79, deviation: 0.18 },
];

const CREATOR_CONTENT_VARIANTS = [
  {
    id: 'muxi',
    handle: '@木兮的车库',
    avatar: '木',
    platform: '小红书',
    role: '策略型',
    style: '家庭视角 · 细腻克制',
    must: ['必须出现 3 岁女儿场景', '必须出现夜间小区停车', '必须出现后排拉开'],
    avoid: ['避免过度对比', '避免讲 0-100 加速'],
    opening: '"上周末接小朋友下课，雨刚停，车位一片水光。我第一次觉得，静是一种可以被看见的奢侈。"'
  },
  {
    id: 'wlsc',
    handle: '@未来说车',
    avatar: '未',
    platform: '抖音',
    role: '策略型',
    style: '理性工程 · 拆解型',
    must: ['静音实测数据', '夜充全流程录制', '对比传统燃油而非竞品'],
    avoid: ['不要主观情绪', '不要使用 "秒杀"'],
    opening: '"今天不聊豪华词汇，只把夜间停车、家充和后排静音拆成三段实测。"'
  },
  {
    id: 'lucia',
    handle: '@Lucia的设计日记',
    avatar: 'L',
    platform: '小红书',
    role: '策略型',
    style: '设计师视角 · 图像感',
    must: ['出现工作室夜归场景', '出现材质特写', '配色参考她的 vision board'],
    avoid: ['避免技术参数堆砌', '避免俯拍视角'],
    opening: '"收工时城市的颜色会变低，车内材质和灯光反而决定了我愿不愿意慢下来。"'
  },
];

const Content = () => {
  const [selected, setSelected] = React.useState(0);
  const [selectedCreator, setSelectedCreator] = React.useState(0);
  const [selectedPlatformRule, setSelectedPlatformRule] = React.useState(0);
  const d = CONTENT_DRAFTS[selected];
  const creator = CREATOR_CONTENT_VARIANTS[selectedCreator];
  const platformRule = AIGC_PLATFORM_VARIANT_RULES[selectedPlatformRule] || AIGC_PLATFORM_VARIANT_RULES[0];
  const prAudit = runPrContentAudit(d);

  return (
    <div className="page wide">
      <div className="page-header">
        <div className="heading">
          <h1>内容生成 · <span className="hl">品牌一致性</span>质检</h1>
          <p>AIGC 不是自由创作。每一条内容生成时受品牌、策略、brief、平台、达人、风险词六层约束。</p>
        </div>
        <div className="actions">
          <button className="btn"><Icon name="spark" className="icon sm"/>再生成一批</button>
          <button className="btn primary">推送审核 <Icon name="arrow" className="icon sm"/></button>
        </div>
      </div>

      <div className="grid" style={{gridTemplateColumns:'320px 1fr 320px'}}>
        <div className="col">
          <div className="card">
            <div className="card-header"><h3>草稿队列</h3><span className="count">{CONTENT_DRAFTS.length}</span></div>
            <div className="card-body flush">
              {CONTENT_DRAFTS.map((c, i) => (
                <div
                  key={i}
                  onClick={() => setSelected(i)}
                  style={{
                    padding:'12px 14px',
                    borderLeft: selected === i ? '2px solid var(--accent)' : '2px solid transparent',
                    borderBottom:'1px solid var(--hairline)',
                    background: selected === i ? 'var(--accent-tint)' : 'transparent',
                    cursor:'pointer',
                  }}
                >
                  <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:4}}>
                    <span className="tag" style={{fontSize:10}}>{c.platform}</span>
                    <span className="mono" style={{fontSize:11, fontWeight:500, color: c.score > 85 ? 'var(--accent)' : c.score > 75 ? 'var(--ink-2)' : 'var(--warn)'}}>{c.score}</span>
                  </div>
                  <div style={{fontSize:12, fontWeight:500, lineHeight:1.4}}>{c.title}</div>
                  <div style={{fontSize:10, color:'var(--ink-4)', marginTop:4, fontFamily:'var(--f-mono)'}}>偏航 {c.deviation.toFixed(2)}</div>
                </div>
              ))}
            </div>
          </div>

          <div className="card">
            <div className="card-header"><h3>达人内容创作</h3><span className="count">{CREATOR_CONTENT_VARIANTS.length}</span></div>
            <div className="card-body flush">
              {CREATOR_CONTENT_VARIANTS.map((item, i) => (
                <div
                  key={item.id}
                  onClick={() => setSelectedCreator(i)}
                  style={{
                    padding:'12px 14px',
                    borderLeft: selectedCreator === i ? '2px solid var(--accent)' : '2px solid transparent',
                    borderBottom:'1px solid var(--hairline)',
                    background: selectedCreator === i ? 'var(--accent-tint)' : 'transparent',
                    cursor:'pointer',
                  }}
                >
                  <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:4}}>
                    <span style={{fontSize:12, fontWeight:600}}>{item.handle}</span>
                    <span className="tag" style={{fontSize:10}}>{item.platform}</span>
                  </div>
                  <div style={{fontSize:11, color:'var(--ink-3)', lineHeight:1.45}}>{item.role} · {item.style}</div>
                </div>
              ))}
            </div>
          </div>

          <div className="card">
            <div className="card-header"><h3>平台变体规则</h3><span className="count">{AIGC_PLATFORM_VARIANT_RULES.length}</span></div>
            <div className="card-body flush">
              {AIGC_PLATFORM_VARIANT_RULES.map((item, i) => (
                <div
                  key={item.id}
                  onClick={() => setSelectedPlatformRule(i)}
                  style={{
                    padding:'12px 14px',
                    borderLeft: selectedPlatformRule === i ? '2px solid var(--accent)' : '2px solid transparent',
                    borderBottom:'1px solid var(--hairline)',
                    background: selectedPlatformRule === i ? 'var(--accent-tint)' : 'transparent',
                    cursor:'pointer',
                  }}
                >
                  <div style={{display:'flex', justifyContent:'space-between', gap:8, alignItems:'center', marginBottom:4}}>
                    <span style={{fontSize:12, fontWeight:600}}>{item.platform}</span>
                    <span className="tag" style={{fontSize:10}}>{item.len}</span>
                  </div>
                  <div style={{fontSize:11, color:'var(--ink-3)', lineHeight:1.45}}>{item.label} · {item.tone}</div>
                </div>
              ))}
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-header">
            <h3>{creator.handle} · 达人改写预览</h3>
            <span className="count">GPT-Aurelia v2</span>
            <div className="actions">
              <button className="btn sm ghost">再生成</button>
              <button className="btn sm ghost">锁定</button>
            </div>
          </div>
          <div className="card-body" style={{display:'flex', flexDirection:'column', gap:14}}>
            <PlatformMock platform={creator.platform || d.platform} title={d.title} body={d.body} hook={d.hook} creator={creator}/>

            <CreatorContentVariantBody creator={creator}/>

            <AigcPlatformRuleBody rule={platformRule}/>

            <Section label="标题候选">
              {CONTENT_DRAFTS.slice(0, 4).map((c, i) => (
                <div key={i} style={{padding:'6px 0', borderBottom: i < 3 ? '1px dashed var(--hairline)' : 'none', display:'flex', alignItems:'center', gap:8}}>
                  <span className="mono" style={{fontSize:10, color:'var(--ink-4)', minWidth:18}}>0{i+1}</span>
                  <span style={{flex:1, fontSize:12}}>{c.title}</span>
                  <span className="mono" style={{fontSize:11, color:'var(--ink-3)'}}>预估 CTR {(3 + i * 0.4).toFixed(1)}%</span>
                </div>
              ))}
            </Section>

            <Section label="钩子 · 前 3 秒">
              <div className="callout" style={{fontSize:13, margin:0}}>{d.hook}</div>
            </Section>

            <Section label="正文结构">
              <div style={{fontSize:13, color:'var(--ink-2)', lineHeight:1.7}}>{d.body}</div>
            </Section>
          </div>
        </div>

        <div className="col">
          <QaScoreCard score={d.score}/>
          <div className="card">
            <div className="card-header"><h3>质检维度</h3></div>
            <div className="card-body" style={{display:'flex', flexDirection:'column', gap:10}}>
              <QaDim label="品牌一致性" v={92} ok/>
              <QaDim label="调性匹配" v={89} ok/>
              <QaDim label="达人风格约束" v={91} ok/>
              <QaDim label="卖点不偏航" v={95} ok/>
              <QaDim label="结构完整度" v={85} ok/>
              <QaDim label="封面吸引力" v={82} ok/>
              <QaDim label="风险词扫描" v={100} ok/>
              <QaDim label="PR 舆情审核" v={prAudit.level === 'S0' ? 100 : prAudit.level === 'S1' ? 86 : 68} ok={['S0', 'S1'].includes(prAudit.level)}/>
              <QaDim label="合规性" v={98} ok/>
              <QaDim label="平台语法适配" v={86} ok/>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><h3>PR 舆情检测</h3><span className="count">{prAudit.level}</span></div>
            <div className="card-body" style={{display:'flex', flexDirection:'column', gap:8}}>
              <div style={{padding:8, background: prAudit.level === 'S0' ? 'var(--accent-tint)' : 'var(--warn-tint)', border:'1px solid var(--hairline)', borderRadius:6}}>
                <div style={{fontSize:10, fontFamily:'var(--f-mono)', color: prAudit.level === 'S0' ? 'var(--accent)' : 'var(--warn)', textTransform:'uppercase', marginBottom:2}}>{PR_REVIEW_SKILL_SUMMARY.name}</div>
                <div style={{fontSize:12}}>{prAudit.action}</div>
              </div>
              {(prAudit.hits.length ? prAudit.hits : [{ token: '无红旗表达', note: '继续监测评论区反馈' }]).map(hit => (
                <div key={hit.token} style={{fontSize:12, padding:'6px 0', borderBottom:'1px dashed var(--hairline)'}}>
                  <b>{hit.token}</b> · {hit.note}
                </div>
              ))}
            </div>
          </div>
          <div className="card">
            <div className="card-header"><h3>修订建议</h3><span className="count">2</span></div>
            <div className="card-body" style={{display:'flex', flexDirection:'column', gap:8}}>
              <div style={{padding:8, background:'var(--warn-tint)', border:'1px solid color-mix(in oklch, var(--warn) 25%, var(--hairline))', borderRadius:6}}>
                <div style={{fontSize:10, fontFamily:'var(--f-mono)', color:'var(--warn)', textTransform:'uppercase', marginBottom:2}}>表述模糊</div>
                <div style={{fontSize:12}}>"充电很快" → 建议量化为"夜间家充 7h 满"</div>
              </div>
              <div style={{padding:8, background:'var(--info-tint)', border:'1px solid color-mix(in oklch, var(--info) 25%, var(--hairline))', borderRadius:6}}>
                <div style={{fontSize:10, fontFamily:'var(--f-mono)', color:'var(--info)', textTransform:'uppercase', marginBottom:2}}>平台建议</div>
                <div style={{fontSize:12}}>小红书标题可加入表情分隔，提升点击</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

const PlatformMock = ({ platform, title, body, hook, creator }) => (
  <div style={{padding:16, background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8}}>
    <div style={{display:'flex', alignItems:'center', gap:10, marginBottom:12}}>
      <div className="avatar" style={{width:28, height:28, fontSize:11}}>{creator?.avatar || 'M'}</div>
      <div style={{flex:1}}>
        <div style={{fontSize:12, fontWeight:500}}>{creator?.handle || '@木兮的车库'}</div>
        <div style={{fontSize:10, color:'var(--ink-4)', fontFamily:'var(--f-mono)'}}>{platform} · 达人内容草稿</div>
      </div>
      <span className="chip ghost mono" style={{fontSize:10}}>DRAFT</span>
    </div>
    {/* placeholder image */}
    <div style={{height:200, background:`repeating-linear-gradient(135deg, var(--bg-3) 0, var(--bg-3) 10px, var(--surface) 10px, var(--surface) 20px)`, borderRadius:6, display:'grid', placeItems:'center', color:'var(--ink-4)', fontFamily:'var(--f-mono)', fontSize:11, marginBottom:12, border:'1px solid var(--hairline)'}}>
      [ 封面图 · 夜景停车场 · 待生成 ]
    </div>
    <div style={{fontSize:16, fontFamily:'var(--f-serif)', lineHeight:1.3, marginBottom:8}}>{title}</div>
    <div style={{fontSize:12, color:'var(--ink-2)', lineHeight:1.7}}>
      <span style={{background:'var(--accent-tint)', padding:'1px 4px', borderRadius:3, color:'var(--accent)'}}>{hook}</span>
      <br/><br/>
      {body}
    </div>
    <div style={{display:'flex', gap:8, marginTop:12}}>
      {['#静奢风', '#AX7', '#夜归人', '#第二台车'].map(t => <span key={t} className="tag accent" style={{fontSize:11}}>{t}</span>)}
    </div>
  </div>
);

const QaScoreCard = ({ score }) => (
  <div className="card">
    <div className="card-header"><h3>总分</h3><span className="count">QA v2</span></div>
    <div className="card-body" style={{display:'flex', alignItems:'center', gap:14}}>
      <svg viewBox="0 0 80 80" width="80" height="80">
        <circle cx="40" cy="40" r="34" fill="none" stroke="var(--hairline)" strokeWidth="6"/>
        <circle cx="40" cy="40" r="34" fill="none" stroke="var(--accent)" strokeWidth="6"
                strokeDasharray={`${(score/100)*214} 214`} transform="rotate(-90 40 40)" strokeLinecap="round"/>
        <text x="40" y="46" textAnchor="middle" fontSize="22" fontFamily="var(--f-serif)" fill="var(--ink)">{score}</text>
      </svg>
      <div>
        <div style={{fontSize:13, fontWeight:500}}>推荐发布</div>
        <div style={{fontSize:11, color:'var(--ink-3)', marginTop:2}}>高于基准 14 分 · 无红旗项</div>
      </div>
    </div>
  </div>
);

const QaDim = ({ label, v, ok }) => (
  <div>
    <div style={{display:'flex', justifyContent:'space-between', fontSize:11, marginBottom:3}}>
      <span>{label}</span>
      <span className="mono" style={{color: ok ? 'var(--accent)' : 'var(--warn)'}}>{v}</span>
    </div>
    <div className={`bar ${ok ? 'accent' : ''}`}><span style={{width: v + '%'}}></span></div>
  </div>
);

// ========== 07 · MONITOR ==========
const getSocialAccount = (accountId) => (SOCIAL_ACCOUNTS || []).find(account => account.account_id === accountId) || {};
const getPostComments = (postId) => (SOCIAL_COMMENTS || []).filter(comment => comment.post_id === postId);
const commentPrRank = { S0: 0, S1: 1, S2: 2, S3: 3, S4: 4 };
const commentLegalRank = { L0: 0, L1: 1, L2: 2, L3: 3, L4: 4 };
const trustBarrierPattern = /不敢买|不敢入|不敢下单|信不过|没信心|跑路|割韭菜|ppt车|概念版权/i;
const smallFactoryPattern = /小厂|小品牌|小公司|新势力/i;
const brandBackgroundClarifiedPattern = /不是小厂|不算小厂|不是什么小厂|背后.{0,12}(广汽|领程|大厂|主机厂|车企)|有.{0,12}(广汽|领程|大厂|主机厂|车企).{0,8}背景|原来.{0,12}(广汽|领程|大厂|主机厂|车企)/i;
const isNegativeBrandTrustText = (text = '') => trustBarrierPattern.test(text) || (smallFactoryPattern.test(text) && !brandBackgroundClarifiedPattern.test(text));
const getCommentRiskRank = (comment = {}) => {
  const sentiment = Number(comment.sentiment || 0);
  const prRank = commentPrRank[String(comment.pr_level || comment.public_opinion_level || '').toUpperCase()] || 0;
  const legalRank = commentLegalRank[String(comment.legal_level || '').toUpperCase()] || 0;
  const text = `${comment.text || ''} ${comment.keyword || ''} ${comment.risk_tag || ''}`;
  if (prRank >= 3 || legalRank >= 3 || sentiment < -0.55) return 3;
  if (comment.risk_tag || prRank >= 2 || legalRank >= 2 || sentiment < -0.15 || isNegativeBrandTrustText(text)) return 2;
  return 1;
};
const isRiskComment = (comment = {}) => getCommentRiskRank(comment) >= 2;
const positiveCommentPattern = /喜欢|想要|期待|终于|太帅|帅|好看|酷|真香|种草|不错|可以|给力|厉害|赞|支持|nice|love|cool|awesome|amazing|great|😍|❤️|👍|🔥/i;
const isPositiveComment = (comment = {}) => {
  if (isRiskComment(comment)) return false;
  if (socialNumber(comment.sentiment) > 0.15) return true;
  const prRank = commentPrRank[String(comment.pr_level || comment.public_opinion_level || '').toUpperCase()] || 0;
  const legalRank = commentLegalRank[String(comment.legal_level || '').toUpperCase()] || 0;
  const riskLevel = String(comment.pr_risk_level || '').toLowerCase();
  const text = `${comment.text || ''} ${comment.keyword || ''}`;
  if (isNegativeBrandTrustText(text)) return false;
  return prRank === 0 && legalRank <= 1 && riskLevel !== 'medium' && positiveCommentPattern.test(text);
};
const getCommentSentimentMeta = (comment = {}) => {
  const score = socialNumber(comment.sentiment);
  if (isRiskComment(comment)) return { tone: 'risk', label: '负面', score };
  if (isPositiveComment(comment)) return { tone: 'good', label: '正向', score };
  return { tone: 'neutral', label: '中性', score };
};
const commentConcernMeta = [
  { id: 'price_purchase', label: '价格/购买门槛', short: '价格购买', keywords: ['价格', '多少钱', '售价', '报价', '贵', '便宜', '买不起', '买得起', 'price', 'cost', 'expensive', 'cheap'] },
  { id: 'availability_delivery', label: '上市/量产/交付', short: '上市交付', keywords: ['上市', '量产', '开卖', '发售', '交付', '预定', '预订', '什么时候', 'available', 'release', 'launch', 'deliver', 'when can', 'want one'] },
  { id: 'product_specs', label: '配置/性能/能力', short: '配置能力', keywords: ['续航', '电池', '空间', '座椅', '内饰', '配置', '动力', '越野', '尺寸', '参数', '功能', '智能', 'range', 'battery', 'interior', 'feature', 'spec'] },
  { id: 'design_aesthetic', label: '外观/设计/科技感', short: '外观设计', keywords: ['外观', '设计', '造型', '方盒', '好看', '帅', '酷', '丑', '垃圾桶', '棺材', 'cybertruck', 'design', 'cool', 'beautiful', 'ugly', 'tescoba'] },
  { id: 'usage_scenario', label: '使用场景/生活方式', short: '使用场景', keywords: ['露营', '房车', '自驾', '旅行', '办公', '数字游民', '钓鱼', '越野穿越', 'camping', 'travel', 'fishing', 'home', 'office'] },
  { id: 'safety_reliability', label: '安全/可靠性', short: '安全可靠', keywords: ['安全', '事故', '失灵', '质量', '耐用', '可靠', '坏', '故障', '保命', '战争', 'toyota', 'reliable', 'safety', 'war'] },
  { id: 'brand_naming', label: '品牌/命名/表达', short: '品牌命名', keywords: ['牛马', '名字', '命名', '冒犯', '玻璃心', '本意不是', '品牌', 'name', 'brand'] },
  { id: 'proof_trust', label: '真实性/证据信任', short: '证据信任', keywords: ['ppt', '概念', '概念版权', '小厂', '不敢买', '不敢入', '信不过', '没信心', '实验室', '真车', '实车', '量产版', '真的假的', '证据', 'concept', 'real', 'prototype', 'fake'] },
  { id: 'competitor_compare', label: '竞品/国家对比', short: '竞品对比', keywords: ['特斯拉', 'tesla', 'cybertruck', 'toyota', 'lada', '中国', 'поднебесная', 'китай', 'китайцы'] },
  { id: 'complaint_after_sales', label: '投诉/售后', short: '投诉售后', keywords: ['投诉', '退款', '维权', '售后', '赔偿', '客服', 'complaint', 'refund', 'service'] },
  { id: 'positive_interest', label: '正向喜爱/种草', short: '正向喜爱', keywords: ['喜欢', '想要', '期待', '太帅', '好看', '酷', '真香', '种草', '不错', '厉害', '赞', 'nice', 'love', 'awesome', 'amazing', 'super', 'бомба', 'респект', 'супер'] },
  { id: 'other', label: '其他/待观察', short: '其他', keywords: [] },
];
const commentConcernById = Object.fromEntries(commentConcernMeta.map(item => [item.id, item]));
const getCommentConcern = (comment = {}) => {
  if (comment.user_concern_category) {
    const meta = commentConcernById[comment.user_concern_category] || {};
    return { id: comment.user_concern_category, label: comment.user_concern_label || meta.label || '其他/待观察', short: meta.short || comment.user_concern_label || '其他' };
  }
  const text = `${comment.text || ''} ${comment.keyword || ''} ${comment.risk_tag || ''}`.toLowerCase();
  const hit = commentConcernMeta.find(item => item.id !== 'other' && item.keywords.some(keyword => text.includes(String(keyword).toLowerCase())));
  if (hit) return hit;
  if (socialNumber(comment.sentiment) > 0.15) return commentConcernById.positive_interest;
  return commentConcernById.other;
};
const socialNumber = (value) => Number(value || 0);
const socialInteraction = (post = {}) => socialNumber(post.likes) + socialNumber(post.comments) + socialNumber(post.shares) + socialNumber(post.saves);
const isExternalSocialPost = (post = {}) => {
  const type = `${post.source_type || ''} ${post.asset_type || ''} ${post.source_post_type || ''} ${post.relationship_status || ''}`.toLowerCase();
  return Boolean(
    post.is_official_post === false ||
    type.includes('earned') ||
    type.includes('ugc') ||
    type.includes('kol') ||
    type.includes('external')
  );
};
const needsChineseTranslation = (text = '') => {
  const clean = String(text || '').trim();
  return Boolean(clean && !/[\u3400-\u9FFF]/.test(clean) && /[A-Za-z\u00C0-\u024F\u0400-\u04FF]/.test(clean));
};
const socialTranslationKey = (type, id) => `${type}:${id}`;
const ContentTitleLink = ({ title, url, className = '', linkClassName = 'content-title-link' }) => {
  const content = <span className={`content-title-clamp ${className}`.trim()}>{title || '未命名内容'}</span>;
  return url ? <a className={linkClassName} href={url} target="_blank" rel="noreferrer">{content}</a> : content;
};
const getSocialPostUrl = (post = {}, account = {}) => {
  const raw = post.raw || {};
  const candidates = [
    post.url,
    post.link,
    post.permalink,
    post.share_url,
    post.manual_metrics?.url,
    raw.url,
    raw.link,
    raw.permalink,
    raw.share_url,
    raw.share_link,
    raw.note_url,
    raw.web_url,
    raw.share_info?.link,
  ];
  const direct = candidates.map(value => String(value || '').trim()).find(value => /^https?:\/\//i.test(value));
  if (direct) return direct;
  if (post.platform === '小红书') {
    const noteId = String(raw.id || raw.note_id || raw.noteId || '').trim();
    if (noteId) return `https://www.xiaohongshu.com/explore/${noteId}`;
  }
  return account.homepage_url || '';
};
const assetScopeMeta = {
  owned: { label: '自有账号', desc: '官方账号发布内容' },
  external: { label: '外部声量', desc: 'KOL / UGC / 第三方自然内容' },
};

const getMonitorDataset = (brandId = 'montx') => {
  const posts = (SOCIAL_POSTS || []).filter(post => post.brand_id === brandId);
  const fallbackPosts = posts.length ? posts : (SOCIAL_POSTS || []);
  const accountIds = new Set(fallbackPosts.map(post => post.account_id));
  const accounts = (SOCIAL_ACCOUNTS || []).filter(account => account.brand_id === brandId || accountIds.has(account.account_id));
  const postIds = new Set(fallbackPosts.map(post => post.post_id));
  const comments = (SOCIAL_COMMENTS || []).filter(comment => postIds.has(comment.post_id));
  return { accounts, posts: fallbackPosts, comments };
};

const mergeSocialRows = (baseRows = [], extraRows = [], idKey) => {
  const map = new Map();
  [...baseRows, ...extraRows].forEach(row => {
    if (!row?.[idKey]) return;
    map.set(row[idKey], { ...(map.get(row[idKey]) || {}), ...row });
  });
  return Array.from(map.values());
};

const monitorRiskRank = { high: 3, medium: 2, low: 1 };
const monitorRiskLabel = { high: 'HIGH', medium: 'MED', low: 'LOW' };
const monitorRiskLevelByRank = { 3: 'high', 2: 'medium', 1: 'low' };
const contentSourceMeta = {
  api: 'API',
  manual: '人工台账',
  external: '外部UGC',
  mixed: 'API+人工',
  unknown: '未标记',
};
const getContentSourceGroup = (post = {}) => {
  const sourceText = `${post.data_channel || ''} ${post.provider || ''} ${post.source_type || ''} ${post.asset_type || ''} ${Array.isArray(post.data_sources) ? post.data_sources.join(' ') : ''}`.toLowerCase();
  if (isExternalSocialPost(post)) return 'external';
  if (/api_plus_manual|manual.*api|api.*manual/.test(sourceText)) return 'mixed';
  if (/manual|sheet|workbook|台账/.test(sourceText)) return 'manual';
  if (/api|tikhub|justone|sync/.test(sourceText)) return 'api';
  return 'unknown';
};
const getMonitorPostBriefMeta = (post = {}) => {
  const brief = post.brief || {};
  const id = brief.primaryBriefId || post.primaryMasterBriefId || post.briefMapping?.primaryMasterBriefId || '';
  const name = brief.primaryBriefName || post.briefMapping?.masterBriefGroup || post.briefMapping?.unifiedTopic || '';
  const parent = brief.parentCampaignId || post.parentCampaignId || post.briefMapping?.parentCampaignId || '';
  return {
    id,
    name,
    parent,
    assigned: Boolean(brief.assigned || id || name || parent),
  };
};
const getMonitorPostBriefFilterValue = (post = {}) => {
  const meta = getMonitorPostBriefMeta(post);
  return meta.id || meta.name || meta.parent || 'unassigned';
};
const getMonitorPostBriefFilterLabel = (post = {}) => {
  const meta = getMonitorPostBriefMeta(post);
  return meta.name || meta.id || meta.parent || '未挂 Brief';
};
const getPostTimeValue = (post = {}) => {
  const raw = String(post.published_at || post.publish_time || post.created_at || '').trim();
  if (!raw) return 0;
  const normalized = raw.replace(/年|月/g, '/').replace(/日/g, '').replace(/\./g, '/');
  const parsed = new Date(normalized);
  return Number.isNaN(parsed.getTime()) ? 0 : parsed.getTime();
};
const getPostCompositeRisk = (post = {}, postComments = []) => {
  const baseRank = monitorRiskRank[post.risk_level] || 1;
  const riskRows = postComments.filter(isRiskComment);
  const maxCommentRank = postComments.reduce((max, comment) => Math.max(max, getCommentRiskRank(comment)), 1);
  const rank = Math.max(baseRank, maxCommentRank);
  const highComments = postComments.filter(comment => getCommentRiskRank(comment) >= 3).length;
  return {
    level: monitorRiskLevelByRank[rank] || 'low',
    rank,
    totalComments: postComments.length,
    riskComments: riskRows.length,
    highComments,
  };
};
const monitorActionLabel = {
  observed: '已观察',
  'needs-response': '待回应',
  'needs-answer': '待解答',
  'needs-review': '待复核',
  reviewing: '复核中',
  resolved: '已关闭',
};
const socialApiUrl = (pathname) => (window.location.protocol === 'file:' ? `http://localhost:4173${pathname}` : pathname);

const Monitor = ({ onNav, workspaceContext }) => {
  const [ticks, setTicks] = React.useState(0);
  const brandId = workspaceContext?.brandId || BRAND?.id || 'montx';
  const activeBrand = (WORKSPACE_BRANDS || []).find(brand => brand.id === brandId) || BRAND;
  const baseDataset = getMonitorDataset(brandId);
  const [syncedAssets, setSyncedAssets] = React.useState({ accounts: [], posts: [], comments: [], postDailyMetricSnapshots: [], postDailyMetricBackfill: null, lastSync: '', nextRun: '', status: 'idle', errors: [], runs: [], syncLog: null });
  const [syncingPlatform, setSyncingPlatform] = React.useState('');
  const accounts = mergeSocialRows(baseDataset.accounts, syncedAssets.accounts, 'account_id');
  const posts = mergeSocialRows(baseDataset.posts, syncedAssets.posts, 'post_id');
  const comments = mergeSocialRows(baseDataset.comments, syncedAssets.comments || [], 'comment_id');
  const postDailyMetricSnapshots = syncedAssets.postDailyMetricSnapshots || [];
  const [assetScope, setAssetScope] = React.useState('owned');
  const [workTab, setWorkTab] = React.useState('content');
  const [filters, setFilters] = React.useState({ platform: 'all', product: 'all', risk: 'all', status: 'all' });
  const [contentFilters, setContentFilters] = React.useState({ query: '', campaign: 'all', brief: 'all', contentType: 'all', dateRange: 'all', commentScope: 'all', source: 'all', sort: 'risk' });
  const [syncLogOpen, setSyncLogOpen] = React.useState(false);
  const [commentConcernFilter, setCommentConcernFilter] = React.useState('all');
  const [activeTrendPostId, setActiveTrendPostId] = React.useState('');
  const [activeCommentId, setActiveCommentId] = React.useState('');
  const [commentActions, setCommentActions] = React.useState({});
  const [replyDraft, setReplyDraft] = React.useState('');
  const [replyGenerating, setReplyGenerating] = React.useState(false);
  const [translations, setTranslations] = React.useState({});
  const [translationStatus, setTranslationStatus] = React.useState('idle');
  const [manualUploadOpen, setManualUploadOpen] = React.useState(false);
  const [manualUploadPreview, setManualUploadPreview] = React.useState(null);
  const [manualUploadStatus, setManualUploadStatus] = React.useState('idle');
  const [manualUploadError, setManualUploadError] = React.useState('');
  const [manualUploadResolutions, setManualUploadResolutions] = React.useState({});
  React.useEffect(() => {
    const int = setInterval(() => setTicks(t => t + 1), 2000);
    return () => clearInterval(int);
  }, []);

  const applySyncPayload = React.useCallback((data = {}) => {
    setSyncedAssets({
      accounts: data.accounts || [],
      posts: data.posts || [],
      comments: data.comments || [],
      postDailyMetricSnapshots: data.postDailyMetricSnapshots || [],
      postDailyMetricBackfill: data.postDailyMetricBackfill || null,
      lastSync: data.lastFinishedAt || '',
      nextRun: data.nextRunAt || '',
      status: data.status || 'idle',
      errors: data.errors || [],
      runs: data.runs || [],
      syncLog: data.syncLog || null,
    });
  }, []);

  const loadSyncStatus = React.useCallback(async () => {
    try {
      const response = await fetch(socialApiUrl('/api/social/sync/status'));
      const data = await response.json();
      if (response.ok) applySyncPayload(data);
    } catch (error) {
      setSyncedAssets(prev => ({ ...prev, errors: [{ platform: 'local', message: error.message || '读取同步状态失败' }] }));
    }
  }, [applySyncPayload]);

  React.useEffect(() => {
    loadSyncStatus();
    const int = setInterval(loadSyncStatus, 30000);
    return () => clearInterval(int);
  }, [loadSyncStatus]);

  const ownedPosts = posts.filter(post => !isExternalSocialPost(post));
  const externalPosts = posts.filter(isExternalSocialPost);
  const scopedPosts = assetScope === 'external' ? externalPosts : ownedPosts;
  const commentsByPost = React.useMemo(() => {
    const map = new Map();
    comments.forEach(comment => {
      const list = map.get(comment.post_id) || [];
      list.push(comment);
      map.set(comment.post_id, list);
    });
    return map;
  }, [comments]);
  const getRowRisk = React.useCallback((post) => getPostCompositeRisk(post, commentsByPost.get(post.post_id) || []), [commentsByPost]);
  const getDisplayedPostComments = React.useCallback((post) => Math.max(pickNativeMetric(post, 'comments'), (commentsByPost.get(post.post_id) || []).length), [commentsByPost]);
  const getDisplayedPostInteraction = React.useCallback((post) => pickNativeMetric(post, 'likes') + getDisplayedPostComments(post) + pickNativeMetric(post, 'shares') + pickNativeMetric(post, 'reposts') + pickNativeMetric(post, 'saves'), [getDisplayedPostComments]);
  const scopedPostIds = new Set(scopedPosts.map(post => post.post_id));
  const scopedComments = comments.filter(comment => scopedPostIds.has(comment.post_id));
  const scopedAccountIds = new Set(scopedPosts.map(post => post.account_id).filter(Boolean));
  const scopedAccounts = accounts.filter(account => scopedAccountIds.has(account.account_id));
  const followerDelta = scopedAccounts.reduce((sum, account) => sum + socialNumber(account.follower_delta_period), 0);
  const followerGrowthStarts = scopedAccounts.map(account => account.follower_delta_start_date).filter(Boolean).sort();
  const followerGrowthEnds = scopedAccounts.map(account => account.follower_delta_end_date).filter(Boolean).sort();
  const followerGrowthStart = followerGrowthStarts[0] || '';
  const followerGrowthEnd = followerGrowthEnds[followerGrowthEnds.length - 1] || '';
  const ownedViews = ownedPosts.reduce((sum, post) => sum + getPostContentViews(post), 0);
  const externalViews = externalPosts.reduce((sum, post) => sum + getPostContentViews(post), 0);
  const scopedPendingMetrics = scopedPosts.filter(isPostMetricPending).length;
  const externalPendingMetrics = externalPosts.filter(isPostMetricPending).length;
  const campaignOptions = Array.from(new Set(scopedPosts.map(post => post.campaign).filter(Boolean))).sort((a, b) => String(a).localeCompare(String(b), 'zh-CN'));
  const briefOptionRows = Array.from(scopedPosts.reduce((map, post) => {
    const id = getMonitorPostBriefFilterValue(post);
    if (!map.has(id)) map.set(id, getMonitorPostBriefFilterLabel(post));
    return map;
  }, new Map()).entries())
    .map(([id, label]) => ({ id, label }))
    .sort((a, b) => String(a.label).localeCompare(String(b.label), 'zh-CN'));
  const briefOptions = briefOptionRows.map(item => item.id);
  const briefLabelById = briefOptionRows.reduce((map, item) => ({ ...map, [item.id]: item.label }), {});
  const contentTypeOptions = Array.from(new Set(scopedPosts.map(post => post.content_type || post.type).filter(Boolean))).sort((a, b) => String(a).localeCompare(String(b), 'zh-CN'));
  const sourceOptions = Array.from(new Set(scopedPosts.map(getContentSourceGroup))).filter(Boolean);
  const contentQuery = contentFilters.query.trim().toLowerCase();
  const now = Date.now();
  const filteredPosts = scopedPosts.filter(post => {
    const rowRisk = getRowRisk(post);
    const rowComments = commentsByPost.get(post.post_id) || [];
    if (filters.platform !== 'all' && post.platform !== filters.platform) return false;
    if (filters.product !== 'all' && post.product !== filters.product) return false;
    if (filters.risk !== 'all' && rowRisk.level !== filters.risk) return false;
    if (filters.status !== 'all' && post.status !== filters.status) return false;
    if (contentFilters.campaign !== 'all' && post.campaign !== contentFilters.campaign) return false;
    if (contentFilters.brief !== 'all' && getMonitorPostBriefFilterValue(post) !== contentFilters.brief) return false;
    if (contentFilters.contentType !== 'all' && (post.content_type || post.type) !== contentFilters.contentType) return false;
    if (contentFilters.source !== 'all' && getContentSourceGroup(post) !== contentFilters.source) return false;
    if (contentFilters.commentScope === 'has-comments' && rowComments.length === 0 && socialNumber(post.comments) === 0) return false;
    if (contentFilters.commentScope === 'no-comments' && (rowComments.length > 0 || socialNumber(post.comments) > 0)) return false;
    if (contentFilters.commentScope === 'risk-comments' && rowRisk.riskComments === 0) return false;
    if (contentFilters.dateRange !== 'all') {
      const days = Number(contentFilters.dateRange.replace('d', '')) || 0;
      const time = getPostTimeValue(post);
      if (!time || now - time > days * 24 * 60 * 60 * 1000) return false;
    }
    if (contentQuery) {
      const account = accounts.find(item => item.account_id === post.account_id) || {};
      const briefMeta = getMonitorPostBriefMeta(post);
      const haystack = [
        post.title,
        post.caption,
        post.product,
        post.campaign,
        briefMeta.id,
        briefMeta.name,
        briefMeta.parent,
        post.platform,
        post.content_type,
        post.creator_handle,
        account.handle,
        account.account_name,
      ].join(' ').toLowerCase();
      if (!haystack.includes(contentQuery)) return false;
    }
    return true;
  }).sort((a, b) => {
    if (contentFilters.sort === 'newest') return getPostTimeValue(b) - getPostTimeValue(a);
    if (contentFilters.sort === 'views') return socialNumber(b.views || b.plays || b.reads) - socialNumber(a.views || a.plays || a.reads);
    if (contentFilters.sort === 'comments') return getDisplayedPostComments(b) - getDisplayedPostComments(a);
    if (contentFilters.sort === 'engagement') return getDisplayedPostInteraction(b) - getDisplayedPostInteraction(a);
    return getRowRisk(b).rank - getRowRisk(a).rank || getPostTimeValue(b) - getPostTimeValue(a);
  });
  const visiblePosts = filteredPosts;
  const trendSnapshotsByPost = React.useMemo(() => {
    const map = new Map();
    postDailyMetricSnapshots.forEach(snapshot => {
      if (!snapshot?.post_id) return;
      const rows = map.get(snapshot.post_id) || [];
      rows.push(snapshot);
      map.set(snapshot.post_id, rows);
    });
    map.forEach(rows => rows.sort((a, b) => String(a.date || '').localeCompare(String(b.date || ''))));
    return map;
  }, [postDailyMetricSnapshots]);
  const activeTrendPost = activeTrendPostId ? posts.find(post => post.post_id === activeTrendPostId) : null;
  const activeTrendAccount = activeTrendPost ? accounts.find(account => account.account_id === activeTrendPost.account_id) || getSocialAccount(activeTrendPost.account_id) : {};
  const activeTrendSnapshots = activeTrendPostId ? trendSnapshotsByPost.get(activeTrendPostId) || [] : [];

  const commentConcernCounts = scopedComments.reduce((map, comment) => {
    const concern = getCommentConcern(comment);
    map[concern.id] = (map[concern.id] || 0) + 1;
    return map;
  }, {});
  const commentConcernOptions = commentConcernMeta.filter(item => item.id !== 'other' && commentConcernCounts[item.id]).concat(commentConcernCounts.other ? [commentConcernById.other] : []);
  const commentStream = scopedComments.filter(comment => commentConcernFilter === 'all' || getCommentConcern(comment).id === commentConcernFilter);
  const riskComments = scopedComments.filter(isRiskComment);
  const openRiskComments = riskComments.filter(comment => (commentActions[comment.comment_id] || comment.action_status) !== 'resolved');
  const totalViews = scopedPosts.reduce((sum, post) => sum + getPostContentViews(post), 0);
  const totalInteraction = scopedPosts.reduce((sum, post) => sum + getDisplayedPostInteraction(post), 0);
  const totalComments = scopedPosts.reduce((sum, post) => sum + getDisplayedPostComments(post), 0);
  const avgSentiment = scopedComments.length ? scopedComments.reduce((sum, comment) => sum + socialNumber(comment.sentiment), 0) / scopedComments.length : 0;
  const topRisk = [...riskComments].sort((a, b) => socialNumber(a.sentiment) - socialNumber(b.sentiment))[0];
  const activeComment = commentStream.find(comment => comment.comment_id === activeCommentId) || topRisk || commentStream[0] || scopedComments[0] || null;
  const activeCommentPost = activeComment ? posts.find(post => post.post_id === activeComment.post_id) : null;
  const activeCommentAccount = activeCommentPost ? accounts.find(account => account.account_id === activeCommentPost.account_id) || getSocialAccount(activeCommentPost.account_id) : {};
  const activeCommentConcern = activeComment ? getCommentConcern(activeComment) : null;
  const activeCommentSentiment = activeComment ? getCommentSentimentMeta(activeComment) : null;
  const activeCommentSourceUrl = activeCommentPost ? getSocialPostUrl(activeCommentPost, activeCommentAccount) : '';
  const platformOptions = Array.from(new Set((scopedPosts.length ? scopedPosts : posts).map(post => post.platform).filter(Boolean)));
  const productOptions = Array.from(new Set(scopedPosts.map(post => post.product).filter(Boolean)));
  const statusOptions = Array.from(new Set(scopedPosts.map(post => post.status).filter(Boolean)));
  const externalTranslationCandidates = React.useMemo(() => {
    if (assetScope !== 'external') return [];
    const items = [];
    visiblePosts.forEach(post => {
      const text = post.title || post.caption || '';
      const key = socialTranslationKey('post', post.post_id);
      if (needsChineseTranslation(text) && !translations[key]) items.push({ id: key, type: 'post_title', text });
    });
    scopedComments.forEach(comment => {
      const key = socialTranslationKey('comment', comment.comment_id);
      if (needsChineseTranslation(comment.text) && !translations[key]) items.push({ id: key, type: 'comment', text: comment.text });
    });
    return items.slice(0, 24);
  }, [assetScope, visiblePosts, scopedComments, translations]);
  const externalTranslationSignature = externalTranslationCandidates.map(item => item.id).join('|');
  const externalTranslatedCount = Object.values(translations).filter(item => item?.translation).length;
  const opportunities = scopedComments
    .filter(comment => !isRiskComment(comment) && comment.keyword)
    .slice(0, 3)
    .map(comment => ({ t: comment.keyword, p: comment.platform, pct: comment.action_status === 'observed' ? '可沉淀' : '待响应' }));
  const updateFilter = (key, value) => setFilters(prev => ({ ...prev, [key]: value }));
  const updateContentFilter = (key, value) => setContentFilters(prev => ({ ...prev, [key]: value }));
  const resetFilters = () => setFilters({ platform: 'all', product: 'all', risk: 'all', status: 'all' });
  const resetContentFilters = () => setContentFilters({ query: '', campaign: 'all', brief: 'all', contentType: 'all', dateRange: 'all', commentScope: 'all', source: 'all', sort: 'risk' });
  const changeAssetScope = (scope) => {
    setAssetScope(scope);
    resetFilters();
    resetContentFilters();
    setCommentConcernFilter('all');
    setActiveCommentId('');
    setActiveTrendPostId('');
    setWorkTab('content');
  };
  const setCommentStatus = (commentId, status) => setCommentActions(prev => ({ ...prev, [commentId]: status }));
  React.useEffect(() => {
    if (!externalTranslationSignature || translationStatus === 'loading') return;
    let cancelled = false;
    const items = externalTranslationCandidates;
    setTranslationStatus('loading');
    fetch(socialApiUrl('/api/social/translate'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ items }),
    })
      .then(response => response.json().then(data => ({ ok: response.ok, data })))
      .then(({ data }) => {
        if (cancelled) return;
        setTranslationStatus(data.ok === false ? 'error' : 'ready');
        setTranslations(prev => {
          const next = { ...prev };
          (data.translations || []).forEach(item => {
            next[item.id] = {
              translation: item.translation || '',
              language: item.language || '',
              error: item.error || '',
              source: data.source || 'unknown',
            };
          });
          return next;
        });
      })
      .catch(error => {
        if (cancelled) return;
        setTranslationStatus('error');
        setTranslations(prev => {
          const next = { ...prev };
          items.forEach(item => {
            next[item.id] = { translation: '', language: '', error: error.message || '翻译失败', source: 'client-error' };
          });
          return next;
        });
      });
    return () => {
      cancelled = true;
    };
  }, [externalTranslationSignature]);
  const syncStatusText = syncedAssets.status === 'running' ? '同步中' : syncedAssets.status === 'error' || syncedAssets.status === 'partial' ? '部分失败' : 'Live';
  const lastSyncText = formatBeijingTime(syncedAssets.lastSync, '等待首次同步');
  const nextRunText = formatBeijingTime(syncedAssets.nextRun, '每日自动同步');
  const formatShortSyncTime = (value, fallback) => formatBeijingTime(value, fallback, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
  const lastSyncShortText = formatShortSyncTime(syncedAssets.lastSync, '待同步');
  const nextRunShortText = formatShortSyncTime(syncedAssets.nextRun, '每日');
  const syncErrorText = (syncedAssets.errors || [])
    .map((item, index) => `${index + 1}. ${item.platform || item.source || '同步'}：${item.message || item.error || JSON.stringify(item)}`)
    .join('\n') || '暂无异常详情';
  const syncAllAccounts = async () => {
    setSyncingPlatform('all');
    setSyncedAssets(prev => ({ ...prev, status: 'running', errors: [] }));
    try {
      const response = await fetch(socialApiUrl('/api/social/sync?trigger=manual'), { method: 'POST' });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || data.message || '同步失败');
      applySyncPayload(data);
    } catch (error) {
      setSyncedAssets(prev => ({ ...prev, status: 'error', errors: [{ platform: 'local', message: error.message || '同步失败' }] }));
    } finally {
      setSyncingPlatform('');
    }
  };
  const generateReply = async (comment = activeComment) => {
    if (!comment) return;
    setActiveCommentId(comment.comment_id);
    const post = posts.find(item => item.post_id === comment.post_id) || {};
    const account = accounts.find(item => item.account_id === post.account_id) || {};
    setReplyGenerating(true);
    setReplyDraft('DeepSeek 正在生成回应建议...');
    try {
      const response = await fetch(socialApiUrl('/api/social/comment/reply-suggestion'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ comment, post, account }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || data.message || '生成失败');
      setReplyDraft([
        data.reply || `感谢你的反馈。关于「${comment.keyword || comment.risk_tag || '这个问题'}」，我们会补充说明。`,
        '',
        `舆情等级：${data.publicOpinionLevel || data.prLevel || 'S1'}`,
        `法务等级：${data.legalLevel || 'L1'}`,
        `语气：${data.tone || '克制、专业'}`,
        `风险：${data.riskLevel || 'medium'}`,
        `动作：${data.nextAction || '人工复核后使用'}`,
        data.legalNextAction ? `法务动作：${data.legalNextAction}` : '',
        data.rationale ? `判断：${data.rationale}` : '',
        data.riskPoints?.length ? `风险点：${data.riskPoints.join('；')}` : '',
        data.legalRiskTypes?.length ? `法务类型：${data.legalRiskTypes.join('；')}` : '',
        data.legalRiskPoints?.length ? `法务风险：${data.legalRiskPoints.join('；')}` : '',
        data.replacementSuggestion ? `文案替换：${data.replacementSuggestion}` : '',
        data.reviewSuggestion ? `复盘建议：${data.reviewSuggestion}` : '',
        data.legalEscalation ? `升级建议：${data.legalEscalation}` : '',
        `来源：${data.source || 'local'}${data.model ? ` · ${data.model}` : ''}${data.modelError ? ` · ${data.modelError}` : ''}`,
      ].filter(Boolean).join('\n'));
    } catch (error) {
      setReplyDraft(`建议回应：感谢你指出「${comment.keyword || comment.risk_tag || '这个问题'}」。这条内容会补充说明适用场景、产品边界和后续节奏，避免造成误解。\n\n来源：local-fallback · ${error.message || '生成失败'}`);
    } finally {
      setReplyGenerating(false);
    }
  };
  const readManualUploadFile = (file) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const bytes = new Uint8Array(reader.result || []);
      let chunk = '';
      let encoded = '';
      for (let i = 0; i < bytes.length; i += 1) {
        chunk += String.fromCharCode(bytes[i]);
        if (chunk.length >= 0x8000) {
          encoded += btoa(chunk);
          chunk = '';
        }
      }
      if (chunk) encoded += btoa(chunk);
      resolve({ name: file.name, size: file.size, contentBase64: encoded });
    };
    reader.onerror = () => reject(reader.error || new Error('文件读取失败'));
    reader.readAsArrayBuffer(file);
  });
  const previewManualUpload = async (event) => {
    const selectedFiles = Array.from(event.target.files || []);
    event.target.value = '';
    if (!selectedFiles.length) return;
    setManualUploadStatus('previewing');
    setManualUploadError('');
    setManualUploadPreview(null);
    setManualUploadResolutions({});
    try {
      const files = await Promise.all(selectedFiles.map(readManualUploadFile));
      const response = await fetch(socialApiUrl('/api/social/manual-upload/preview'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ files }),
      });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '上传预检失败');
      const conflicts = data.preview?.conflicts || [];
      setManualUploadPreview(data);
      setManualUploadResolutions(Object.fromEntries(conflicts.map(conflict => [
        conflict.id,
        conflict.recommended_action === 'use_upload' ? 'use_upload' : 'keep_system',
      ])));
      setManualUploadStatus('ready');
    } catch (error) {
      setManualUploadStatus('error');
      setManualUploadError(error.message || '上传预检失败');
    }
  };
  const applyManualUpload = async () => {
    if (!manualUploadPreview?.sessionId) return;
    setManualUploadStatus('applying');
    setManualUploadError('');
    try {
      const response = await fetch(socialApiUrl('/api/social/manual-upload/apply'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ sessionId: manualUploadPreview.sessionId, resolutions: manualUploadResolutions }),
      });
      const data = await response.json();
      if (!response.ok || data.ok === false) throw new Error(data.error || data.message || '写入失败');
      applySyncPayload(data);
      setManualUploadStatus('applied');
      setTimeout(() => setManualUploadOpen(false), 700);
    } catch (error) {
      setManualUploadStatus('error');
      setManualUploadError(error.message || '写入失败');
    }
  };
  return (
    <div className="page wide monitor-page">
      <div className="page-header">
        <div className="heading">
          <h1>社媒资产 · <span className="hl">Live Monitor</span></h1>
          <p>{activeBrand.name} 的账号、文章、评论和回应建议集中监控。系统每日自动同步，支持手动即时刷新。</p>
        </div>
        <div className="actions">
          <button type="button" className={`chip compact monitor-sync-status-trigger ${syncedAssets.status === 'error' || syncedAssets.status === 'partial' ? 'warn' : 'accent'} mono`} title={syncedAssets.errors?.length ? syncErrorText : syncStatusText} onClick={() => setSyncLogOpen(true)}>
            <span className="dot"></span>{syncedAssets.errors?.length ? `${syncStatusText} · ${syncedAssets.errors.length} 异常` : syncStatusText}
          </button>
          <span className="chip compact mono" title={`上次同步（北京时间）：${lastSyncText}\n下次任务（北京时间）：${nextRunText}`}>北京时间 {lastSyncShortText} → {nextRunShortText}</span>
          <button className="btn primary" onClick={syncAllAccounts} disabled={syncingPlatform === 'all' || syncedAssets.status === 'running'}>
            <Icon name="refresh" className="icon sm"/>{syncingPlatform === 'all' || syncedAssets.status === 'running' ? '同步中' : '同步'}
          </button>
          <button className="btn" onClick={() => setManualUploadOpen(true)}><Icon name="upload" className="icon sm"/>上传表格</button>
          <button className="btn" onClick={() => onNav?.('reports')}>导出报表</button>
        </div>
      </div>

      <div className="monitor-stat-strip">
        <MonitorStat label={assetScopeMeta[assetScope].label} value={scopedPosts.length} meta={`${assetScopeMeta[assetScope].desc} · ${platformOptions.length} 平台`} tone="good"/>
        <MonitorStat
          label={assetScope === 'external' ? '外部互动' : '粉丝净增'}
          value={assetScope === 'external' ? formatCompactNumber(totalInteraction) : `${followerDelta >= 0 ? '+' : ''}${formatCompactNumber(followerDelta)}`}
          meta={assetScope === 'external' ? `评论 ${formatCompactNumber(totalComments)} · 账号 ${scopedAccounts.length}` : (followerGrowthStart ? `${followerGrowthStart} - ${followerGrowthEnd}` : `当前视图 ${scopedPosts.length} · 全部 ${posts.length}`)}
          tone={assetScope === 'external' || followerDelta > 0 ? 'good' : 'neutral'}
        />
        <MonitorStat label={assetScope === 'external' ? '外部声量' : '自有曝光'} value={assetScope === 'external' && scopedPendingMetrics ? `${scopedPendingMetrics} 待补` : formatCompactNumber(totalViews)} meta={`自有 ${formatCompactNumber(ownedViews)} · 外部 ${formatCompactNumber(externalViews)}${scopedPendingMetrics ? ` · 待补 ${scopedPendingMetrics}` : ''}`} tone="neutral"/>
        <MonitorStat label="开放风险" value={openRiskComments.length} meta={`评论 ${formatCompactNumber(totalComments)} · 情绪 ${avgSentiment >= 0 ? '+' : ''}${avgSentiment.toFixed(2)}`} tone={openRiskComments.length ? 'risk' : 'good'}/>
      </div>

      <div className="monitor-scope-tabs" role="tablist" aria-label="社媒资产类型">
        <button type="button" role="tab" aria-selected={assetScope === 'owned'} className={assetScope === 'owned' ? 'active' : ''} onClick={() => changeAssetScope('owned')}>
          <span>自有账号</span>
          <b>{ownedPosts.length} 篇 · {formatCompactNumber(ownedViews)}</b>
        </button>
        <button type="button" role="tab" aria-selected={assetScope === 'external'} className={assetScope === 'external' ? 'active' : ''} onClick={() => changeAssetScope('external')}>
          <span>外部声量</span>
          <b>{externalPosts.length} 篇 · {externalPendingMetrics ? `${externalPendingMetrics} 待补数` : formatCompactNumber(externalViews)}</b>
        </button>
      </div>

      <div className="monitor-work-tabs" role="tablist" aria-label="社媒资产工作区">
        <button type="button" role="tab" aria-selected={workTab === 'content'} className={workTab === 'content' ? 'active' : ''} onClick={() => setWorkTab('content')}>
          <Icon name="content" className="icon sm"/><span>内容</span><b>{visiblePosts.length}</b>
        </button>
        <button type="button" role="tab" aria-selected={workTab === 'comments'} className={workTab === 'comments' ? 'active' : ''} onClick={() => setWorkTab('comments')}>
          <Icon name="brief" className="icon sm"/><span>评论</span><b>{scopedComments.length}</b>
        </button>
        <button type="button" role="tab" aria-selected={workTab === 'risk'} className={workTab === 'risk' ? 'active' : ''} onClick={() => setWorkTab('risk')}>
          <Icon name="warn" className="icon sm"/><span>风险</span><b>{openRiskComments.length}</b>
        </button>
      </div>

      <div className="monitor-filter-bar">
        <MonitorSelect label="平台" value={filters.platform} options={platformOptions} onChange={value => updateFilter('platform', value)}/>
        <MonitorSelect label="产品" value={filters.product} options={productOptions} onChange={value => updateFilter('product', value)}/>
        <MonitorSelect label="风险" value={filters.risk} options={['high', 'medium', 'low']} onChange={value => updateFilter('risk', value)} render={value => value === 'all' ? '全部' : monitorRiskLabel[value]}/>
        <MonitorSelect label="状态" value={filters.status} options={statusOptions} onChange={value => updateFilter('status', value)} render={value => value === 'all' ? '全部' : value}/>
        <button className="btn sm" onClick={() => { resetFilters(); resetContentFilters(); }}><Icon name="x" className="icon sm"/>重置</button>
        <span className="mono monitor-filter-count">
          {assetScope === 'external' && translationStatus === 'loading' ? `已译 ${externalTranslatedCount} · DeepSeek 翻译中` : assetScope === 'external' ? `已译 ${externalTranslatedCount}` : `匹配 ${filteredPosts.length}/${scopedPosts.length} 篇`}
        </span>
      </div>

      {workTab === 'content' && (
        <div className="monitor-workbench monitor-workbench-content">
          <section className="monitor-main-panel">
            <div className="monitor-panel-head">
              <div>
                <h2>内容资产库</h2>
                <p>{assetScopeMeta[assetScope].label} · {visiblePosts.length} 篇命中 · 内容池筛选</p>
              </div>
              <span className="monitor-count">{visiblePosts.length}</span>
            </div>
            <div className="monitor-content-filter-bar">
              <label className="monitor-search-field">
                <span>关键词</span>
                <input value={contentFilters.query} onChange={e => updateContentFilter('query', e.target.value)} placeholder="标题 / 账号 / 活动"/>
              </label>
              <MonitorSelect label="活动" value={contentFilters.campaign} options={campaignOptions} onChange={value => updateContentFilter('campaign', value)}/>
              <MonitorSelect label="Brief" value={contentFilters.brief} options={briefOptions} onChange={value => updateContentFilter('brief', value)} render={value => briefLabelById[value] || value}/>
              <MonitorSelect label="类型" value={contentFilters.contentType} options={contentTypeOptions} onChange={value => updateContentFilter('contentType', value)}/>
              <MonitorSelect label="时间" value={contentFilters.dateRange} options={['7d', '14d', '30d']} onChange={value => updateContentFilter('dateRange', value)} render={value => ({ '7d': '近 7 天', '14d': '近 14 天', '30d': '近 30 天' }[value] || value)}/>
              <MonitorSelect label="评论" value={contentFilters.commentScope} options={['has-comments', 'risk-comments', 'no-comments']} onChange={value => updateContentFilter('commentScope', value)} render={value => ({ 'has-comments': '有评论', 'risk-comments': '有风险评论', 'no-comments': '无评论' }[value] || value)}/>
              <MonitorSelect label="来源" value={contentFilters.source} options={sourceOptions} onChange={value => updateContentFilter('source', value)} render={value => contentSourceMeta[value] || value}/>
              <MonitorSelect label="排序" value={contentFilters.sort} options={['risk', 'newest', 'views', 'comments', 'engagement']} onChange={value => updateContentFilter('sort', value === 'all' ? 'risk' : value)} render={value => ({ risk: '风险优先', newest: '最新发布', views: '观看/阅读', comments: '评论数', engagement: '互动数' }[value] || value)}/>
              <button className="btn sm" onClick={resetContentFilters}><Icon name="x" className="icon sm"/>清空</button>
            </div>
            <div className="monitor-table-wrap">
              <table className="tbl monitor-asset-table">
                <colgroup>
                  <col className="monitor-col-title"/>
                  <col className="monitor-col-channel"/>
                  <col className="monitor-col-view"/>
                  <col className="monitor-col-metric"/>
                  <col className="monitor-col-metric"/>
                  <col className="monitor-col-metric"/>
                  <col className="monitor-col-metric"/>
                  <col className="monitor-col-trend"/>
                  <col className="monitor-col-risk"/>
                </colgroup>
                <thead>
                  <tr>
                    <th>文章</th>
                    <th>渠道</th>
                    <th className="monitor-align-right">观看/阅读</th>
                    <th className="monitor-align-right">点赞</th>
                    <th className="monitor-align-right">评论</th>
                    <th className="monitor-align-right">收藏</th>
                    <th className="monitor-align-right">转发</th>
                    <th>趋势</th>
                    <th title="风险基于内容 risk_level 与评论风险数：HIGH=高风险，MED=中风险，LOW=低风险。">风险</th>
                  </tr>
                </thead>
                <tbody>
			                  {visiblePosts.map(p => {
			                    const account = accounts.find(item => item.account_id === p.account_id) || getSocialAccount(p.account_id);
			                    const rowRisk = getRowRisk(p);
			                    const titleTranslation = translations[socialTranslationKey('post', p.post_id)];
			                    const postUrl = getSocialPostUrl(p, account);
			                    const postViews = getPostContentViews(p);
			                    const metricPending = isPostMetricPending(p);
			                    const displayedComments = getDisplayedPostComments(p);
			                    const trendRows = trendSnapshotsByPost.get(p.post_id) || [];
			                    const briefMeta = getMonitorPostBriefMeta(p);
			                    return (
	                      <tr key={p.post_id}>
	                        <td>
	                          <div className="monitor-title-cell">
	                            <ContentTitleLink title={p.title || '未命名文章'} url={postUrl} className="monitor-content-title"/>
	                            {isExternalSocialPost(p) && titleTranslation?.translation && <small className="monitor-translation-line">译：{titleTranslation.translation}</small>}
	                            <span>{p.publish_time || '-'} · {p.product || '-'} · {p.campaign || '-'}</span>
	                            {briefMeta.assigned && <em className="monitor-brief-note" title={briefMeta.id || briefMeta.parent || briefMeta.name}>Brief · {briefMeta.name || briefMeta.id || briefMeta.parent}</em>}
	                            {isExternalSocialPost(p) && <em className="monitor-source-note">外部声量 · {p.relationship_status || 'organic'} · {p.creator_handle || 'KOL/UGC'}</em>}
                          </div>
                        </td>
                        <td>
                          <div className="monitor-channel-cell">
                            <span className="tag">{p.platform}</span>
                            <span>{account.handle || account.account_name || '-'}</span>
                          </div>
                        </td>
                        <td className="monitor-number-cell">{metricPending ? <span className="monitor-metric-pending" title={p.metric_note || '接口暂未返回观看/阅读指标'}>待补数</span> : <b>{formatCompactNumber(postViews)}</b>}</td>
                        <td className="monitor-number-cell"><b>{formatCompactNumber(pickNativeMetric(p, 'likes'))}</b></td>
                        <td className="monitor-number-cell"><b>{formatCompactNumber(displayedComments)}</b></td>
                        <td className="monitor-number-cell"><b>{formatCompactNumber(pickNativeMetric(p, 'saves'))}</b></td>
	                        <td className="monitor-number-cell"><b>{formatCompactNumber(pickNativeMetric(p, 'shares') + pickNativeMetric(p, 'reposts'))}</b></td>
                        <td className="monitor-trend-cell">
                          <button type="button" className="monitor-trend-button" disabled={!trendRows.length} title={trendRows.length ? `${trendRows.length} 天内容级快照` : '暂无内容级趋势快照'} onClick={() => setActiveTrendPostId(p.post_id)}>
                            趋势
                          </button>
                        </td>
	                        <td>
	                          {rowRisk.level === 'high' && <span className="monitor-risk-chip high" title={`高风险：文章风险或评论风险触发优先复核；风险评论 ${rowRisk.riskComments} 条，高风险评论 ${rowRisk.highComments} 条。`}>HIGH · {rowRisk.riskComments}</span>}
	                          {rowRisk.level === 'medium' && <span className="monitor-risk-chip medium" title={`中风险：文章风险或评论区出现质疑、负面情绪、PR/法务 L2/S2 风险；风险评论 ${rowRisk.riskComments} 条。`}>MED · {rowRisk.riskComments}</span>}
	                          {rowRisk.level === 'low' && <span className="monitor-risk-chip low" title={`低风险：当前未发现明显需要处置的问题；已入库评论 ${rowRisk.totalComments} 条。`}>LOW</span>}
	                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
              {!filteredPosts.length && scopedPosts.length > 0 && (
                <div className="monitor-empty-note">当前筛选没有命中文章。可以清空内容池筛选或调整平台、产品、风险条件。</div>
              )}
              {!scopedPosts.length && (
                <div className="monitor-empty-note">暂无文章数据。可以点击同步获取平台资产。</div>
              )}
            </div>
          </section>

        </div>
      )}

      {workTab === 'comments' && (
        <div className="monitor-workbench monitor-workbench-comments">
          <section className="monitor-main-panel monitor-comment-panel">
            <div className="monitor-panel-head">
              <div>
                <h2>评论池</h2>
                <p>{assetScopeMeta[assetScope].label} · {commentStream.length}/{scopedComments.length} 条 · 按用户关心问题分类</p>
              </div>
              <span className="monitor-count">{commentStream.length}</span>
            </div>
            <div className="monitor-comment-topic-strip" role="tablist" aria-label="评论问题分类">
              <button type="button" className={commentConcernFilter === 'all' ? 'active' : ''} onClick={() => setCommentConcernFilter('all')}>
                <span>全部</span><b>{scopedComments.length}</b>
              </button>
              {commentConcernOptions.map(item => (
                <button type="button" key={item.id} className={commentConcernFilter === item.id ? 'active' : ''} onClick={() => setCommentConcernFilter(item.id)}>
                  <span>{item.short || item.label}</span><b>{commentConcernCounts[item.id] || 0}</b>
                </button>
              ))}
            </div>
            <CommentStream
              comments={commentStream}
              posts={posts}
              accounts={accounts}
              tick={ticks}
	              activeCommentId={activeComment?.comment_id}
	              commentActions={commentActions}
	              translations={translations}
	              onSelect={comment => setActiveCommentId(comment.comment_id)}
	              expanded
	            />
          </section>

          <aside className="monitor-side-rail">
            <section className="monitor-panel monitor-response">
              <div className="monitor-panel-head compact">
                <div>
                  <h2>评论上下文</h2>
                  <p>{activeComment?.platform || '-'} · {activeComment?.user_name || '未选择'}</p>
                </div>
              </div>
              <div className="monitor-comment-focus">
                <div className="monitor-comment-focus-head">
                  <b>{activeCommentConcern?.label || activeComment?.keyword || activeComment?.risk_tag || '普通评论'}</b>
                  {activeCommentSentiment && <span className={`monitor-sentiment-pill ${activeCommentSentiment.tone}`} title={`情绪：${activeCommentSentiment.label} · 分值 ${activeCommentSentiment.score >= 0 ? '+' : ''}${activeCommentSentiment.score.toFixed(2)}`}>{activeCommentSentiment.label}</span>}
                </div>
                <span>{activeComment?.text || '从评论池中选择一条评论。'}</span>
                {activeComment && translations[socialTranslationKey('comment', activeComment.comment_id)]?.translation && <span className="monitor-translation-line">译：{translations[socialTranslationKey('comment', activeComment.comment_id)].translation}</span>}
                <small>
                  源内容：{activeCommentSourceUrl ? <a href={activeCommentSourceUrl} target="_blank" rel="noreferrer">{activeCommentPost?.title || '-'}</a> : (activeCommentPost?.title || '-')}
                  {activeComment?.parent_comment_id ? ' · 二级评论' : ' · 一级评论'} · 情绪 {activeCommentSentiment ? `${activeCommentSentiment.score >= 0 ? '+' : ''}${activeCommentSentiment.score.toFixed(2)}` : '-'}
                </small>
              </div>
              <div className="monitor-action-row">
                <button className="btn sm" onClick={() => activeComment && setCommentStatus(activeComment.comment_id, 'reviewing')} disabled={!activeComment}>标记待复核</button>
                <button className="btn ghost sm" onClick={() => activeComment && setCommentStatus(activeComment.comment_id, 'resolved')} disabled={!activeComment}>关闭</button>
              </div>
            </section>
            <section className="monitor-panel">
              <div className="monitor-panel-head compact"><div><h2>机会词</h2><p>可沉淀的正向/中性评论</p></div></div>
              <div className="monitor-opportunity-list">
                {(opportunities.length ? opportunities : [{t:'等待更多评论数据', p:'live', pct:'暂无'}]).map((o, i) => (
                  <div key={i}>
                    <Icon name="lightning" className="icon sm"/>
                    <span>{o.t}</span>
                    <b>{o.p} · {o.pct}</b>
                  </div>
                ))}
              </div>
            </section>
          </aside>
        </div>
      )}

      {workTab === 'risk' && (
        <div className="monitor-workbench monitor-workbench-risk">
          <section className="monitor-main-panel monitor-risk-workspace">
            <div className="monitor-panel-head">
              <div>
                <h2>风险队列</h2>
                <p>{assetScopeMeta[assetScope].label} · {riskComments.length} 条风险评论 · {openRiskComments.length} 条待处理</p>
              </div>
              <span className="monitor-risk-chip high">{openRiskComments.length} open</span>
            </div>
            <div className="monitor-risk-queue expanded">
	              {riskComments.map(comment => {
	                const post = posts.find(item => item.post_id === comment.post_id);
	                const status = commentActions[comment.comment_id] || comment.action_status || 'observed';
	                const commentTranslation = translations[socialTranslationKey('comment', comment.comment_id)];
	                return (
	                  <button key={comment.comment_id} className={activeComment?.comment_id === comment.comment_id ? 'active' : ''} onClick={() => setActiveCommentId(comment.comment_id)}>
	                    <span className="monitor-risk-dot"></span>
	                    <div>
	                      <b>{comment.keyword || comment.risk_tag || '风险评论'}</b>
	                      <small>{comment.platform} · {post?.title || '未知文章'}</small>
	                      <p>{comment.text}</p>
	                      {commentTranslation?.translation && <span className="monitor-translation-line">译：{commentTranslation.translation}</span>}
	                    </div>
                    <em>{monitorActionLabel[status] || status}</em>
                  </button>
                );
              })}
              {!riskComments.length && <div className="monitor-empty-note inline">暂无风险评论</div>}
            </div>
          </section>

          <aside className="monitor-side-rail">
            <section className="monitor-risk-hero">
              <div className="monitor-panel-head">
                <div>
                  <h2>风险响应中心</h2>
                  <p>{activeComment?.platform || topRisk?.platform || '暂无平台'} · PR/法务建议</p>
                </div>
                <span className="monitor-risk-chip high">{openRiskComments.length} open</span>
              </div>
              <div className="monitor-risk-copy">
                {activeComment ? (
	                  <>
	                    <b>{activeComment.keyword || activeComment.risk_tag || '风险评论'}</b>
	                    <p>{activeComment.text}</p>
	                    {translations[socialTranslationKey('comment', activeComment.comment_id)]?.translation && <p className="monitor-translation-block">译：{translations[socialTranslationKey('comment', activeComment.comment_id)].translation}</p>}
	                    <span>来源：{activeComment.platform} · 《{activeCommentPost?.title || '未知文章'}》</span>
	                  </>
                ) : (
                  <>
                    <b>暂无高风险评论</b>
                    <p>当前实时同步未发现需要立即处理的评论。</p>
                    <span>系统仍会按每日任务继续监控。</span>
                  </>
                )}
              </div>
              <div className="monitor-action-row">
                <button className="btn primary sm" onClick={() => activeComment && setCommentStatus(activeComment.comment_id, 'reviewing')} disabled={!activeComment}>标记待复核</button>
                <button className="btn sm" onClick={() => generateReply(activeComment)} disabled={!activeComment || replyGenerating}>{replyGenerating ? '生成中' : '生成回应建议'}</button>
                <button className="btn ghost sm" onClick={() => activeComment && setCommentStatus(activeComment.comment_id, 'resolved')} disabled={!activeComment}>关闭风险</button>
              </div>
            </section>
            <section className="monitor-panel monitor-response">
              <div className="monitor-panel-head compact"><div><h2>回应工作台</h2><p>{activeComment?.user_name || '未选择评论'}</p></div></div>
              <textarea className="input monitor-reply-box" value={replyDraft} onChange={e => setReplyDraft(e.target.value)} placeholder="点击生成回应建议，或直接记录人工处置口径。"/>
              <div className="monitor-action-row">
                <button className="btn sm" onClick={() => generateReply(activeComment)} disabled={!activeComment || replyGenerating}>{replyGenerating ? '生成中' : '重写建议'}</button>
                <button className="btn ghost sm" onClick={() => setReplyDraft('')}>清空</button>
              </div>
            </section>
          </aside>
        </div>
      )}
      {activeTrendPost && (
        <ContentTrendDrawer
          post={activeTrendPost}
          account={activeTrendAccount}
          snapshots={activeTrendSnapshots}
          onClose={() => setActiveTrendPostId('')}
        />
      )}
      {syncLogOpen && (
        <SyncLogModal
          log={syncedAssets.syncLog}
          status={syncStatusText}
          lastSync={lastSyncText}
          nextRun={nextRunText}
          onClose={() => setSyncLogOpen(false)}
        />
      )}
      {manualUploadOpen && (
        <ManualSocialUploadDialog
          preview={manualUploadPreview}
          status={manualUploadStatus}
          error={manualUploadError}
          resolutions={manualUploadResolutions}
          onResolutionChange={(id, value) => setManualUploadResolutions(prev => ({ ...prev, [id]: value }))}
          onFileChange={previewManualUpload}
          onApply={applyManualUpload}
          onClose={() => setManualUploadOpen(false)}
        />
      )}
    </div>
  );
};

const trendMetricOptions = [
  { key: 'views', label: '观看/阅读' },
  { key: 'likes', label: '点赞' },
  { key: 'comments', label: '评论' },
  { key: 'saves', label: '收藏' },
  { key: 'shares', label: '转发' },
];

const getTrendMetric = (snapshot = {}, key = 'views') => socialNumber(snapshot.metrics?.[key]);
const getTrendDelta = (snapshot = {}, key = 'views') => socialNumber(snapshot.daily_delta?.[key]);
const formatTrendDelta = (value = 0) => `${value > 0 ? '+' : ''}${formatCompactNumber(value)}`;
const getTrendDateLabel = (date = '') => String(date || '').slice(5).replace('-', '/');
const isEstimatedTrendSnapshot = (snapshot = {}) => snapshot.source === 'estimated_trend_gap_fill' || Boolean(snapshot.estimate_method);
const getTrendSourceLabel = (snapshot = {}) => {
  if (!isEstimatedTrendSnapshot(snapshot)) return '真实快照';
  return snapshot.estimate_method === 'linear_interpolation_between_snapshots' ? '趋势插值' : '首发期估算';
};

const ContentTrendDrawer = ({ post = {}, account = {}, snapshots = [], onClose }) => {
  const [metricKey, setMetricKey] = React.useState('views');
  const rows = [...snapshots].sort((a, b) => String(a.date || '').localeCompare(String(b.date || '')));
  const metric = trendMetricOptions.find(item => item.key === metricKey) || trendMetricOptions[0];
  const current = rows.length ? getTrendMetric(rows[rows.length - 1], metricKey) : 0;
  const first = rows.length ? getTrendMetric(rows[0], metricKey) : 0;
  const deltaRows = rows.map(row => ({ ...row, value: getTrendMetric(row, metricKey), delta: getTrendDelta(row, metricKey) }));
  const peakDelta = deltaRows.length > 1
    ? Math.max(...deltaRows.slice(1).map(row => row.delta), 0)
    : Math.max(...deltaRows.map(row => row.delta), 0);
  const peakRow = deltaRows.find(row => row.delta === peakDelta) || deltaRows[0] || {};
  const latestDelta = deltaRows.length ? deltaRows[deltaRows.length - 1].delta : 0;
  const decay = peakDelta > 0 ? Math.round(((peakDelta - latestDelta) / peakDelta) * 100) : null;
  const dateRange = rows.length ? `${rows[0].date} - ${rows[rows.length - 1].date}` : '暂无快照';
  const sourceFiles = Array.from(new Set(rows.map(row => row.source_file).filter(Boolean)));
  const estimatedCount = rows.filter(isEstimatedTrendSnapshot).length;
  const realCount = rows.length - estimatedCount;
  const width = 520;
  const lineHeight = 154;
  const barHeight = 112;
  const padX = 24;
  const maxValue = Math.max(...deltaRows.map(row => row.value), 1);
  const maxAbsDelta = Math.max(...deltaRows.map(row => Math.abs(row.delta)), 1);
  const xFor = (index) => rows.length <= 1 ? width / 2 : padX + (index / (rows.length - 1)) * (width - padX * 2);
  const yFor = (value) => 18 + (1 - (value / maxValue)) * (lineHeight - 36);
  const points = deltaRows.map((row, index) => `${xFor(index)},${yFor(row.value)}`).join(' ');
  const baseline = 58;

  return (
    <div className="monitor-drawer-backdrop" onClick={onClose}>
      <aside className="monitor-trend-drawer" role="dialog" aria-modal="true" aria-label="内容增长趋势" onClick={event => event.stopPropagation()}>
        <div className="monitor-trend-head">
          <div>
            <span>{post.platform || '-'} · {account.handle || account.account_name || '-'}</span>
            <h2>{post.title || '未命名内容'}</h2>
            <p>{post.publish_time || post.published_at || '发布时间未知'} · {dateRange} · {rows.length} 天快照</p>
          </div>
          <button type="button" className="btn-icon" onClick={onClose} aria-label="关闭趋势图">
            <Icon name="x" className="icon sm"/>
          </button>
        </div>

        <div className="monitor-trend-summary">
          <MonitorStat label="当前累计" value={formatCompactNumber(current)} meta={metric.label} tone="good"/>
          <MonitorStat label="首个快照" value={formatCompactNumber(first)} meta={rows[0]?.date || '-'} tone="neutral"/>
          <MonitorStat label="最高日增" value={formatTrendDelta(peakDelta)} meta={peakRow.date || '-'} tone={peakDelta > 0 ? 'good' : 'neutral'}/>
          <MonitorStat label="最近日增" value={formatTrendDelta(latestDelta)} meta={decay === null ? '无峰值' : `较峰值 ${decay}%`} tone={latestDelta < 0 ? 'risk' : 'neutral'}/>
        </div>

        <div className="monitor-trend-metric-tabs" role="tablist" aria-label="趋势指标">
          {trendMetricOptions.map(item => (
            <button type="button" key={item.key} className={metricKey === item.key ? 'active' : ''} onClick={() => setMetricKey(item.key)}>
              {item.label}
            </button>
          ))}
        </div>

        <section className="monitor-trend-chart-panel">
          <div className="monitor-trend-chart-head">
            <b>累计增长曲线</b>
            <span>{metric.label} · 真实 {realCount} 天 · 推断 {estimatedCount} 天</span>
          </div>
          {rows.length ? (
            <svg className="monitor-trend-line-chart" viewBox={`0 0 ${width} ${lineHeight}`} role="img" aria-label={`${metric.label}累计增长曲线`}>
              <line x1={padX} y1={lineHeight - 18} x2={width - padX} y2={lineHeight - 18}></line>
              <polyline points={points}></polyline>
              {deltaRows.map((row, index) => (
                <g key={row.date}>
                  <circle className={isEstimatedTrendSnapshot(row) ? 'estimated' : ''} cx={xFor(index)} cy={yFor(row.value)} r="3.5"></circle>
                  <text x={xFor(index)} y={lineHeight - 4} textAnchor="middle">{getTrendDateLabel(row.date)}</text>
                  <title>{row.date} · {getTrendSourceLabel(row)} · 累计 {formatCompactNumber(row.value)}</title>
                </g>
              ))}
            </svg>
          ) : <div className="monitor-empty-note inline">暂无内容级每日快照。</div>}
        </section>

        <section className="monitor-trend-chart-panel">
          <div className="monitor-trend-chart-head">
            <b>日新增柱状图</b>
            <span>日增 = 当日累计 - 前一日累计</span>
          </div>
          {rows.length ? (
            <svg className="monitor-trend-bar-chart" viewBox={`0 0 ${width} ${barHeight}`} role="img" aria-label={`${metric.label}日新增柱状图`}>
              <line x1={padX} y1={baseline} x2={width - padX} y2={baseline}></line>
              {deltaRows.map((row, index) => {
                const barWidth = Math.max(12, (width - padX * 2) / Math.max(deltaRows.length, 1) - 14);
                const x = xFor(index) - barWidth / 2;
                const h = Math.max(2, Math.abs(row.delta) / maxAbsDelta * 44);
                const y = row.delta >= 0 ? baseline - h : baseline;
                return (
                  <g key={row.date}>
                    <rect className={`${row.delta < 0 ? 'negative' : ''} ${isEstimatedTrendSnapshot(row) ? 'estimated' : ''}`.trim()} x={x} y={y} width={barWidth} height={h}></rect>
                    <text x={xFor(index)} y={barHeight - 7} textAnchor="middle">{formatTrendDelta(row.delta)}</text>
                    <title>{row.date} · {getTrendSourceLabel(row)} · 日增 {formatTrendDelta(row.delta)}</title>
                  </g>
                );
              })}
            </svg>
          ) : <div className="monitor-empty-note inline">暂无日增数据。</div>}
        </section>

        <div className="monitor-trend-foot">
          <span>空心点/浅色柱为趋势推断值；平台后台回算或导出范围变化可能导致个别互动指标出现负增。</span>
          <small>{sourceFiles.length ? `来源文件 ${sourceFiles.length} 个` : '暂无来源文件'}</small>
        </div>
      </aside>
    </div>
  );
};

const manualUploadActionLabel = {
  keep_system: '保留系统值',
  use_upload: '使用上传值',
  merge_candidate: '合并到候选内容',
  skip: '跳过',
};

const ManualSocialUploadDialog = ({ preview, status, error, resolutions, onResolutionChange, onFileChange, onApply, onClose }) => {
  const summary = preview?.preview?.summary || {};
  const conflicts = preview?.preview?.conflicts || [];
  const updates = preview?.preview?.updates || [];
  const files = preview?.files || [];
  const missingFiles = preview?.preview?.missing_files || [];
  const changedUpdates = updates.filter(item => Number(item.diff || 0) !== 0);
  return (
    <div className="monitor-sync-overlay" role="dialog" aria-modal="true" aria-label="上传运营表格" onClick={onClose}>
      <div className="monitor-sync-dialog monitor-upload-dialog" onClick={event => event.stopPropagation()}>
        <div className="monitor-sync-head">
          <div>
            <h2>上传运营表格</h2>
            <p>上传表为导出时点的全量累计数据；系统用相邻快照计算每日增长。公众号收藏只使用“收藏(人)”。</p>
          </div>
          <button className="btn sm ghost" onClick={onClose}>关闭</button>
        </div>
        <div className="monitor-upload-body">
          <label className="monitor-upload-drop">
            <Icon name="upload" className="icon"/>
            <span>{status === 'previewing' ? '正在预检...' : '选择 CSV / XLSX 表格'}</span>
            <small>支持社媒粉丝数、视频号、Instagram、小红书、YouTube、公众号表格，可一次多选。内容互动按累计值保底。</small>
            <input type="file" multiple accept=".csv,.xlsx,.xls" onChange={onFileChange} disabled={status === 'previewing' || status === 'applying'}/>
          </label>
          {error && <div className="monitor-upload-error">{error}</div>}
          {files.length > 0 && (
            <div className="monitor-upload-file-list">
              {files.map(file => <span key={file.name}>{file.name}</span>)}
            </div>
          )}
          {preview && (
            <>
              <div className="monitor-upload-summary">
                <div><b>{formatCompactNumber(summary.account_updates || 0)}</b><span>账号更新</span></div>
                <div><b>{formatCompactNumber(summary.post_metric_updates || 0)}</b><span>字段更新</span></div>
                <div><b>{formatCompactNumber(summary.conflicts || 0)}</b><span>需确认</span></div>
                <div><b>{formatCompactNumber(summary.missing_files || 0)}</b><span>缺少表格</span></div>
              </div>
              {missingFiles.length > 0 && (
                <div className="monitor-upload-warning">
                  {missingFiles.map(item => <span key={item}>{item}</span>)}
                </div>
              )}
              {conflicts.length > 0 && (
                <div className="monitor-upload-section">
                  <h3>需要运营确认</h3>
                  <div className="monitor-upload-conflicts">
                    {conflicts.map(conflict => (
                      <div className="monitor-upload-conflict" key={conflict.id}>
                        <div>
                          <b>{conflict.platform} · {conflict.title || conflict.upload_title || conflict.candidate_title || '数据冲突'}</b>
                          <p>{conflict.message}</p>
                          <small>{conflict.field ? `${conflict.field}：系统 ${formatCompactNumber(conflict.system_value)} / 上传 ${formatCompactNumber(conflict.upload_value)}` : conflict.candidate_post_id ? `候选：${conflict.candidate_title}` : ''}</small>
                        </div>
                        <select value={resolutions[conflict.id] || conflict.recommended_action || 'keep_system'} onChange={event => onResolutionChange(conflict.id, event.target.value)}>
                          {(conflict.options || ['keep_system', 'use_upload']).map(option => <option key={option} value={option}>{manualUploadActionLabel[option] || option}</option>)}
                        </select>
                      </div>
                    ))}
                  </div>
                </div>
              )}
              <div className="monitor-upload-section">
                <h3>预检更新</h3>
                <div className="monitor-upload-update-list">
                  {changedUpdates.slice(0, 18).map(update => (
                    <div key={update.id} className="monitor-upload-update">
                      <span>{update.platform} · {update.title || update.account_id}</span>
                      <b>{update.field}</b>
                      <em title={update.metric_basis === 'full_snapshot_cumulative' ? '上传值为累计全量快照' : ''}>{formatCompactNumber(update.old)} → {formatCompactNumber(update.new)}</em>
                    </div>
                  ))}
                  {!changedUpdates.length && <div className="monitor-empty-note inline">本次没有发现需要更新的字段。</div>}
                </div>
              </div>
            </>
          )}
        </div>
        <div className="monitor-upload-footer">
          <span>{status === 'applied' ? '已写入' : status === 'applying' ? '正在写入...' : preview ? '预检完成，将按全量累计快照写入系统' : '先选择表格进行预检'}</span>
          <button className="btn primary" onClick={onApply} disabled={!preview || status === 'previewing' || status === 'applying'}>
            {status === 'applying' ? '写入中' : status === 'applied' ? '已写入' : '确认写入'}
          </button>
        </div>
      </div>
    </div>
  );
};

const MonitorStat = ({ label, value, meta, tone = 'neutral' }) => (
  <div className={`monitor-stat ${tone}`}>
    <span>{label}</span>
    <b>{value}</b>
    <small>{meta}</small>
  </div>
);

const MonitorSelect = ({ label, value, options = [], onChange, render }) => (
  <label className="monitor-select">
    <span>{label}</span>
    <select value={value} onChange={e => onChange(e.target.value)}>
      <option value="all">全部</option>
      {options.map(option => <option key={option} value={option}>{render ? render(option) : option}</option>)}
    </select>
  </label>
);

const syncSectionLabel = {
  ok: '成功',
  empty: '无新增',
  error: '失败',
  running: '同步中',
  idle: '待同步',
};

const SyncSectionPill = ({ section = {} }) => (
  <span className={`monitor-sync-pill ${section.status || 'idle'}`} title={section.message || ''}>
    {syncSectionLabel[section.status] || section.status || '待同步'} · {formatCompactNumber(socialNumber(section.count))}
  </span>
);

const SyncMetricLine = ({ metrics = {} }) => {
  const rows = [
    ['观看/阅读', metrics.views],
    ['点赞', metrics.likes],
    ['评论', metrics.comments],
    ['收藏', metrics.saves],
    ['转发', metrics.shares],
  ];
  return (
    <span className="monitor-sync-detail-metrics">
      {rows.map(([label, value]) => <em key={label}>{label} {formatCompactNumber(socialNumber(value))}</em>)}
    </span>
  );
};

const SyncLogDetailSection = ({ title, rows = [], omitted = 0, type }) => (
  <div className="monitor-sync-detail-section">
    <header>
      <b>{title}</b>
      <span>{rows.length}{omitted ? ` +${omitted}` : ''}</span>
    </header>
    <div className="monitor-sync-detail-list">
      {rows.map((row, index) => (
        <div className="monitor-sync-detail-item" key={`${type}-${row.post_id || row.comment_id || row.account_id || index}`}>
          {type === 'account' && (
            <>
              <b>{row.account_name || row.handle || row.account_id || '-'}</b>
              <small>{row.handle || row.account_id || '-'} · 粉丝 {formatCompactNumber(socialNumber(row.followers))}</small>
            </>
          )}
          {type === 'post' && (
            <>
              <b>{row.url ? <a href={row.url} target="_blank" rel="noreferrer">{row.title || '未命名内容'}</a> : (row.title || '未命名内容')}</b>
              <small>{formatBeijingTime(row.published_at, '时间未知')} · {row.content_type || row.post_id || '-'}</small>
              <SyncMetricLine metrics={row.metrics}/>
            </>
          )}
          {type === 'comment' && (
            <>
              <b>{row.user_name || '匿名用户'}</b>
              <small>{formatBeijingTime(row.published_at, '时间未知')} · {row.risk_tag || row.keyword || row.post_id || '-'}</small>
              <p>{row.text || '-'}</p>
            </>
          )}
        </div>
      ))}
      {!rows.length && <p className="monitor-sync-detail-empty">本次未返回明细</p>}
    </div>
  </div>
);

const SyncLogDetails = ({ item = {} }) => {
  const detail = item.items || {};
  const hasDetail = (detail.accounts?.length || detail.posts?.length || detail.comments?.length || detail.omitted?.accounts || detail.omitted?.posts || detail.omitted?.comments);
  if (!hasDetail) return null;
  return (
    <details className="monitor-sync-detail">
      <summary>查看本次返回：账号 {formatCompactNumber(socialNumber(item.account?.count))} · 内容 {formatCompactNumber(socialNumber(item.content?.count))} · 评论 {formatCompactNumber(socialNumber(item.comments?.count))}</summary>
      <div className="monitor-sync-detail-grid">
        <SyncLogDetailSection title="账号信息" type="account" rows={detail.accounts || []} omitted={detail.omitted?.accounts || 0}/>
        <SyncLogDetailSection title="内容池" type="post" rows={detail.posts || []} omitted={detail.omitted?.posts || 0}/>
        <SyncLogDetailSection title="评论池" type="comment" rows={detail.comments || []} omitted={detail.omitted?.comments || 0}/>
      </div>
    </details>
  );
};

const SyncLogModal = ({ log = {}, status, lastSync, nextRun, onClose }) => {
  const platforms = log?.platforms || [];
  return (
    <div className="monitor-sync-overlay" role="dialog" aria-modal="true" aria-label="平台同步日志" onClick={onClose}>
      <section className="monitor-sync-dialog" onClick={event => event.stopPropagation()}>
        <div className="monitor-sync-head">
          <div>
            <h2>平台同步日志</h2>
            <p>{status} · 上次 {lastSync} · 下次 {nextRun}</p>
          </div>
          <button type="button" className="btn ghost sm" onClick={onClose}><Icon name="x" className="icon sm"/>关闭</button>
        </div>
        <div className="monitor-sync-meta">
          <span>触发：{log?.trigger || '-'}</span>
          <span>开始：{formatBeijingTime(log?.startedAt, '-')}</span>
          <span>完成：{formatBeijingTime(log?.finishedAt, '-')}</span>
        </div>
        <div className="monitor-sync-log-table">
          <div className="monitor-sync-log-row head">
            <span>平台 / 账号</span>
            <span>账号信息</span>
            <span>内容池</span>
            <span>评论池</span>
            <span>说明</span>
          </div>
          {platforms.map((item, index) => (
            <div className="monitor-sync-log-entry" key={`${item.platform}-${item.account_id || index}`}>
              <div className="monitor-sync-log-row">
                <div>
                  <b>{item.platform}</b>
                  <small>{item.account_name || item.handle || item.account_id || '-'}</small>
                </div>
                <SyncSectionPill section={item.account}/>
                <SyncSectionPill section={item.content}/>
                <SyncSectionPill section={item.comments}/>
                <p title={item.message || item.account?.message || item.content?.message || ''}>
                  {item.message || item.account?.message || item.content?.message || item.comments?.message || '-'}
                  {item.elapsedMs ? ` · ${(item.elapsedMs / 1000).toFixed(1)}s` : ''}
                </p>
              </div>
              <SyncLogDetails item={item}/>
            </div>
          ))}
          {!platforms.length && <div className="monitor-empty-note inline">暂无平台同步日志。</div>}
        </div>
        {log?.errors?.length > 0 && (
          <div className="monitor-sync-errors">
            {log.errors.map((error, index) => <span key={index}>{error.platform || error.account_name || '同步'}：{error.message || '未知异常'}</span>)}
          </div>
        )}
      </section>
    </div>
  );
};

const formatCommentPublishTime = (comment = {}) => {
  const raw = String(comment.publish_time || comment.published_at || comment.created_at || comment.raw?.time || '').trim();
  if (!raw) return '时间未知';
  if (/^\d{4}[/-]\d{1,2}[/-]\d{1,2}/.test(raw)) return raw;
  return formatBeijingTime(raw, raw, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
};

const CommentStream = ({ comments = [], posts = [], accounts = [], tick, activeCommentId, commentActions = {}, translations = {}, onSelect, expanded = false }) => {
  const postById = new Map(posts.map(post => [post.post_id, post]));
  const accountById = new Map(accounts.map(account => [account.account_id, account]));
  const show = [...comments]
    .sort((a, b) => Number(isRiskComment(b)) - Number(isRiskComment(a)) || String(b.publish_time || '').localeCompare(String(a.publish_time || '')))
    .slice(0, expanded ? comments.length : Math.min(comments.length, 5 + (tick % 3)));
  return (
    <div className={`monitor-comment-stream ${expanded ? 'expanded' : ''}`}>
      {show.map((c, i) => {
        const concern = getCommentConcern(c);
        const sentimentMeta = getCommentSentimentMeta(c);
        const sourcePost = postById.get(c.post_id) || {};
        const sourceAccount = accountById.get(sourcePost.account_id) || getSocialAccount(sourcePost.account_id);
        const sourceUrl = getSocialPostUrl(sourcePost, sourceAccount);
        return (
          <button key={c.comment_id || i} onClick={() => onSelect?.(c)} className={activeCommentId === c.comment_id ? 'active' : ''}>
            <span className={`monitor-sentiment-bar ${sentimentMeta.tone}`}></span>
            <div>
              <div className="monitor-comment-line-head">
                <span className={`monitor-sentiment-pill ${sentimentMeta.tone}`} title={`情绪：${sentimentMeta.label} · 分值 ${sentimentMeta.score >= 0 ? '+' : ''}${sentimentMeta.score.toFixed(2)}`}>{sentimentMeta.label}</span>
                <span className={`monitor-topic-chip ${concern.id}`}>{concern.short || concern.label}</span>
                {c.parent_comment_id && <span className="monitor-topic-chip reply">二级评论</span>}
              </div>
              <b>{c.text}</b>
              {translations[socialTranslationKey('comment', c.comment_id)]?.translation && <span className="monitor-translation-line">译：{translations[socialTranslationKey('comment', c.comment_id)].translation}</span>}
              <small>{formatCommentPublishTime(c)} · {c.user_name || '匿名'} · {c.platform} · {c.keyword || c.risk_tag || '-'} · {monitorActionLabel[commentActions[c.comment_id] || c.action_status] || '已观察'}</small>
              <small className="monitor-comment-source" title={sourceUrl || ''}>源内容：{sourcePost.title || c.post_id || '-'}</small>
            </div>
            <em className={`monitor-sentiment-score ${sentimentMeta.tone}`}>{sentimentMeta.score >= 0 ? '+' : ''}{sentimentMeta.score.toFixed(2)}</em>
          </button>
        );
      })}
      {!show.length && <div className="monitor-empty-note inline">暂无评论数据</div>}
    </div>
  );
};

const ExposureChart = () => (
  <svg viewBox="0 0 420 160" style={{width:'100%', height:160}}>
    {[30,60,90,120].map(y => <line key={y} x1="0" x2="420" y1={y} y2={y} stroke="var(--hairline)" strokeDasharray="2 3"/>)}
    {[
      {c:'oklch(0.45 0.12 155)', pts:'0,120 50,110 100,90 150,70 200,55 250,42 300,35 350,28 400,22'},
      {c:'oklch(0.55 0.14 155)', pts:'0,130 50,125 100,115 150,100 200,88 250,76 300,66 350,58 400,50'},
      {c:'oklch(0.65 0.1 245)',  pts:'0,135 50,130 100,125 150,118 200,110 250,100 300,92 350,86 400,80'},
      {c:'oklch(0.5 0.08 245)',  pts:'0,140 50,138 100,130 150,120 200,105 250,95 300,88 350,78 400,68'},
      {c:'oklch(0.55 0.18 25)',  pts:'0,145 50,142 100,135 150,128 200,120 250,115 300,108 350,100 400,95'},
      {c:'oklch(0.55 0.12 60)',  pts:'0,150 50,148 100,145 150,140 200,135 250,130 300,126 350,122 400,118'},
    ].map((l, i) => <polyline key={i} points={l.pts} fill="none" stroke={l.c} strokeWidth="1.4" opacity="0.85"/>)}
  </svg>
);

// ========== 08 · REPORTS ==========
const REPORT_PERIODS = [
  { id: '7d', label: '近 7 天', days: 7 },
  { id: '14d', label: '近 14 天', days: 14 },
  { id: '30d', label: '近 30 天', days: 30 },
  { id: 'all', label: '全部同步', days: null },
];

const DOMESTIC_SOCIAL_PLATFORMS = new Set(['小红书', '视频号', '微信公众号', '抖音', '微博', 'B站', 'B 站']);
const getSocialRegion = (platform = '') => DOMESTIC_SOCIAL_PLATFORMS.has(platform) ? 'domestic' : 'overseas';
const reportRegionLabel = (region) => ({ domestic: '国内', overseas: '海外' }[region] || '未归类');
const REPORT_PLATFORM_ORDER = ['微信公众号', '视频号', '小红书', 'Instagram', 'YouTube'];
const reportPlatformDisplayName = (platform = '') => platform === '视频号' ? '微信视频号' : platform;
const normalizeReportPlatformName = (platform = '') => {
  const text = String(platform || '').trim().toLowerCase();
  if (/公众号|official/.test(text)) return '微信公众号';
  if (/视频号|channels/.test(text)) return '视频号';
  if (/小红书|red/.test(text)) return '小红书';
  if (/instagram|ig/.test(text)) return 'Instagram';
  if (/youtube|yt/.test(text)) return 'YouTube';
  return platform || '';
};
const compareReportPlatformOrder = (a = '', b = '') => {
  const aIndex = REPORT_PLATFORM_ORDER.indexOf(normalizeReportPlatformName(a));
  const bIndex = REPORT_PLATFORM_ORDER.indexOf(normalizeReportPlatformName(b));
  const safeA = aIndex === -1 ? REPORT_PLATFORM_ORDER.length : aIndex;
  const safeB = bIndex === -1 ? REPORT_PLATFORM_ORDER.length : bIndex;
  return safeA - safeB || String(a).localeCompare(String(b), 'zh-CN');
};

function formatCompactNumber(value) {
  if (value >= 1000000) return `${(value / 1000000).toFixed(2)}M`;
  if (value >= 10000) return `${(value / 10000).toFixed(1)}w`;
  return value.toLocaleString();
}

const BEIJING_TIME_ZONE = 'Asia/Shanghai';
const formatBeijingTime = (value, fallback = '', options = {}) => {
  if (!value) return fallback;
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return fallback;
  return date.toLocaleString('zh-CN', {
    timeZone: BEIJING_TIME_ZONE,
    hour12: false,
    ...options,
  });
};

function downloadReportFile(filename, content, type) {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

const parseSocialDate = (value = '') => {
  if (!value) return null;
  const date = new Date(String(value).replace(/\//g, '-'));
  return Number.isNaN(date.getTime()) ? null : date;
};

const getReportPostDateValue = (post = {}) => post.manual_metrics?.publish_time
  || post.manual_metrics?.published_at
  || post.published_at
  || post.publish_time
  || post.created_at
  || post.raw?.published_at
  || post.raw?.publish_time
  || '';
const getReportPostDate = (post = {}) => parseSocialDate(getReportPostDateValue(post));
const formatReportDateOnly = (value, fallback = '') => formatBeijingTime(value, fallback, { year: 'numeric', month: 'numeric', day: 'numeric' });
const buildReportDateRange = ({ posts = [], periodConfig = {}, generatedAt = new Date() }) => {
  const endDate = generatedAt instanceof Date ? generatedAt : new Date(generatedAt);
  const validEndDate = Number.isNaN(endDate.getTime()) ? new Date() : endDate;
  const datedPosts = posts.map(getReportPostDate).filter(Boolean).sort((a, b) => a - b);
  const startDate = periodConfig.days
    ? new Date(validEndDate.getTime() - periodConfig.days * 24 * 60 * 60 * 1000)
    : datedPosts[0] || null;
  const actualEndDate = periodConfig.days ? validEndDate : datedPosts[datedPosts.length - 1] || validEndDate;
  const label = startDate
    ? `${formatReportDateOnly(startDate)} - ${formatReportDateOnly(actualEndDate)}`
    : formatReportDateOnly(actualEndDate, periodConfig.label || '');
  return {
    start: startDate ? startDate.toISOString() : '',
    end: actualEndDate.toISOString(),
    label,
  };
};

const getReportFollowerGrowth = (account = {}) => {
  const value = account.follower_delta_period ?? account.follower_delta_7d ?? account.follower_growth ?? account.followers_delta ?? account.new_followers ?? account.followersGrowth;
  return value === undefined || value === null || value === '' ? null : socialNumber(value);
};

const getReportFollowers = (account = {}) => {
  const growthValue = getReportFollowerGrowth(account);
  if (growthValue !== null) return growthValue;
  return socialNumber(account.followers ?? account.follower_count ?? account.followers_count ?? account.subscriber_count ?? account.raw?.edge_followed_by?.count);
};
const normalizeFollowerGrowthDate = (value = '') => {
  const raw = String(value || '').trim();
  const match = raw.match(/20\d{2}(?:[-/年])\d{1,2}(?:[-/月])\d{1,2}/);
  if (!match) return '';
  const [year, month, day] = match[0].match(/\d+/g) || [];
  if (!year || !month || !day) return '';
  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
};
const buildFollowerGrowthTrend = (accounts = []) => {
  const byDate = new Map();
  accounts.forEach(account => {
    const region = getSocialRegion(account.platform);
    (Array.isArray(account.follower_growth_history) ? account.follower_growth_history : []).forEach(item => {
      const date = normalizeFollowerGrowthDate(item.date);
      if (!date) return;
      const row = byDate.get(date) || {
        date,
        domestic: 0,
        overseas: 0,
        total: 0,
        platforms: {},
      };
      const delta = socialNumber(item.delta);
      row[region] += delta;
      row.total += delta;
      row.platforms[account.platform] = (row.platforms[account.platform] || 0) + delta;
      byDate.set(date, row);
    });
  });
  return Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date));
};

const normalizeReportDateKey = (value) => {
  if (!value) return '';
  const raw = String(value).trim();
  const rawDate = normalizeFollowerGrowthDate(raw);
  if (rawDate) return rawDate;
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return normalizeFollowerGrowthDate(value);
  return date.toISOString().slice(0, 10);
};

const enumerateDateKeys = (start = '', end = '') => {
  const startDate = new Date(`${start}T00:00:00Z`);
  const endDate = new Date(`${end}T00:00:00Z`);
  if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) return [];
  const dates = [];
  for (let current = new Date(startDate); current <= endDate; current.setUTCDate(current.getUTCDate() + 1)) {
    dates.push(current.toISOString().slice(0, 10));
  }
  return dates;
};

const roundChartMax = (value = 0, step = 1) => Math.max(step * 2, Math.ceil(Math.max(0, value) / step) * step);
const buildChartTicks = (maxValue = 0, step = 1) => {
  const max = roundChartMax(maxValue, step);
  const ticks = [];
  for (let value = max; value >= 0; value -= step) ticks.push(value);
  return ticks;
};

const getPostMasterBriefId = (post = {}) => post.primaryMasterBriefId
  || post.briefMapping?.primaryMasterBriefId
  || (Array.isArray(post.masterBriefIds) ? post.masterBriefIds.find(id => id !== 'montx-auto-china-2026') : '')
  || 'unmapped-master-brief';

const getPostMasterBriefTitle = (post = {}) => cleanReportLabel(
  post.briefMapping?.masterBriefGroup
  || post.excelCampaign
  || post.briefMapping?.unifiedTopic
  || post.manual_title
  || post.title
  || '未命名 Master Brief'
);

const cleanReportLabel = (value = '') => String(value).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
const looksLikeSocialAccountId = (value = '') => /^@?[a-f0-9]{16,}$/i.test(cleanReportLabel(value)) || /^@?(user|id|uid)[-_]?[a-z0-9]{8,}$/i.test(cleanReportLabel(value));
const getReportAccountName = (account = {}) => {
  const handle = cleanReportLabel(account.handle);
  const accountName = cleanReportLabel(account.account_name || account.name);
  if (accountName && accountName.length <= 28) return accountName;
  if (handle && !looksLikeSocialAccountId(handle)) return handle;
  if (accountName) return accountName.slice(0, 28);
  return cleanReportLabel(account.account_id) || '未命名账号';
};

const PLATFORM_METRIC_CONFIG = {
  '小红书': { viewSource: 'views', viewLabel: '浏览/播放', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  '视频号': { viewSource: 'plays', viewLabel: '播放', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  '微信公众号': { viewSource: 'reads', viewLabel: '阅读', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: false },
  '抖音': { viewSource: 'plays', viewLabel: '播放', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  '微博': { viewSource: 'views', viewLabel: '阅读/播放', interactionFormula: 'likes + comments + shares + reposts', comparableWhenViewReady: false },
  'B站': { viewSource: 'views', viewLabel: '播放/阅读', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  'B 站': { viewSource: 'views', viewLabel: '播放/阅读', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  'bilibili': { viewSource: 'views', viewLabel: '播放/阅读', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  'Instagram': { viewSource: 'views', viewLabel: 'Reels views / plays', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
  'YouTube': { viewSource: 'views', viewLabel: 'Views', interactionFormula: 'likes + comments + shares', comparableWhenViewReady: true },
};

const METRIC_CANDIDATES = {
  organicImpressions: ['organic_impressions', 'organicImpressions', 'impressions', 'impression_count', 'raw.organic_impressions', 'raw.impressions', 'raw.impression_count'],
  adImpressions: ['ad_impressions', 'adImpressions', 'paid_impressions', 'raw.ad_impressions', 'raw.paid_impressions'],
  reachUsers: ['reach_users', 'reachUsers', 'reach', 'raw.reach_users', 'raw.reach'],
  views: ['views', 'view_count', 'play_count', 'plays', 'read_count', 'raw.view_count', 'raw.play_count', 'raw.video_view_count', 'raw.video_play_count', 'raw.read_count'],
  viewUsers: ['view_users', 'viewUsers', 'viewer_count', 'raw.view_users', 'raw.viewer_count'],
  reads: ['reads', 'read_count', 'raw.read_count', 'raw.read_num'],
  plays: ['plays', 'play_count', 'views', 'raw.play_count', 'raw.video_play_count'],
  thumbnailImpressions: ['thumbnail_impressions', 'thumbnailImpressions', 'raw.thumbnail_impressions'],
  ctr: ['ctr', 'click_through_rate', 'raw.ctr', 'raw.click_through_rate'],
  likes: ['likes', 'like_count', 'raw.likes', 'raw.liked_count', 'raw.like_count'],
  comments: ['comments', 'comment_count', 'raw.comments', 'raw.comment_count'],
  shares: ['shares', 'share_count', 'raw.shares', 'raw.share_count'],
  shareUsers: ['share_users', 'shareUsers', 'raw.share_users'],
  reposts: ['reposts', 'repost_count', 'forward_count', 'raw.reposts', 'raw.repost_count', 'raw.forward_count'],
  saves: ['saves', 'collects', 'collected_count', 'raw.saves', 'raw.collect_count', 'raw.collected_count'],
  saveUsers: ['save_users', 'saveUsers', 'favorite_users', 'raw.save_users', 'raw.favorite_users'],
  favorites: ['favorites', 'favorite_count', 'raw.favorite_count'],
  danmaku: ['danmaku_count', 'danmaku', 'raw.danmaku_count'],
  coins: ['coin_count', 'coins', 'raw.coin_count'],
  dislikes: ['dislike_count', 'dislikes', 'raw.dislike_count'],
  downloads: ['download_count', 'downloads', 'raw.download_count'],
  totalInteractions: ['total_interactions', 'totalInteractions', 'raw.total_interactions'],
  watchTime: ['watch_time_seconds', 'watch_time', 'watchTime', 'raw.watch_time_seconds', 'raw.watch_time'],
  avgWatchTime: ['avg_watch_time_seconds', 'avg_watch_time', 'avgWatchTime', 'raw.avg_watch_time_seconds', 'raw.avg_watch_time'],
  avgViewPercentage: ['avg_view_percentage', 'avgViewPercentage', 'raw.avg_view_percentage'],
  completionRate: ['completion_rate', 'completionRate', 'raw.completion_rate'],
  profileVisits: ['profile_visits', 'profileVisits', 'raw.profile_visits'],
  followersGained: ['followers_gained', 'followersGained', 'new_followers', 'raw.followers_gained'],
  followersLost: ['followers_lost', 'followersLost', 'raw.followers_lost'],
  linkClicks: ['link_clicks', 'linkClicks', 'raw.link_clicks'],
  conversionCount: ['conversion_count', 'conversionCount', 'raw.conversion_count'],
};

const getMetricByPath = (row = {}, path = '') => path.split('.').reduce((value, key) => value?.[key], row);
const MANUAL_METRIC_CANDIDATES = {
  views: ['manual_metrics.views', 'manual_metrics.plays', 'manual_metrics.reads'],
  reads: ['manual_metrics.reads', 'manual_metrics.views'],
  plays: ['manual_metrics.plays', 'manual_metrics.views'],
  likes: ['manual_metrics.likes'],
  comments: ['manual_metrics.comments'],
  shares: ['manual_metrics.shares', 'manual_metrics.reposts', 'manual_metrics.forward_count'],
  reposts: ['manual_metrics.reposts', 'manual_metrics.forward_count'],
  saves: ['manual_metrics.saves', 'manual_metrics.collects', 'manual_metrics.favorites'],
  favorites: ['manual_metrics.favorites', 'manual_metrics.saves'],
  totalInteractions: ['manual_metrics.totalInteractions', 'manual_metrics.interaction'],
};
const pickNativeMetric = (row = {}, metric) => {
  const candidates = METRIC_CANDIDATES[metric] || [metric];
  let native = 0;
  for (const path of candidates) {
    const value = getMetricByPath(row, path);
    if (value !== undefined && value !== null && value !== '') {
      native = socialNumber(value);
      if (native > 0) return native;
    }
  }
  const manualCandidates = MANUAL_METRIC_CANDIDATES[metric] || [`manual_metrics.${metric}`];
  for (const path of manualCandidates) {
    const value = getMetricByPath(row, path);
    if (value !== undefined && value !== null && value !== '') {
      const manual = socialNumber(value);
      if (manual > 0) return manual;
    }
  }
  return native;
};
const sumNativeMetric = (rows = [], metric) => rows.reduce((sum, row) => sum + pickNativeMetric(row, metric), 0);
const getPlatformMetricConfig = (platform = '') => PLATFORM_METRIC_CONFIG[platform] || {
  viewSource: 'views',
  viewLabel: 'Views / reads',
  interactionFormula: 'likes + comments + shares',
  comparableWhenViewReady: true,
};
const getPostContentViews = (post = {}) => {
  const config = getPlatformMetricConfig(post.platform);
  return pickNativeMetric(post, config.viewSource) || pickNativeMetric(post, 'views') || pickNativeMetric(post, 'plays') || pickNativeMetric(post, 'reads');
};
const getPostInteractionParts = (post = {}) => ({
  likes: pickNativeMetric(post, 'likes'),
  comments: pickNativeMetric(post, 'comments'),
  shares: pickNativeMetric(post, 'shares') + pickNativeMetric(post, 'reposts'),
});
const getPostEngagementsCore = (post = {}) => {
  const parts = getPostInteractionParts(post);
  return parts.likes + parts.comments + parts.shares;
};
const isPostMetricPending = (post = {}) => post.metric_status === 'pending_metrics' && !getPostContentViews(post);
const getSnapshotDate = (syncState = {}) => {
  const source = syncState.lastSync || new Date().toISOString();
  const date = new Date(source);
  return Number.isNaN(date.getTime()) ? new Date().toISOString().slice(0, 10) : date.toISOString().slice(0, 10);
};
const getStandardContentFields = (post = {}, syncState = {}) => ({
  platform: post.platform || '',
  account_id: post.account_id || '',
  content_id: post.content_id || post.post_id || '',
  content_type: post.content_type || '',
  title: cleanReportLabel(post.title || post.caption || post.description || '未命名内容'),
  cover_url: getPostCoverUrl(post),
  published_at: post.published_at || post.publish_time || '',
  snapshot_date: post.snapshot_date || getSnapshotDate(syncState),
  data_channel: post.data_channel || post.source_channel || (post.campaign?.includes('Sync') ? 'api_or_sync' : 'organic'),
  raw_metrics_json: post.raw || null,
});
const getPlatformLimitation = ({ platform, platformPosts, contentViews, config, followerGrowthValue }) => {
  if (!platformPosts.length) return `${platform} 本周期暂无有效发布，只保留账号状态，不参与内容效率判断。`;
  if (!contentViews) return `${platform} 缺少 ${config.viewLabel} 字段；当前只能判断互动活跃，不能计算阅读/播放到互动转化 CVR。`;
  if (!config.comparableWhenViewReady) return `${platform} 的 ${config.viewLabel} 属于平台原生口径，适合平台内诊断，不建议和视频/图文平台直接横比。`;
  if (followerGrowthValue === null) return `${platform} 缺少历史粉丝基线，涨粉诊断需等待下一周期。`;
  return `${platform} 核心字段覆盖完整，可进入跨平台对比，但仍需结合原生指标解释。`;
};
const getPlatformComparability = ({ platformPosts, contentViews, interaction, config }) => {
  if (!platformPosts.length) return 'missing';
  if (contentViews && config.comparableWhenViewReady) return 'ready';
  if (interaction && !contentViews) return 'partial';
  return 'native_only';
};

const extractPostTextValue = (value) => {
  if (!value) return '';
  if (typeof value === 'string' || typeof value === 'number') return String(value);
  if (Array.isArray(value)) return value.map(extractPostTextValue).filter(Boolean).join('\n');
  if (typeof value === 'object') {
    return [
      value.text,
      value.desc,
      value.description,
      value.title,
      value.display_title,
      value.caption?.text,
    ].map(extractPostTextValue).filter(Boolean).join('\n');
  }
  return '';
};

const getPostCaptionText = (post = {}) => [
  post.title,
  post.manual_title,
  post.caption,
  post.description,
  post.text,
  post.raw?.caption,
  post.raw?.desc,
  post.raw?.description,
  post.raw?.display_title,
  post.raw?.title,
  post.raw?.edge_media_to_caption?.edges?.[0]?.node?.text,
].map(extractPostTextValue).filter(Boolean).join('\n');

const getPostHashtags = (post = {}) => {
  const rawTags = [...(post.hashtags || []), ...(post.raw?.caption?.hashtags || [])];
  const textTags = Array.from(getPostCaptionText(post).matchAll(/#[\p{L}\p{N}_-]+/gu)).map(match => match[0]);
  return Array.from(new Set([...rawTags, ...textTags].map(tag => String(tag).replace(/^#/, '').trim()).filter(Boolean)));
};

const getPostContentScore = (post = {}) => getPostEngagementsCore(post) + Math.round(getPostContentViews(post) * 0.08);
const isUgcPost = (post = {}) => Boolean(post.is_ugc || post.ugc || post.source_type === 'ugc' || /ugc|用户|自来水/i.test(`${post.content_type || ''} ${post.campaign || ''} ${post.title || ''}`));
const isExternalReportPost = (post = {}) => isExternalSocialPost(post);
const isReportCoverUrlSupported = (url = '') => (/^https?:\/\//i.test(String(url || '')) || /^assets\/social-covers\//i.test(String(url || ''))) && !/wx\.qlogo\.cn/i.test(String(url || ''));
const getPostCoverUrl = (post = {}) => [
  post.cached_cover_url,
  post.local_cover_url,
  post.media_url,
  post.cover_url,
  post.thumbnail_url,
  post.image_url,
  post.raw?.media_url,
  post.raw?.cover_url,
  post.raw?.full_cover_url,
  post.raw?.share_cover_url,
  post.raw?.thumbnail_url,
  post.raw?.display_url,
  post.raw?.image_url,
  post.raw?.CoverImgUrl,
  post.raw?.CoverImgUrl_16_9,
  post.raw?.SuggestedCoverImg?.url,
  post.raw?.mediaItem?.cover_url,
  post.raw?.mediaItem?.coverUrl,
  post.raw?.mediaItem?.thumb_url,
  post.raw?.mediaItem?.thumbUrl,
  post.raw?.images?.[0]?.url,
  post.raw?.images_list?.[0]?.url,
  post.raw?.images_list?.[0]?.info_list?.[0]?.url,
].find(isReportCoverUrlSupported) || '';

const PLATFORM_LOGO_META = {
  '小红书': { abbr: '小红书', src: 'assets/platform-logos/xiaohongshu.jpg' },
  '视频号': { abbr: '视频号', src: 'assets/platform-logos/wechat-channels.jpg' },
  '微信公众号': { abbr: '公众号', src: 'assets/platform-logos/wechat-official.jpg' },
  Instagram: { abbr: 'IG', src: 'assets/platform-logos/instagram.png' },
  YouTube: { abbr: 'YT', src: 'assets/platform-logos/youtube.jpg' },
  抖音: { abbr: '抖音' },
  微博: { abbr: '微博' },
  B站: { abbr: 'B站' },
};

const buildReportCommentSample = (comment = {}, posts = []) => {
  const sourcePost = posts.find(post => post.post_id === comment.post_id) || {};
  const platform = comment.platform || sourcePost.platform || '';
  const sourceAccount = getSocialAccount(sourcePost.account_id);
  const concern = getCommentConcern(comment);
  return {
    comment_id: comment.comment_id,
    post_id: comment.post_id,
    platform,
    region: getSocialRegion(platform),
    user_name: comment.user_name,
    text: comment.text,
    sentiment: socialNumber(comment.sentiment),
    keyword: comment.keyword || comment.risk_tag || '',
    risk_tag: comment.risk_tag || '',
    user_concern_category: concern.id,
    user_concern_label: concern.label,
    user_concern_short: concern.short || concern.label,
    comment_level: socialNumber(comment.comment_level || (comment.parent_comment_id ? 2 : 1)),
    parent_comment_id: comment.parent_comment_id || '',
    action_status: comment.action_status || 'observed',
    source_post_title: cleanReportLabel(sourcePost.title || sourcePost.caption || '未匹配到原帖'),
    source_post_type: sourcePost.content_type || '',
    source_post_url: getSocialPostUrl(sourcePost, sourceAccount),
  };
};

const buildReportContentRow = (post = {}, syncState = {}, accounts = []) => {
  const standardFields = getStandardContentFields(post, syncState);
  const views = getPostContentViews(post);
  const interactionParts = getPostInteractionParts(post);
  const interaction = getPostEngagementsCore(post);
  const account = accounts.find(item => item.account_id === post.account_id) || getSocialAccount(post.account_id);
  return {
    ...standardFields,
    post_id: post.post_id,
    region: getSocialRegion(post.platform),
    publish_time: post.publish_time || '',
    url: getSocialPostUrl(post, account),
    media_url: post.media_url || '',
    source_type: post.source_type || '',
    creator_name: getReportAccountName(account),
    creator_handle: post.creator_handle || account.handle || '',
    views,
    interaction,
    ...interactionParts,
    engagements_core: interaction,
    view_to_interaction_rate: views ? interaction / views : null,
    engagement_rate_by_view: views ? interaction / views : null,
    score: getPostContentScore(post),
    hashtags: getPostHashtags(post),
    comparison_text: getPostCaptionText(post),
  };
};

const buildCommentPlatformBreakdown = ({ platforms = [], comments = [], posts = [] }) => platforms.map(platformRow => {
  const platformComments = comments.filter(comment => {
    const sourcePost = posts.find(post => post.post_id === comment.post_id) || {};
    return (comment.platform || sourcePost.platform) === platformRow.platform;
  });
  const positiveComments = platformComments.filter(isPositiveComment);
  const riskComments = platformComments.filter(isRiskComment);
  const neutralComments = platformComments.filter(comment => !isPositiveComment(comment) && !isRiskComment(comment));
  const positive = positiveComments.length;
  const risk = riskComments.length;
  const neutral = neutralComments.length;
  const keywordMap = new Map();
  platformComments.forEach(comment => {
    const concern = getCommentConcern(comment);
    const keyword = String(concern.label || comment.keyword || comment.risk_tag || '').replace(/^#/, '').trim();
    if (!keyword) return;
    keywordMap.set(keyword, (keywordMap.get(keyword) || 0) + 1);
  });
  const topKeywords = Array.from(keywordMap.entries())
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5)
    .map(([keyword, count]) => ({ keyword, count }));
  return {
    platform: platformRow.platform,
    region: platformRow.region,
    posts: platformRow.posts,
    total: platformComments.length,
    positive,
    neutral,
    risk,
    wordClouds: {
      positive: buildCommentWordCloud(positiveComments),
      neutral: buildCommentWordCloud(neutralComments),
      risk: buildCommentWordCloud(riskComments),
    },
    topKeywords,
    samples: platformComments.slice(0, 3).map(comment => buildReportCommentSample(comment, posts)),
    insight: platformComments.length
      ? `${platformRow.platform} 当前同步 ${platformComments.length} 条评论，主要关注 ${topKeywords.length ? topKeywords.map(item => item.keyword).join('、') : '未标记话题'}；该平台反馈仅代表本平台用户语境。`
      : `${platformRow.platform} 当前暂无同步评论，不能推断该平台用户关注点。`,
  };
});

const COMMENT_WORD_STOPWORDS = new Set(['这个', '没有', '可以', '我们', '你们', '就是', '还是', '已经', '不是', '什么', '怎么', '感觉', '一个', 'the', 'and', 'for', 'this', 'that', 'with', 'from']);
const COMMENT_EMOJI_PATTERN = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}](?:\uFE0F|\uFE0E)?/gu;
const isCommentEmojiToken = (value = '') => /^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}](?:\uFE0F|\uFE0E)?$/u.test(String(value || ''));
const buildCommentWordCloud = (comments = []) => {
  const keywordMap = new Map();
  comments.forEach(comment => {
    const explicit = cleanReportLabel(comment.keyword || comment.risk_tag || '');
    const text = cleanReportLabel(comment.text || '');
    const emojiCandidates = Array.from(text.matchAll(COMMENT_EMOJI_PATTERN)).map(match => match[0]);
    const candidates = explicit
      ? [explicit, ...emojiCandidates]
      : [
          ...Array.from(text.matchAll(/[\u4e00-\u9fa5]{2,8}|[A-Za-z][A-Za-z0-9_-]{2,}/g)).map(match => match[0]),
          ...emojiCandidates,
        ];
    candidates.forEach(raw => {
      const keyword = String(raw || '').replace(/^#/, '').trim();
      if (!keyword || COMMENT_WORD_STOPWORDS.has(keyword.toLowerCase())) return;
      keywordMap.set(keyword, (keywordMap.get(keyword) || 0) + 1);
    });
  });
  const max = Math.max(...keywordMap.values(), 1);
  return Array.from(keywordMap.entries())
    .sort((a, b) => b[1] - a[1] || Number(isCommentEmojiToken(b[0])) - Number(isCommentEmojiToken(a[0])) || a[0].localeCompare(b[0], 'zh-CN'))
    .slice(0, 12)
    .map(([keyword, count]) => ({ keyword, count, weight: Math.max(1, Math.ceil((count / max) * 4)) }));
};

const CONTENT_FAMILY_RULES = [
  { id: 'p10-platform', label: 'P10 全域立体出行平台', patterns: ['p10', '全域立体出行平台', 'all-terrain 3d mobility'] },
  { id: 'v10-mobile-pod', label: 'V10 全域移动整备舱 / Mobile Logistics Pod', patterns: ['全域移动整备舱', 'mobile logistics pod', '不止是车'] },
  { id: 'v10-adaptive-hub', label: 'V10 Adaptive Mobile Hub', patterns: ['adaptive mobile hub'] },
  { id: 'v10-home-industrial-art', label: 'V10 折叠家的工业艺术品', patterns: ['折叠进工业艺术品', 'folding home', 'industrial art', '天地间筑家'] },
  { id: 'global-debut', label: 'MONTX 全球首发 / Global Debut', patterns: ['全球首发', 'global debut', '领现在，越未来', '全域跨界新物种'] },
  { id: 'new-breed-teaser', label: '硬核方盒 · 新物种预热', patterns: ['硬核方盒', 'redefine the order', 'new breed lands'] },
  { id: 'nomad-co-creation', label: '牛马野逍遥共创内容', patterns: ['牛马野逍遥', '座驾你来定义'] },
  { id: 'v10-exterior', label: 'V10 外观视频', patterns: ['v10外观', '外观视频'] },
  { id: 'v10-scene', label: 'V10 场景视频', patterns: ['v10场景', '场景视频'] },
  { id: 'v10-function', label: 'V10 功能视频', patterns: ['v10功能', '功能视频'] },
  { id: 'launch-recap', label: '车展发布会速览', patterns: ['车展发布会速览', '发布会速览'] },
  { id: 'leadership-quotes', label: '领导金句盘点', patterns: ['领导金句'] },
  { id: 'v10-teaser', label: 'V10 预热视频', patterns: ['v10预热', '预热视频'] },
];

const normalizeContentText = (value = '') => cleanReportLabel(value).toLowerCase().replace(/montx|领越|「|」|"|“|”|:|：|\||｜/g, ' ').replace(/\s+/g, ' ').trim();
const getContentFamily = (row = {}) => {
  const text = normalizeContentText(`${row.title || ''} ${row.content_type || ''}`);
  const rule = CONTENT_FAMILY_RULES.find(item => item.patterns.some(pattern => text.includes(pattern.toLowerCase())));
  if (rule) return { id: rule.id, label: rule.label, confidence: 'rule' };
  const fallback = text.split(/\s+/).slice(0, 8).join(' ') || row.content_id || row.post_id || 'unknown-content';
  return { id: `title:${fallback}`, label: cleanReportLabel(row.title || '未命名内容').slice(0, 36), confidence: 'title' };
};

const CONTENT_COMPARISON_EQUIVALENTS = [
  [/mobile logistics pod/gi, '全域移动整备舱'],
  [/all[-\s]?terrain 3d mobility platform/gi, '全域立体出行平台'],
  [/adaptive mobile hub/gi, 'adaptive mobile hub'],
  [/global debut/gi, '全球首发'],
  [/new breed/gi, '新物种'],
  [/no boundaries/gi, '无边界'],
  [/lead the present[,]?\s*transcend the future/gi, '领现在越未来'],
  [/folding\s+home|folding\s+["“”']?home["“”']?/gi, '折叠家'],
  [/industrial art/gi, '工业艺术品'],
  [/more than a vehicle/gi, '不止是车'],
  [/tough boxy build/gi, '硬核方盒'],
  [/redefine the order/gi, '重构秩序'],
  [/co[-\s]?creation/gi, '共创'],
];

const normalizeComparisonText = (value = '') => {
  let text = cleanReportLabel(value).toLowerCase();
  CONTENT_COMPARISON_EQUIVALENTS.forEach(([pattern, replacement]) => {
    text = text.replace(pattern, replacement);
  });
  return text
    .replace(/https?:\/\/\S+/g, ' ')
    .replace(/#[\p{L}\p{N}_-]+/gu, ' ')
    .replace(/montx|领越|gac|auto china 2026|autochina2026/g, ' ')
    .replace(/[「」"“”'‘’`´:：|｜,，.。!！?？()[\]【】{}<>《》/\\_-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
};

const getComparisonTokens = (value = '') => {
  const text = normalizeComparisonText(value);
  const tokens = new Set();
  Array.from(text.matchAll(/[a-z0-9]+/g)).forEach(match => {
    if (match[0].length >= 2) tokens.add(match[0]);
  });
  Array.from(text.matchAll(/[\u4e00-\u9fa5]{2,}/g)).forEach(match => {
    const word = match[0];
    if (word.length <= 4) {
      tokens.add(word);
    } else {
      for (let i = 0; i < word.length - 1; i += 1) tokens.add(word.slice(i, i + 2));
      for (let i = 0; i < word.length - 3; i += 2) tokens.add(word.slice(i, i + 4));
    }
  });
  return tokens;
};

const getTokenSimilarity = (a = new Set(), b = new Set()) => {
  if (!a.size || !b.size) return 0;
  let intersection = 0;
  a.forEach(token => {
    if (b.has(token)) intersection += 1;
  });
  return (2 * intersection) / (a.size + b.size);
};

const getComparisonSignature = (row = {}) => {
  const text = normalizeComparisonText(`${row.title || ''} ${row.comparison_text || ''}`);
  const product = /p10/.test(text) ? 'p10' : (/v10/.test(text) ? 'v10' : '');
  const concepts = [
    [/不止是车|more than a vehicle|三段式|滑移方向盘|全域移动整备舱/, 'mobile-pod'],
    [/折叠家|工业艺术品|天地间筑家|folding home|industrial art/, 'industrial-art'],
    [/全域立体出行平台|all terrain 3d mobility|无人区|地空|3d mobility/, 'p10-platform'],
    [/全球首发|global debut/, 'global-debut'],
    [/adaptive mobile hub|全域跨界新物种|新物种|无边界|no boundaries/, 'new-breed'],
    [/硬核方盒|重构秩序|新物种降临|4 24/, 'teaser'],
    [/牛马野逍遥|freeroad|座驾你来定义|共创/, 'co-creation'],
    [/领导金句/, 'leadership-quotes'],
  ].filter(([pattern]) => pattern.test(text)).map(([, id]) => id);
  return [product, ...concepts].filter(Boolean).join(':') || '';
};

const getComparisonProfile = (row = {}) => {
  const title = normalizeComparisonText(row.title || '');
  const body = normalizeComparisonText(row.comparison_text || row.title || '');
  return {
    title,
    body,
    signature: getComparisonSignature(row),
    titleTokens: getComparisonTokens(title),
    bodyTokens: getComparisonTokens(body),
  };
};

const getContentMatchScore = (row = {}, group = {}) => {
  const profile = getComparisonProfile(row);
  return (group.rows || []).reduce((best, sample) => {
    const sampleProfile = sample.comparisonProfile || getComparisonProfile(sample);
    const titleSimilarity = getTokenSimilarity(profile.titleTokens, sampleProfile.titleTokens);
    const bodySimilarity = getTokenSimilarity(profile.bodyTokens, sampleProfile.bodyTokens);
    const exactTitle = Boolean(profile.title && sampleProfile.title && profile.title === sampleProfile.title);
    const sameSignature = Boolean(profile.signature && sampleProfile.signature && profile.signature === sampleProfile.signature);
    const matched = exactTitle
      || titleSimilarity >= 0.72
      || (titleSimilarity >= 0.46 && bodySimilarity >= 0.22)
      || (sameSignature && titleSimilarity >= 0.42)
      || (sameSignature && titleSimilarity >= 0.3 && bodySimilarity >= 0.18);
    const score = (exactTitle ? 1 : 0) + titleSimilarity * 0.62 + bodySimilarity * 0.38 + (sameSignature ? 0.18 : 0);
    return matched && score > best.score ? { matched, score, titleSimilarity, bodySimilarity, exactTitle, sameSignature } : best;
  }, { matched: false, score: 0, titleSimilarity: 0, bodySimilarity: 0, exactTitle: false, sameSignature: false });
};

const chooseComparisonGroupTitle = (rows = []) => {
  const candidates = [...rows].sort((a, b) => {
    const aChinese = /[\u4e00-\u9fa5]/.test(a.title || '') ? 1 : 0;
    const bChinese = /[\u4e00-\u9fa5]/.test(b.title || '') ? 1 : 0;
    return bChinese - aChinese || socialNumber(b.interaction) - socialNumber(a.interaction) || socialNumber(b.views) - socialNumber(a.views);
  });
  return cleanReportLabel(candidates[0]?.title || '未命名内容').slice(0, 52);
};

const buildContentComparisons = ({ contentRows = [], accounts = [] }) => {
  const allPlatforms = Array.from(new Set(accounts.map(account => account.platform).filter(Boolean)));
  const comparisonGroups = [];
  contentRows.forEach(row => {
    const comparisonProfile = getComparisonProfile(row);
    const rowWithProfile = { ...row, comparisonProfile };
    const bestGroup = comparisonGroups.reduce((best, group) => {
      const match = getContentMatchScore(rowWithProfile, group);
      return match.matched && match.score > best.score ? { group, ...match } : best;
    }, { group: null, matched: false, score: 0 });
    const prev = bestGroup.group || {
      content_group_id: `content-match-${comparisonGroups.length + 1}`,
      title: cleanReportLabel(row.title || '未命名内容').slice(0, 52),
      confidence: 'title-copy-match',
      rows: [],
	      platforms: new Set(),
		      regions: new Set(),
		      cover_url: '',
          cover_urls: new Set(),
		      url: '',
		      views: 0,
      likes: 0,
      comments: 0,
      shares: 0,
      interaction: 0,
      matchEvidence: [],
    };
	    prev.rows.push(rowWithProfile);
	    prev.platforms.add(row.platform);
		    prev.regions.add(row.region);
		    if (!prev.cover_url && row.cover_url) prev.cover_url = row.cover_url;
        if (row.cover_url) prev.cover_urls.add(row.cover_url);
		    if (!prev.url && row.url) prev.url = row.url;
		    prev.views += socialNumber(row.views);
    prev.likes += socialNumber(row.likes);
    prev.comments += socialNumber(row.comments);
    prev.shares += socialNumber(row.shares);
    prev.interaction += socialNumber(row.interaction);
    if (bestGroup.group) {
      prev.matchEvidence.push({
        post_id: row.post_id,
        platform: row.platform,
        titleSimilarity: bestGroup.titleSimilarity,
        bodySimilarity: bestGroup.bodySimilarity,
        exactTitle: bestGroup.exactTitle,
      });
    } else {
      comparisonGroups.push(prev);
    }
  });
  return comparisonGroups
    .map(group => {
	      const platformMap = new Map();
	      group.rows.forEach(row => {
	        const candidate = {
	          platform: row.platform,
		          region: row.region,
		          posts: 1,
              sampleCount: 1,
		          sampleTitle: row.title,
		          sampleContentType: row.content_type || '',
		          cover_url: row.cover_url || '',
              cover_urls: new Set(row.cover_url ? [row.cover_url] : []),
		          url: row.url || '',
		          latestPublishedAt: row.published_at || row.publish_time || '',
	          views: socialNumber(row.views),
	          likes: socialNumber(row.likes),
	          comments: socialNumber(row.comments),
	          shares: socialNumber(row.shares),
	          interaction: socialNumber(row.interaction),
	        };
	        const prev = platformMap.get(row.platform) || {
	          platform: row.platform,
		          region: row.region,
		          posts: 0,
              sampleCount: 0,
		          sampleTitle: row.title,
		          sampleContentType: row.content_type || '',
		          cover_url: row.cover_url || '',
              cover_urls: new Set(row.cover_url ? [row.cover_url] : []),
		          url: row.url || '',
		          latestPublishedAt: row.published_at || row.publish_time || '',
	          views: 0,
	          likes: 0,
	          comments: 0,
	          shares: 0,
	          interaction: 0,
	        };
          const sampleCount = prev.sampleCount + 1;
          const prevScore = socialNumber(prev.interaction) * 1000000 + socialNumber(prev.views);
          const candidateScore = candidate.interaction * 1000000 + candidate.views;
          const next = candidateScore > prevScore ? { ...candidate, cover_urls: prev.cover_urls } : prev;
          next.posts = 1;
          next.sampleCount = sampleCount;
		        if (!next.sampleContentType && row.content_type) next.sampleContentType = row.content_type;
		        if (!next.cover_url && row.cover_url) next.cover_url = row.cover_url;
            if (row.cover_url) prev.cover_urls.add(row.cover_url);
		        if (!next.url && row.url) next.url = row.url;
	        const publishedAt = row.published_at || row.publish_time || '';
	        if (publishedAt && (!next.latestPublishedAt || publishedAt > next.latestPublishedAt)) next.latestPublishedAt = publishedAt;
	        platformMap.set(row.platform, next);
	      });
	      const platformRows = Array.from(platformMap.values())
		        .map(row => ({
		          ...row,
		          content_type: row.sampleContentType,
		          view_to_interaction_rate: row.views ? row.interaction / row.views : null,
		        }))
	        .sort((a, b) => socialNumber(b.interaction) - socialNumber(a.interaction) || socialNumber(b.views) - socialNumber(a.views));
      const bestPlatform = [...platformRows].sort((a, b) => b.interaction - a.interaction || b.views - a.views)[0] || null;
      const missingPlatforms = allPlatforms.filter(platform => !group.platforms.has(platform));
      const representativeViews = platformRows.reduce((sum, row) => sum + socialNumber(row.views), 0);
      const representativeLikes = platformRows.reduce((sum, row) => sum + socialNumber(row.likes), 0);
      const representativeComments = platformRows.reduce((sum, row) => sum + socialNumber(row.comments), 0);
      const representativeShares = platformRows.reduce((sum, row) => sum + socialNumber(row.shares), 0);
      const representativeInteraction = platformRows.reduce((sum, row) => sum + socialNumber(row.interaction), 0);
      const duplicateSamples = platformRows.reduce((sum, row) => sum + Math.max(0, socialNumber(row.sampleCount) - 1), 0);
      const rate = representativeViews ? representativeInteraction / representativeViews : null;
      return {
		        content_group_id: group.content_group_id,
		        title: chooseComparisonGroupTitle(group.rows),
		        cover_url: group.cover_url || platformRows.find(row => row.cover_url)?.cover_url || '',
            cover_urls: Array.from(new Set([...group.cover_urls, ...platformRows.flatMap(row => Array.from(row.cover_urls || []))])).filter(Boolean),
		        url: group.url,
        confidence: group.confidence,
        matchEvidence: group.matchEvidence,
        platformCount: group.platforms.size,
        platforms: Array.from(group.platforms),
        missingPlatforms,
        regions: Array.from(group.regions),
        duplicateSamples,
        views: representativeViews,
        likes: representativeLikes,
        comments: representativeComments,
        shares: representativeShares,
        interaction: representativeInteraction,
        view_to_interaction_rate: rate,
        bestPlatform,
        platformRows,
        diagnosis: bestPlatform
          ? `${group.title} 在 ${bestPlatform.platform} 的互动贡献最高；代表发布总阅读/播放 ${formatCompactNumber(representativeViews)}，互动转化 CVR ${rate === null ? '待计算' : `${(rate * 100).toFixed(2)}%`}。${duplicateSamples ? '同平台多条命中内容已取最高表现样本，不累加重复发布。' : ''}${missingPlatforms.length ? `尚未覆盖：${missingPlatforms.join('、')}。` : '已覆盖当前全部账号平台。'}`
          : `${group.title} 暂无可比较的平台表现。`,
      };
    })
    .filter(group => group.platformRows.length >= 2)
    .sort((a, b) => b.interaction - a.interaction || b.views - a.views)
    .slice(0, 5);
};

const buildMontxContentDiagnosis = ({ topPosts = [], topics = [], platforms = [], dataCoverage = {} }) => {
  const strongestPlatform = [...platforms].sort((a, b) => b.views - a.views || b.interaction - a.interaction)[0];
  const topicWords = topics.slice(0, 6).map(topic => topic.keyword);
  return {
    strategicJudgement: 'MONTX 当前内容资产已经形成“高端机能主义 + 数字游民生活方式 + 文明秩序感 + 新物种品类切割”的强识别方向；下一步要把品牌语气共识升级为可持续的栏目、场景、人群与转化路径。',
    currentContentSummary: [
      topPosts.length ? `当前高表现内容仍以产品定义、展会节点和功能表达为主，代表内容包括「${topPosts[0].title.slice(0, 28)}」。` : '当前缺少足够的高表现内容样本，暂不能稳定总结内容主题。',
      topicWords.length ? `已出现可沉淀关键词：${topicWords.join('、')}；这些词适合进入选题标签库，但需要分层使用。` : '当前话题词样本不足，建议继续采集标题、caption、hashtag 与评论关键词。',
      strongestPlatform ? `${strongestPlatform.platform} 当前贡献最高阅读/播放或互动，应优先复盘其内容形态并判断能否跨平台复用。` : '平台表现样本不足，暂不能判断内容复用优先级。',
    ],
    toneLayering: [
      { level: '核心母语', words: ['新物种', '全域移动整备舱', '机能美学', '场景自由'], usage: '品牌定位、产品定义、长期资产沉淀。' },
      { level: '高频可用词', words: ['秩序', '整备', '边界', '掌控', '全域'], usage: '日常内容可用，但必须搭配具体场景或功能证据。' },
      { level: '低频慎用词', words: ['文明底牌', '方舟', '堡垒', '图腾', '末世', '生存宣言'], usage: '仅用于发布会、车展、重磅产品稿，避免日常推文套话化。' },
    ],
    columnMatrix: [
      { column: '产品定义类', goal: '解释 V10 / P10 是什么', writingRule: '可以高冷、硬核、宣言式，但必须给出产品结构证据。' },
      { column: '场景应用类', goal: '让用户看到如何使用', writingRule: '小红书/视频号要生活化，强调办公、收纳、拍摄、雨天、长途等具体颗粒度。' },
      { column: '共创社群类', goal: '沉淀牛马野逍遥和用户参与', writingRule: '保持精英邀请感，减少距离感，突出参与机制和反馈闭环。' },
      { column: '品牌世界观类', goal: '沉淀 New Breed / No Boundaries', writingRule: '适合公众号和官网资产，不宜每篇都用宏大词。' },
      { column: '技术解释类', goal: '把线控、滑移方向盘、空间编程讲清楚', writingRule: '降低文学性，用技术转译和图解逻辑建立信任。' },
    ],
    nextDirections: [
      { priority: 'P0', direction: '真实使用场景内容', reason: '当前内容偏产品与品牌概念，需要补“人如何使用这台车”的证据。', suggestedFormats: ['小红书图文', '视频号短视频', 'Instagram Reel'], evidence: ['topPosts', 'platforms'] },
      { priority: 'P0', direction: '栏目化技术转译', reason: '线控、空间编程、生物性智能等概念需要长期解释，否则只会停留在酷词。', suggestedFormats: ['公众号长文', 'B站/YouTube 技术短片', 'FAQ 卡片'], evidence: ['contentRows', 'topics'] },
      { priority: 'P1', direction: '搜索语言纠偏内容', reason: '正文不自称房车，但可在对比、纠偏、SEO 和话题标签中承接用户搜索语言。', suggestedFormats: ['“如果你还在用房车理解 V10”系列', '小红书搜索向标题', '公众号解释稿'], evidence: ['topic gaps'] },
      { priority: 'P1', direction: '海外双语语气分流', reason: 'Command / Conquer 一类硬核词不适合所有海外场景，需要技术版和生活方式版两套表达。', suggestedFormats: ['Instagram 英文 caption', 'YouTube Shorts 标题', '海外官网短文案'], evidence: ['overseas platforms'] },
      { priority: 'P2', direction: 'UGC 与第三方证言', reason: dataCoverage.ugc ? '已有 UGC 样本，可继续扩展真实证言。' : '当前 UGC 数据不足，信任内容还缺真实用户和第三方视角。', suggestedFormats: ['用户故事', '共创反馈', '真实场景复盘'], evidence: ['ugc', 'comments'] },
    ],
    assumptionsToValidate: [
      '“高净值数字游民”“末世生存感”“女性受众需要柔性硬核”等判断应标记为待验证假设，不应直接入库为客户铁律。',
      '“权威感/情绪价值/亲和力”的比例表达应降级为内容测试假设，用平台数据和评论反馈验证。',
      '小红书不能只继承公众号高冷语气，必须验证生活化颗粒度对收藏、评论和搜索进入的影响。',
    ],
  };
};

const buildRestrainedIssueLabels = (words = []) => {
  const labels = new Set();
  const text = words.join(' ');
  if (/量产|上市|开卖|交付|什么时候/i.test(text)) labels.add('量产与上市节奏');
  if (/ppt|概念|真假|真实|落地|画饼/i.test(text)) labels.add('产品真实性与落地证据');
  if (/牛马|打工|自嘲|冒犯|阴阳/i.test(text)) labels.add('表达方式与用户身份感受');
  if (/价格|贵|权益|优惠|订金|定金/i.test(text)) labels.add('价格、权益与规则清晰度');
  if (/安全|续航|性能|通过|越野|可靠/i.test(text)) labels.add('性能与安全证据边界');
  return Array.from(labels).slice(0, 4);
};

const buildExpertReviews = ({ commentSentiment = {}, issueLabels = [], platforms = [], topics = [], externalVoiceRows = [] }) => {
  const strongestPlatform = [...platforms].sort((a, b) => b.interaction - a.interaction || b.views - a.views)[0];
  const topicNames = topics.slice(0, 3).map(item => item.keyword).join('、');
  const riskNote = issueLabels.length ? `需关注${issueLabels.join('、')}。` : '当前未形成明确高频风险议题。';
  return [
    {
      role: 'Chief Social Media Marketing Officer',
      title: '社媒运营判断',
      level: '运营建议',
      judgement: strongestPlatform ? `${reportPlatformDisplayName(strongestPlatform.platform)} 当前更适合承担内容验证与放大角色，但各平台不能直接复用同一套表达。` : '当前平台样本不足，暂不能判断稳定的平台角色。',
      action: '将平台拆解从“数据对比”升级为“平台角色 + 内容适配 + 评论运营”三段式诊断。',
      evidence: strongestPlatform ? `${reportPlatformDisplayName(strongestPlatform.platform)}互动或阅读/播放领先` : '平台数据覆盖不足',
    },
    {
      role: 'Chief Brand Growth Officer',
      title: '品牌增长判断',
      level: '增长机会',
      judgement: 'MONTX 已具备较强识别度，但还需要把声量转化为信任、搜索、询盘和长期栏目资产。',
      action: topicNames ? `围绕 ${topicNames} 建立栏目化内容与搜索承接路径。` : '先补齐内容主题、搜索词和转化路径的验证。',
      evidence: topicNames || '有效话题词样本不足',
    },
    {
      role: 'Chief PR Officer',
      title: '舆情与声誉判断',
      level: commentSentiment.risk ? 'S1 · 轻度争议' : 'S0 · 监测',
      judgement: `${riskNote} 目前更适合用解释型内容和评论区温和回应处理，不建议放大为公开声明。`,
      action: '建立评论区二级内容机制：置顶解释、FAQ 回复、风险评论人工复核。',
      evidence: commentSentiment.total ? `基于 ${commentSentiment.total} 条自营后台评论` : '后台评论不足',
    },
    {
      role: 'Chief Legal Officer',
      title: '法务与合规判断',
      level: externalVoiceRows.length ? 'L1 · 需边界说明' : 'L0 · 常规监测',
      judgement: '内容建议涉及产品能力、上市交付、用户素材和外部声量引用时，需要保留证据、授权和适用条件。',
      action: '对性能、交付、用户评论、UGC/KOL 二次传播建立“证据/授权/审批”三项检查。',
      evidence: externalVoiceRows.length ? '已接入外部声量内容' : '外部声量样本不足',
    },
  ];
};

function buildSocialAssetReport({ brand, accounts, posts, comments, period, regionFilter, syncState }) {
  const periodConfig = REPORT_PERIODS.find(item => item.id === period) || REPORT_PERIODS[1];
  const generatedAtDate = new Date();
  const periodStart = periodConfig.days ? new Date(generatedAtDate.getTime() - periodConfig.days * 24 * 60 * 60 * 1000) : null;
  const rawPeriodPosts = posts.filter(post => {
    const publishDate = getReportPostDate(post);
    if (periodStart && publishDate && publishDate < periodStart) return false;
    return regionFilter === 'all' || getSocialRegion(post.platform) === regionFilter;
  });
  const periodRange = buildReportDateRange({ posts: rawPeriodPosts, periodConfig, generatedAt: generatedAtDate });
  const externalPeriodPosts = rawPeriodPosts.filter(isExternalReportPost);
  const periodPosts = rawPeriodPosts.filter(post => !isExternalReportPost(post));
  const externalAccountIds = new Set(externalPeriodPosts.map(post => post.account_id).filter(Boolean));
  const accountIds = new Set(periodPosts.map(post => post.account_id));
  const reportAccounts = accounts.filter(account => {
    const regionOk = regionFilter === 'all' || getSocialRegion(account.platform) === regionFilter;
    return regionOk && !externalAccountIds.has(account.account_id) && (accountIds.size === 0 || accountIds.has(account.account_id) || account.brand_id === brand?.id);
  });
  const postIds = new Set(periodPosts.map(post => post.post_id));
  const externalPostIds = new Set(externalPeriodPosts.map(post => post.post_id));
  const reportComments = comments.filter(comment => postIds.has(comment.post_id));
  const externalReportComments = comments.filter(comment => externalPostIds.has(comment.post_id));
  const totalViews = periodPosts.reduce((sum, post) => sum + getPostContentViews(post), 0);
  const totalLikes = periodPosts.reduce((sum, post) => sum + getPostInteractionParts(post).likes, 0);
  const totalComments = periodPosts.reduce((sum, post) => sum + getPostInteractionParts(post).comments, 0) + reportComments.length;
  const totalShares = periodPosts.reduce((sum, post) => sum + getPostInteractionParts(post).shares, 0);
  const totalInteraction = totalLikes + totalComments + totalShares;
  const followers = reportAccounts.reduce((sum, account) => sum + getReportFollowers(account), 0);
  const growthValues = reportAccounts.map(getReportFollowerGrowth).filter(value => value !== null);
  const followerGrowth = growthValues.length ? growthValues.reduce((sum, value) => sum + value, 0) : null;
  const followerGrowthTrend = buildFollowerGrowthTrend(reportAccounts);
  const regions = ['domestic', 'overseas'].map(region => {
    const regionPosts = periodPosts.filter(post => getSocialRegion(post.platform) === region);
    const regionPostIds = new Set(regionPosts.map(post => post.post_id));
    const regionAccountIds = new Set(regionPosts.map(post => post.account_id));
	    const regionAccounts = reportAccounts.filter(account => getSocialRegion(account.platform) === region && (regionAccountIds.has(account.account_id) || account.brand_id === brand?.id));
    const regionGrowthValues = regionAccounts.map(getReportFollowerGrowth).filter(value => value !== null);
    const regionLikes = regionPosts.reduce((sum, post) => sum + getPostInteractionParts(post).likes, 0);
    const regionComments = regionPosts.reduce((sum, post) => sum + getPostInteractionParts(post).comments, 0) + reportComments.filter(comment => regionPostIds.has(comment.post_id)).length;
    const regionShares = regionPosts.reduce((sum, post) => sum + getPostInteractionParts(post).shares, 0);
    return {
      region,
      label: reportRegionLabel(region),
      accounts: regionAccounts.length,
      posts: regionPosts.length,
      views: regionPosts.reduce((sum, post) => sum + getPostContentViews(post), 0),
      interaction: regionLikes + regionComments + regionShares,
      likes: regionLikes,
      comments: regionComments,
      shares: regionShares,
      followers: regionAccounts.reduce((sum, account) => sum + getReportFollowers(account), 0),
      followerGrowth: regionGrowthValues.length ? regionGrowthValues.reduce((sum, value) => sum + value, 0) : null,
      platforms: Array.from(new Set([...regionAccounts.map(account => account.platform), ...regionPosts.map(post => post.platform)].filter(Boolean))),
    };
  });
  const platforms = Array.from(new Set([...reportAccounts.map(account => account.platform), ...periodPosts.map(post => post.platform)].filter(Boolean)))
    .map(platform => {
      const platformPosts = periodPosts.filter(post => post.platform === platform);
      const platformAccountIds = new Set(platformPosts.map(post => post.account_id));
      const platformAccounts = reportAccounts.filter(account => account.platform === platform && (platformAccountIds.size === 0 || platformAccountIds.has(account.account_id) || account.brand_id === brand?.id));
      const platformPostIds = new Set(platformPosts.map(post => post.post_id));
      const platformComments = reportComments.filter(comment => comment.platform === platform || platformPostIds.has(comment.post_id));
      const config = getPlatformMetricConfig(platform);
      const nativeMetrics = {
        views: sumNativeMetric(platformPosts, 'views'),
        view_users: sumNativeMetric(platformPosts, 'viewUsers'),
        reads: sumNativeMetric(platformPosts, 'reads'),
        plays: sumNativeMetric(platformPosts, 'plays'),
        likes: sumNativeMetric(platformPosts, 'likes'),
        comments: sumNativeMetric(platformPosts, 'comments'),
        shares: sumNativeMetric(platformPosts, 'shares'),
        share_users: sumNativeMetric(platformPosts, 'shareUsers'),
        repost_forward_count: sumNativeMetric(platformPosts, 'reposts'),
        saves: sumNativeMetric(platformPosts, 'saves'),
        save_favorite_users: sumNativeMetric(platformPosts, 'saveUsers'),
        favorites: sumNativeMetric(platformPosts, 'favorites'),
        danmaku_count: sumNativeMetric(platformPosts, 'danmaku'),
        coin_count: sumNativeMetric(platformPosts, 'coins'),
        dislike_count: sumNativeMetric(platformPosts, 'dislikes'),
        download_count: sumNativeMetric(platformPosts, 'downloads'),
        total_interactions: sumNativeMetric(platformPosts, 'totalInteractions'),
        watch_time_seconds: sumNativeMetric(platformPosts, 'watchTime'),
        avg_watch_time_seconds: platformPosts.length ? sumNativeMetric(platformPosts, 'avgWatchTime') / platformPosts.length : 0,
        avg_view_percentage: platformPosts.length ? sumNativeMetric(platformPosts, 'avgViewPercentage') / platformPosts.length : 0,
        completion_rate: platformPosts.length ? sumNativeMetric(platformPosts, 'completionRate') / platformPosts.length : 0,
        profile_visits: sumNativeMetric(platformPosts, 'profileVisits'),
        followers_gained: sumNativeMetric(platformPosts, 'followersGained'),
        followers_lost: sumNativeMetric(platformPosts, 'followersLost'),
        link_clicks: sumNativeMetric(platformPosts, 'linkClicks'),
        conversion_count: sumNativeMetric(platformPosts, 'conversionCount'),
      };
      const contentViews = nativeMetrics[config.viewSource] || nativeMetrics.views || nativeMetrics.plays || nativeMetrics.reads || 0;
      const likes = nativeMetrics.likes;
      const postComments = nativeMetrics.comments;
      const shares = nativeMetrics.shares + nativeMetrics.repost_forward_count;
      const interactionComments = postComments + platformComments.length;
      const interaction = likes + interactionComments + shares;
      const followersTotal = platformAccounts.reduce((sum, account) => sum + getReportFollowers(account), 0);
      const platformGrowthValues = platformAccounts.map(getReportFollowerGrowth).filter(value => value !== null);
      const followerGrowthValue = platformGrowthValues.length ? platformGrowthValues.reduce((sum, value) => sum + value, 0) : null;
      const topPostInPlatform = [...platformPosts].sort((a, b) => getPostContentScore(b) - getPostContentScore(a))[0];
      const topPostsInPlatform = [...platformPosts]
        .sort((a, b) => getPostContentScore(b) - getPostContentScore(a))
        .slice(0, 3)
        .map(post => buildReportContentRow(post, syncState, accounts));
      const mix = { likes, comments: interactionComments, shares };
      const leadingInteraction = Object.entries(mix).sort((a, b) => b[1] - a[1])[0] || ['likes', 0];
      const leadingLabel = { likes: '点赞', comments: '评论', shares: '分享', saves: '收藏' }[leadingInteraction[0]];
      const accountNames = platformAccounts.map(getReportAccountName).filter(Boolean);
      const comparability = getPlatformComparability({ platformPosts, contentViews, interaction, config });
      const limitation = getPlatformLimitation({ platform, platformPosts, contentViews, config, followerGrowthValue });
      const accountInsight = platformAccounts.length
        ? `${platform} 当前纳入 ${platformAccounts.length} 个账号：${accountNames.slice(0, 3).join('、') || '未命名账号'}；粉丝数 ${formatCompactNumber(followersTotal)}，${followerGrowthValue === null ? '涨粉待历史基线。' : `按期初 0 口径，本周期净增 ${formatCompactNumber(followerGrowthValue)}。`}`
        : `${platform} 当前没有可归属账号数据，账号诊断仅能基于发布内容反推。`;
      const interactionInsight = platformPosts.length
        ? contentViews
          ? `${platform} 本周期发布 ${platformPosts.length} 条，${config.viewLabel} ${formatCompactNumber(contentViews)}，点赞 ${formatCompactNumber(likes)}、评论 ${formatCompactNumber(interactionComments)}、转发 ${formatCompactNumber(shares)}，阅读/播放到互动转化 CVR ${(interaction / Math.max(contentViews, 1) * 100).toFixed(2)}%；主要互动来自${leadingLabel} ${formatCompactNumber(leadingInteraction[1])}${topPostInPlatform ? `，代表内容为「${cleanReportLabel(topPostInPlatform.title || '未命名内容').slice(0, 24)}」。` : '。'}`
          : `${platform} 本周期发布 ${platformPosts.length} 条，${config.viewLabel} 字段为 0，但已同步到点赞/评论/转发 ${formatCompactNumber(interaction)}；需要补齐阅读/播放口径后再判断内容转化 CVR。`
        : `${platform} 本周期暂无有效发布，互动诊断暂不成立。`;
      return {
        platform,
        region: getSocialRegion(platform),
        accounts: platformAccounts.length,
        accountNames,
        posts: platformPosts.length,
        views: contentViews,
        interaction,
        likes,
        comments: interactionComments,
        shares,
        followers: followersTotal,
        followerGrowth: followerGrowthValue,
        engagementRate: contentViews ? interaction / contentViews : 0,
        normalized: {
          views: contentViews,
          interaction,
          likes,
          comments: interactionComments,
          shares,
          engagements_core: interaction,
          engagements_platform: interaction + nativeMetrics.danmaku_count + nativeMetrics.coin_count + nativeMetrics.download_count,
          followers: followersTotal,
          followerGrowth: followerGrowthValue,
          view_to_interaction_rate: contentViews ? interaction / contentViews : null,
          engagement_rate_by_view: contentViews ? interaction / contentViews : null,
          engagementRate: contentViews ? interaction / contentViews : 0,
        },
        nativeMetrics,
        metricMapping: {
          viewSource: config.viewSource,
          viewLabel: config.viewLabel,
          interactionFormula: 'likes + comments + shares',
        },
        dataQuality: {
          views: contentViews ? 'ready' : platformPosts.length ? 'missing' : 'missing',
          followerGrowth: followerGrowthValue === null ? 'missing_baseline' : 'ready',
          comparability,
        },
        interactionMix: mix,
        topPost: topPostInPlatform ? {
          post_id: topPostInPlatform.post_id,
          title: cleanReportLabel(topPostInPlatform.title || '未命名内容'),
          url: getSocialPostUrl(topPostInPlatform, platformAccounts.find(account => account.account_id === topPostInPlatform.account_id) || getSocialAccount(topPostInPlatform.account_id)),
          views: getPostContentViews(topPostInPlatform),
          interaction: getPostEngagementsCore(topPostInPlatform),
          ...getPostInteractionParts(topPostInPlatform),
        } : null,
        topPosts: topPostsInPlatform,
        diagnosis: {
          accountInsight,
          interactionInsight,
          limitation,
        },
        accountInsight,
        interactionInsight,
        limitation,
        status: !platformPosts.length ? 'no_posts' : contentViews ? (comparability === 'native_only' ? 'native_only' : 'ready') : 'missing_views',
      };
    })
    .sort((a, b) => compareReportPlatformOrder(a.platform, b.platform) || b.views - a.views || b.interaction - a.interaction || a.platform.localeCompare(b.platform, 'zh-CN'));
		  const contentRows = periodPosts.map(post => buildReportContentRow(post, syncState, accounts));
		  const interactionTrend = Array.isArray(syncState.assetInteractionTrend) ? syncState.assetInteractionTrend : [];
	  const externalContentRows = externalPeriodPosts.map(post => buildReportContentRow(post, syncState, accounts));
	  const topPosts = [...contentRows]
	    .sort((a, b) => b.score - a.score)
	    .slice(0, 6);
	  const contentComparisons = buildContentComparisons({ contentRows, accounts: reportAccounts });
	  const externalVoiceRows = [...externalContentRows].sort((a, b) => b.score - a.score);
	  const externalPlatforms = Array.from(new Set(externalContentRows.map(row => row.platform).filter(Boolean))).map(platform => ({
	    platform,
	    region: getSocialRegion(platform),
	    posts: externalContentRows.filter(row => row.platform === platform).length,
	  }));
	  const externalCommentBreakdown = buildCommentPlatformBreakdown({ platforms: externalPlatforms, comments: externalReportComments, posts: externalPeriodPosts });
	  const externalVoice = {
	    overview: {
	      posts: externalContentRows.length,
	      accounts: new Set(externalContentRows.map(row => row.account_id).filter(Boolean)).size,
	      views: externalContentRows.reduce((sum, row) => sum + socialNumber(row.views), 0),
	      interaction: externalContentRows.reduce((sum, row) => sum + socialNumber(row.interaction), 0),
	      likes: externalContentRows.reduce((sum, row) => sum + socialNumber(row.likes), 0),
	      comments: externalContentRows.reduce((sum, row) => sum + socialNumber(row.comments), 0) + externalReportComments.length,
	      shares: externalContentRows.reduce((sum, row) => sum + socialNumber(row.shares), 0),
	    },
	    contentRows: externalVoiceRows,
	    comments: {
	      total: externalReportComments.length,
	      byPlatform: externalCommentBreakdown,
	      samples: externalReportComments.slice(0, 6).map(comment => buildReportCommentSample(comment, externalPeriodPosts)),
	    },
	  };
	  const ugc = externalVoiceRows;
  const topicMap = new Map();
  periodPosts.forEach(post => {
    getPostHashtags(post).forEach(keyword => {
      const prev = topicMap.get(keyword) || { keyword, source: 'hashtag', frequency: 0, views: 0, interaction: 0, likes: 0, comments: 0, shares: 0, platforms: new Set() };
      const parts = getPostInteractionParts(post);
      prev.frequency += 1;
      prev.views += getPostContentViews(post);
      prev.interaction += getPostEngagementsCore(post);
      prev.likes += parts.likes;
      prev.comments += parts.comments;
      prev.shares += parts.shares;
      prev.platforms.add(post.platform);
      topicMap.set(keyword, prev);
    });
  });
  reportComments.forEach(comment => {
    const keyword = String(comment.keyword || comment.risk_tag || '').replace(/^#/, '').trim();
    if (!keyword) return;
    const post = periodPosts.find(item => item.post_id === comment.post_id) || {};
    const prev = topicMap.get(keyword) || { keyword, source: 'comment', frequency: 0, views: 0, interaction: 0, likes: 0, comments: 0, shares: 0, platforms: new Set() };
    const parts = getPostInteractionParts(post);
    prev.frequency += 1;
    prev.views += getPostContentViews(post);
    prev.interaction += getPostEngagementsCore(post);
    prev.likes += parts.likes;
    prev.comments += parts.comments + 1;
    prev.shares += parts.shares;
    prev.platforms.add(comment.platform || post.platform);
    topicMap.set(keyword, prev);
  });
  const topics = Array.from(topicMap.values())
    .map(item => ({ ...item, platforms: Array.from(item.platforms) }))
    .sort((a, b) => b.interaction - a.interaction || b.frequency - a.frequency)
    .slice(0, 8);
	  const positiveComments = reportComments.filter(isPositiveComment);
	  const riskComments = reportComments.filter(isRiskComment);
	  const neutralComments = reportComments.filter(comment => !positiveComments.includes(comment) && !riskComments.includes(comment));
	  const commentSamples = reportComments.map(comment => buildReportCommentSample(comment, periodPosts));
	  const commentPlatformBreakdown = buildCommentPlatformBreakdown({ platforms, comments: reportComments, posts: periodPosts });
	  const commentSentimentTotal = reportComments.length;
	  const commentSentiment = {
	    total: commentSentimentTotal,
	    positive: positiveComments.length,
	    neutral: neutralComments.length,
	    risk: riskComments.length,
	    positiveRate: commentSentimentTotal ? positiveComments.length / commentSentimentTotal : 0,
	    neutralRate: commentSentimentTotal ? neutralComments.length / commentSentimentTotal : 0,
	    riskRate: commentSentimentTotal ? riskComments.length / commentSentimentTotal : 0,
	    topPositiveWords: buildCommentWordCloud(positiveComments).slice(0, 3).map(item => item.keyword),
	    topRiskWords: buildCommentWordCloud(riskComments).slice(0, 3).map(item => item.keyword),
	  };
	  const restrainedIssueLabels = buildRestrainedIssueLabels(commentSentiment.topRiskWords);
	  const topPost = topPosts[0];
  const strongTopics = topics.slice(0, 3).map(item => item.keyword);
  const bestRegion = [...regions].sort((a, b) => b.views - a.views)[0];
  const weakRegion = [...regions].sort((a, b) => a.views - b.views)[0];
  const lessonsGood = [
    topPost ? `高表现内容集中在「${topPost.title.slice(0, 28)}」：${topPost.platform} 贡献 ${formatCompactNumber(topPost.views)} 阅读/播放。` : '',
    strongTopics.length ? `有效内容关键词：${strongTopics.join('、')}，建议沉淀为后续选题标签。` : '',
    commentSentiment.total ? `用户总体舆情中正向反馈 ${commentSentiment.positive} 条，占 ${Math.round(commentSentiment.positiveRate * 100)}%；${commentSentiment.topPositiveWords.length ? `认可点集中在「${commentSentiment.topPositiveWords.join('、')}」。` : '说明当前内容已形成一定兴趣与认可。'}` : '',
    bestRegion?.views ? `${bestRegion.label}区域当前阅读/播放规模领先，占总阅读/播放 ${Math.round((bestRegion.views / Math.max(totalViews, 1)) * 100)}%。` : '',
  ].filter(Boolean);
  const lessonsImprove = [
    followerGrowth === null ? '涨粉缺少历史基线，需在账号同步中保存上一周期 followers 才能形成增长诊断。' : '',
    !reportComments.length ? '后台评论数据不足，暂不能提炼用户反馈与风险语义。' : '',
    commentSentiment.risk ? `用户总体舆情中风险反馈 ${commentSentiment.risk} 条，占 ${Math.round(commentSentiment.riskRate * 100)}%；${restrainedIssueLabels.length ? `需优先解释${restrainedIssueLabels.join('、')}相关疑问。` : '需补充回应口径和 FAQ。'}` : '',
	    !externalVoiceRows.length ? '未识别到外部声量内容，需要接入 earned_media / UGC / KOL 来源。' : '',
    weakRegion && weakRegion.posts === 0 ? `${weakRegion.label}区域暂无有效发布，区域对比诊断不完整。` : '',
  ].filter(Boolean);
  const dataCoverage = {
    overview: periodPosts.length > 0,
    regions: regions.some(row => row.posts > 0),
	    platforms: platforms.length > 0,
	    contentComparisons: contentComparisons.length > 0,
	    topPosts: topPosts.length > 0,
	    externalVoice: externalVoiceRows.length > 0,
	    ugc: externalVoiceRows.length > 0,
    topics: topics.length > 0,
    comments: reportComments.length > 0,
    followerGrowth: followerGrowth !== null,
    missing: [
	      !externalVoiceRows.length ? '外部声量来源' : '',
      !platforms.length ? '平台拆解' : '',
      !reportComments.length ? '后台评论' : '',
      followerGrowth === null ? '历史粉丝基线' : '',
    ].filter(Boolean),
  };
  const contentDiagnosis = buildMontxContentDiagnosis({ topPosts, topics, platforms, dataCoverage });
  contentDiagnosis.expertReviews = buildExpertReviews({ commentSentiment, issueLabels: restrainedIssueLabels, platforms, topics, externalVoiceRows });
  contentDiagnosis.restrainedIssueLabels = restrainedIssueLabels;
  return {
    reportType: 'SocialAssetReport',
    generatedAt: formatBeijingTime(generatedAtDate),
    brand: brand?.name || BRAND?.name || 'MONTX',
    period: periodConfig.label,
    periodRange,
    regionFilter,
    sync: {
      status: syncState.status || 'idle',
      lastSync: syncState.lastSync || '',
      nextRun: syncState.nextRun || '',
      errors: syncState.errors || [],
    },
    overview: {
      accounts: reportAccounts.length,
      posts: periodPosts.length,
      publishedPosts: periodPosts.filter(post => post.status === 'published').length,
      views: totalViews,
      interaction: totalInteraction,
      likes: totalLikes,
      comments: totalComments,
      shares: totalShares,
      engagements_core: totalInteraction,
      followers,
      followerGrowth,
      view_to_interaction_rate: totalViews ? totalInteraction / totalViews : null,
      engagement_rate_by_view: totalViews ? totalInteraction / totalViews : null,
	      engagementRate: totalViews ? totalInteraction / totalViews : 0,
	    },
	    followerSync: {
	      status: followerGrowth === null ? 'missing_history_baseline' : 'ready',
	      explanation: followerGrowth === null
	        ? '当前没有可用的粉丝净增字段，粉丝数会回退到平台返回的真实总量；外部声量账号不计入自营粉丝。'
	        : '本报表按期初粉丝为 0 处理，粉丝数优先使用本周期净涨粉推算；外部声量账号不计入自营粉丝。',
	    },
    followerGrowthTrend,
    interactionTrend,
    regions,
	    platforms,
	    contentRows,
	    contentComparisons,
	    topPosts,
	    ugc,
	    externalVoice,
	    topics,
	    comments: {
	      total: reportComments.length,
	      positive: positiveComments.length,
	      neutral: neutralComments.length,
	      risk: riskComments.length,
        wordClouds: {
          positive: buildCommentWordCloud(positiveComments),
          neutral: buildCommentWordCloud(neutralComments),
          risk: buildCommentWordCloud(riskComments),
        },
	      byPlatform: commentPlatformBreakdown,
	      samples: commentSamples.slice(0, 6),
	    },
    contentDiagnosis,
    lessons: { good: lessonsGood, improve: lessonsImprove, sentiment: commentSentiment },
    dataCoverage,
  };
}

const Reports = ({ workspaceContext }) => {
  const [period, setPeriod] = React.useState('14d');
  const [viewMode, setViewMode] = React.useState('deck');
  const [publishing, setPublishing] = React.useState(false);
  const [publishStatus, setPublishStatus] = React.useState(null);
  const [syncedAssets, setSyncedAssets] = React.useState({ accounts: [], posts: [], comments: [], assetInteractionTrend: [], lastSync: '', nextRun: '', status: 'idle', errors: [] });
  const [reportTranslations, setReportTranslations] = React.useState({});
  const [reportTranslationStatus, setReportTranslationStatus] = React.useState('idle');
  const reportRef = React.useRef(null);
  const brandId = workspaceContext?.brandId || BRAND?.id || 'montx';
  const activeBrand = (WORKSPACE_BRANDS || []).find(brand => brand.id === brandId) || BRAND;
  const baseDataset = getMonitorDataset(brandId);
  const accounts = mergeSocialRows(baseDataset.accounts, syncedAssets.accounts, 'account_id');
  const posts = mergeSocialRows(baseDataset.posts, syncedAssets.posts, 'post_id');
  const comments = mergeSocialRows(baseDataset.comments, syncedAssets.comments || [], 'comment_id');
  const periodLabel = REPORT_PERIODS.find(item => item.id === period)?.label || REPORT_PERIODS[1].label;
  const SocialAssetReport = buildSocialAssetReport({ brand: activeBrand, accounts, posts, comments, period, regionFilter: 'all', syncState: syncedAssets });
  const reportTranslationCandidates = React.useMemo(() => {
    const items = [];
    SocialAssetReport.externalVoice.contentRows.forEach(row => {
      const key = socialTranslationKey('report-post', row.post_id);
      if (needsChineseTranslation(row.title) && !reportTranslations[key]) items.push({ id: key, type: 'external_report_title', text: row.title });
    });
    SocialAssetReport.externalVoice.comments.byPlatform.forEach(platformRow => {
      platformRow.samples.forEach(comment => {
        const key = socialTranslationKey('report-comment', comment.comment_id);
        if (needsChineseTranslation(comment.text) && !reportTranslations[key]) items.push({ id: key, type: 'external_report_comment', text: comment.text });
      });
    });
    return items.slice(0, 24);
  }, [SocialAssetReport, reportTranslations]);
  const reportTranslationSignature = reportTranslationCandidates.map(item => item.id).join('|');
  const reportTranslatedCount = Object.values(reportTranslations).filter(item => item?.translation).length;

  const applySyncPayload = React.useCallback((data = {}) => {
    setSyncedAssets({
      accounts: data.accounts || [],
      posts: data.posts || [],
      comments: data.comments || [],
      assetInteractionTrend: data.assetInteractionTrend || [],
      lastSync: data.lastFinishedAt || '',
      nextRun: data.nextRunAt || '',
      status: data.status || 'idle',
      errors: data.errors || [],
    });
  }, []);

  React.useEffect(() => {
    let cancelled = false;
    const load = async () => {
      try {
        const days = REPORT_PERIODS.find(item => item.id === period)?.days || 0;
        const response = await fetch(`/api/social/sync/status${days ? `?days=${days}` : ''}`);
        const data = await response.json();
        if (!cancelled && response.ok) applySyncPayload(data);
      } catch (error) {
        if (!cancelled) setSyncedAssets(prev => ({ ...prev, errors: [{ platform: 'local', message: error.message || '读取同步状态失败' }] }));
      }
    };
    load();
    const int = setInterval(load, 30000);
    return () => {
      cancelled = true;
      clearInterval(int);
    };
  }, [applySyncPayload, period]);

  React.useEffect(() => {
    if (!reportTranslationSignature || reportTranslationStatus === 'loading') return;
    let cancelled = false;
    const items = reportTranslationCandidates;
    setReportTranslationStatus('loading');
    fetch('/api/social/translate', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ items }),
    })
      .then(response => response.json().then(data => ({ ok: response.ok, data })))
      .then(({ data }) => {
        if (cancelled) return;
        setReportTranslationStatus(data.ok === false ? 'error' : 'ready');
        setReportTranslations(prev => {
          const next = { ...prev };
          (data.translations || []).forEach(item => {
            next[item.id] = {
              translation: item.translation || '',
              language: item.language || '',
              error: item.error || '',
              source: data.source || 'unknown',
            };
          });
          return next;
        });
      })
      .catch(error => {
        if (cancelled) return;
        setReportTranslationStatus('error');
        setReportTranslations(prev => {
          const next = { ...prev };
          items.forEach(item => {
            next[item.id] = { translation: '', language: '', error: error.message || '翻译失败', source: 'client-error' };
          });
          return next;
        });
      });
    return () => {
      cancelled = true;
    };
  }, [reportTranslationSignature]);

  const publishPublicDeck = async () => {
    setPublishing(true);
    setPublishStatus({ state: 'loading', message: `正在生成${viewMode === 'deck' ? '16:9 画布' : '连续长页面'}报告并同步公网...` });
    try {
      const response = await fetch('/api/reports/publish', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          SocialAssetReport,
	          translations: reportTranslations,
	          period,
	          periodLabel: SocialAssetReport.periodRange?.label || periodLabel,
	          viewMode,
	          renderedHtml: reportRef.current?.outerHTML || '',
	        }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || '同步公网失败');
      setPublishStatus({
        state: data.ok ? 'ready' : 'error',
        message: data.message || (data.ok ? '已推送公网。' : 'GitHub 推送失败。'),
        publicUrl: data.ok ? data.publicUrl : '',
        reportId: data.reportId,
        error: data.git?.error || '',
      });
    } catch (error) {
      setPublishStatus({ state: 'error', message: error.message || '同步公网失败' });
    } finally {
      setPublishing(false);
    }
  };

  return (
    <div className="page wide reports-page">
      <div className="page-header">
        <div className="heading">
          <h1>社媒资产运营报表 · <span className="hl">Social Asset</span> Operating Report</h1>
          <p>基于真实同步账号、已发布内容与后台评论生成运营看板；缺失数据会明确标记，不用示例内容补位。</p>
        </div>
        <div className="actions">
	          <button className="btn" onClick={() => setViewMode(mode => mode === 'deck' ? 'long' : 'deck')}>
	            <Icon name="refresh" className="icon sm"/>切换视图 · {viewMode === 'deck' ? '长页面' : '画布'}
	          </button>
	          <button className="btn primary" onClick={publishPublicDeck} disabled={publishing}><Icon name="upload" className="icon sm"/>{publishing ? '同步中' : '同步公网'}</button>
	        </div>
      </div>
      {publishStatus && (
        <div className={`report-publish-status ${publishStatus.state}`}>
          <b>{publishStatus.state === 'ready' ? '同步完成' : publishStatus.state === 'loading' ? '同步中' : '同步失败'}</b>
          <span>{publishStatus.message}</span>
          {publishStatus.publicUrl && <a href={publishStatus.publicUrl} target="_blank" rel="noreferrer">{publishStatus.publicUrl}</a>}
          {publishStatus.error && <small>{publishStatus.error}</small>}
        </div>
      )}

      <div className="report-top-filter">
        <div>
          <label className="field-label">报告周期</label>
          <div className="segmented report-period-control">
            {REPORT_PERIODS.map(item => <button key={item.id} className={item.id === period ? 'active' : ''} onClick={() => setPeriod(item.id)}>{item.label}</button>)}
          </div>
        </div>
        <div className="report-top-meta">
	          <span className={`chip ${syncedAssets.errors?.length ? 'warn' : 'accent'} mono`} style={{fontSize:10}}>sync · {syncedAssets.status || 'idle'}</span>
	          <span>{reportTranslationStatus === 'loading' ? `DeepSeek 翻译中 · 已译 ${reportTranslatedCount}` : `外部声量译文 ${reportTranslatedCount}`}</span>
	          <span>{syncedAssets.lastSync ? `最近同步（北京时间）：${formatBeijingTime(syncedAssets.lastSync)}` : '尚未读取到同步完成时间'}</span>
          {syncedAssets.errors?.length > 0 && <span className="warn">同步异常 {syncedAssets.errors.length} 项，报告仅基于已取回数据。</span>}
        </div>
      </div>

      <div className="grid g-4">
        <MetricCard label="发布内容" value={String(SocialAssetReport.overview.posts)} delta={`已发布 ${SocialAssetReport.overview.publishedPosts} · 账号 ${SocialAssetReport.overview.accounts}`} accent/>
        <MetricCard label="阅读/播放" value={formatCompactNumber(SocialAssetReport.overview.views)} delta={`点赞 ${formatCompactNumber(SocialAssetReport.overview.likes)} · 评论 ${formatCompactNumber(SocialAssetReport.overview.comments)}`}/>
        <MetricCard label="互动转化 CVR" value={`${(SocialAssetReport.overview.engagementRate * 100).toFixed(2)}%`} delta={`转发 ${formatCompactNumber(SocialAssetReport.overview.shares)} · 互动 ${formatCompactNumber(SocialAssetReport.overview.interaction)}`}/>
        <MetricCard label="粉丝数" value={formatCompactNumber(SocialAssetReport.overview.followers)} delta={SocialAssetReport.overview.followerGrowth === null ? '涨粉：待历史基线' : `期初0 · 净增 +${formatCompactNumber(SocialAssetReport.overview.followerGrowth)}`}/>
      </div>

      <div className="report-view-mode-note">
        <b>{viewMode === 'deck' ? '画布预览模式' : '连续长页面模式'}</b>
        <span>{viewMode === 'deck' ? '同步公网将发布 16:9 分页画布，适合导出 PDF / PPT。' : '同步公网将发布连续长页面，适合在线阅读和滚动浏览。'}</span>
      </div>

	      {viewMode === 'deck'
        ? <ReportDeck report={SocialAssetReport} periodLabel={periodLabel} translations={reportTranslations} reportRef={reportRef}/>
        : <ReportLongReport report={SocialAssetReport} periodLabel={periodLabel} translations={reportTranslations} reportRef={reportRef}/>}
    </div>
  );
};

const ReportLongReport = ({ report, periodLabel = '', translations = {}, reportRef }) => (
  <div className="structured-report report-neutral" ref={reportRef}>
    <div className="structured-report-head">
      <div>
        <span className="report-kicker">SocialAssetReport · 连续长页面</span>
        <h2>{report.brand} 社媒资产运营报表</h2>
        <p>{report.periodRange?.label || periodLabel} · 国内 + 海外</p>
      </div>
      <span className="chip ghost mono" style={{fontSize:10}}>Long View</span>
    </div>

    <section className="report-section report-chapter">
      <h3>1. 发布一览及整体数据</h3>
      <div className="report-summary-grid">
        <ReportMiniMetric label="账号" value={String(report.overview.accounts)} note="active"/>
        <ReportMiniMetric label="发布" value={String(report.overview.posts)} note={`已发布 ${report.overview.publishedPosts}`}/>
        <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(report.overview.views)} note="views / plays"/>
        <ReportMiniMetric label="点赞" value={formatCompactNumber(report.overview.likes)} note="likes"/>
        <ReportMiniMetric label="评论" value={formatCompactNumber(report.overview.comments)} note="comments"/>
        <ReportMiniMetric label="转发" value={formatCompactNumber(report.overview.shares)} note="shares"/>
        <ReportMiniMetric label="互动转化 CVR" value={`${(report.overview.engagementRate * 100).toFixed(2)}%`} note={formatCompactNumber(report.overview.interaction)}/>
      </div>
      <p>数据来自社媒同步状态和本地社媒资产表；同步异常不会补齐为示例内容，覆盖情况展示在右侧思考过程。</p>
      <div className="report-subsection">
        <h4>高表现发布内容</h4>
        <ReportDeckTopPostsTable rows={report.topPosts || []}/>
      </div>
    </section>

    <section className="report-section report-chapter">
      <h3>2. 国内 & 海外阅读/播放、互动、涨粉情况</h3>
      <ReportRegionChart rows={report.regions || []}/>
      <div className="report-region-cards">
        {(report.regions || []).map(row => (
          <div key={row.region}>
            <div className="report-region-head">
              <b>{row.label}</b>
              <span>{row.platforms.join(' / ') || '暂无平台'}</span>
            </div>
            <div className="report-region-metrics">
              <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(row.views)} note={`${row.posts} posts`}/>
              <ReportMiniMetric label="点赞" value={formatCompactNumber(row.likes)} note="likes"/>
              <ReportMiniMetric label="评论" value={formatCompactNumber(row.comments)} note="comments"/>
              <ReportMiniMetric label="转发" value={formatCompactNumber(row.shares)} note="shares"/>
              <ReportMiniMetric label="互动转化 CVR" value={`${(row.views ? (row.interaction / row.views) * 100 : 0).toFixed(2)}%`} note={formatCompactNumber(row.interaction)}/>
              <ReportMiniMetric label="粉丝" value={formatCompactNumber(row.followers)} note={row.followerGrowth === null ? '待历史基线' : `+${formatCompactNumber(row.followerGrowth)}`}/>
            </div>
          </div>
        ))}
      </div>
      <div className="report-subsection">
        <h4>粉丝每日增长趋势</h4>
        <ReportFollowerGrowthTrend rows={report.followerGrowthTrend || []} briefRows={report.interactionTrend || []} region="domestic" compact/>
        <ReportFollowerGrowthTrend rows={report.followerGrowthTrend || []} briefRows={report.interactionTrend || []} region="overseas" compact/>
      </div>
      <div className="report-subsection">
        <h4>互动数据趋势与发布节点</h4>
        <ReportInteractionTrend rows={report.interactionTrend || []} region="domestic"/>
        <ReportInteractionTrend rows={report.interactionTrend || []} region="overseas"/>
      </div>
      <div className="report-subsection">
        <h4>平台拆解：平台内容分析</h4>
        {(report.platforms || []).length ? <ReportPlatformBreakdown rows={report.platforms}/> : <ReportEmptyState title="暂无平台数据" body="当前周期没有可用于平台拆解的账号或发布内容。"/>}
      </div>
    </section>

    <section className="report-section report-chapter">
      <h3>3. 优质外部声量内容</h3>
      <p>本节只展示 earned media / KOL / UGC 等外部内容，不计入自营账号、平台表现、话题词和内容横向比较。非中文标题由 DeepSeek 自动翻译。</p>
      {report.externalVoice?.contentRows?.length ? <ReportContentTable rows={report.externalVoice.contentRows} translations={translations}/> : <ReportEmptyState title="暂无外部声量内容" body="当前周期未同步到 source_type=earned_media / ugc / external 的内容。"/>}
    </section>

    <section className="report-section report-chapter">
      <h3>4. 外部声量内容评论分析</h3>
      {report.externalVoice?.comments?.byPlatform?.length ? <ReportCommentPlatformBreakdown rows={report.externalVoice.comments.byPlatform} translations={translations} translationScope="report-comment"/> : <ReportEmptyState title="暂无外部声量评论" body="当前外部声量内容没有同步到可归属平台的评论。"/>}
    </section>

    <section className="report-section report-chapter">
      <h3>5. 有效话题词分析</h3>
      <p>计算逻辑：仅基于自营账号内容；从标题、caption、hashtags 与自营评论 keyword / risk_tag 抽取词项，按出现频次与对应内容互动贡献排序。外部声量内容已排除，避免把第三方传播语境混入自营选题判断。</p>
      {(report.topics || []).length ? <ReportTopicList topics={report.topics}/> : <ReportEmptyState title="暂无可分析话题词" body="未从内容 caption、hashtags 或评论 keyword 中提取到有效词。"/>}
    </section>

    <section className="report-section report-chapter">
      <h3>6. 内容横向比较</h3>
      {(report.contentComparisons || []).length ? <ReportContentComparison rows={report.contentComparisons}/> : <ReportEmptyState title="暂无可横向比较内容" body="当前周期没有可归并到同一内容主题的真实发布数据。"/>}
    </section>

    <section className="report-section report-chapter">
      <h3>7. Lesson & Learn / 内容创作建议</h3>
      <ReportContentDiagnosis diagnosis={report.contentDiagnosis}/>
      <ReportLessonLearn lessons={report.lessons}/>
    </section>

    <section className="report-section report-chapter">
      <h3>8. 用户评论反馈与风险提炼</h3>
      {report.comments?.byPlatform?.length ? (
        <div className="report-comment-summary">
          <div>
            <ReportMiniMetric label="正向" value={String(report.comments.positive)} note="positive"/>
            <ReportMiniMetric label="中性" value={String(report.comments.neutral)} note="neutral"/>
            <ReportMiniMetric label="风险" value={String(report.comments.risk)} note="risk"/>
          </div>
          <ReportCommentPlatformBreakdown rows={report.comments.byPlatform}/>
        </div>
      ) : <ReportEmptyState title="后台评论数据不足" body="当前没有可归属平台的评论数据，无法进行用户反馈提炼和风险语义诊断。"/>}
    </section>
  </div>
);

const chunkReportItems = (items = [], size = 1) => {
  const chunks = [];
  for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
  return chunks.length ? chunks : [[]];
};

const deckText = (value = '', max = 84) => {
  const text = String(value || '').replace(/\s+/g, ' ').trim();
  return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
};

const limitCommentRows = (rows = [], sampleLimit = 4) => rows.map(row => ({
  ...row,
  samples: (row.samples || []).slice(0, sampleLimit),
}));

const ReportDeckFrame = ({ report, periodLabel, title, kicker, pageNumber, totalPages, children, variant = '' }) => (
  <section className={`report-deck-page ${variant}`}>
    <header className="report-deck-page-head">
      <div>
        <span>{kicker}</span>
        <h2>{title}</h2>
      </div>
      <div className="report-deck-brand">
        <span className="report-deck-brand-text">{report.brand || 'MONTX'}</span>
      </div>
    </header>
    <div className="report-deck-page-body">{children}</div>
    <footer className="report-deck-page-foot">
      <span>Social Asset Operating Report</span>
      <b>{String(pageNumber).padStart(2, '0')} / {String(totalPages).padStart(2, '0')}</b>
    </footer>
  </section>
);

const ReportDeckTopPostsTable = ({ rows = [] }) => (
  rows.length ? (
    <table className="tbl compact report-post-table">
      <colgroup>
        <col style={{width:'34%'}}/>
        <col style={{width:'calc(66% / 6)'}}/>
        <col style={{width:'calc(66% / 6)'}}/>
        <col style={{width:'calc(66% / 6)'}}/>
        <col style={{width:'calc(66% / 6)'}}/>
        <col style={{width:'calc(66% / 6)'}}/>
        <col style={{width:'calc(66% / 6)'}}/>
      </colgroup>
      <thead><tr><th>内容</th><th className="report-post-even-col">区域</th><th className="report-post-even-col">阅读/播放</th><th className="report-post-even-col">点赞</th><th className="report-post-even-col">评论</th><th className="report-post-even-col">转发</th><th className="report-post-even-col">互动转化 CVR</th></tr></thead>
      <tbody>
        {rows.map(post => (
          <tr key={post.post_id}>
            <td>
              <div style={{fontWeight:600, fontSize:11}}><ReportContentLink title={post.title} url={post.url}/></div>
              <div className="mono" style={{fontSize:9, color:'var(--ink-4)'}}><ReportPlatformLogo platform={post.platform} small/> {post.platform} · {post.content_type || '内容'}</div>
            </td>
            <td className="report-post-even-col">{reportRegionLabel(post.region)}</td>
            <td className="report-post-even-col mono">{formatCompactNumber(post.views)}</td>
            <td className="report-post-even-col mono">{formatCompactNumber(post.likes)}</td>
            <td className="report-post-even-col mono">{formatCompactNumber(post.comments)}</td>
            <td className="report-post-even-col mono">{formatCompactNumber(post.shares)}</td>
            <td className="report-post-even-col mono">{post.views ? `${(((post.likes || 0) + (post.comments || 0) + (post.shares || 0)) / post.views * 100).toFixed(2)}%` : '-'}</td>
          </tr>
        ))}
      </tbody>
    </table>
  ) : <ReportEmptyState title="暂无发布数据" body="没有可用于排序的真实内容。"/>
);

const ReportDeckRegionDetail = ({ row = {}, topPosts = [], platforms = [] }) => {
  const regionPlatforms = platforms.filter(item => item.region === row.region);
  return (
    <div className="report-deck-stack report-region-detail-page">
      <div className="report-region-detail-grid">
        <div className="report-region-detail-main">
          <div className="report-region-head">
            <b>{row.label || '未归类'}</b>
            <span>{(row.platforms || []).map(reportPlatformDisplayName).join(' / ') || '暂无平台'}</span>
          </div>
          <div className="report-region-metrics">
            <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(row.views || 0)} note={`${row.posts || 0} posts`}/>
            <ReportMiniMetric label="点赞" value={formatCompactNumber(row.likes || 0)} note="likes"/>
            <ReportMiniMetric label="评论" value={formatCompactNumber(row.comments || 0)} note="comments"/>
            <ReportMiniMetric label="转发" value={formatCompactNumber(row.shares || 0)} note="shares"/>
            <ReportMiniMetric label="互动转化 CVR" value={`${(row.views ? (row.interaction / row.views) * 100 : 0).toFixed(2)}%`} note={formatCompactNumber(row.interaction || 0)}/>
            <ReportMiniMetric label="粉丝" value={formatCompactNumber(row.followers || 0)} note={row.followerGrowth === null ? '待历史基线' : `+${formatCompactNumber(row.followerGrowth || 0)}`}/>
          </div>
        </div>
        <div className="report-region-platform-summary">
          <h4>区域平台构成</h4>
          {regionPlatforms.length ? regionPlatforms.slice(0, 5).map(platform => (
            <article key={platform.platform}>
              <span><ReportPlatformLogo platform={platform.platform} small/> {reportPlatformDisplayName(platform.platform)}</span>
              <b>{formatCompactNumber(platform.views || 0)}</b>
              <em>{platform.view_to_interaction_rate === null ? '-' : `${((platform.view_to_interaction_rate || 0) * 100).toFixed(2)}%`}</em>
            </article>
          )) : <ReportEmptyState title="暂无平台数据" body="当前区域没有可展示的平台发布数据。"/>}
        </div>
      </div>
      <div className="report-subsection">
        <h4>{row.label || '区域'}高表现发布内容</h4>
        <ReportDeckTopPostsTable rows={topPosts.filter(post => post.region === row.region).slice(0, 5)}/>
      </div>
    </div>
  );
};

const ReportDeckContentOverviewPage = ({ overview = {}, rows = [], title = '高表现发布内容', emptyTitle = '暂无发布数据', emptyBody = '没有可用于排序的真实内容。' }) => {
  const interaction = overview.interaction ?? ((overview.likes || 0) + (overview.comments || 0) + (overview.shares || 0));
  const engagementRate = overview.engagementRate ?? (overview.views ? interaction / overview.views : 0);
  return (
    <div className="report-deck-stack">
      <div className="report-summary-grid">
        <ReportMiniMetric label="内容数" value={String(overview.posts || rows.length || 0)} note="content"/>
        <ReportMiniMetric label="账号/创作者" value={String(overview.accounts || 0)} note="accounts"/>
        <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(overview.views || 0)} note="views / plays"/>
        <ReportMiniMetric label="点赞" value={formatCompactNumber(overview.likes || 0)} note="likes"/>
        <ReportMiniMetric label="评论" value={formatCompactNumber(overview.comments || 0)} note="comments"/>
        <ReportMiniMetric label="转发" value={formatCompactNumber(overview.shares || 0)} note="shares"/>
        <ReportMiniMetric label="互动转化 CVR" value={`${((engagementRate || 0) * 100).toFixed(2)}%`} note={formatCompactNumber(interaction || 0)}/>
      </div>
      <div className="report-subsection">
        <h4>{title}</h4>
        {rows.length ? <ReportDeckTopPostsTable rows={rows}/> : <ReportEmptyState title={emptyTitle} body={emptyBody}/>}
      </div>
    </div>
  );
};

const getReportCommentTone = (comment = {}) => {
  if (isRiskComment(comment)) return '风险';
  if (isPositiveComment(comment)) return '正向';
  return '中性';
};

const ReportExternalCommentSamples = ({ samples = [], translations = {} }) => (
  <div className="report-external-comment-sample-list">
    {samples.length ? samples.map(comment => (
      <article key={comment.comment_id}>
        <div>
          <b><ReportPlatformLogo platform={comment.platform} small/> {reportPlatformDisplayName(comment.platform)}</b>
          <em className={getReportCommentTone(comment)}>{getReportCommentTone(comment)}</em>
        </div>
        <div className="report-external-comment-copy">
          <p>{deckText(comment.text || '暂无评论文本', 82)}</p>
          {translations[socialTranslationKey('report-comment', comment.comment_id)]?.translation && <span className="monitor-translation-line">译：{deckText(translations[socialTranslationKey('report-comment', comment.comment_id)].translation, 52)}</span>}
        </div>
        <footer>
          <span>{comment.user_concern_short || comment.user_concern_label || comment.keyword || comment.risk_tag || '其他'}{comment.comment_level === 2 ? ' · 二级评论' : ''}</span>
          <ReportContentLink title={comment.source_post_title || '未匹配到原帖'} url={comment.source_post_url}/>
        </footer>
      </article>
    )) : <ReportEmptyState title="暂无评论样本" body="当前没有可展示的外部声量评论文本。"/>}
  </div>
);

const ReportDeckExternalCommentAnalysis = ({ commentRows = [], samples = [], translations = {} }) => {
  const topSamples = [...(samples || [])]
    .sort((a, b) => Number(isRiskComment(b)) - Number(isRiskComment(a)) || Math.abs(socialNumber(b.sentiment)) - Math.abs(socialNumber(a.sentiment)))
    .slice(0, 5);
  return (
    <div className="report-deck-stack report-external-comment-page">
      {commentRows.length ? (
        <table className="tbl compact report-external-comment-table">
          <thead>
            <tr><th>平台</th><th>正向数量</th><th>中性数量</th><th>风险数量</th></tr>
          </thead>
          <tbody>
            {commentRows.map(row => (
              <tr key={row.platform}>
                <td><ReportPlatformLogo platform={row.platform} small/> {reportPlatformDisplayName(row.platform)}</td>
                <td className="mono">{formatCompactNumber(row.positive || 0)}</td>
                <td className="mono">{formatCompactNumber(row.neutral || 0)}</td>
                <td className="mono">{formatCompactNumber(row.risk || 0)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      ) : <ReportEmptyState title="暂无外部声量评论" body="当前外部声量内容没有同步到可归属平台的评论。"/>}
      <div className="report-external-comment-cards">
        <div className="report-external-comment-cards-head">
          <b>外部声量评论反馈 Top 5</b>
          <span>按风险优先，其次按情绪强度排序；显示评论来源内容</span>
        </div>
        <ReportExternalCommentSamples samples={topSamples} translations={translations}/>
      </div>
    </div>
  );
};

const ReportCommentWordCloud = ({ words = [] }) => (
  <div className="report-comment-word-cloud">
    {words.length ? words.map(item => (
      <span key={item.keyword} className={`weight-${item.weight || 1} ${isCommentEmojiToken(item.keyword) ? 'emoji' : ''}`}>{item.keyword}<small>{item.count}</small></span>
    )) : <em>暂无关键词</em>}
  </div>
);

const ReportOwnedCommentOverview = ({ comments = {} }) => {
  const scopeLabel = comments.scopeLabel || '自营账号';
  const rows = [
    { key: 'positive', label: '正向', value: comments.positive || 0, note: '认可 / 兴趣 / 购买意向', tone: 'positive' },
    { key: 'neutral', label: '中性', value: comments.neutral || 0, note: '询问 / 信息补充 / 普通反馈', tone: 'neutral' },
    { key: 'risk', label: '风险', value: comments.risk || 0, note: '质疑 / 误解 / 需响应议题', tone: 'risk' },
  ];
  return (
    <div className="report-owned-comment-overview">
      <div className="report-owned-comment-hero">
        <div>
          <b>{scopeLabel === '自营账号' ? '评论反馈总览' : `${scopeLabel} 评论反馈总览`}</b>
          <p>仅基于{scopeLabel}已同步评论生成；词云来自真实 keyword、risk_tag 与评论文本关键词，用于判断用户关注点和潜在风险议题。</p>
        </div>
        <strong>{formatCompactNumber(comments.total || 0)}<span>comments</span></strong>
      </div>
      <div className="report-owned-comment-clouds">
        {rows.map(row => (
          <article key={row.key} className={row.tone}>
            <div className="report-owned-comment-count">
              <span>{row.label}</span>
              <b>{formatCompactNumber(row.value)}</b>
              <small>{row.note}</small>
            </div>
            <ReportCommentWordCloud words={comments.wordClouds?.[row.key] || []}/>
          </article>
        ))}
      </div>
    </div>
  );
};

const ReportDeckTopicList = ({ topics = [] }) => (
  <div className="report-deck-topic-grid">
    {topics.map(topic => (
      <article key={topic.keyword}>
        <b>#{deckText(topic.keyword, 22)}</b>
        <span>{deckText(topic.platforms?.join(' / ') || topic.source || '内容词', 38)}</span>
        <small>{topic.frequency} 次 · 阅读/播放 {formatCompactNumber(topic.views)} · 互动 {formatCompactNumber((topic.likes || 0) + (topic.comments || 0) + (topic.shares || 0))}</small>
        <p>{topic.analysis || buildTopicAnalysis(topic)}</p>
      </article>
    ))}
  </div>
);

const buildTopicAnalysis = (topic = {}) => {
  const platforms = topic.platforms?.length ? topic.platforms.map(reportPlatformDisplayName).join(' / ') : '当前平台';
  const interaction = (topic.likes || 0) + (topic.comments || 0) + (topic.shares || 0);
  const rate = topic.views ? `${(interaction / topic.views * 100).toFixed(2)}%` : '待补阅读/播放';
  if (topic.source === 'comment') {
    return `来自评论反馈的用户关注点，在 ${platforms} 出现 ${topic.frequency || 0} 次；可用于后续评论回应、FAQ 或风险解释内容，关联内容互动转化 CVR ${rate}。`;
  }
  return `来自标题、文案或 hashtag 的内容主题，在 ${platforms} 覆盖 ${topic.frequency || 0} 条内容；说明该方向已形成可复用选题标签，关联内容互动转化 CVR ${rate}。`;
};

const ReportToneLayer = ({ layer = {}, compact = false }) => (
  <div className="report-tone-layer">
    <b>{layer.level}</b>
    <div className="report-tone-word-list">
      {(layer.words || []).map(word => <span key={word}>{word}</span>)}
    </div>
    <small>{compact ? deckText(layer.usage || '', 42) : layer.usage}</small>
  </div>
);

const ReportDeckDiagnosisSummary = ({ diagnosis }) => {
  if (!diagnosis) return <ReportEmptyState title="内容诊断数据不足" body="当前没有可用于生成内容创作建议的诊断结构。"/>;
  return (
    <div className="report-deck-diagnosis-summary">
      <div className="report-diagnosis-hero">
        <b>内容诊断判断</b>
        <p>{deckText(diagnosis.strategicJudgement, 128)}</p>
      </div>
      <div className="report-diagnosis-grid">
        <div>
          <h4>目前内容写了什么</h4>
          {(diagnosis.currentContentSummary || []).slice(0, 3).map(item => <p key={item}>{deckText(item, 86)}</p>)}
        </div>
        <div>
          <h4>语气资产分层</h4>
          {(diagnosis.toneLayering || []).slice(0, 3).map(layer => (
            <ReportToneLayer key={layer.level} layer={layer} compact/>
          ))}
        </div>
      </div>
    </div>
  );
};

const ReportDeckColumnMatrix = ({ items = [] }) => (
  <div className="report-column-matrix report-deck-column-matrix">
    <h4>长期栏目矩阵</h4>
    <div>
      {items.map(item => (
        <article key={item.column}>
          <b>{deckText(item.column, 18)}</b>
          <span>{deckText(item.goal, 34)}</span>
          <p>{deckText(item.writingRule, 76)}</p>
        </article>
      ))}
    </div>
  </div>
);

const ReportDeckNextDirections = ({ items = [] }) => (
  <div className="report-next-directions report-deck-next-directions">
    <h4>下一阶段新增方向</h4>
    {items.map(item => (
      <article key={item.direction}>
        <em>{item.priority}</em>
        <div>
          <b>{deckText(item.direction, 30)}</b>
          <p>{deckText(item.reason, 88)}</p>
          <span>{deckText((item.suggestedFormats || []).join(' / '), 66)}</span>
        </div>
      </article>
    ))}
  </div>
);

const ReportDeckExpertReviews = ({ reviews = [] }) => (
  <div className="report-expert-review-grid">
    {reviews.map(item => (
      <article key={item.role}>
        <div>
          <b>{item.title}</b>
          <em>{item.level}</em>
        </div>
        <small>{item.role}</small>
        <p>{deckText(item.judgement, 92)}</p>
        <span>{deckText(item.action, 78)}</span>
        <footer>{deckText(item.evidence, 42)}</footer>
      </article>
    ))}
  </div>
);

const ReportDeckAssumptions = ({ items = [] }) => (
  <div className="report-deck-assumption-list">
    <h4>待验证假设</h4>
    {items.map((item, index) => (
      <article key={item}>
        <em>H{index + 1}</em>
        <p>{deckText(item, 118)}</p>
      </article>
    ))}
  </div>
);

const ReportLessonLearn = ({ lessons = {}, compact = false }) => {
  const sentiment = lessons.sentiment || {};
  const goodItems = lessons.good?.length ? lessons.good : ['真实数据不足，暂不生成正向结论。'];
  const improveItems = lessons.improve?.length ? lessons.improve : ['暂无明显数据缺口。'];
  const textLimit = compact ? 108 : 180;
  return (
    <div className="report-lesson-learn">
      <div className="report-lesson-sentiment">
        <div>
          <b>用户总体舆情</b>
          <span>{sentiment.total ? `基于 ${formatCompactNumber(sentiment.total)} 条自营后台评论` : '后台评论数据不足'}</span>
        </div>
        <div className="report-lesson-sentiment-metrics">
          <ReportMiniMetric label="正向" value={String(sentiment.positive || 0)} note={sentiment.total ? `占比 ${Math.round((sentiment.positiveRate || 0) * 100)}%` : 'pending'}/>
          <ReportMiniMetric label="中性" value={String(sentiment.neutral || 0)} note={sentiment.total ? `占比 ${Math.round((sentiment.neutralRate || 0) * 100)}%` : 'pending'}/>
          <ReportMiniMetric label="风险" value={String(sentiment.risk || 0)} note={sentiment.total ? `占比 ${Math.round((sentiment.riskRate || 0) * 100)}%` : 'pending'}/>
        </div>
      </div>
      <div className="report-learning-columns">
        <div>
          <h4>做得好的地方</h4>
          {goodItems.slice(0, compact ? 3 : 6).map(item => <p key={item}>{compact ? deckText(item, textLimit) : item}</p>)}
        </div>
        <div>
          <h4>待改进建议</h4>
          {improveItems.slice(0, compact ? 3 : 6).map(item => <p key={item}>{compact ? deckText(item, textLimit) : item}</p>)}
        </div>
      </div>
    </div>
  );
};

const buildReportDeckPages = ({ report, periodLabel, translations = {} }) => {
  const pages = [];
  const push = (title, kicker, body, variant = '') => pages.push({ title, kicker, body, variant });
  const overview = report.overview || {};
  const regions = report.regions || [];
  const platforms = report.platforms || [];
  const externalVoice = report.externalVoice || { contentRows: [], comments: { byPlatform: [] } };
  const reportComments = report.comments || { byPlatform: [], positive: 0, neutral: 0, risk: 0 };
  const lessons = report.lessons || { good: [], improve: [] };
  const reportDateRangeLabel = report.periodRange?.label || periodLabel;

  push('核心摘要', '00 · Executive Summary', (
    <div className="report-deck-cover">
      <div>
        <div className="report-cover-brand-text">{report.brand || 'MONTX'}</div>
        <h1>社媒资产运营报表</h1>
        <p>{reportDateRangeLabel} · 国内 + 海外 · 基于真实同步内容、评论与账号数据生成</p>
      </div>
      <div className="report-deck-hero-grid">
        <ReportMiniMetric label="发布内容" value={String(overview.posts || 0)} note={`已发布 ${overview.publishedPosts || 0} · 账号 ${overview.accounts || 0}`}/>
        <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(overview.views || 0)} note={`点赞 ${formatCompactNumber(overview.likes || 0)} · 评论 ${formatCompactNumber(overview.comments || 0)}`}/>
        <ReportMiniMetric label="互动转化 CVR" value={`${((overview.engagementRate || 0) * 100).toFixed(2)}%`} note={`转发 ${formatCompactNumber(overview.shares || 0)} · 互动 ${formatCompactNumber(overview.interaction || 0)}`}/>
        <ReportMiniMetric label="粉丝数" value={formatCompactNumber(overview.followers || 0)} note={overview.followerGrowth === null ? '涨粉待基线' : `期初0 · 净增 +${formatCompactNumber(overview.followerGrowth || 0)}`}/>
      </div>
    </div>
  ), 'cover');

  push('发布一览及整体数据', '01 · Publishing Overview', (
    <div className="report-deck-stack">
      <div className="report-summary-grid">
        <ReportMiniMetric label="账号" value={String(overview.accounts || 0)} note="active"/>
        <ReportMiniMetric label="发布" value={String(overview.posts || 0)} note="summit"/>
        <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(overview.views || 0)} note="views / plays"/>
        <ReportMiniMetric label="点赞" value={formatCompactNumber(overview.likes || 0)} note="likes"/>
        <ReportMiniMetric label="评论" value={formatCompactNumber(overview.comments || 0)} note="comments"/>
        <ReportMiniMetric label="转发" value={formatCompactNumber(overview.shares || 0)} note="shares"/>
        <ReportMiniMetric label="互动转化 CVR" value={`${((overview.engagementRate || 0) * 100).toFixed(2)}%`}/>
      </div>
      <div className="report-subsection">
        <h4>高表现发布内容</h4>
        <ReportDeckTopPostsTable rows={(report.topPosts || []).slice(0, 5)}/>
      </div>
    </div>
  ));

  push('国内 & 海外表现', '02 · Region Performance', (
    <div className="report-region-performance-deck">
      <div className="report-region-cards report-region-two-col">
        {regions.map(row => (
          <div key={row.region}>
            <div className="report-region-head">
              <b>{row.label}</b>
              <span>{row.platforms.join(' / ') || '暂无平台'}</span>
            </div>
            <div className="report-region-metrics">
              <ReportMiniMetric label="阅读/播放" value={formatCompactNumber(row.views)} note={`${row.posts} posts`}/>
              <ReportMiniMetric label="点赞" value={formatCompactNumber(row.likes)} note="likes"/>
              <ReportMiniMetric label="评论" value={formatCompactNumber(row.comments)} note="comments"/>
              <ReportMiniMetric label="转发" value={formatCompactNumber(row.shares)} note="shares"/>
              <ReportMiniMetric label="互动转化 CVR" value={`${(row.views ? (row.interaction / row.views) * 100 : 0).toFixed(2)}%`} note={formatCompactNumber(row.interaction)}/>
              <ReportMiniMetric label="粉丝" value={formatCompactNumber(row.followers)} note={row.followerGrowth === null ? '待历史基线' : `+${formatCompactNumber(row.followerGrowth)}`}/>
            </div>
          </div>
        ))}
      </div>
    </div>
  ));

  ['domestic', 'overseas'].forEach((region, index) => {
    const label = reportRegionLabel(region);
    push(`粉丝每日增长趋势 · ${label}`, `02${index ? 'C' : 'B'} · Follower Growth`, (
      <div className="report-follower-growth-page">
        <ReportFollowerGrowthTrend rows={report.followerGrowthTrend || []} briefRows={report.interactionTrend || []} region={region}/>
      </div>
    ));
  });

  ['domestic', 'overseas'].forEach((region, index) => {
    const label = reportRegionLabel(region);
    push(`互动数据趋势与发布节点 · ${label}`, `02${index ? 'E' : 'D'} · Interaction Trend`, (
      <div className="report-interaction-trend-page">
        <ReportInteractionTrend rows={report.interactionTrend || []} region={region}/>
      </div>
    ));
  });

  push('平台聚合数据', '03 · Platform Aggregate', (
    <div className="report-deck-stack">
      <ReportPlatformAggregateTable rows={platforms}/>
    </div>
  ));

  chunkReportItems(platforms, 1).forEach((chunk, index) => {
    const platformTitle = chunk[0]?.platform ? reportPlatformDisplayName(chunk[0].platform) : `平台内容分析${index ? `（续 ${index + 1}）` : ''}`;
    push(`平台拆解：${platformTitle}`, '03 · Platform Breakdown', (
      chunk.length ? <ReportPlatformBreakdown rows={chunk}/> : <ReportEmptyState title="暂无平台数据" body="当前周期没有可用于平台拆解的账号或发布内容。"/>
    ));
  });

  chunkReportItems(externalVoice.contentRows || [], 5).forEach((chunk, index) => {
    push(`优质外部声量内容${index ? `（续 ${index + 1}）` : ''}`, '04 · Earned Media', (
      <ReportDeckContentOverviewPage
        overview={externalVoice.overview || {}}
        rows={chunk}
        title="外部声量高表现内容"
        emptyTitle="暂无外部声量内容"
        emptyBody="当前周期未同步到 source_type=earned_media / ugc / external 的内容。"
      />
    ));
  });

  push('外部声量内容评论分析', '05 · Earned Comments', (
    <ReportDeckExternalCommentAnalysis
      commentRows={externalVoice.comments?.byPlatform || []}
      samples={externalVoice.comments?.samples || []}
      translations={translations}
    />
  ));

  {
    const topicChunk = (report.topics || []).slice(0, 4);
    push('有效话题词分析', '06 · Effective Topics', (
      <div className="report-deck-stack">
        <p>计算逻辑：仅基于自营账号内容；从标题、caption、hashtags 与自营评论 keyword / risk_tag 抽取词项，按出现频次与对应内容互动贡献排序。</p>
        {topicChunk.length ? <ReportDeckTopicList topics={topicChunk}/> : <ReportEmptyState title="暂无可分析话题词" body="未从内容 caption、hashtags 或评论 keyword 中提取到有效词。"/>}
      </div>
    ));
  }

  chunkReportItems(report.contentComparisons || [], 1).forEach((chunk, index) => {
    const contentTitle = chunk[0]?.title ? deckText(chunk[0].title, 28) : `内容 ${index + 1}`;
    push(`内容横向比较 · ${contentTitle}`, '07 · Content Comparison', (
      chunk.length ? <ReportContentComparison rows={chunk}/> : <ReportEmptyState title="暂无可横向比较内容" body="当前周期没有可归并到同一内容主题的真实发布数据。"/>
    ));
  });

  push('Lesson & Learn / 内容创作建议', '08 · Lesson & Learn', (
    <ReportLessonLearn lessons={lessons} compact/>
  ));

  push('诊断与风险边界', '09 · Expert Review', (
    <ReportDeckExpertReviews reviews={report.contentDiagnosis?.expertReviews || []}/>
  ));

  push('用户评论反馈与风险', '10 · Owned Comments', (
    reportComments.total ? <ReportOwnedCommentOverview comments={reportComments}/> : <ReportEmptyState title="后台评论数据不足" body="当前没有可归属平台的评论数据，无法进行用户反馈提炼和风险语义诊断。"/>
  ));

  (reportComments.byPlatform || []).forEach(row => {
    const platformName = reportPlatformDisplayName(row.platform || '');
    push(`用户评论反馈与风险 · ${platformName}`, '10 · Owned Comments', (
      <ReportOwnedCommentOverview
        comments={{
          total: row.total || 0,
          positive: row.positive || 0,
          neutral: row.neutral || 0,
          risk: row.risk || 0,
          wordClouds: row.wordClouds || { positive: [], neutral: [], risk: [] },
          scopeLabel: platformName,
        }}
      />
    ));
  });

  push('内容创作诊断：品牌语气与现状', '11 · Content Diagnosis', (
    <div className="report-deck-stack">
      <ReportDeckDiagnosisSummary diagnosis={report.contentDiagnosis}/>
    </div>
  ));

  chunkReportItems(report.contentDiagnosis?.columnMatrix || [], 5).forEach((chunk, index) => {
    push(`内容栏目矩阵${index ? `（续 ${index + 1}）` : ''}`, '12 · Content System', (
      chunk.length ? <ReportDeckColumnMatrix items={chunk}/> : <ReportEmptyState title="暂无栏目矩阵" body="当前没有可展示的栏目化内容建议。"/>
    ));
  });

  chunkReportItems(report.contentDiagnosis?.nextDirections || [], 3).forEach((chunk, index) => {
    push(`新增内容方向${index ? `（续 ${index + 1}）` : ''}`, '13 · Next Directions', (
      chunk.length ? <ReportDeckNextDirections items={chunk}/> : <ReportEmptyState title="暂无新增方向" body="当前没有可展示的后续选题方向。"/>
    ));
  });

  push('待验证假设', '14 · Hypothesis', (
    <ReportDeckAssumptions items={report.contentDiagnosis?.assumptionsToValidate || []}/>
  ));

  return pages;
};

const ReportDeck = ({ report, periodLabel = '', translations = {}, reportRef }) => {
  const pages = buildReportDeckPages({ report, periodLabel, translations });
  return (
    <div className="report-deck report-neutral" ref={reportRef}>
      {pages.map((page, index) => (
        <div className="report-deck-page-shell" key={`${page.kicker}-${index}`}>
          <ReportDeckFrame
            report={report}
            periodLabel={periodLabel}
            title={page.title}
            kicker={page.kicker}
            pageNumber={index + 1}
            totalPages={pages.length}
            variant={page.variant}
          >
            {page.body}
          </ReportDeckFrame>
        </div>
      ))}
    </div>
  );
};

const PublicDeckReport = () => {
  const reportId = location.pathname.replace(/^\/+|\/+$/g, '');
  const [payload, setPayload] = React.useState(null);
  const [error, setError] = React.useState('');

  React.useEffect(() => {
    document.body.classList.add('deck-public-body');
    const meta = document.createElement('meta');
    meta.name = 'robots';
    meta.content = 'noindex,nofollow';
    document.head.appendChild(meta);
    return () => {
      document.body.classList.remove('deck-public-body');
      meta.remove();
    };
  }, []);

  React.useEffect(() => {
    let cancelled = false;
    fetch(`/reports/${reportId}/report.json`)
      .then(response => response.json().then(data => ({ ok: response.ok, data })))
      .then(({ ok, data }) => {
        if (cancelled) return;
        if (!ok) throw new Error(data.error || '报告不存在');
        setPayload(data);
      })
      .catch(err => {
        if (!cancelled) setError(err.message || '报告加载失败');
      });
    return () => { cancelled = true; };
  }, [reportId]);

  if (error) return <div className="public-deck-state"><b>报告加载失败</b><span>{error}</span></div>;
  if (!payload) return <div className="public-deck-state"><b>加载报告中</b><span>{reportId}</span></div>;

  return (
    <div className="public-deck-shell">
      <div className="public-deck-toolbar">
        <div>
          <b>{payload.SocialAssetReport?.brand || 'MONTX'} 社媒资产运营报表</b>
          <span>{payload.reportId} · {formatBeijingTime(payload.publishedAt)}</span>
        </div>
        <button className="btn primary" onClick={() => window.print()}><Icon name="doc" className="icon sm"/>导出 PDF</button>
      </div>
      {payload.viewMode === 'long'
        ? <ReportLongReport report={payload.SocialAssetReport} periodLabel={payload.periodLabel} translations={payload.translations || {}}/>
        : <ReportDeck report={payload.SocialAssetReport} periodLabel={payload.periodLabel} translations={payload.translations || {}}/>}
    </div>
  );
};

const ReportPlatformLogo = ({ platform, small = false }) => {
  const meta = PLATFORM_LOGO_META[platform] || { abbr: String(platform || '?').slice(0, 3) };
  return (
    <span className={`report-platform-logo ${meta.src ? 'has-image' : ''} ${small ? 'small' : ''}`} title={platform || '未知平台'}>
      {meta.src ? <img src={meta.src} alt={`${platform} logo`} loading="eager" decoding="async"/> : meta.abbr}
    </span>
  );
};

const getReportCoverRatioClass = (platform = '', contentType = '') => {
  const text = `${platform} ${contentType}`.toLowerCase();
  if (/youtube/.test(text)) return 'ratio-wide';
  if (/视频号|reel|short|竖屏|vertical/.test(text)) return 'ratio-vertical';
  if (/instagram/.test(text)) return /video|视频|reel/.test(text) ? 'ratio-vertical' : 'ratio-square';
  if (/小红书/.test(text)) return /video|视频/.test(text) ? 'ratio-vertical' : 'ratio-portrait';
  if (/公众号|wechat/.test(text)) return 'ratio-article';
  return 'ratio-portrait';
};

const ReportContentCover = ({ src, title, platform, contentType = '', compact = false, candidates = [], preferVertical = false }) => {
  const fallbackLogoSrc = PLATFORM_LOGO_META[platform]?.src || '';
  const sources = React.useMemo(() => {
    const rawSources = Array.from(new Set([src, ...candidates].filter(Boolean)));
    const isUnstableSource = (url = '') => /rednotecdn\.com|finder\.video\.qq\.com/i.test(String(url));
    return Array.from(new Set([
      ...rawSources.filter(url => !isUnstableSource(url)),
      fallbackLogoSrc,
      ...rawSources.filter(isUnstableSource),
    ].filter(Boolean)));
  }, [src, candidates, fallbackLogoSrc]);
  const [orderedSources, setOrderedSources] = React.useState(sources);
  const [sourceIndex, setSourceIndex] = React.useState(0);
  const [loaded, setLoaded] = React.useState(false);
  const [naturalRatio, setNaturalRatio] = React.useState('');
  React.useEffect(() => {
    let cancelled = false;
    setOrderedSources(sources);
    setSourceIndex(0);
    if (!preferVertical || compact || !sources.length) return undefined;
    const scoreSource = (url, index) => new Promise(resolve => {
      if (/assets\/platform-logos\//.test(url)) {
        resolve({ url, index, loaded: true, width: 1, height: 1, score: -10 });
        return;
      }
      const image = new Image();
      image.referrerPolicy = 'no-referrer';
      image.onload = () => {
        const width = image.naturalWidth || 1;
        const height = image.naturalHeight || 1;
        const ratio = width / height;
        const isVertical = height > width * 1.08;
        const isLandscape = width > height * 1.08;
        resolve({
          url,
          index,
          loaded: true,
          width,
          height,
          score: (isVertical ? 300 : isLandscape ? 100 : 200) + Math.min(height, 2400) / 10000 - index / 100,
        });
      };
      image.onerror = () => resolve({ url, index, loaded: false, width: 0, height: 0, score: -100 - index });
      image.src = url;
    });
    Promise.all(sources.map(scoreSource)).then(results => {
      if (cancelled) return;
      const sorted = results
        .filter(item => item.loaded)
        .sort((a, b) => b.score - a.score || a.index - b.index)
        .map(item => item.url);
      const failed = results.filter(item => !item.loaded).map(item => item.url);
      const nextSources = Array.from(new Set([...sorted, ...failed]));
      setOrderedSources(nextSources.length ? nextSources : sources);
      setSourceIndex(0);
    });
    return () => { cancelled = true; };
  }, [sources.join('|'), preferVertical, compact]);
  React.useEffect(() => { setSourceIndex(0); }, [orderedSources.join('|')]);
  React.useEffect(() => { setLoaded(false); setNaturalRatio(''); }, [sourceIndex, orderedSources.join('|')]);
  const activeSrc = orderedSources[sourceIndex] || '';
  React.useEffect(() => {
    if (!activeSrc || loaded) return undefined;
    if (/assets\/platform-logos\//.test(activeSrc)) return undefined;
    const timer = window.setTimeout(() => {
      setSourceIndex(index => index + 1 < orderedSources.length ? index + 1 : orderedSources.length);
    }, 2500);
    return () => window.clearTimeout(timer);
  }, [activeSrc, loaded, orderedSources.length]);
  return (
    <div
      className={`report-content-cover ${getReportCoverRatioClass(platform, contentType)} ${compact ? 'compact' : ''} ${preferVertical ? 'prefer-vertical' : ''} ${activeSrc ? '' : 'missing'}`}
      style={naturalRatio ? {'--cover-ratio': naturalRatio} : undefined}
    >
      {activeSrc ? <img src={activeSrc} alt={title || '内容封面'} loading="eager" referrerPolicy="no-referrer" onLoad={event => {
        const image = event.currentTarget;
        if (image.naturalWidth && image.naturalHeight) setNaturalRatio(`${image.naturalWidth} / ${image.naturalHeight}`);
        setLoaded(true);
      }} onError={() => {
        setSourceIndex(index => index + 1 < orderedSources.length ? index + 1 : orderedSources.length);
      }}/> : null}
      <ReportPlatformLogo platform={platform} small/>
      <span>{activeSrc ? '' : 'NO COVER'}</span>
    </div>
  );
};

const ReportContentLink = ({ title, url, className = '' }) => {
  return <ContentTitleLink title={title} url={url} className={`report-title-clamp ${className}`} linkClassName="content-title-link report-content-link"/>;
};

const ReportMiniMetric = ({ label, value, note }) => (
  <div className={/阅读\/播放|互动转化/.test(label) ? 'report-mini-metric compact-label' : 'report-mini-metric'}>
    <span className={/阅读\/播放|互动转化/.test(label) ? 'report-metric-label-compact' : ''}>{label}</span>
    <b>{value}</b>
    {note ? <small>{note}</small> : null}
  </div>
);

const getPlatformContentAngle = (platform = '', topPost = {}) => {
  const title = String(topPost.title || '');
  const hashtags = (topPost.hashtags || []).slice(0, 3);
  const keywordText = hashtags.length ? hashtags.join('、') : cleanReportLabel(title).slice(0, 18);
  if (platform === '微信公众号') return `长文平台能承接定义解释；「${keywordText || '产品定义'}」把品牌概念转成可阅读叙事。`;
  if (platform === '视频号') return `视频号依赖首屏和现场感；「${keywordText || '现场/产品画面'}」降低理解成本，利于播放后的轻互动。`;
  if (platform === '小红书') return `小红书依赖可搜索场景词；「${keywordText || '场景关键词'}」比抽象宣言更容易被理解。`;
  if (platform === 'Instagram') return `Instagram 依赖视觉识别和短语传播；「${keywordText || '视觉主题'}」适合形成海外记忆点。`;
  if (platform === 'YouTube') return `YouTube 依赖标题和画面解释力；「${keywordText || '产品/场景'}」适合承接搜索与长尾播放。`;
  return `高表现内容集中在「${keywordText || '核心主题'}」，说明具体场景和清晰标题更有效。`;
};

const getPlatformInsightCards = (row = {}) => {
  const topPost = (row.topPosts || [])[0] || row.topPost || null;
  if (!topPost) {
    return [
      {
        label: '平台内容分析',
        text: `${reportPlatformDisplayName(row.platform)} 本周期暂无可分析的高表现内容。需要至少 1 条真实发布内容，并包含阅读/播放、点赞、评论、转发字段，才能判断标题、封面、场景词和互动结构是否有效。`,
      },
    ];
  }
  const mixEntries = Object.entries({
    likes: topPost.likes,
    comments: topPost.comments,
    shares: topPost.shares,
  }).sort((a, b) => socialNumber(b[1]) - socialNumber(a[1]));
  const [leadingKey, leadingValue] = mixEntries[0] || ['likes', 0];
  const leadingLabel = { likes: '点赞', comments: '评论', shares: '分享', saves: '收藏' }[leadingKey] || '互动';
  const avgViews = row.posts ? (row.views || 0) / row.posts : 0;
  const avgInteraction = row.posts ? (row.interaction || 0) / row.posts : 0;
  const viewLift = avgViews && topPost.views ? topPost.views / avgViews : 0;
  const interactionLift = avgInteraction && topPost.interaction ? topPost.interaction / avgInteraction : 0;
  const liftText = interactionLift >= viewLift && interactionLift >= 1.15
    ? `互动约为均值 ${interactionLift.toFixed(1)} 倍`
    : viewLift >= 1.15
      ? `阅读/播放约为均值 ${viewLift.toFixed(1)} 倍`
      : '综合排序领先';
  const cvrValue = typeof topPost.view_to_interaction_rate === 'number' ? topPost.view_to_interaction_rate : null;
  const cvr = cvrValue === null || !topPost.views
    ? 'CVR 待补阅读/播放'
    : `CVR ${(cvrValue * 100).toFixed(2)}%`;
  const title = cleanReportLabel(topPost.title || '未命名内容');
  const conciseTitle = title.length > 20 ? `${title.slice(0, 20)}...` : title;
  const angle = getPlatformContentAngle(row.platform, topPost);
  return [
    {
      label: '平台内容分析',
      text: `《${conciseTitle}》是本平台高表现样本，${liftText}。${angle} 当前互动由${leadingLabel}主导（${formatCompactNumber(leadingValue)}），说明内容先完成了兴趣确认。下一轮建议沿用这篇的主题入口与表达颗粒度，继续测试封面/标题首句，并补强评论引导。${cvr}。`,
    },
  ];
};

const ReportEmptyState = ({ title, body }) => (
  <div className="report-empty-state">
    <b>{title}</b>
    <span>{body}</span>
  </div>
);

const ReportRegionChart = ({ rows }) => {
  const maxViews = Math.max(...rows.map(row => row.views), 1);
  return (
    <div className="report-region-chart">
      {rows.map(row => (
        <div key={row.region}>
          <span>{row.label}</span>
          <div><i style={{width: `${Math.max(5, (row.views / maxViews) * 100)}%`}}></i></div>
          <b>{formatCompactNumber(row.views)}</b>
          <small>{row.followerGrowth === null ? '涨粉待基线' : `涨粉 +${formatCompactNumber(row.followerGrowth)}`}</small>
        </div>
      ))}
    </div>
  );
};

const REPORT_CHART_COLORS = ['var(--accent)', 'var(--info)', 'var(--warn)', 'var(--danger)', 'var(--accent-2)', 'var(--ink-3)', 'var(--hairline-2)'];
const getReportChartColor = (index = 0) => REPORT_CHART_COLORS[index % REPORT_CHART_COLORS.length];
const getTrendPlatformNames = (rows = []) => Array.from(new Set(rows.flatMap(row => Object.keys(row.platforms || {}))))
  .sort((a, b) => compareReportPlatformOrder(a, b));
const getTrendPlatformMetric = (row = {}, platform = '', key = 'total') => {
  const value = row.platforms?.[platform];
  if (typeof value === 'number') return key === 'total' || key === 'views' ? socialNumber(value) : 0;
  if (!value || typeof value !== 'object') return 0;
  if (key === 'total') return socialNumber(value.total ?? value.views ?? value.followers ?? value.delta);
  return socialNumber(value[key]);
};
const getTrendPlatformsByRegion = (rows = [], region = '') => getTrendPlatformNames(rows)
  .filter(platform => !region || getSocialRegion(platform) === region);

const ReportFollowerGrowthTrend = ({ rows = [], briefRows = [], compact = false, region = '' }) => {
  if (!rows.length) {
    return <ReportEmptyState title="暂无粉丝每日增长数据" body="上传社媒粉丝数表后，系统会按国内 / 海外账号聚合每日净增粉丝。"/>;
  }
  const platforms = getTrendPlatformsByRegion(rows, region);
  const scopedRows = rows.map(row => {
    const platformTotal = platforms.reduce((sum, platform) => sum + getTrendPlatformMetric(row, platform, 'total'), 0);
    return {
      ...row,
      scopedTotal: platformTotal || socialNumber(region ? row[region] : row.total),
    };
  });
  if (region && !platforms.length && !scopedRows.some(row => row.scopedTotal)) {
    return <ReportEmptyState title={`暂无${reportRegionLabel(region)}粉丝增长数据`} body="当前区域没有可用于趋势展示的平台粉丝快照。"/>;
  }
  const maxValue = Math.max(...scopedRows.map(row => row.scopedTotal || 0), 1);
  const axisStep = 100;
  const axisMax = roundChartMax(maxValue, axisStep);
  const axisTicks = buildChartTicks(maxValue, axisStep);
  const total = scopedRows.reduce((sum, row) => sum + socialNumber(row.scopedTotal), 0);
  const scopeLabel = region ? reportRegionLabel(region) : '全平台';
  const dateLabel = (date = '') => date.slice(5).replace('-', '/');
  const platformTotals = platforms.map((platform, index) => ({
    platform,
    total: rows.reduce((sum, row) => sum + getTrendPlatformMetric(row, platform, 'total'), 0),
    color: getReportChartColor(index),
  }));
  const briefRowsByDate = new Map(filterInteractionRowsByRegion(briefRows, region).map(row => [row.date, row.briefs || []]));
  return (
    <div className={`report-follower-growth-trend ${compact ? 'compact' : ''}`}>
      <div className="report-follower-growth-head">
        <div>
          <b>粉丝每日增长趋势 · {scopeLabel}</b>
          <span>{rows[0]?.date} - {rows[rows.length - 1]?.date} · y 轴每格 +{axisStep}</span>
        </div>
        <div className="report-follower-growth-legend">
          {platformTotals.map(item => (
            <React.Fragment key={item.platform}>
              <em style={{background:item.color}}></em><span>{reportPlatformDisplayName(item.platform)} +{formatCompactNumber(item.total)}</span>
            </React.Fragment>
          ))}
          <strong>合计 +{formatCompactNumber(total)}</strong>
        </div>
      </div>
      <div className="report-trend-chart-frame">
        <div className="report-trend-y-axis">
          {axisTicks.map(value => <span key={value}>{value ? `+${formatCompactNumber(value)}` : '0'}</span>)}
        </div>
        <div className="report-follower-growth-bars" style={{gridTemplateColumns: `repeat(${rows.length}, minmax(0, 1fr))`}}>
          {scopedRows.map(row => (
            <div key={row.date} className="report-follower-growth-day" title={`${row.date} ${scopeLabel}合计 +${row.scopedTotal || 0}`}>
              <div className={`report-follower-growth-stack ${(briefRowsByDate.get(row.date) || []).length ? 'has-briefs' : ''}`}>
                {(briefRowsByDate.get(row.date) || []).length > 0 && (
                  <div className="report-interaction-brief-markers">
                    {(briefRowsByDate.get(row.date) || []).slice(0, 3).map(brief => (
                      <span key={brief.id} title={`${brief.title} · ${brief.postCount || 0} 条发布`}>
                        {deckText(brief.title, 18)}
                      </span>
                    ))}
                  </div>
                )}
                {platforms.map((platform, index) => {
                  const value = getTrendPlatformMetric(row, platform, 'total');
                  if (!value) return null;
                  return (
                    <i
                      key={platform}
                      title={`${row.date} · ${reportPlatformDisplayName(platform)} 净增 +${formatCompactNumber(value)}`}
                      style={{height: `${Math.max(5, (value / axisMax) * 100)}%`, background: getReportChartColor(index)}}
                    ></i>
                  );
                })}
              </div>
              <b>+{formatCompactNumber(row.scopedTotal || 0)}</b>
              <span>{dateLabel(row.date)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

const filterInteractionRowsByRegion = (rows = [], region = '') => {
  if (!region) return rows;
  return rows.map(row => {
    const platformEntries = Object.entries(row.platforms || {})
      .filter(([platform]) => getSocialRegion(platform) === region);
    const scopedPlatforms = Object.fromEntries(platformEntries);
    const metrics = platformEntries.reduce((sum, [, value]) => {
      if (typeof value === 'number') {
        sum.views += socialNumber(value);
        return sum;
      }
      sum.views += socialNumber(value?.views);
      sum.likes += socialNumber(value?.likes);
      sum.comments += socialNumber(value?.comments);
      sum.shares += socialNumber(value?.shares);
      sum.interaction += socialNumber(value?.interaction) || socialNumber(value?.likes) + socialNumber(value?.comments) + socialNumber(value?.shares);
      return sum;
    }, { views: 0, likes: 0, comments: 0, shares: 0, interaction: 0 });
    const briefRows = (row.briefs || []).filter(brief => (brief.platforms || []).some(platform => getSocialRegion(platform) === region));
    return {
      ...row,
      ...metrics,
      platforms: scopedPlatforms,
      briefs: briefRows,
    };
  });
};

const ReportInteractionTrend = ({ rows = [], region = '' }) => {
  if (!rows.length) {
    return <ReportEmptyState title="暂无互动趋势数据" body="当前周期没有可按发布日期聚合的自营发布内容。"/>;
  }
  const scopedRows = filterInteractionRowsByRegion(rows, region);
  const scopeLabel = region ? reportRegionLabel(region) : '全平台';
  if (region && !scopedRows.some(row => row.views || row.likes || row.comments || row.shares || row.interaction || (row.briefs || []).length)) {
    return <ReportEmptyState title={`暂无${scopeLabel}互动趋势数据`} body="当前区域没有可用于趋势展示的平台互动快照。"/>;
  }
  const maxViews = Math.max(...scopedRows.map(row => row.views || 0), 1);
  const axisStep = 10000;
  const axisMax = roundChartMax(maxViews, axisStep);
  const axisTicks = buildChartTicks(maxViews, axisStep);
  const maxInteraction = Math.max(...scopedRows.map(row => Math.max(row.likes || 0, row.comments || 0, row.shares || 0, row.interaction || 0)), 1);
  const total = scopedRows.reduce((sum, row) => ({
    views: sum.views + socialNumber(row.views),
    likes: sum.likes + socialNumber(row.likes),
    comments: sum.comments + socialNumber(row.comments),
    shares: sum.shares + socialNumber(row.shares),
  }), { views: 0, likes: 0, comments: 0, shares: 0 });
  const dateLabel = (date = '') => date.slice(5).replace('-', '/');
  return (
    <div className="report-interaction-trend">
      <div className="report-follower-growth-head">
        <div>
          <b>互动数据趋势与发布节点 · {scopeLabel}</b>
          <span>{rows[0]?.date} - {rows[rows.length - 1]?.date} · x 轴按天展示，y 轴每格 1 万</span>
        </div>
        <div className="report-follower-growth-legend">
          <strong>浏览/播放 {formatCompactNumber(total.views)}</strong>
          <span>点赞 {formatCompactNumber(total.likes)}</span>
          <span>评论 {formatCompactNumber(total.comments)}</span>
          <span>转发 {formatCompactNumber(total.shares)}</span>
        </div>
      </div>
      <div className="report-interaction-chart">
        <div className="report-trend-y-axis">
          {axisTicks.map(value => <span key={value}>{formatCompactNumber(value)}</span>)}
        </div>
        <div className="report-interaction-days" style={{gridTemplateColumns: `repeat(${scopedRows.length}, minmax(0, 1fr))`}}>
          {scopedRows.map(row => (
            <div key={row.date} className="report-interaction-day" title={`${row.date} 浏览/播放 ${row.views} / 点赞 ${row.likes} / 评论 ${row.comments} / 转发 ${row.shares}`}>
              <div className="report-interaction-bars">
                <i className="views" title={`${row.date} · 浏览/播放 ${formatCompactNumber(row.views || 0)}`} style={{height: `${Math.max(row.views ? 8 : 0, (row.views / axisMax) * 100)}%`}}></i>
                <i className="likes" title={`${row.date} · 点赞 ${formatCompactNumber(row.likes || 0)}`} style={{height: `${Math.max(row.likes ? 6 : 0, (row.likes / maxInteraction) * 100)}%`}}></i>
                <i className="comments" title={`${row.date} · 评论 ${formatCompactNumber(row.comments || 0)}`} style={{height: `${Math.max(row.comments ? 6 : 0, (row.comments / maxInteraction) * 100)}%`}}></i>
                <i className="shares" title={`${row.date} · 转发 ${formatCompactNumber(row.shares || 0)}`} style={{height: `${Math.max(row.shares ? 6 : 0, (row.shares / maxInteraction) * 100)}%`}}></i>
                <div className="report-interaction-brief-markers">
                  {(row.briefs || []).slice(0, 3).map(brief => (
                    <span key={brief.id} title={`${brief.title} · ${brief.postCount || 0} 条发布`}>
                      {deckText(brief.title, 18)}
                    </span>
                  ))}
                </div>
              </div>
              <b>{dateLabel(row.date)}</b>
            </div>
          ))}
        </div>
      </div>
      <div className="report-interaction-legend">
        <span className="views">浏览/播放</span>
        <span className="likes">点赞</span>
        <span className="comments">评论</span>
        <span className="shares">转发</span>
      </div>
    </div>
  );
};

const ReportPlatformTopPosts = ({ rows = [] }) => (
  <div className="report-platform-top-posts">
    <div className="report-platform-top-posts-head">
      <b>平台内高表现发布内容</b>
      <span>{rows.length > 3 ? `Top 3 / 共 ${rows.length} 条` : '按阅读/播放与互动综合排序'}</span>
    </div>
    {rows.length ? (
      <div className="report-platform-top-post-list">
        {rows.slice(0, 3).map((post, index) => (
          <article key={post.post_id || `${post.title}-${index}`}>
            <em>{index + 1}</em>
            <div>
              <b><ReportContentLink title={post.title} url={post.url}/></b>
              <span className="report-platform-top-post-meta">
                <i>{post.content_type || '内容'}</i>
                <i>阅读/播放 {formatCompactNumber(post.views || 0)}</i>
                <i>点赞 {formatCompactNumber(post.likes || 0)}</i>
                <i>评论 {formatCompactNumber(post.comments || 0)}</i>
                <i>转发 {formatCompactNumber(post.shares || 0)}</i>
              </span>
            </div>
          </article>
        ))}
      </div>
    ) : <ReportEmptyState title="暂无高表现内容" body="该平台本周期没有可展示的发布内容。"/>}
  </div>
);

const ReportPlatformAggregateTable = ({ rows = [] }) => (
  rows.length ? (
    <div className="report-platform-aggregate">
      <table className="tbl compact report-platform-aggregate-table">
        <colgroup>
          <col style={{width:'18%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
          <col style={{width:'10.25%'}}/>
        </colgroup>
        <thead>
          <tr>
            <th>平台 Platform</th>
            <th>账号 Account</th>
            <th>粉丝 Followers</th>
            <th>发布 Posts</th>
            <th>阅读/播放 Views</th>
            <th>点赞 Likes</th>
            <th>评论 Comments</th>
            <th>转发 Shares</th>
            <th>互动转化 CVR</th>
          </tr>
        </thead>
        <tbody>
          {rows.map(row => (
            <tr key={row.platform}>
              <td>
                <div className="report-platform-aggregate-name">
                  <ReportPlatformLogo platform={row.platform} small/>
                  <span>{reportPlatformDisplayName(row.platform)}</span>
                </div>
              </td>
              <td className="mono">{String(row.accounts || 0)}</td>
              <td className="mono">{formatCompactNumber(row.followers || 0)}</td>
              <td className="mono">{String(row.posts || 0)}</td>
              <td className="mono">{formatCompactNumber(row.normalized?.views || row.views || 0)}</td>
              <td className="mono">{formatCompactNumber(row.likes || 0)}</td>
              <td className="mono">{formatCompactNumber(row.comments || 0)}</td>
              <td className="mono">{formatCompactNumber(row.shares || 0)}</td>
              <td className="mono">{`${((row.engagementRate || 0) * 100).toFixed(2)}%`}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  ) : <ReportEmptyState title="暂无平台聚合数据" body="当前周期没有可用于平台聚合查看的自营平台数据。"/>
);

const ReportPlatformBreakdown = ({ rows }) => (
  <div className="report-platform-breakdown">
    {rows.map(row => (
      <article key={row.platform} className="report-platform-row">
	        <div className="report-platform-title">
	          <div className="report-platform-title-lockup">
	            <ReportPlatformLogo platform={row.platform}/>
	            <div>
	              <b>{reportPlatformDisplayName(row.platform)}</b>
	              <span className="report-platform-title-meta">{reportRegionLabel(row.region)} · {row.accountNames.slice(0, 2).join(' / ') || '暂无账号名'}</span>
	            </div>
	          </div>
        </div>
        <div className="report-platform-metrics">
          <ReportMiniMetric label="账号 Account" value={String(row.accounts)}/>
          <ReportMiniMetric label="粉丝 Followers" value={formatCompactNumber(row.followers)}/>
          <ReportMiniMetric label="发布 Posts" value={String(row.posts)}/>
          <ReportMiniMetric label="阅读/播放 Views" value={formatCompactNumber(row.normalized.views)}/>
          <ReportMiniMetric label="点赞 Likes" value={formatCompactNumber(row.likes)}/>
          <ReportMiniMetric label="评论 Comments" value={formatCompactNumber(row.comments)}/>
          <ReportMiniMetric label="转发 Shares" value={formatCompactNumber(row.shares)}/>
          <ReportMiniMetric label="互动转化 CVR" value={`${(row.engagementRate * 100).toFixed(2)}%`}/>
        </div>
        <div className="report-platform-native">
          <span>阅读/播放口径：{row.metricMapping.viewSource} / {row.metricMapping.viewLabel}</span>
          <span>互动公式：{row.metricMapping.interactionFormula}</span>
          <span>可比性：{row.dataQuality.comparability}</span>
        </div>
        <div className="report-platform-body">
          <ReportPlatformTopPosts rows={row.topPosts || (row.topPost ? [row.topPost] : [])}/>
          <div className="report-platform-insights">
            {getPlatformInsightCards(row).map(card => (
              <p key={card.label}><b>{card.label}</b>{card.text}</p>
            ))}
          </div>
        </div>
      </article>
    ))}
  </div>
);

const ReportContentComparison = ({ rows }) => (
  <div className="report-content-comparison">
    {rows.map(row => (
	      <article key={row.content_group_id} className="report-content-group">
	        <div className="report-content-group-head">
	          <div>
		            <b className="report-content-ranked-title"><ReportContentLink title={row.title} url={row.url}/></b>
	            <span>{row.platformCount} 个平台 · {row.platforms.join(' / ')} · {row.regions.map(reportRegionLabel).join(' + ')}</span>
	          </div>
	          <em>{row.bestPlatform ? `最强平台：${row.bestPlatform.platform}` : '待比较'}</em>
	        </div>
	        <div className="report-content-comparison-body">
	          <ReportContentCover src={row.cover_url} candidates={row.cover_urls || []} title={row.title} platform={row.bestPlatform?.platform || row.platforms[0]} contentType={row.bestPlatform?.content_type} preferVertical/>
          <div className="report-content-platform-table">
            <div>
              <span>平台</span>
              <span>代表发布</span>
              <span>阅读/播放</span>
              <span>点赞</span>
              <span>评论</span>
              <span>转发</span>
              <span>转化率</span>
            </div>
            <div className="summary">
              <b>平台汇总</b>
              <span>{row.platformRows.length}平台</span>
              <span>{formatCompactNumber(row.views)}</span>
              <span>{formatCompactNumber(row.likes)}</span>
              <span>{formatCompactNumber(row.comments)}</span>
              <span>{formatCompactNumber(row.shares)}</span>
              <span>{row.view_to_interaction_rate === null ? '-' : `${(row.view_to_interaction_rate * 100).toFixed(2)}%`}</span>
            </div>
	            {row.platformRows.slice(0, 4).map(platform => (
	              <div key={`${row.content_group_id}-${platform.platform}`}>
	                <b><ReportPlatformLogo platform={platform.platform} small/> {reportPlatformDisplayName(platform.platform)}</b>
	                <span>{platform.posts}</span>
                <span>{formatCompactNumber(platform.views)}</span>
                <span>{formatCompactNumber(platform.likes)}</span>
                <span>{formatCompactNumber(platform.comments)}</span>
                <span>{formatCompactNumber(platform.shares)}</span>
                <span>{platform.view_to_interaction_rate === null ? '-' : `${(platform.view_to_interaction_rate * 100).toFixed(2)}%`}</span>
              </div>
            ))}
          </div>
        </div>
        <p>{row.diagnosis}</p>
      </article>
    ))}
  </div>
);

const ReportCommentPlatformBreakdown = ({ rows, translations = {}, translationScope = '' }) => {
  const regions = ['domestic', 'overseas'];
  return (
    <div className="report-comment-platform-breakdown">
      {regions.map(region => {
        const regionRows = rows
          .filter(row => row.region === region)
          .sort((a, b) => b.total - a.total || b.posts - a.posts || a.platform.localeCompare(b.platform, 'zh-CN'));
        if (!regionRows.length) return null;
        return (
          <div key={region} className="report-comment-region-block">
            <h4>{reportRegionLabel(region)}平台评论反馈</h4>
            <div>
              {regionRows.map(row => (
                <article key={row.platform} className={`report-comment-platform-card ${row.total ? '' : 'empty'} ${row.samples.length ? 'with_samples' : ''}`}>
	                  <div className="report-comment-platform-head">
	                    <div>
	                      <b><ReportPlatformLogo platform={row.platform}/> {row.platform}</b>
	                      <span>{row.posts} 条发布 · {row.total} 条评论</span>
	                    </div>
                    <em>{row.total ? `风险 ${row.risk}` : '暂无评论'}</em>
                  </div>
                  <div className="report-comment-platform-metrics">
                    <ReportMiniMetric label="正向" value={String(row.positive)} note="positive"/>
                    <ReportMiniMetric label="中性" value={String(row.neutral)} note="neutral"/>
                    <ReportMiniMetric label="风险" value={String(row.risk)} note="risk"/>
                  </div>
                  <div className="report-comment-keywords">
                    {row.topKeywords.length ? row.topKeywords.map(item => <span key={item.keyword}>{item.keyword} × {item.count}</span>) : <span>暂无关注问题</span>}
                  </div>
                  <p className="report-comment-platform-insight">{row.insight}</p>
                  {row.samples.length ? (
                    <div className="report-comment-samples">
                      {row.samples.map(comment => (
                        <article key={comment.comment_id}>
	                          <div>
	                            <b><ReportPlatformLogo platform={comment.platform} small/> {comment.user_name || comment.platform || '匿名'}</b>
                            <span>{comment.source_post_type || '内容'} · {comment.comment_level === 2 ? '二级评论' : '一级评论'} · {monitorActionLabel?.[comment.action_status] || comment.action_status}</span>
	                          </div>
	                          <strong><ReportContentLink title={comment.source_post_title} url={comment.source_post_url}/></strong>
	                          <p><em>{comment.user_concern_short || comment.user_concern_label || comment.keyword || comment.risk_tag || '其他'}</em>{comment.text}</p>
	                          {translationScope && translations[socialTranslationKey(translationScope, comment.comment_id)]?.translation && <span className="monitor-translation-line">译：{translations[socialTranslationKey(translationScope, comment.comment_id)].translation}</span>}
	                        </article>
                      ))}
                    </div>
                  ) : <ReportEmptyState title="暂无评论样本" body="该平台本周期没有同步到可展示评论。"/>}
                </article>
              ))}
            </div>
          </div>
        );
      })}
    </div>
  );
};

const ReportContentDiagnosis = ({ diagnosis }) => {
  if (!diagnosis) return null;
  return (
    <div className="report-content-diagnosis">
      <div className="report-diagnosis-hero">
        <b>内容诊断判断</b>
        <p>{diagnosis.strategicJudgement}</p>
      </div>
      <div className="report-diagnosis-grid">
        <div>
          <h4>目前内容写了什么</h4>
          {diagnosis.currentContentSummary.map(item => <p key={item}>{item}</p>)}
        </div>
        <div>
          <h4>语气资产分层</h4>
          {diagnosis.toneLayering.map(layer => (
            <ReportToneLayer key={layer.level} layer={layer}/>
          ))}
        </div>
      </div>
      <div className="report-column-matrix">
        <h4>长期栏目矩阵</h4>
        <div>
          {diagnosis.columnMatrix.map(item => (
            <article key={item.column}>
              <b>{item.column}</b>
              <span>{item.goal}</span>
              <p>{item.writingRule}</p>
            </article>
          ))}
        </div>
      </div>
      <div className="report-expert-review-grid">
        {diagnosis.expertReviews?.map(item => (
          <article key={item.role}>
            <div>
              <b>{item.title}</b>
              <em>{item.level}</em>
            </div>
            <small>{item.role}</small>
            <p>{item.judgement}</p>
            <span>{item.action}</span>
            <footer>{item.evidence}</footer>
          </article>
        ))}
      </div>
      <div className="report-next-directions">
        <h4>下一阶段新增方向</h4>
        {diagnosis.nextDirections.map(item => (
          <article key={item.direction}>
            <em>{item.priority}</em>
            <div>
              <b>{item.direction}</b>
              <p>{item.reason}</p>
              <span>{item.suggestedFormats.join(' / ')}</span>
            </div>
          </article>
        ))}
      </div>
      <div className="report-assumption-list">
        <h4>待验证假设</h4>
        {diagnosis.assumptionsToValidate.map(item => <p key={item}>{item}</p>)}
      </div>
    </div>
  );
};

const ReportContentTable = ({ rows, translations = {} }) => (
  <table className="tbl compact">
    <thead><tr><th>内容</th><th>平台</th><th style={{textAlign:'right'}}>阅读/播放</th><th style={{textAlign:'right'}}>点赞</th><th style={{textAlign:'right'}}>评论</th><th style={{textAlign:'right'}}>转发</th></tr></thead>
    <tbody>
	      {rows.map(row => (
	        <tr key={row.post_id}>
		          <td><div className="report-post-title-cell"><ReportContentCover src={row.cover_url} title={row.title} platform={row.platform} contentType={row.content_type} compact/><div><ReportContentLink title={row.title} url={row.url}/>{translations[socialTranslationKey('report-post', row.post_id)]?.translation && <span className="monitor-translation-line">译：{translations[socialTranslationKey('report-post', row.post_id)].translation}</span>}<small>{row.creator_name || row.creator_handle || ''}</small></div></div></td>
	          <td><ReportPlatformLogo platform={row.platform} small/> {row.platform}</td>
          <td style={{textAlign:'right'}} className="mono">{formatCompactNumber(row.views)}</td>
          <td style={{textAlign:'right'}} className="mono">{formatCompactNumber(row.likes)}</td>
          <td style={{textAlign:'right'}} className="mono">{formatCompactNumber(row.comments)}</td>
          <td style={{textAlign:'right'}} className="mono">{formatCompactNumber(row.shares)}</td>
        </tr>
      ))}
    </tbody>
  </table>
);

const ReportTopicList = ({ topics }) => (
  <div className="report-topic-list">
    {topics.map(topic => (
      <div key={topic.keyword}>
        <b>#{topic.keyword}</b>
        <span>{topic.platforms.join(' / ') || topic.source}</span>
        <small>{topic.frequency} 次 · 阅读/播放 {formatCompactNumber(topic.views)} · 赞 {formatCompactNumber(topic.likes)} · 评 {formatCompactNumber(topic.comments)} · 转 {formatCompactNumber(topic.shares)}</small>
      </div>
    ))}
  </div>
);

// ========== 09 · REVIEW / LEARNING ==========
const reviewApiUrl = (pathname) => (window.location.protocol === 'file:' ? `http://localhost:4173${pathname}` : pathname);
const reviewPostIds = (post = {}) => [post.post_id, post.content_id, post.id].filter(Boolean);
const reviewPostBriefIds = (post = {}) => [
  post.primaryMasterBriefId,
  post.briefMapping?.primaryMasterBriefId,
  ...(post.masterBriefIds || []),
  ...(post.briefMapping?.masterBriefIds || []),
].filter(Boolean);
const reviewPostViews = (post = {}) => Math.max(getPostContentViews(post), socialNumber(post.organic_impressions));
const reviewPostComments = (post = {}, commentsByPost = new Map()) => Math.max(pickNativeMetric(post, 'comments'), (commentsByPost.get(post.post_id) || []).length);
const reviewPostInteraction = (post = {}, commentsByPost = new Map()) => pickNativeMetric(post, 'likes') + reviewPostComments(post, commentsByPost) + pickNativeMetric(post, 'shares') + pickNativeMetric(post, 'reposts') + pickNativeMetric(post, 'saves');
const reviewMatchesPost = (initiative = {}, post = {}) => {
  const ids = new Set(initiative.assetPostIds || []);
  if (reviewPostIds(post).some(id => ids.has(id))) return true;
  if (ids.size) return false;
  const briefIds = new Set(reviewPostBriefIds(post));
  if (initiative.id && briefIds.has(initiative.id)) return true;
  const parentIds = [post.parentCampaignId, post.campaignId, post.briefMapping?.parentCampaignId].filter(Boolean);
  if (initiative.id && parentIds.includes(initiative.id)) return true;
  if (briefIds.size || parentIds.length) return false;
  const labels = initiative.assetCampaignLabels?.length ? initiative.assetCampaignLabels : [initiative.name];
  return labels.some(label => label && [post.campaign, post.product, post.content_type, post.title, post.caption].join(' ').includes(label));
};
const reviewGroupCount = (rows = [], getKey = item => item) => {
  const map = new Map();
  rows.forEach(row => {
    const key = getKey(row);
    if (!key) return;
    map.set(key, (map.get(key) || 0) + 1);
  });
  return [...map.entries()].sort((a, b) => b[1] - a[1]);
};
const reviewLearningItem = (type, text, source = '') => ({ type, text, source });

const buildReviewModel = ({ initiatives = [], social = {}, organicHeat = {} }) => {
  const posts = social.posts || [];
  const comments = social.comments || [];
  const commentsByPost = new Map();
  comments.forEach(comment => {
    const list = commentsByPost.get(comment.post_id) || [];
    list.push(comment);
    commentsByPost.set(comment.post_id, list);
  });
  const activeInitiative = initiatives.find(item => item.status === 'active' && posts.some(post => reviewMatchesPost(item, post)))
    || initiatives.find(item => posts.some(post => reviewMatchesPost(item, post)))
    || initiatives[0]
    || null;
  const linkedPosts = activeInitiative ? posts.filter(post => reviewMatchesPost(activeInitiative, post)) : posts;
  const ownedPosts = linkedPosts.filter(post => !isExternalSocialPost(post));
  const externalPosts = linkedPosts.filter(isExternalSocialPost);
  const linkedPostIds = new Set(linkedPosts.map(post => post.post_id));
  const linkedComments = comments.filter(comment => linkedPostIds.has(comment.post_id));
  const riskComments = linkedComments.filter(isRiskComment);
  const totalViews = linkedPosts.reduce((sum, post) => sum + reviewPostViews(post), 0);
  const totalInteraction = linkedPosts.reduce((sum, post) => sum + reviewPostInteraction(post, commentsByPost), 0);
  const engagementRate = totalViews ? totalInteraction / totalViews : null;
  const avgSentiment = linkedComments.length
    ? linkedComments.reduce((sum, item) => sum + socialNumber(item.sentiment), 0) / linkedComments.length
    : null;
  const topPosts = [...linkedPosts]
    .sort((a, b) => reviewPostInteraction(b, commentsByPost) - reviewPostInteraction(a, commentsByPost) || reviewPostViews(b) - reviewPostViews(a))
    .slice(0, 6);
  const platformRows = reviewGroupCount(linkedPosts, post => post.platform).map(([platform]) => {
    const platformPosts = linkedPosts.filter(post => post.platform === platform);
    const views = platformPosts.reduce((sum, post) => sum + reviewPostViews(post), 0);
    const interaction = platformPosts.reduce((sum, post) => sum + reviewPostInteraction(post, commentsByPost), 0);
    return { platform, posts: platformPosts.length, views, interaction, engagementRate: views ? interaction / views : null };
  }).sort((a, b) => b.interaction - a.interaction || b.views - a.views);
  const creatorRows = reviewGroupCount(externalPosts, post => post.creator_handle || post.creator_name || post.account_name || post.author_name).map(([creator]) => {
    const creatorPosts = externalPosts.filter(post => (post.creator_handle || post.creator_name || post.account_name || post.author_name) === creator);
    const interaction = creatorPosts.reduce((sum, post) => sum + reviewPostInteraction(post, commentsByPost), 0);
    const views = creatorPosts.reduce((sum, post) => sum + reviewPostViews(post), 0);
    const topPost = [...creatorPosts].sort((a, b) => reviewPostInteraction(b, commentsByPost) - reviewPostInteraction(a, commentsByPost))[0];
    return { creator, platform: topPost?.platform || '', posts: creatorPosts.length, views, interaction, topTitle: topPost?.title || topPost?.caption || '未命名内容' };
  }).sort((a, b) => b.interaction - a.interaction || b.views - a.views).slice(0, 8);
  const heatScores = organicHeat.scores || [];
  const heatPatterns = organicHeat.patterns || [];
  const brandLearnings = [
    riskComments.length ? reviewLearningItem('warn', `本轮有 ${riskComments.length} 条评论进入风险/复核视图，优先沉淀高频风险词和回应口径。`, 'Social Sync') : null,
    ...reviewGroupCount(riskComments, item => item.risk_tag || item.keyword || item.pr_level).slice(0, 2).map(([tag, count]) => reviewLearningItem('warn', `风险信号「${tag}」出现 ${count} 次，需要人工确认是否写入品牌边界。`, 'Comment Review')),
    social.commentReview?.lastReviewedAt ? reviewLearningItem('ok', `评论批量复核最近完成于 ${formatBeijingTime(social.commentReview.lastReviewedAt, social.commentReview.lastReviewedAt)}。`, 'Comment Review') : null,
  ].filter(Boolean);
  const creatorLearnings = creatorRows.slice(0, 3).map(row => reviewLearningItem('new', `${row.creator} 在 ${row.platform || '未标记平台'} 有 ${row.posts} 条外部内容，互动 ${formatCompactNumber(row.interaction)}；需人工判断是否进入达人画像。`, 'External Voice'));
  const contentLearnings = [
    ...heatPatterns.slice(0, 3).map(pattern => reviewLearningItem('ok', `${pattern.title || pattern.pattern_label || '内容模式'}：${pattern.summary || '已从 Organic Heat 识别为可复盘模式。'}`, 'Organic Heat')),
    ...heatScores.slice(0, 2).map(score => reviewLearningItem('new', `内容「${cleanReportLabel(score.title || score.content_id || '').slice(0, 28) || score.content_id}」Organic Heat ${score.organic_heat_score ?? '-'}${score.benchmark_percentile ? `，benchmark P${Math.round(score.benchmark_percentile)}` : ''}。`, 'Organic Heat')),
  ];
  const strategyLearnings = [
    activeInitiative ? reviewLearningItem('ok', `复盘对象：${activeInitiative.name}；已关联 ${linkedPosts.length} 条内容、${linkedComments.length} 条评论。`, 'Campaign State') : null,
    platformRows[0] ? reviewLearningItem('new', `${platformRows[0].platform} 当前互动贡献最高：${formatCompactNumber(platformRows[0].interaction)} 互动，${formatCompactNumber(platformRows[0].views)} 阅读/播放。`, 'Performance Attribution') : null,
    engagementRate !== null ? reviewLearningItem('ok', `本轮互动转化 CVR ${(engagementRate * 100).toFixed(2)}%，可作为下一轮策略推演的真实基线。`, 'Performance Attribution') : null,
  ].filter(Boolean);
  const writebackCandidates = [
    ...brandLearnings.map(item => `[ 品牌 ] ${item.text}`),
    ...creatorLearnings.map(item => `[ 达人 ] ${item.text}`),
    ...contentLearnings.map(item => `[ 内容 ] ${item.text}`),
    ...strategyLearnings.map(item => `[ 策略 ] ${item.text}`),
  ].slice(0, 12);
  return {
    activeInitiative,
    linkedPosts,
    linkedComments,
    ownedPosts,
    externalPosts,
    totals: { posts: linkedPosts.length, views: totalViews, interaction: totalInteraction, comments: linkedComments.length, riskComments: riskComments.length, avgSentiment, engagementRate },
    topPosts,
    platformRows,
    creatorRows,
    learningGroups: [
      { title: '品牌学习', intro: '客户真正喜欢什么表达、反复否掉什么表达、品牌边界在哪里被强化。', findings: brandLearnings },
      { title: '达人学习', intro: '哪类达人适合哪种命题、执行是否稳定、评论区质量是否值得沉淀。', findings: creatorLearnings },
      { title: '内容学习', intro: '标题、钩子、脚本结构和 CTA 是否被 Organic Heat 与 benchmark 支持。', findings: contentLearnings },
      { title: '策略学习', intro: '命题、平台、人群组合和节奏是否形成下一轮策略规则。', findings: strategyLearnings },
    ],
    writebackCandidates,
  };
};

const normalizeFlywheelReviewModel = (review = {}) => {
  const summary = review.campaignReviewSummary || {};
  const assets = review.learningAssets || {};
  const writeBackQueue = review.writeBackQueue || [];
  return {
    raw: review,
    activeInitiative: review.initiative || null,
    linkedPosts: review.performanceAttribution?.topPosts || [],
    linkedComments: [],
    ownedPosts: { length: summary.ownedPosts || 0 },
    externalPosts: { length: summary.externalPosts || 0 },
    totals: {
      posts: summary.posts || 0,
      ownedPosts: summary.ownedPosts || 0,
      externalPosts: summary.externalPosts || 0,
      views: summary.views || 0,
      interaction: summary.interaction || 0,
      comments: summary.comments || 0,
      riskComments: summary.riskComments || 0,
      avgSentiment: summary.avgSentiment ?? null,
      engagementRate: summary.engagementRate ?? null,
    },
    topPosts: review.performanceAttribution?.topPosts || [],
    platformRows: review.performanceAttribution?.platforms || [],
    creatorRows: review.performanceAttribution?.creators || [],
    learningGroups: [
      { title: '品牌学习', intro: '客户真正喜欢什么表达、反复否掉什么表达、品牌边界在哪里被强化。', findings: assets.brand || [] },
      { title: '达人学习', intro: '哪类达人适合哪种命题、执行是否稳定、评论区质量是否值得沉淀。', findings: assets.creator || [] },
      { title: '内容学习', intro: '标题、钩子、脚本结构和 CTA 是否被 Organic Heat 与 benchmark 支持。', findings: assets.content || [] },
      { title: '策略学习', intro: '命题、平台、人群组合和节奏是否形成下一轮策略规则。', findings: assets.strategy || [] },
    ],
    writebackCandidates: writeBackQueue.map(item => item.label || item.item?.text).filter(Boolean),
    writeBackQueue,
    upstream: review.upstream || {},
    downstreamHandoffs: review.downstreamHandoffs || [],
    strategyRecommendations: review.strategyUpdateRecommendation || [],
    contentPatternUpdate: review.contentPatternUpdate || {},
  };
};

const Review = ({ onNav, workspaceContext }) => {
  const brandStoreReady = Boolean(window.BrandMemoryStore?.useBrandMemoryProfiles);
  const brandState = brandStoreReady ? window.BrandMemoryStore.useBrandMemoryProfiles() : { brands: [], setBrands: () => {}, ready: false };
  const brandId = workspaceContext?.brandId || BRAND?.id || 'montx';
  const brand = brandStoreReady ? window.BrandMemoryStore.getBrandProfile(brandId, brandState.brands) : null;
  const [state, setState] = React.useState({ initiatives: [], social: { posts: [], comments: [] }, organicHeat: {}, flywheel: null, loading: true, error: '' });
  const [writeStatus, setWriteStatus] = React.useState('');
  const loadReviewData = React.useCallback(async () => {
    setState(prev => ({ ...prev, loading: true, error: '' }));
    try {
      const [flywheelRes, initiativesRes, socialRes, heatRes] = await Promise.all([
        fetch(reviewApiUrl('/api/review/flywheel')),
        fetch(reviewApiUrl('/api/initiatives')),
        fetch(reviewApiUrl('/api/social/sync/status')),
        fetch(reviewApiUrl('/api/market/organic-heat?source=owned&limit=120')),
      ]);
      const [flywheelData, initiativesData, socialData, heatData] = await Promise.all([flywheelRes.json(), initiativesRes.json(), socialRes.json(), heatRes.json()]);
      setState({
        initiatives: initiativesData.initiatives || initiativesData.campaigns || [],
        social: socialData || { posts: [], comments: [] },
        organicHeat: heatData || {},
        flywheel: flywheelRes.ok ? flywheelData : null,
        loading: false,
        error: flywheelRes.ok ? '' : (flywheelData.error || '复盘闭环接口未返回'),
      });
    } catch (error) {
      setState(prev => ({ ...prev, loading: false, error: error.message || '复盘数据读取失败' }));
    }
  }, []);
  React.useEffect(() => { loadReviewData(); }, [loadReviewData]);
  const model = React.useMemo(
    () => state.flywheel ? normalizeFlywheelReviewModel(state.flywheel) : buildReviewModel(state),
    [state.flywheel, state.initiatives, state.social, state.organicHeat]
  );
  const hasEvidence = model.linkedPosts.length || model.linkedComments.length || model.writebackCandidates.length;
  const writeBackToSystem = async () => {
    if (!model.raw?.reviewId || !model.activeInitiative?.id) {
      setWriteStatus('缺少复盘记录，无法写回');
      return;
    }
    setWriteStatus('writing');
    try {
      const response = await fetch(reviewApiUrl('/api/review/flywheel/writeback'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ initiativeId: model.activeInitiative.id, review: model.raw, targets: ['initiative', 'strategy_handoff'] }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || '写回失败');
      setWriteStatus(`已写回策略对象，并生成 Strategy handoff：${data.strategyHandoff?.handoff_id || 'ready'}`);
      loadReviewData();
    } catch (error) {
      setWriteStatus(error.message || '写回失败');
    }
  };
  const writeBackToBrandMemory = () => {
    if (!brandStoreReady || !brand || !model.writeBackQueue?.length) {
      setWriteStatus('品牌记忆未就绪或暂无可写回项');
      return;
    }
    const stampedAt = new Date().toLocaleString('zh-CN', { hour12: false });
    const learning = {
      id: `review-learning-${model.raw?.reviewId || Date.now()}`,
      status: 'pending',
      type: 'learning-flywheel-review',
      source: 'review-flywheel',
      title: `${model.activeInitiative?.name || 'Workspace'} 复盘学习项`,
      text: `复盘沉淀 ${model.writeBackQueue.length} 条待确认学习项，覆盖品牌、达人、内容和策略。`,
      createdAt: stampedAt,
      reviewId: model.raw?.reviewId || '',
      writeBackQueue: model.writeBackQueue,
      evidence: model.raw?.campaignReviewSummary || {},
    };
    brandState.setBrands(prev => prev.map(item => item.id === brand.id
      ? window.BrandMemoryStore.normalizeBrandProfile({
          ...item,
          learnings: [...(item.learnings || []).filter(row => row.id !== learning.id), learning],
          updatedAt: stampedAt,
        })
      : item
    ));
    setWriteStatus('已写入 Brand Memory 待确认学习项');
  };
  return (
    <div className="page wide">
      <div className="page-header">
        <div className="heading">
          <h1>复盘 · <span className="hl">学习闭环</span>写回长期记忆</h1>
          <p>根据 PRD，复盘只吸收真实结果，并沉淀品牌、达人、内容和策略四类学习资产。</p>
        </div>
        <div className="actions">
          <button className="btn" onClick={loadReviewData} disabled={state.loading}><Icon name="refresh" className="icon sm"/>刷新复盘数据</button>
          <button className="btn" onClick={() => onNav?.('reports')}>查看报表中心</button>
          <button className="btn" onClick={writeBackToSystem} disabled={!model.writebackCandidates.length || writeStatus === 'writing'}>写回策略对象</button>
          <button className="btn primary" onClick={writeBackToBrandMemory} disabled={!model.writebackCandidates.length || !brandStoreReady}>写回品牌记忆 <Icon name="arrow" className="icon sm"/></button>
        </div>
      </div>

      {state.error && <div className="card"><div className="card-body"><span className="chip danger">{state.error}</span></div></div>}
      {writeStatus && <div className="card"><div className="card-body"><span className={`chip ${writeStatus.includes('失败') || writeStatus.includes('缺少') ? 'danger' : 'info'}`}>{writeStatus === 'writing' ? '正在写回学习闭环...' : writeStatus}</span></div></div>}

      <ReviewFlowCard upstream={model.upstream} downstream={model.downstreamHandoffs} recommendations={model.strategyRecommendations}/>

      <div className="card">
        <div className="card-header">
          <h3>Campaign Review Summary · {model.activeInitiative?.name || '未选择复盘对象'}</h3>
          <span className="count">{state.loading ? '读取中' : hasEvidence ? '真实同步数据' : '待数据沉淀'}</span>
        </div>
        <div className="card-body">
          <div className="grid g-4">
            <ResultKPI label="关联内容" actual={formatCompactNumber(model.totals.posts)} meta={`${model.totals.ownedPosts ?? model.ownedPosts.length} 自有 · ${model.totals.externalPosts ?? model.externalPosts.length} 外部`}/>
            <ResultKPI label="阅读/播放" actual={formatCompactNumber(model.totals.views)} meta="views / plays / reads"/>
            <ResultKPI label="互动转化 CVR" actual={model.totals.engagementRate === null ? '待计算' : `${(model.totals.engagementRate * 100).toFixed(2)}%`} meta={`${formatCompactNumber(model.totals.interaction)} 互动`}/>
            <ResultKPI label="待复核风险" actual={formatCompactNumber(model.totals.riskComments)} meta={`${formatCompactNumber(model.totals.comments)} 评论 · 情绪 ${model.totals.avgSentiment === null ? '待样本' : model.totals.avgSentiment.toFixed(2)}`} tone={model.totals.riskComments ? 'warn' : 'ok'}/>
          </div>
          {!hasEvidence && <ReviewEmptyState text="当前还没有可沉淀的发布结果。复盘页不会补写 mockup 成绩；请先同步社媒资产、生成报表或完成人工复核。"/>}
        </div>
      </div>

      <div className="grid g-2">
        {model.learningGroups.map(group => <LearningCard key={group.title} {...group}/>)}
      </div>

      <div className="grid" style={{gridTemplateColumns:'1fr 1fr'}}>
        <ReviewAttributionCard title="Performance Attribution · 平台 / 内容" rows={model.platformRows} topPosts={model.topPosts}/>
        <CreatorLearningCard rows={model.creatorRows}/>
      </div>

      <div className="card">
        <div className="card-header"><h3>长期记忆 · 待人工确认写回</h3><span className="count">{model.writebackCandidates.length}</span></div>
        <div className="card-body" style={{display:'flex', flexDirection:'column', gap:8, maxHeight:360, overflowY:'auto'}}>
          {model.writebackCandidates.length
            ? model.writebackCandidates.map((item, index) => <MemoryCandidate key={index} text={item}/>)
            : <ReviewEmptyState text="暂无达到写回条件的学习项。PRD 要求复盘结果进入长期学习层，但写回前必须有真实证据和人工确认。"/>}
        </div>
      </div>
    </div>
  );
};

const ReviewFlowCard = ({ upstream = {}, downstream = [], recommendations = [] }) => {
  const upstreamRows = [
    ['Brand Memory', upstream.brandMemory?.status || 'client-local', upstream.brandMemory?.requiredForWriteback ? '写回前需人工确认' : ''],
    ['Market Sensing', upstream.marketSensing?.status || 'waiting', `${upstream.marketSensing?.signals || 0} signals · ${upstream.marketSensing?.reviewQueue || 0} reviews`],
    ['Strategy / Thesis', upstream.strategy?.status || 'waiting', upstream.strategy?.thesis?.title || '策略母题待读取'],
    ['Brief', upstream.brief?.status || 'waiting', upstream.brief?.masterBrief?.title || 'Master Brief 待生成'],
    ['Publishing Monitor', upstream.publishingMonitor?.status || 'waiting-sync', `${upstream.publishingMonitor?.posts || 0} posts · ${upstream.publishingMonitor?.comments || 0} comments`],
    ['Organic Heat', upstream.organicHeat?.status || 'waiting-data', `${upstream.organicHeat?.aggregate?.count || 0} scored contents`],
  ];
  return (
    <div className="card">
      <div className="card-header"><h3>上下游闭环 · PRD Information Flow</h3><span className="count">Step 1-9</span></div>
      <div className="card-body">
        <div className="grid g-3">
          <div>
            <div style={{fontSize:11, fontWeight:700, marginBottom:8}}>上游输入</div>
            <div style={{display:'flex', flexDirection:'column', gap:6}}>
              {upstreamRows.map(row => (
                <div key={row[0]} style={{display:'grid', gridTemplateColumns:'120px 90px 1fr', gap:8, alignItems:'center', fontSize:11, padding:'6px 8px', border:'1px solid var(--hairline)', borderRadius:6}}>
                  <b>{row[0]}</b>
                  <span className="tag mono">{row[1]}</span>
                  <span style={{color:'var(--ink-3)'}}>{row[2]}</span>
                </div>
              ))}
            </div>
          </div>
          <div>
            <div style={{fontSize:11, fontWeight:700, marginBottom:8}}>复盘输出</div>
            <div style={{display:'flex', flexDirection:'column', gap:6}}>
              {['Campaign Review Summary', 'Performance Attribution', 'Creator Learning Update', 'Content Pattern Update', 'Strategy Update Recommendation'].map(item => (
                <div key={item} style={{fontSize:11, padding:'7px 9px', border:'1px solid var(--hairline)', borderRadius:6, background:'var(--bg-2)'}}>{item}</div>
              ))}
            </div>
          </div>
          <div>
            <div style={{fontSize:11, fontWeight:700, marginBottom:8}}>下游动作</div>
            <div style={{display:'flex', flexDirection:'column', gap:6}}>
              {downstream.length ? downstream.map(item => (
                <div key={item.target_module} style={{fontSize:11, padding:'7px 9px', border:'1px solid var(--hairline)', borderRadius:6}}>
                  <b>{item.title}</b>
                  <small style={{display:'block', color:'var(--ink-3)', marginTop:2}}>{item.target_module} · {item.status}</small>
                </div>
              )) : <ReviewEmptyState text="暂无下游 handoff，等待复盘证据。"/>}
              {recommendations.slice(0, 2).map(item => (
                <small key={item.recommendation_id} style={{color:'var(--ink-3)'}}>建议：{item.title}</small>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

const ResultKPI = ({ label, actual, meta, tone }) => {
  const style = tone === 'warn'
    ? { borderColor: 'color-mix(in oklch, var(--warn) 25%, var(--hairline))' }
    : tone === 'ok'
      ? { borderColor: 'var(--accent-line)', background: 'var(--accent-tint)' }
      : {};
  return (
    <div className="metric" style={style}>
      <div className="label">{label}</div>
      <div className="value" style={{color: tone === 'warn' ? 'var(--warn)' : tone === 'ok' ? 'var(--accent)' : 'var(--ink)'}}>{actual}</div>
      <div className="delta">{meta}</div>
    </div>
  );
};

const LearningCard = ({ title, intro, findings = [] }) => {
  const icons = { ok: '✓', warn: '⚠', new: '✦' };
  const colors = { ok: 'var(--accent)', warn: 'var(--warn)', new: 'var(--info)' };
  return (
    <div className="card">
      <div className="card-header"><h3>{title}</h3><span className="count">{findings.length || '待沉淀'}</span></div>
      <div className="card-body">
        <div style={{fontSize:11, color:'var(--ink-3)', marginBottom:12, fontStyle:'italic'}}>{intro}</div>
        <div style={{display:'flex', flexDirection:'column', gap:8}}>
          {findings.length ? findings.map((finding, index) => (
            <div key={index} style={{display:'flex', gap:10, alignItems:'flex-start', fontSize:12, lineHeight:1.55}}>
              <span style={{color: colors[finding.type] || 'var(--ink-3)', fontFamily:'var(--f-mono)', fontSize:14, minWidth:14}}>{icons[finding.type] || '•'}</span>
              <span style={{flex:1}}>{finding.text}<small style={{display:'block', color:'var(--ink-4)', marginTop:2}}>{finding.source}</small></span>
            </div>
          )) : <ReviewEmptyState text="没有足够证据，不生成学习结论。"/>}
        </div>
      </div>
    </div>
  );
};

const ReviewEmptyState = ({ text }) => (
  <div style={{fontSize:12, color:'var(--ink-3)', padding:'10px 12px', border:'1px dashed var(--hairline)', borderRadius:6, background:'var(--bg-2)'}}>{text}</div>
);

const ReviewAttributionCard = ({ title, rows = [], topPosts = [] }) => (
  <div className="card">
    <div className="card-header"><h3>{title}</h3><span className="count">{rows.length}</span></div>
    <div className="card-body">
      {rows.length ? (
        <table className="tbl" style={{fontSize:11}}>
          <thead><tr><th>平台</th><th>内容</th><th>阅读/播放</th><th>互动</th><th>CVR</th></tr></thead>
          <tbody>
            {rows.slice(0, 6).map(row => (
              <tr key={row.platform}>
                <td>{row.platform}</td>
                <td className="mono" style={{textAlign:'right'}}>{row.posts}</td>
                <td className="mono" style={{textAlign:'right'}}>{formatCompactNumber(row.views)}</td>
                <td className="mono" style={{textAlign:'right'}}>{formatCompactNumber(row.interaction)}</td>
                <td className="mono" style={{textAlign:'right'}}>{row.engagementRate === null ? '-' : `${(row.engagementRate * 100).toFixed(2)}%`}</td>
              </tr>
            ))}
          </tbody>
        </table>
      ) : <ReviewEmptyState text="暂无平台表现归因；需要先有已关联内容。"/>}
      {topPosts.length ? (
        <div style={{display:'flex', flexDirection:'column', gap:6, marginTop:12}}>
          {topPosts.slice(0, 3).map(post => <small key={post.post_id || post.title} style={{color:'var(--ink-3)'}}>代表内容：{post.platform} · {cleanReportLabel(post.title || post.caption || '未命名内容').slice(0, 42)}</small>)}
        </div>
      ) : null}
    </div>
  </div>
);

const CreatorLearningCard = ({ rows = [] }) => (
  <div className="card">
    <div className="card-header"><h3>Creator Learning Update · 外部声量</h3><span className="count">{rows.length}</span></div>
    <div className="card-body">
      {rows.length ? (
        <table className="tbl" style={{fontSize:11}}>
          <thead><tr><th>来源/达人</th><th>平台</th><th>内容</th><th>阅读/播放</th><th>互动</th></tr></thead>
          <tbody>
            {rows.map(row => (
              <tr key={`${row.creator}-${row.platform}`}>
                <td>{row.creator}</td>
                <td>{row.platform || '-'}</td>
                <td className="mono" style={{textAlign:'right'}}>{row.posts}</td>
                <td className="mono" style={{textAlign:'right'}}>{formatCompactNumber(row.views)}</td>
                <td className="mono" style={{textAlign:'right'}}>{formatCompactNumber(row.interaction)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      ) : <ReviewEmptyState text="暂无外部达人/UGC 证据，不生成达人学习结论。"/>}
    </div>
  </div>
);

const MemoryCandidate = ({ text }) => (
  <div style={{fontSize:12, padding:'6px 10px', background:'var(--bg-2)', borderRadius:5, border:'1px solid var(--hairline)', fontFamily:'var(--f-mono)', lineHeight:1.5}}>
    {text}
  </div>
);

Object.assign(window, { Brief, Content, Monitor, Reports, Review });
