// screens-risk-analytics.jsx — Risk (16), Analytics (20)
// Risk 已接真后端：信号 / 黑名单 / 设备 / 事件追溯
// Analytics 接 events/stats，TrendDetectionScreen 在 screens-pool-ops.jsx 中。

const _NA = window.NebulaAdmin;
const adminFetch  = _NA.adminFetch;
const useAdminPoll = _NA.useAdminPoll;

// ─── helpers ────────────────────────────────────────────────────────────────
function sevColor(sev) {
  if (sev === 'high') return 'var(--danger)';
  if (sev === 'mid')  return 'var(--warning)';
  return 'var(--success)';
}
function sevBadge(sev, t) {
  const tx = (t && t.tx) ? t.tx : ((zh) => zh);
  const cls = sev === 'high' ? 'danger' : sev === 'mid' ? 'warning' : 'info';
  const label = sev === 'high' ? tx('高危','High') : sev === 'mid' ? tx('中等','Mid') : tx('低','Low');
  return <span className={'badge ' + cls}>{label}</span>;
}
function flagColor(flag) {
  if (flag === 'banned')   return 'var(--danger)';
  if (flag === 'shadow')   return 'var(--warning)';
  if (flag === 'flagged')  return 'var(--warning)';
  return 'var(--success)';
}
function relTime(iso, t) {
  if (!iso) return '—';
  const ms = Date.now() - new Date(iso).getTime();
  if (Number.isNaN(ms)) return iso;
  const s = Math.floor(ms / 1000);
  const tx = (t && t.tx) ? t.tx : ((zh) => zh);
  const suffix = tx(' 前',' ago');
  if (s < 60)   return s + 's' + suffix;
  if (s < 3600) return Math.floor(s / 60) + 'm' + suffix;
  if (s < 86400)return Math.floor(s / 3600) + 'h' + suffix;
  return Math.floor(s / 86400) + 'd' + suffix;
}
function shortId(id, n) { n = n || 12; if (!id) return ''; return id.length > n ? id.slice(0, n) + '…' : id; }

// ─── 16 Risk Control ─────────────────────────────────────────────────────────
function RiskScreen({ t, push }) {
  const [tab, setTab] = React.useState('signals');
  const [focusUser, setFocusUser] = React.useState('');

  const [signals]    = useAdminPoll('/api/risk/signals', 5000, tab === 'signals' || tab === 'overview');
  const [devList]    = useAdminPoll('/api/admin/devices?limit=50', 8000, tab === 'devices');
  const [evtStats]   = useAdminPoll('/api/admin/events/stats', 10000, true);

  const stats = (signals && signals.stats) || { total_signals: 0, high: 0, mid: 0, low: 0 };
  const blacklistCount = (signals && signals.blacklist_count) || 0;
  const devTotal = (devList && devList.devices && devList.devices.length) || 0;
  const login24h = (evtStats && evtStats.counts_24h && evtStats.counts_24h.login) || 0;

  const gotoUserEvents = (uid) => {
    setFocusUser(uid);
    setTab('events');
  };

  return (
    <div className="stack">
      <div className="page-h">
        <div>
          <h1>{t.p_risk}</h1>
          <div className="sub">{t.tx('实时风控引擎 · 30 条规则 · 信号 / 设备 / 事件追溯','Realtime risk engine · 30 rules · Signals / Devices / Event trace')}</div>
        </div>
        <div className="actions">
          <button className="btn btn-sm" onClick={() => push(t.tx('规则引擎 (TODO)','Rule engine (TODO)'))}>{t.tx('规则引擎','Rule Engine')}</button>
          <button className="btn btn-pri btn-sm" onClick={() => setTab('blacklist')}><Icons.plus /> {t.tx('加黑','Blacklist')}</button>
        </div>
      </div>

      <div className="row-5">
        <KPI label={t.tx('实时信号','Live Signals')} value={String(stats.total_signals)} delta={`high ${stats.high}`} live={!!signals}
             spark={<MiniBars data={[stats.low, stats.mid, stats.high]} w={100} h={24} color="oklch(0.58 0.20 25)" />} />
        <KPI label={t.tx('高危信号','High-Risk Signals')} value={String(stats.high)} delta={stats.high ? t.tx('需处理','Action needed') : '—'} live={!!signals} />
        <KPI label={t.tx('黑名单条目','Blacklist Entries')} value={String(blacklistCount)} delta={t.tx('用户 + IP','User + IP')} live={!!signals} />
        <KPI label={t.tx('设备样本','Device Samples')} value={String(devTotal)} delta={t.tx('近 50 个','Latest 50')} live={!!devList} />
        <KPI label={t.tx('24h 登录','24h Logins')} value={String(login24h)} delta={t.tx('事件流','Event stream')} live={!!evtStats} />
      </div>

      <div className="tabs">
        {[['signals',t.tx('信号','Signals')],['scores',t.tx('风险评分','Risk Scores')],['fps',t.tx('设备指纹','Device Fingerprints')],['blacklist',t.tx('黑名单','Blacklist')],['devices',t.tx('设备','Devices')],['events',t.tx('事件追溯','Event Trace')],['clusters',t.tx('设备聚类','Device Clusters')]].map(([k,l]) => (
          <div key={k} className={'tab' + (tab === k ? ' active' : '')} onClick={() => setTab(k)}>{l}</div>
        ))}
      </div>

      {tab === 'signals'   && <SignalsTab signals={signals} onUser={gotoUserEvents} onBlack={() => setTab('blacklist')} push={push} t={t} />}
      {tab === 'scores'    && <RiskScoresTab push={push} onUser={gotoUserEvents} t={t} />}
      {tab === 'fps'       && <FingerprintsTab push={push} onUser={gotoUserEvents} t={t} />}
      {tab === 'blacklist' && <BlacklistTab push={push} t={t} />}
      {tab === 'devices'   && <DevicesTab devList={devList} push={push} onUser={gotoUserEvents} t={t} />}
      {tab === 'events'    && <EventsTab initial={focusUser} push={push} t={t} />}
      {tab === 'clusters'  && <ClustersTab push={push} onUser={gotoUserEvents} t={t} />}
    </div>
  );
}

// ─── Tab 5: Clusters ────────────────────────────────────────────────────────
function sizeColor(n) {
  if (n >= 20) return 'var(--danger)';
  if (n >= 5)  return 'var(--warning)';
  return 'var(--success)';
}
function clusterFlagBadge(flag) {
  const f = flag || 'normal';
  const cls = f === 'banned' ? 'danger' : f === 'suspicious' ? 'warning' : 'info';
  return <span className={'badge ' + cls}>{f}</span>;
}

function FlagModal({ cluster, onClose, onSubmit, t }) {
  const [flag, setFlag] = React.useState((cluster && cluster.risk_flag) || 'suspicious');
  const [note, setNote] = React.useState((cluster && cluster.note) || '');
  const [busy, setBusy] = React.useState(false);
  const submit = async (e) => {
    e.preventDefault();
    setBusy(true);
    try { await onSubmit(flag, note); } finally { setBusy(false); }
  };
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
      <form onClick={e => e.stopPropagation()} onSubmit={submit} className="card" style={{ width: 420, padding: 0 }}>
        <div className="card-h"><h3>{(t && t.tx ? t.tx : ((zh) => zh))('标记聚类','Flag Cluster')}</h3><span className="meta text-mono">{shortId(cluster && cluster.signature, 16)}</span></div>
        <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div>
            <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>risk_flag</div>
            <select className="input" value={flag} onChange={e => setFlag(e.target.value)}>
              <option value="normal">normal</option>
              <option value="suspicious">suspicious</option>
              <option value="banned">banned</option>
            </select>
          </div>
          <div>
            <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>note</div>
            <input className="input" style={{ width: '100%' }} placeholder={(t && t.tx ? t.tx : ((zh) => zh))('备注（可选）','Note (optional)')} value={note} onChange={e => setNote(e.target.value)} />
          </div>
          <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', marginTop: 4 }}>
            <button type="button" className="btn btn-sm" onClick={onClose}>{(t && t.tx ? t.tx : ((zh) => zh))('取消','Cancel')}</button>
            <button type="submit" className="btn btn-pri btn-sm" disabled={busy}>{busy ? (t && t.tx ? t.tx : ((zh) => zh))('提交中…','Submitting…') : (t && t.tx ? t.tx : ((zh) => zh))('保存','Save')}</button>
          </div>
        </div>
      </form>
    </div>
  );
}

function ClustersTab({ push, onUser, t }) {
  const [riskFilter, setRiskFilter] = React.useState('all');
  const [minSize, setMinSize] = React.useState(2);
  const [list, setList] = React.useState(null);
  const [selected, setSelected] = React.useState(null);
  const [detail, setDetail] = React.useState(null);
  const [flagging, setFlagging] = React.useState(null);
  const [tick, setTick] = React.useState(0);

  const reload = React.useCallback(async () => {
    try {
      const qs = new URLSearchParams();
      if (riskFilter !== 'all') qs.set('risk', riskFilter);
      qs.set('min_size', String(minSize || 1));
      const d = await adminFetch('/api/admin/devices/clusters?' + qs.toString());
      setList(d.clusters || []);
    } catch (e) {
      push((t && t.tx ? t.tx : ((zh) => zh))('聚类列表加载失败: ','Failed to load clusters: ') + (e.body && e.body.error || e.message));
      setList([]);
    }
  }, [riskFilter, minSize, push]);

  React.useEffect(() => { reload(); }, [reload, tick]);
  React.useEffect(() => {
    const id = setInterval(() => setTick(x => x + 1), 10000);
    return () => clearInterval(id);
  }, []);

  const openDetail = async (cid) => {
    setSelected(cid);
    setDetail(null);
    try {
      const d = await adminFetch('/api/admin/devices/clusters/' + encodeURIComponent(cid));
      setDetail(d);
    } catch (e) {
      push(t.tx('详情加载失败: ','Failed to load details: ') + (e.body && e.body.error || e.message));
    }
  };

  const submitFlag = async (flag, note) => {
    if (!flagging) return;
    try {
      await adminFetch('/api/admin/devices/clusters/' + encodeURIComponent(flagging.id) + '/flag', {
        method: 'POST',
        body: JSON.stringify({ risk_flag: flag, note }),
      });
      push(t.tx('已更新 ','Updated ') + shortId(flagging.signature, 10));
      setFlagging(null);
      reload();
      if (selected === flagging.id) openDetail(flagging.id);
    } catch (e) {
      push(t.tx('标记失败: ','Flag failed: ') + (e.body && e.body.error || e.message));
    }
  };

  const banAll = async (c) => {
    if (!window.confirm(t.tx(`封禁聚类 ${shortId(c.signature, 12)} 内全部 ${c.size} 台设备？此操作不可撤销。`, `Ban all ${c.size} devices in cluster ${shortId(c.signature, 12)}? This cannot be undone.`))) return;
    try {
      await adminFetch('/api/admin/devices/clusters/' + encodeURIComponent(c.id) + '/ban-all', { method: 'POST' });
      push(t.tx('已封禁聚类 ','Banned cluster ') + shortId(c.signature, 10));
      reload();
      if (selected === c.id) openDetail(c.id);
    } catch (e) {
      push(t.tx('封禁失败: ','Ban failed: ') + (e.body && e.body.error || e.message));
    }
  };

  const recompute = async () => {
    try {
      await adminFetch('/api/admin/devices/recompute', { method: 'POST' });
      push(t.tx('已触发，10s 后刷新','Triggered, refreshing in 10s'));
      setTimeout(() => setTick(x => x + 1), 10000);
    } catch (e) {
      push(t.tx('触发失败: ','Trigger failed: ') + (e.body && e.body.error || e.message));
    }
  };

  const clusters = list || [];

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: 16 }}>
      <div className="card">
        <div className="filter-bar">
          {[['all',t.tx('全部','All')],['normal','normal'],['suspicious','suspicious'],['banned','banned']].map(([k,l]) => (
            <span key={k} className={'chip' + (riskFilter === k ? ' active' : '')} onClick={() => setRiskFilter(k)}>{l}</span>
          ))}
          <span className="text-muted" style={{ fontSize: 11, marginLeft: 6 }}>min_size</span>
          <input className="input" type="number" min={1} style={{ width: 70 }}
                 value={minSize} onChange={(e) => setMinSize(Math.max(1, Number(e.target.value) || 1))} />
          <div style={{ flex: 1 }} />
          <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('10s 自动刷新','10s auto-refresh')} · {clusters.length}</span>
          <button className="btn btn-sm" onClick={recompute}>{t.tx('立即重算','Recompute Now')}</button>
        </div>
        <div className="tbl-scroll">
        <table className="tbl tbl-stack">
          <thead><tr><th>signature</th><th>size</th><th>risk_flag</th><th>first_seen</th><th>last_seen</th><th></th></tr></thead>
          <tbody>
            {!list && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</td></tr>}
            {list && clusters.length === 0 && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('暂无聚类','No clusters')}</td></tr>}
            {clusters.map(c => (
              <tr key={c.id} onClick={() => openDetail(c.id)}
                  style={{ cursor: 'pointer', background: selected === c.id ? 'var(--bg-inset)' : 'transparent' }}>
                <td data-label="signature" className="text-mono" title={c.signature} style={{ fontSize: 11.5 }}>{shortId(c.signature, 12)}</td>
                <td data-label="size"><span className="tabular" style={{ fontSize: 14, fontWeight: 600, color: sizeColor(c.size) }}>{c.size}</span></td>
                <td data-label="risk_flag">{clusterFlagBadge(c.risk_flag)}</td>
                <td data-label="first_seen" className="muted text-mono text-faint" style={{ fontSize: 11 }}>{relTime(c.first_seen, t)}</td>
                <td data-label="last_seen" className="muted text-mono text-faint" style={{ fontSize: 11 }}>{relTime(c.last_seen, t)}</td>
                <td data-label={t.tx('操作', 'Actions')} className="tbl-actions" onClick={e => e.stopPropagation()}>
                  <button className="btn btn-xs btn-ghost" onClick={() => openDetail(c.id)}>{t.tx('详情','Detail')}</button>
                  <button className="btn btn-xs" onClick={() => setFlagging(c)}>flag</button>
                  <button className="btn btn-xs" onClick={() => banAll(c)} style={{ color: 'var(--danger)' }}>ban-all</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        </div>
      </div>

      <div className="card">
        <div className="card-h">
          <h3>{t.tx('聚类详情','Cluster Details')}</h3>
          {selected && <span className="meta text-mono">{shortId(selected, 14)}</span>}
        </div>
        {!selected && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('点击左侧聚类查看成员','Click a cluster on the left to view members')}</div>}
        {selected && !detail && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</div>}
        {detail && detail.cluster && (
          <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, fontSize: 12 }}>
              <div><span className="text-muted">signature</span> <span className="text-mono" title={detail.cluster.signature}>{shortId(detail.cluster.signature, 18)}</span></div>
              <div><span className="text-muted">size</span> <span className="tabular" style={{ fontWeight: 600, color: sizeColor(detail.cluster.size) }}>{detail.cluster.size}</span></div>
              <div><span className="text-muted">flag</span> {clusterFlagBadge(detail.cluster.risk_flag)}</div>
              <div><span className="text-muted">last_seen</span> <span className="text-faint text-mono" style={{ fontSize: 11 }}>{relTime(detail.cluster.last_seen, t)}</span></div>
            </div>
            {detail.cluster.note && (
              <div>
                <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>note</div>
                <div style={{ fontSize: 12 }}>{detail.cluster.note}</div>
              </div>
            )}
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>members ({(detail.members || []).length})</div>
              <div className="tbl-scroll">
              <table className="tbl tbl-stack" style={{ marginTop: 0 }}>
                <thead><tr><th>device</th><th>similarity</th><th>last_seen</th><th>accounts</th></tr></thead>
                <tbody>
                  {(!detail.members || detail.members.length === 0) && (
                    <tr><td colSpan={4} style={{ padding: 12, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('无成员','No members')}</td></tr>
                  )}
                  {(detail.members || []).map(m => {
                    const pct = Math.round((m.similarity || 0) * 100);
                    return (
                      <tr key={m.device_id}>
                        <td data-label="device" className="text-mono" title={m.device_id} style={{ fontSize: 11 }}>{shortId(m.device_id, 12)}</td>
                        <td data-label="similarity" style={{ minWidth: 110 }}>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                            <span className="barmini" style={{ flex: 1 }}><i style={{ width: pct + '%', background: pct >= 90 ? 'var(--danger)' : pct >= 70 ? 'var(--warning)' : 'var(--accent)' }} /></span>
                            <span className="tabular text-muted" style={{ fontSize: 10.5, width: 32, textAlign: 'right' }}>{pct}%</span>
                          </div>
                        </td>
                        <td data-label="last_seen" className="muted text-mono text-faint" style={{ fontSize: 10.5 }}>{relTime(m.last_seen, t)}</td>
                        <td data-label="accounts" style={{ fontSize: 11 }}>
                          {(m.accounts || []).length === 0 && <span className="text-faint">—</span>}
                          {(m.accounts || []).map((uid, i) => (
                            <React.Fragment key={uid}>
                              {i > 0 && <span className="text-faint">, </span>}
                              <span className="text-mono" style={{ cursor: 'pointer', color: 'var(--accent)' }} onClick={() => onUser(uid)}>{uid}</span>
                            </React.Fragment>
                          ))}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
              </div>
            </div>
          </div>
        )}
      </div>

      {flagging && <FlagModal cluster={flagging} onClose={() => setFlagging(null)} onSubmit={submitFlag} t={t} />}
    </div>
  );
}

// ─── Tab 1: Signals ─────────────────────────────────────────────────────────
function SignalsTab({ signals, onUser, push, t }) {
  const [filter, setFilter] = React.useState('all');
  const list = (signals && signals.signals) || [];
  const filtered = filter === 'all' ? list : list.filter(s => s.severity === filter);

  const blacklistUser = async (uid) => {
    if (!uid) return;
    if (!window.confirm(t.tx(`确认将 ${uid} 加入黑名单？`, `Confirm adding ${uid} to blacklist?`))) return;
    try {
      await adminFetch('/api/risk/blacklist', {
        method: 'POST',
        body: JSON.stringify({ kind: 'user', value: uid, reason: t.tx('风控信号一键加黑','Quick blacklist from risk signal') }),
      });
      push(`${t.tx('已加黑','Blacklisted')} ${uid}`);
    } catch (e) {
      push(t.tx('加黑失败: ','Blacklist failed: ') + (e.body && e.body.error || e.message));
    }
  };

  return (
    <div className="card">
      <div className="filter-bar">
        {[['all',t.tx('全部 ','All ') + list.length],['high',t.tx('高危 ','High ') + list.filter(s=>s.severity==='high').length],['mid',t.tx('中 ','Mid ') + list.filter(s=>s.severity==='mid').length],['low',t.tx('低 ','Low ') + list.filter(s=>s.severity==='low').length]].map(([k,l]) => (
          <span key={k} className={'chip' + (filter === k ? ' active' : '')} onClick={() => setFilter(k)}>{l}</span>
        ))}
        <div style={{ flex: 1 }} />
        <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('5s 自动刷新','5s auto-refresh')}</span>
      </div>
      {!signals && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</div>}
      {signals && filtered.length === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('暂无信号','No signals')}</div>}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))', gap: 12, padding: 12 }}>
        {filtered.map((s, i) => (
          <div key={i} style={{ border: '1px solid var(--border)', borderLeft: `3px solid ${sevColor(s.severity)}`, borderRadius: 8, padding: 12, background: 'var(--bg-card)' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
              {sevBadge(s.severity, t)}
              <span style={{ fontWeight: 600, fontSize: 12.5 }}>{s.type}</span>
              <div style={{ flex: 1 }} />
              <button className="btn btn-xs btn-ghost" onClick={() => onUser(s.user_id)}>{t.tx('事件流','Events')}</button>
              <button className="btn btn-xs" onClick={() => blacklistUser(s.user_id)}>{t.tx('加黑','Blacklist')}</button>
            </div>
            <div style={{ fontSize: 12, marginBottom: 4 }}>{s.details}</div>
            <div className="text-mono text-faint" style={{ fontSize: 11 }}>user_id <span style={{ color: 'var(--text)', cursor: 'pointer' }} onClick={() => onUser(s.user_id)}>{s.user_id}</span></div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Tab 2: Blacklist ───────────────────────────────────────────────────────
function BlacklistTab({ push, t }) {
  const [kind, setKind] = React.useState('user');
  const [data, setData] = React.useState(null);
  const [adding, setAdding] = React.useState({ value: '', reason: '' });
  const [busy, setBusy] = React.useState(false);

  const reload = React.useCallback(async () => {
    try {
      const d = await adminFetch(`/api/risk/blacklist?kind=${kind}`);
      setData(d);
    } catch (e) {
      push(t.tx('加载失败: ','Load failed: ') + (e.body && e.body.error || e.message));
    }
  }, [kind, push]);

  React.useEffect(() => { reload(); }, [reload]);

  const onAdd = async (e) => {
    e.preventDefault();
    if (!adding.value.trim()) { push(t.tx('请填写值','Please enter a value')); return; }
    setBusy(true);
    try {
      await adminFetch('/api/risk/blacklist', {
        method: 'POST',
        body: JSON.stringify({ kind, value: adding.value.trim(), reason: adding.reason.trim() || t.tx('人工添加','Manually added') }),
      });
      setAdding({ value: '', reason: '' });
      push(t.tx('已添加','Added'));
      reload();
    } catch (e) {
      push(t.tx('添加失败: ','Add failed: ') + (e.body && e.body.error || e.message));
    } finally {
      setBusy(false);
    }
  };

  const onDel = async (id) => {
    if (!window.confirm(t.tx(`移除 #${id}？`, `Remove #${id}?`))) return;
    try {
      await adminFetch(`/api/risk/blacklist/${id}`, { method: 'DELETE' });
      push(t.tx('已移除','Removed'));
      reload();
    } catch (e) {
      push(t.tx('删除失败: ','Delete failed: ') + (e.body && e.body.error || e.message));
    }
  };

  const entries = (data && data.entries) || [];

  return (
    <div className="card">
      <div className="filter-bar">
        {[['user',t.tx('用户','User')],['ip','IP']].map(([k,l]) => (
          <span key={k} className={'chip' + (kind === k ? ' active' : '')} onClick={() => setKind(k)}>{l} {data && kind === k ? entries.length : ''}</span>
        ))}
        <div style={{ flex: 1 }} />
        <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('共','Total')} {(data && data.total) || 0}</span>
      </div>

      <form onSubmit={onAdd} style={{ display: 'flex', gap: 8, padding: 12, borderBottom: '1px solid var(--border)', alignItems: 'center' }}>
        <span className="text-muted" style={{ fontSize: 11 }}>{t.tx('新增','Add')}</span>
        <select className="input" style={{ width: 90 }} value={kind} onChange={(e) => setKind(e.target.value)}>
          <option value="user">user</option>
          <option value="ip">ip</option>
        </select>
        <input className="input" placeholder={kind === 'user' ? t.tx('user_id, 如 U-882104','user_id, e.g. U-882104') : t.tx('IP, 如 45.33.32.10','IP, e.g. 45.33.32.10')} style={{ width: 240 }}
               value={adding.value} onChange={(e) => setAdding({ ...adding, value: e.target.value })} />
        <input className="input" placeholder={t.tx('原因（可选）','Reason (optional)')} style={{ flex: 1, minWidth: 160 }}
               value={adding.reason} onChange={(e) => setAdding({ ...adding, reason: e.target.value })} />
        <button className="btn btn-pri btn-sm" disabled={busy}>{t.tx('加入','Add')}</button>
      </form>

      <div className="tbl-scroll">
      <table className="tbl tbl-stack">
        <thead><tr><th>ID</th><th>kind</th><th>value</th><th>{t.tx('原因','Reason')}</th><th>{t.tx('添加人','Added By')}</th><th>{t.tx('时间','Time')}</th><th></th></tr></thead>
        <tbody>
          {!data && <tr><td colSpan={7} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</td></tr>}
          {data && entries.length === 0 && <tr><td colSpan={7} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('无条目','No entries')}</td></tr>}
          {entries.map(r => (
            <tr key={r.id}>
              <td data-label="ID" className="text-mono text-faint" style={{ fontSize: 11 }}>#{r.id}</td>
              <td data-label="kind"><span className="badge outline">{r.kind}</span></td>
              <td data-label="value" className="text-mono">{r.value}</td>
              <td data-label={t.tx('原因','Reason')}>{r.reason || '—'}</td>
              <td data-label={t.tx('添加人','Added By')} className="muted">{r.added_by || '—'}</td>
              <td data-label={t.tx('时间','Time')} className="muted">{relTime(r.added_at, t)}</td>
              <td data-label={t.tx('操作', 'Actions')} className="tbl-actions"><button className="btn btn-xs btn-ghost" onClick={() => onDel(r.id)}>{t.tx('移出','Remove')}</button></td>
            </tr>
          ))}
        </tbody>
      </table>
      </div>
    </div>
  );
}

// ─── Tab 3: Devices ─────────────────────────────────────────────────────────
function DevicesTab({ devList, push, onUser, t }) {
  const [selected, setSelected] = React.useState(null);
  const [detail, setDetail] = React.useState(null);
  const [reason, setReason] = React.useState('');

  const open = async (did) => {
    setSelected(did);
    setDetail(null);
    try {
      const d = await adminFetch(`/api/admin/devices/${did}`);
      setDetail(d);
    } catch (e) {
      push(t.tx('详情加载失败: ','Failed to load details: ') + e.message);
    }
  };

  const ban = async () => {
    if (!selected) return;
    if (!window.confirm(t.tx(`封禁设备 ${shortId(selected, 12)}?`, `Ban device ${shortId(selected, 12)}?`))) return;
    try {
      await adminFetch(`/api/admin/devices/${selected}/ban`, {
        method: 'POST',
        body: JSON.stringify({ reason: reason || t.tx('人工封禁','Manual ban') }),
      });
      push(t.tx('已封禁','Banned'));
      open(selected);
    } catch (e) {
      push(t.tx('封禁失败: ','Ban failed: ') + e.message);
    }
  };
  const clear = async () => {
    if (!selected) return;
    try {
      await adminFetch(`/api/admin/devices/${selected}/clear`, { method: 'POST' });
      push(t.tx('已清除标记','Flag cleared'));
      open(selected);
    } catch (e) {
      push(t.tx('清除失败: ','Clear failed: ') + e.message);
    }
  };

  const devices = (devList && devList.devices) || [];

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 16 }}>
      <div className="card">
        <div className="card-h">
          <h3>{t.tx('设备列表','Device List')}</h3>
          <span className="meta">{devices.length} · {t.tx('last_seen 倒序','last_seen desc')}</span>
        </div>
        {!devList && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</div>}
        {devList && devices.length === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('无设备','No devices')}</div>}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 8, padding: 12 }}>
          {devices.map(d => (
            <div key={d.device_id} onClick={() => open(d.device_id)}
                 style={{
                   border: '1px solid ' + (selected === d.device_id ? 'var(--accent)' : 'var(--border)'),
                   borderLeft: `3px solid ${flagColor(d.flag)}`,
                   borderRadius: 7, padding: 10, cursor: 'pointer',
                   background: selected === d.device_id ? 'var(--bg-inset)' : 'var(--bg-card)',
                 }}>
              <div className="text-mono" style={{ fontSize: 11.5, fontWeight: 600 }}>{shortId(d.device_id, 16)}</div>
              <div className="text-muted text-mono" style={{ fontSize: 10.5, marginTop: 2 }}>{d.last_ip || '—'}</div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 6 }}>
                <span className="badge outline" style={{ fontSize: 10, color: flagColor(d.flag) }}>{d.flag || 'normal'}</span>
                <span className="text-faint" style={{ fontSize: 10, marginLeft: 'auto' }}>{relTime(d.last_seen_at, t)}</span>
              </div>
            </div>
          ))}
        </div>
      </div>

      <div className="card">
        <div className="card-h">
          <h3>{t.tx('设备详情','Device Details')}</h3>
          {selected && <span className="meta text-mono">{shortId(selected, 14)}</span>}
        </div>
        {!selected && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('点击左侧设备查看详情','Click a device on the left to view details')}</div>}
        {selected && !detail && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</div>}
        {detail && detail.device && (
          <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, fontSize: 12 }}>
              <div><span className="text-muted">flag</span> <span className="badge outline" style={{ color: flagColor(detail.device.flag) }}>{detail.device.flag || 'normal'}</span></div>
              <div><span className="text-muted">risk_score</span> <RiskDot score={detail.device.risk_score || 0} /></div>
              <div><span className="text-muted">first_ip</span> <span className="text-mono">{detail.device.first_ip || '—'}</span></div>
              <div><span className="text-muted">last_ip</span> <span className="text-mono">{detail.device.last_ip || '—'}</span></div>
              <div><span className="text-muted">first_seen</span> <span className="text-mono text-faint" style={{ fontSize: 11 }}>{relTime(detail.device.first_seen_at, t)}</span></div>
              <div><span className="text-muted">last_seen</span> <span className="text-mono text-faint" style={{ fontSize: 11 }}>{relTime(detail.device.last_seen_at, t)}</span></div>
            </div>
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>UA</div>
              <div className="text-mono text-faint" style={{ fontSize: 11, wordBreak: 'break-all' }}>{detail.device.user_agent || '—'}</div>
            </div>
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('关联账号','Linked Accounts')} ({(detail.accounts || []).length})</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
                {(detail.accounts || []).map(uid => (
                  <span key={uid} className="badge outline text-mono" style={{ fontSize: 10.5, cursor: 'pointer' }} onClick={() => onUser(uid)}>{uid}</span>
                ))}
                {(!detail.accounts || detail.accounts.length === 0) && <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('无','None')}</span>}
              </div>
            </div>
            {/* components TODO: 折叠展示，目前简单 JSON */}
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>components <span className="text-faint">(TODO {t.tx('折叠','collapse')})</span></div>
              <pre className="text-mono text-faint" style={{ fontSize: 10.5, background: 'var(--bg-inset)', padding: 8, borderRadius: 6, maxHeight: 110, overflow: 'auto', margin: 0 }}>
                {JSON.stringify(detail.device.components || {}, null, 2)}
              </pre>
            </div>
            <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
              <input className="input" placeholder={t.tx('封禁原因','Ban reason')} style={{ flex: 1 }} value={reason} onChange={(e) => setReason(e.target.value)} />
              <button className="btn btn-sm" onClick={clear}>{t.tx('清除标记','Clear Flag')}</button>
              <button className="btn btn-pri btn-sm" onClick={ban}>{t.tx('封禁','Ban')}</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ─── Tab 4: Events ──────────────────────────────────────────────────────────
function EventsTab({ initial, push, t }) {
  const [uid, setUid] = React.useState(initial || '');
  const [input, setInput] = React.useState(initial || '');
  const [kind, setKind] = React.useState('');
  const [days, setDays] = React.useState(7);
  const [events, setEvents] = React.useState(null);
  const [loading, setLoading] = React.useState(false);

  const load = React.useCallback(async () => {
    if (!uid) { setEvents(null); return; }
    setLoading(true);
    try {
      const qs = new URLSearchParams({ days: String(days), limit: '200' });
      if (kind) qs.set('kind', kind);
      const d = await adminFetch(`/api/admin/events/user/${encodeURIComponent(uid)}?${qs.toString()}`);
      setEvents(d.events || []);
    } catch (e) {
      push(t.tx('查询失败: ','Query failed: ') + (e.body && e.body.error || e.message));
      setEvents([]);
    } finally {
      setLoading(false);
    }
  }, [uid, kind, days, push]);

  React.useEffect(() => { load(); }, [load]);

  const submit = (e) => { e.preventDefault(); setUid(input.trim()); };

  return (
    <div className="card">
      <form onSubmit={submit} className="filter-bar">
        <input className="input" placeholder={t.tx('user_id, 如 U-882104','user_id, e.g. U-882104')} style={{ width: 240 }}
               value={input} onChange={(e) => setInput(e.target.value)} />
        <select className="input" style={{ width: 130 }} value={kind} onChange={(e) => setKind(e.target.value)}>
          <option value="">{t.tx('全部 kind','All kinds')}</option>
          <option value="login">login</option>
          <option value="bet">bet</option>
          <option value="deposit">deposit</option>
          <option value="withdraw">withdraw</option>
          <option value="bonus">bonus</option>
        </select>
        <select className="input" style={{ width: 100 }} value={days} onChange={(e) => setDays(Number(e.target.value))}>
          <option value={1}>{t.tx('1 天','1 day')}</option>
          <option value={7}>{t.tx('7 天','7 days')}</option>
          <option value={30}>{t.tx('30 天','30 days')}</option>
        </select>
        <button className="btn btn-pri btn-sm">{t.tx('查询','Query')}</button>
        <div style={{ flex: 1 }} />
        {events && <span className="text-faint" style={{ fontSize: 11 }}>{events.length} {t.tx('条 (TODO 分页)','(TODO pagination)')}</span>}
      </form>

      {!uid && <div style={{ padding: 32, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('输入 user_id 或从信号面板点击跳转','Enter user_id or jump from signals panel')}</div>}
      {uid && loading && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</div>}
      {uid && !loading && events && events.length === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('无事件','No events')}</div>}
      {uid && !loading && events && events.length > 0 && (
        <div style={{ padding: '8px 16px 16px' }}>
          <div style={{ borderLeft: '2px solid var(--border)', paddingLeft: 14, marginLeft: 60 }}>
            {events.map(ev => (
              <div key={ev.id} style={{ position: 'relative', padding: '8px 0' }}>
                <span style={{ position: 'absolute', left: -76, top: 10, fontSize: 11, color: 'var(--text-faint)', fontFamily: 'var(--font-mono)', width: 60, textAlign: 'right' }}>
                  {relTime(ev.ts, t)}
                </span>
                <span style={{ position: 'absolute', left: -19, top: 14, width: 8, height: 8, borderRadius: '50%', background: 'var(--accent)', border: '2px solid var(--bg-card)' }} />
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <span className="badge outline" style={{ fontSize: 10 }}>{ev.kind}</span>
                  {ev.ref && <span className="text-mono text-faint" style={{ fontSize: 11 }}>{ev.ref}</span>}
                  {ev.amount != null && ev.amount !== 0 && <span className="tabular" style={{ fontWeight: 500 }}>{fmtCN(ev.amount)}</span>}
                  {ev.ip && <span className="text-mono text-faint" style={{ fontSize: 10.5 }}>{ev.ip}</span>}
                  {ev.device_id && <span className="text-mono text-faint" style={{ fontSize: 10.5 }}>dev {shortId(ev.device_id, 10)}</span>}
                </div>
                {ev.data && Object.keys(ev.data).length > 0 && (
                  <div className="text-mono text-faint" style={{ fontSize: 10.5, marginTop: 2 }}>{JSON.stringify(ev.data)}</div>
                )}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Tab 6: Risk Scores (V2) ────────────────────────────────────────────────
function scoreColor(score) {
  const s = Number(score) || 0;
  if (s <= 30) return 'var(--success)';
  if (s <= 60) return 'var(--warning)';
  if (s <= 85) return 'oklch(0.68 0.18 50)'; // 橙
  return 'var(--danger)';
}
function bandChip(band, t) {
  const b = band || 'safe';
  const map = {
    safe:   { cls: 'info',    zh: 'safe',   en: 'safe' },
    watch:  { cls: 'info',    zh: 'watch',  en: 'watch' },
    limit:  { cls: 'warning', zh: 'limit',  en: 'limit' },
    freeze: { cls: 'danger',  zh: 'freeze', en: 'freeze' },
  };
  const m = map[b] || map.safe;
  return <span className={'badge ' + m.cls}>{t ? t.tx(m.zh, m.en) : m.zh}</span>;
}
function userDisplayName(uid) {
  // 没有 user 表查询，简单截一下作为名字
  if (!uid) return '';
  return uid.replace(/^U-/, '#');
}

function ManualActionModal({ user, onClose, onSubmit, t }) {
  const [action, setAction] = React.useState('limit_withdraw');
  const [reason, setReason] = React.useState('');
  const [ttl, setTtl] = React.useState(24);
  const [busy, setBusy] = React.useState(false);
  const submit = async (e) => {
    e.preventDefault();
    if (!reason.trim()) { return; }
    setBusy(true);
    try { await onSubmit({ action, reason: reason.trim(), expires_in_hours: ttl }); } finally { setBusy(false); }
  };
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
      <form onClick={e => e.stopPropagation()} onSubmit={submit} className="card" style={{ width: 440, padding: 0 }}>
        <div className="card-h">
          <h3>{t.tx('人工调整 · ','Manual Action · ') + (user && user.user_id)}</h3>
          {user && <span className="meta">{t.tx('当前分数 ','score ')}<span className="tabular" style={{ color: scoreColor(user.score), fontWeight: 600 }}>{user.score}</span></span>}
        </div>
        <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div>
            <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('措施','Action')}</div>
            <select className="input" value={action} onChange={e => setAction(e.target.value)} style={{ width: '100%' }}>
              <option value="limit_withdraw">limit_withdraw {t.tx('（限制提现）','(limit withdrawals)')}</option>
              <option value="freeze">freeze {t.tx('（冻结账户）','(freeze account)')}</option>
              <option value="force_kyc">force_kyc {t.tx('（强制 KYC）','(force KYC)')}</option>
              <option value="rate_limit">rate_limit {t.tx('（限流）','(rate limit)')}</option>
            </select>
          </div>
          <div>
            <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('到期','Expires in')}</div>
            <div style={{ display: 'flex', gap: 6 }}>
              {[[1,'1h'],[24,'24h'],[168,'7d'],[0,t.tx('永久','Forever')]].map(([h,l]) => (
                <span key={h} className={'chip' + (ttl === h ? ' active' : '')} onClick={() => setTtl(h)}>{l}</span>
              ))}
            </div>
          </div>
          <div>
            <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('原因','Reason')} <span className="text-faint">*</span></div>
            <input className="input" style={{ width: '100%' }} placeholder={t.tx('记录到审计日志','Recorded in audit log')} value={reason} onChange={e => setReason(e.target.value)} />
          </div>
          <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', marginTop: 4 }}>
            <button type="button" className="btn btn-sm" onClick={onClose}>{t.tx('取消','Cancel')}</button>
            <button type="submit" className="btn btn-pri btn-sm" disabled={busy || !reason.trim()}>{busy ? t.tx('提交中…','Submitting…') : t.tx('应用','Apply')}</button>
          </div>
        </div>
      </form>
    </div>
  );
}

function RiskScoresTab({ push, onUser, t }) {
  const [band, setBand] = React.useState('all');
  const [list, setList] = React.useState(null);
  const [expanded, setExpanded] = React.useState(null);
  const [profile, setProfile] = React.useState(null);
  const [acting, setActing] = React.useState(null);
  const [tick, setTick] = React.useState(0);

  const reload = React.useCallback(async () => {
    try {
      const qs = new URLSearchParams({ limit: '50' });
      if (band !== 'all') qs.set('band', band);
      const d = await adminFetch('/api/admin/risk/scores?' + qs.toString());
      setList(d.scores || []);
    } catch (e) {
      push(t.tx('评分列表加载失败: ','Failed to load scores: ') + (e.body && e.body.error || e.message));
      setList([]);
    }
  }, [band, push, t]);

  React.useEffect(() => { reload(); }, [reload, tick]);
  React.useEffect(() => {
    const id = setInterval(() => setTick(x => x + 1), 15000);
    return () => clearInterval(id);
  }, []);

  const openExpand = async (uid) => {
    if (expanded === uid) { setExpanded(null); setProfile(null); return; }
    setExpanded(uid);
    setProfile(null);
    try {
      const d = await adminFetch('/api/admin/risk/profile/' + encodeURIComponent(uid));
      setProfile(d);
    } catch (e) {
      push(t.tx('画像加载失败: ','Failed to load profile: ') + (e.body && e.body.error || e.message));
    }
  };

  const submitAction = async (payload) => {
    if (!acting) return;
    try {
      await adminFetch('/api/admin/risk/' + encodeURIComponent(acting.user_id) + '/manual-action', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
      push(t.tx('已应用 ','Applied ') + payload.action + ' → ' + acting.user_id);
      setActing(null);
      reload();
      if (expanded === acting.user_id) openExpand(acting.user_id); // refresh profile
    } catch (e) {
      push(t.tx('应用失败: ','Apply failed: ') + (e.body && e.body.error || e.message));
    }
  };

  const release = async (uid) => {
    if (!window.confirm(t.tx(`解除 ${uid} 的全部风控措施？`, `Release all risk actions for ${uid}?`))) return;
    try {
      await adminFetch('/api/admin/risk/' + encodeURIComponent(uid) + '/release', { method: 'POST' });
      push(t.tx('已解除 ','Released ') + uid);
      reload();
      if (expanded === uid) openExpand(uid);
    } catch (e) {
      push(t.tx('解除失败: ','Release failed: ') + (e.body && e.body.error || e.message));
    }
  };

  const scores = list || [];

  return (
    <div className="card">
      <div className="filter-bar">
        {[['all',t.tx('全部','All')],['safe','safe'],['watch','watch'],['limit','limit'],['freeze','freeze']].map(([k,l]) => (
          <span key={k} className={'chip' + (band === k ? ' active' : '')} onClick={() => setBand(k)}>{l}</span>
        ))}
        <div style={{ flex: 1 }} />
        <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('15s 自动刷新','15s auto-refresh')} · {scores.length}</span>
      </div>
      <div className="tbl-scroll">
      <table className="tbl tbl-stack">
        <thead>
          <tr>
            <th>{t.tx('用户','User')}</th>
            <th>{t.tx('分数','Score')}</th>
            <th>{t.tx('波段','Band')}</th>
            <th>{t.tx('主要原因','Top Reasons')}</th>
            <th>{t.tx('当前措施','Action')}</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {!list && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</td></tr>}
          {list && scores.length === 0 && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('暂无评分','No scores')}</td></tr>}
          {scores.map(s => {
            const isOpen = expanded === s.user_id;
            const reasons = s.reasons || [];
            const top3 = reasons.slice(0, 3);
            return (
              <React.Fragment key={s.user_id}>
                <tr onClick={() => openExpand(s.user_id)}
                    style={{ cursor: 'pointer', background: isOpen ? 'var(--bg-inset)' : 'transparent' }}>
                  <td data-label={t.tx('用户','User')}>
                    <div className="text-mono" style={{ fontSize: 12, fontWeight: 600 }}>{s.user_id}</div>
                    <div className="text-faint" style={{ fontSize: 10.5 }}>{userDisplayName(s.user_id)}</div>
                  </td>
                  <td data-label={t.tx('分数','Score')}>
                    <span className="tabular" style={{ fontSize: 16, fontWeight: 700, color: scoreColor(s.score) }}>{s.score}</span>
                  </td>
                  <td data-label={t.tx('波段','Band')}>{bandChip(s.band, t)}</td>
                  <td data-label={t.tx('主要原因','Top Reasons')} style={{ fontSize: 11 }}>
                    {top3.length === 0 && <span className="text-faint">—</span>}
                    {top3.map((r, i) => (
                      <span key={i} className="badge outline" style={{ fontSize: 10, marginRight: 4 }} title={'weight ' + r.weight}>{r.code}<span className="text-faint" style={{ marginLeft: 3 }}>+{r.weight}</span></span>
                    ))}
                    {reasons.length > 3 && <span className="text-faint" style={{ fontSize: 10 }}>+{reasons.length - 3}</span>}
                  </td>
                  <td data-label={t.tx('当前措施','Action')}>
                    {s.action_taken && s.action_taken !== 'none' && s.action_taken !== ''
                      ? <span className="badge outline text-mono" style={{ fontSize: 10 }}>{s.action_taken}</span>
                      : <span className="text-faint">—</span>}
                  </td>
                  <td data-label={t.tx('操作', 'Actions')} className="tbl-actions" onClick={e => e.stopPropagation()}>
                    <button className="btn btn-xs" onClick={() => setActing(s)}>{t.tx('人工调整','Adjust')}</button>
                    <button className="btn btn-xs btn-ghost" onClick={() => release(s.user_id)} style={{ color: 'var(--danger)' }}>{t.tx('解除','Release')}</button>
                    <button className="btn btn-xs btn-ghost" onClick={() => onUser(s.user_id)}>{t.tx('事件流','Events')}</button>
                  </td>
                </tr>
                {isOpen && (
                  <tr>
                    <td colSpan={6} style={{ background: 'var(--bg-inset)', padding: 14 }}>
                      {!profile && <div style={{ textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载画像…','Loading profile…')}</div>}
                      {profile && (
                        <ProfileBreakdown profile={profile} t={t} />
                      )}
                    </td>
                  </tr>
                )}
              </React.Fragment>
            );
          })}
        </tbody>
      </table>
      </div>
      {acting && <ManualActionModal user={acting} onClose={() => setActing(null)} onSubmit={submitAction} t={t} />}
    </div>
  );
}

function ProfileBreakdown({ profile, t }) {
  const p = profile.profile || {};
  const dims = [
    ['activity_score',  p.activity_score,  100, t.tx('活跃度','Activity')],
    ['spend_score',     p.spend_score,     100, t.tx('消费','Spend')],
    ['gambling_score',  p.gambling_score,  100, t.tx('博彩','Gambling')],
    ['social_score',    p.social_score,    100, t.tx('社交','Social')],
  ];
  const fpct = Math.min(100, (p.fp_count || 0) * 10);
  const ipct = Math.min(100, (p.ip_count || 0) * 10);
  const abusepct = Math.min(100, (p.abuse_signals || 0) * 20);
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
      <div>
        <div className="text-muted" style={{ fontSize: 11, marginBottom: 6 }}>{t.tx('维度分布','Dimension Scores')}</div>
        {dims.map(([key, val, max, label]) => {
          const v = Number(val) || 0;
          const pct = Math.max(0, Math.min(100, (v / max) * 100));
          return (
            <div key={key} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 50px', gap: 8, alignItems: 'center', fontSize: 12, padding: '3px 0' }}>
              <span className="text-muted">{label}</span>
              <span className="barmini"><i style={{ width: pct + '%', background: scoreColor(v) }} /></span>
              <span className="tabular text-muted" style={{ textAlign: 'right' }}>{v.toFixed(1)}</span>
            </div>
          );
        })}
        <div style={{ marginTop: 10 }}>
          <div className="text-muted" style={{ fontSize: 11, marginBottom: 6 }}>{t.tx('信号量','Signal Counts')}</div>
          {[
            ['fp_count',     p.fp_count || 0,     fpct,    t.tx('指纹数','FP count')],
            ['ip_count',     p.ip_count || 0,     ipct,    t.tx('IP 数','IP count')],
            ['abuse_signals',p.abuse_signals || 0,abusepct,t.tx('滥用信号','Abuse signals')],
          ].map(([key, val, pct, label]) => (
            <div key={key} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 50px', gap: 8, alignItems: 'center', fontSize: 12, padding: '3px 0' }}>
              <span className="text-muted">{label}</span>
              <span className="barmini"><i style={{ width: pct + '%', background: pct >= 70 ? 'var(--danger)' : pct >= 40 ? 'var(--warning)' : 'var(--accent)' }} /></span>
              <span className="tabular text-muted" style={{ textAlign: 'right' }}>{val}</span>
            </div>
          ))}
        </div>
      </div>
      <div>
        <div className="text-muted" style={{ fontSize: 11, marginBottom: 6 }}>{t.tx('全部原因','All Reasons')}</div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 10 }}>
          {((profile.score && profile.score.reasons) || []).map((r, i) => (
            <span key={i} className="badge outline" style={{ fontSize: 10 }}>{r.code}<span className="text-faint" style={{ marginLeft: 3 }}>+{r.weight}</span></span>
          ))}
          {(!profile.score || !profile.score.reasons || profile.score.reasons.length === 0) && <span className="text-faint" style={{ fontSize: 11 }}>—</span>}
        </div>
        <div className="text-muted" style={{ fontSize: 11, marginBottom: 6 }}>{t.tx('近期措施','Recent Actions')}</div>
        <div style={{ maxHeight: 120, overflow: 'auto', border: '1px solid var(--border)', borderRadius: 6 }}>
          {(profile.actions || []).length === 0 && <div className="text-faint" style={{ padding: 8, fontSize: 11, textAlign: 'center' }}>{t.tx('无','None')}</div>}
          {(profile.actions || []).map(a => (
            <div key={a.id} style={{ display: 'flex', gap: 6, padding: '4px 8px', borderBottom: '1px solid var(--border)', fontSize: 11, alignItems: 'center' }}>
              <span className="badge outline text-mono" style={{ fontSize: 10 }}>{a.action}</span>
              <span className="text-faint">@{a.score_at}</span>
              <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.reason || '—'}</span>
              <span className="text-faint text-mono">{a.auto ? 'auto' : (a.reviewer || 'manual')}</span>
            </div>
          ))}
        </div>
        <div style={{ marginTop: 8, fontSize: 11, display: 'flex', gap: 8 }}>
          <span className="text-muted">{t.tx('状态','State')}</span>
          {profile.is_frozen && <span className="badge danger">frozen</span>}
          {profile.is_limited && !profile.is_frozen && <span className="badge warning">limited</span>}
          {!profile.is_frozen && !profile.is_limited && <span className="text-faint">{t.tx('正常','normal')}</span>}
        </div>
      </div>
    </div>
  );
}

// ─── Tab 7: Device Fingerprints (V2) ────────────────────────────────────────
function FingerprintsTab({ push, onUser, t }) {
  const [list, setList] = React.useState(null);
  const [selected, setSelected] = React.useState(null);
  const [detail, setDetail] = React.useState(null);
  const [minUsers, setMinUsers] = React.useState(2);
  const [tick, setTick] = React.useState(0);

  const reload = React.useCallback(async () => {
    try {
      const qs = new URLSearchParams({ limit: '50', min_users: String(minUsers || 2) });
      const d = await adminFetch('/api/admin/fingerprints?' + qs.toString());
      setList(d.fingerprints || []);
    } catch (e) {
      push(t.tx('指纹列表加载失败: ','Failed to load fingerprints: ') + (e.body && e.body.error || e.message));
      setList([]);
    }
  }, [minUsers, push, t]);

  React.useEffect(() => { reload(); }, [reload, tick]);
  React.useEffect(() => {
    const id = setInterval(() => setTick(x => x + 1), 15000);
    return () => clearInterval(id);
  }, []);

  const openDetail = async (fp) => {
    setSelected(fp);
    setDetail(null);
    try {
      const d = await adminFetch('/api/admin/fingerprints/' + encodeURIComponent(fp));
      setDetail(d);
    } catch (e) {
      push(t.tx('指纹详情加载失败: ','Failed to load fingerprint detail: ') + (e.body && e.body.error || e.message));
    }
  };

  const fps = list || [];

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
      <div className="card">
        <div className="filter-bar">
          <span className="text-muted" style={{ fontSize: 11 }}>min_users</span>
          <input className="input" type="number" min={2} style={{ width: 70 }}
                 value={minUsers} onChange={(e) => setMinUsers(Math.max(2, Number(e.target.value) || 2))} />
          <div style={{ flex: 1 }} />
          <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('15s 自动刷新','15s auto-refresh')} · {fps.length}</span>
        </div>
        <div className="tbl-scroll">
        <table className="tbl tbl-stack">
          <thead>
            <tr>
              <th>fp_hash</th>
              <th>{t.tx('用户数','Users')}</th>
              <th>{t.tx('IP 数','IPs')}</th>
              <th>{t.tx('首见 / 最后见','First / Last')}</th>
              <th>{t.tx('UA 简略','UA')}</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {!list && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</td></tr>}
            {list && fps.length === 0 && <tr><td colSpan={6} style={{ textAlign: 'center', padding: 24, color: 'var(--text-muted)' }}>{t.tx('暂无多账号指纹','No multi-account fingerprints')}</td></tr>}
            {fps.map(f => (
              <tr key={f.fp_hash} onClick={() => openDetail(f.fp_hash)}
                  style={{ cursor: 'pointer', background: selected === f.fp_hash ? 'var(--bg-inset)' : 'transparent' }}>
                <td data-label="fp_hash" className="text-mono" title={f.fp_hash} style={{ fontSize: 11.5 }}>{shortId(f.fp_hash, 14)}</td>
                <td data-label={t.tx('用户数','Users')}>
                  <span className="tabular" style={{ fontSize: 13, fontWeight: 600, color: f.user_count >= 5 ? 'var(--danger)' : f.user_count >= 3 ? 'var(--warning)' : 'var(--accent)' }}>{f.user_count}</span>
                </td>
                <td data-label={t.tx('IP 数','IPs')} className="tabular text-muted" style={{ fontSize: 12 }}>{f.ip_count}</td>
                <td data-label={t.tx('首见 / 最后见','First / Last')} className="muted text-mono text-faint" style={{ fontSize: 10.5 }}>
                  <div>{relTime(f.first_seen, t)}</div>
                  <div>{relTime(f.last_seen, t)}</div>
                </td>
                <td data-label={t.tx('UA 简略','UA')} className="text-mono text-faint" style={{ fontSize: 10.5, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={f.ua}>{f.ua || '—'}</td>
                <td data-label={t.tx('操作', 'Actions')} className="tbl-actions" onClick={e => e.stopPropagation()}>
                  <button className="btn btn-xs btn-ghost" onClick={() => openDetail(f.fp_hash)}>{t.tx('详情','Detail')}</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        </div>
      </div>

      <div className="card">
        <div className="card-h">
          <h3>{t.tx('指纹详情','Fingerprint Details')}</h3>
          {selected && <span className="meta text-mono">{shortId(selected, 14)}</span>}
        </div>
        {!selected && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('点击左侧指纹查看详情','Click a fingerprint on the left to view details')}</div>}
        {selected && !detail && <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)' }}>{t.tx('加载中…','Loading…')}</div>}
        {detail && (
          <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, fontSize: 12 }}>
              <div><span className="text-muted">canvas_hash</span> <span className="text-mono text-faint" title={detail.canvas_hash}>{shortId(detail.canvas_hash, 14) || '—'}</span></div>
              <div><span className="text-muted">audio_hash</span> <span className="text-mono text-faint" title={detail.audio_hash}>{shortId(detail.audio_hash, 14) || '—'}</span></div>
              <div><span className="text-muted">font_hash</span> <span className="text-mono text-faint" title={detail.font_hash}>{shortId(detail.font_hash, 14) || '—'}</span></div>
              <div><span className="text-muted">screen_res</span> <span className="text-mono">{detail.screen_res || '—'}</span></div>
              <div><span className="text-muted">tz_offset</span> <span className="text-mono">{detail.tz_offset != null ? detail.tz_offset : '—'}</span></div>
              <div><span className="text-muted">lang</span> <span className="text-mono">{detail.lang || '—'}</span></div>
              <div><span className="text-muted">first_seen</span> <span className="text-mono text-faint" style={{ fontSize: 11 }}>{relTime(detail.first_seen, t)}</span></div>
              <div><span className="text-muted">last_seen</span> <span className="text-mono text-faint" style={{ fontSize: 11 }}>{relTime(detail.last_seen, t)}</span></div>
            </div>
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>UA</div>
              <div className="text-mono text-faint" style={{ fontSize: 11, wordBreak: 'break-all', background: 'var(--bg-inset)', padding: 6, borderRadius: 4 }}>{detail.ua || '—'}</div>
            </div>
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('用户列表','User List')} ({(detail.user_ids || []).length})</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
                {(detail.user_ids || []).map(uid => (
                  <span key={uid} className="badge outline text-mono" style={{ fontSize: 10.5, cursor: 'pointer' }} onClick={() => onUser(uid)}>{uid}</span>
                ))}
                {(!detail.user_ids || detail.user_ids.length === 0) && <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('无','None')}</span>}
              </div>
            </div>
            <div>
              <div className="text-muted" style={{ fontSize: 11, marginBottom: 4 }}>{t.tx('IP 列表','IP List')} ({(detail.ip_list || []).length})</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, maxHeight: 100, overflow: 'auto' }}>
                {(detail.ip_list || []).map(ip => (
                  <span key={ip} className="badge outline text-mono" style={{ fontSize: 10.5 }}>{ip}</span>
                ))}
                {(!detail.ip_list || detail.ip_list.length === 0) && <span className="text-faint" style={{ fontSize: 11 }}>{t.tx('无','None')}</span>}
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ─── 20 Analytics ────────────────────────────────────────────────────────────
function AnalyticsScreen({ t, push }) {
  const [stats] = useAdminPoll('/api/admin/events/stats', 10000, true);
  const [flow, setFlow] = React.useState(null);

  React.useEffect(() => {
    let stopped = false;
    const load = async () => {
      try {
        const d = await adminFetch('/api/finance/flow');
        if (!stopped) setFlow(d);
      } catch (e) {
        if (!stopped) setFlow({ error: e.message });
      }
    };
    load();
  }, []);

  const c24 = (stats && stats.counts_24h) || {};
  const c7d = (stats && stats.counts_7d) || {};
  const allKinds = Array.from(new Set([...Object.keys(c24), ...Object.keys(c7d)])).sort();
  const max24 = Math.max(1, ...Object.values(c24));
  const max7d = Math.max(1, ...Object.values(c7d));

  const totalDay = Object.values(c24).reduce((a, b) => a + b, 0);
  const totalWeek = Object.values(c7d).reduce((a, b) => a + b, 0);
  const anomalies = (stats && stats.anomalies) || [];

  return (
    <div className="stack">
      <div className="page-h">
        <div>
          <h1>{t.p_analytics}</h1>
          <div className="sub">{t.tx('事件统计 · 资金流 · 自定义报表 (TODO)','Event Stats · Cash Flow · Custom Reports (TODO)')}</div>
        </div>
        <div className="actions">
          <button className="btn btn-sm" onClick={() => push(t.tx('报表库 (TODO)','Report Library (TODO)'))}>{t.tx('报表库','Reports')}</button>
          <button className="btn btn-sm" onClick={() => push(t.tx('导出已发起 (需审批)','Export initiated (approval required)'))}><Icons.download /> {t.tx('导出','Export')}</button>
        </div>
      </div>

      <div className="row-4">
        <KPI label={t.tx('24h 事件总数','24h Total Events')} value={fmtNum(totalDay)} delta={`${allKinds.length} ${t.tx('类','kinds')}`} live={!!stats}
             spark={<MiniBars data={allKinds.map(k => c24[k] || 0)} w={100} h={24} color="oklch(0.55 0.18 264)" />} />
        <KPI label={t.tx('7d 事件总数','7d Total Events')} value={fmtNum(totalWeek)} delta={t.tx('按 kind 拆分','Split by kind')} live={!!stats}
             spark={<MiniBars data={allKinds.map(k => c7d[k] || 0)} w={100} h={24} color="oklch(0.6 0.16 145)" />} />
        <KPI label={t.tx('异常告警','Anomalies')} value={String(anomalies.length)} delta={anomalies.length ? t.tx('需关注','Attention') : t.tx('正常','Normal')} live={!!stats} />
        <KPI label={t.tx('资金流','Cash Flow')} value={flow && flow.summary ? fmtCN(flow.summary.net || 0) : '—'} delta={flow ? '24h net' : t.tx('加载中','Loading')} live={!!flow} />
      </div>

      <div className="card">
        <div className="card-h"><h3>{t.tx('事件类型分布','Event Kind Distribution')}</h3><span className="meta">{t.tx('24h vs 7d · 来自 /api/admin/events/stats','24h vs 7d · from /api/admin/events/stats')}</span></div>
        <div style={{ padding: 16 }}>
          {!stats && <div className="text-muted" style={{ padding: 16, textAlign: 'center' }}>{t.tx('加载中…','Loading…')}</div>}
          {stats && allKinds.length === 0 && <div className="text-muted" style={{ padding: 16, textAlign: 'center' }}>{t.tx('暂无事件数据','No event data')}</div>}
          {stats && allKinds.map(k => (
            <div key={k} style={{ display: 'grid', gridTemplateColumns: '80px 1fr 1fr', gap: 12, padding: '6px 0', fontSize: 12, alignItems: 'center' }}>
              <span style={{ fontWeight: 500 }}>{k}</span>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span className="text-faint" style={{ fontSize: 10, width: 24 }}>24h</span>
                <span className="barmini" style={{ flex: 1 }}><i style={{ width: `${(c24[k] || 0) / max24 * 100}%`, background: 'var(--accent)' }} /></span>
                <span className="tabular text-muted" style={{ width: 50, textAlign: 'right' }}>{fmtNum(c24[k] || 0)}</span>
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span className="text-faint" style={{ fontSize: 10, width: 24 }}>7d</span>
                <span className="barmini" style={{ flex: 1 }}><i style={{ width: `${(c7d[k] || 0) / max7d * 100}%`, background: 'oklch(0.6 0.16 145)' }} /></span>
                <span className="tabular text-muted" style={{ width: 50, textAlign: 'right' }}>{fmtNum(c7d[k] || 0)}</span>
              </div>
            </div>
          ))}
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
        <div className="card">
          <div className="card-h"><h3>{t.tx('资金流','Cash Flow')}</h3><span className="meta">/api/finance/flow</span></div>
          <div style={{ padding: 16, fontSize: 12 }}>
            {!flow && <div className="text-muted">{t.tx('加载中…','Loading…')}</div>}
            {flow && flow.error && <div className="text-muted">{t.tx('未启用或不可用: ','Disabled or unavailable: ')}{flow.error}</div>}
            {flow && !flow.error && (
              <pre className="text-mono text-faint" style={{ fontSize: 11, background: 'var(--bg-inset)', padding: 10, borderRadius: 6, maxHeight: 220, overflow: 'auto', margin: 0 }}>
                {JSON.stringify(flow, null, 2)}
              </pre>
            )}
          </div>
        </div>

        <div className="card">
          <div className="card-h"><h3>{t.tx('异常告警','Anomalies')}</h3><span className="meta">events.anomalies</span></div>
          <div style={{ padding: 12 }}>
            {anomalies.length === 0 && <div className="text-muted" style={{ padding: 12, textAlign: 'center' }}>{t.tx('暂无异常','No anomalies')}</div>}
            {anomalies.map((a, i) => (
              <div key={i} style={{ display: 'flex', gap: 8, padding: '8px 4px', borderBottom: '1px solid var(--border)', fontSize: 12 }}>
                <span className="badge danger">!</span>
                <span style={{ flex: 1 }}>{typeof a === 'string' ? a : JSON.stringify(a)}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { RiskScreen, AnalyticsScreen });
