'use client'

import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useLocale } from 'next-intl'
import type {
  KolPostSummary, KolPostDetail, KolTicker,
  KolDigest, KolDigestTicker, KolHoldingChange, KolDivergence,
} from '@/types'
import { getKolPosts, getKolPost, getKolDigest, getKolChanges, getKolDivergence } from '@/lib/api'
import { swrFetch } from '@/lib/cache'
import PageHint from '@/components/ui/PageHint'
import Pagination from '@/components/ui/Pagination'

/**
 * KOL feed — independent module. Lists analyzed posts from tracked KOLs
 * (Substack today, Twitter later). Click a row to open the full post body
 * + structured ticker calls inline.
 */

const ACTION_COLOR: Record<KolTicker['action'], string> = {
  buy:      '#16a34a',
  bullish:  '#16a34a',
  sell:     '#dc2626',
  bearish:  '#dc2626',
  reduce:   '#f59e0b',
  mention:  '#9ca3af',
}

const WINDOW_OPTIONS = [1, 7, 30, 90] as const

function actionLabel(action: KolTicker['action'], isZh: boolean) {
  const labels: Record<KolTicker['action'], string> = {
    buy: 'BUY',
    sell: 'SELL',
    reduce: 'Reduce',
    bullish: 'Bullish',
    bearish: 'Bearish',
    mention: 'Mention',
  }
  return labels[action]
}

function windowLabel(days: number, isZh: boolean) {
  if (days === 1) return 'Today'
  return `Last ${days}d`
}

function changeLabel(changeType: KolHoldingChange['change_type'], isZh: boolean) {
  const labels: Record<KolHoldingChange['change_type'], string> = {
    new_position: 'New',
    increased: 'Added',
    decreased: 'Reduced',
    closed: 'Closed',
  }
  return labels[changeType]
}

function divergenceMeta(signalType: KolDivergence['signal_type'], isZh: boolean) {
  return signalType === 'divergence'
    ? {
        label: '⚠️ Divergence',
        color: '#f59e0b',
        bg: '#f59e0b22',
        tip: 'Public stance and wallet action disagree. Follow the wallet, not the words.',
      }
    : {
        label: '✅ Alignment',
        color: '#22c55e',
        bg: '#22c55e22',
        tip: 'Public stance and wallet action align, which increases confidence.',
      }
}

function directionLabel(direction: KolDivergence['direction'], isZh: boolean) {
  if (direction === 'long') return 'Long ▲'
  return 'Short ▼'
}

function postActionLabel(action: string, isZh: boolean) {
  const labels: Record<string, string> = {
    buy: 'BUY',
    sell: 'SELL',
    reduce: 'Reducing',
    bullish: 'Bullish post',
    bearish: 'Bearish post',
  }
  return labels[action] ?? action
}

function chainActionLabel(action: string, isZh: boolean) {
  const labels: Record<string, string> = {
    new_position: 'Opened on-chain',
    increased: 'Added on-chain',
    decreased: 'Reduced on-chain',
    closed: 'Closed on-chain',
  }
  return labels[action] ?? action
}

function sourceLabel(source: string, isZh: boolean) {
  if (source === 'substack') return 'Substack'
  if (source === 'blog') return 'Blog'
  if (source === 'podcast') return 'Podcast'
  if (source === 'twitter') return 'X'
  return source
}

function formatShortUsd(value: number) {
  if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`
  return `$${(value / 1e3).toFixed(0)}K`
}

function digestSideCopy(side: KolDigestTicker['side']) {
  if (side === 'long') return 'Bullish flow'
  if (side === 'short') return 'Bearish flow'
  return 'Mention flow'
}

function compactChangeCopy(change: KolHoldingChange) {
  const verb = change.change_type === 'new_position'
    ? 'opened'
    : change.change_type === 'increased'
      ? 'added'
      : change.change_type === 'decreased'
        ? 'reduced'
        : 'closed'
  const size = change.usd_after != null && change.usd_after > 0
    ? ` ${formatShortUsd(change.usd_after)}`
    : ''
  const pct = change.pct_change != null
    ? ` (${change.pct_change > 0 ? '+' : ''}${change.pct_change.toFixed(0)}%)`
    : ''
  return `@${change.handle} ${verb}${size}${pct}`
}

function DigestWidget({
  onTickerClick,
  activeTicker,
  isZh,
  initialDigest = null,
  days,
}: {
  onTickerClick: (ticker: string) => void
  activeTicker?: string | null
  isZh: boolean
  initialDigest?: KolDigest | null
  days: number
}) {
  const [data, setData]   = useState<KolDigest | null>(initialDigest)
  const [loading, setLoading] = useState(initialDigest === null)
  const [err, setErr]     = useState('')

  useEffect(() => {
    if (initialDigest === null || days !== (initialDigest?.window_days ?? 30)) {
      setLoading(true)
    }
    swrFetch(
      `kol-digest-${days}`,
      15 * 60_000,
      () => getKolDigest(days),
      fresh => setData(fresh),
    )
      .then(d => { setData(d); setErr('') })
      .catch(e => setErr(e instanceof Error ? e.message : ('Failed to load digest')))
      .finally(() => setLoading(false))
  }, [days, initialDigest, isZh])

  return (
    <div style={{ marginBottom: 10 }}>
      {/* Compact meta line — no header label needed, context is obvious */}
      {data && !loading && (
        <div style={{ fontSize: 12, color: 'var(--ink-4)', marginBottom: 8 }}>
          {data.post_count} posts · {data.ticker_count} assets
        </div>
      )}
      {loading && (
        <div style={{ padding: '8px 0', color: 'var(--ink-3)', fontSize: 12 }}>Loading…</div>
      )}
      {err && (
        <div style={{ padding: 12, color: '#dc2626', fontSize: 12 }}>{err}</div>
      )}
      {!loading && !err && data && data.tickers.length === 0 && (
        <div style={{ padding: '8px 0', color: 'var(--ink-3)', fontSize: 13 }}>
          No repeat mentions in this window.
        </div>
      )}
      {!loading && !err && data && data.tickers.length > 0 && (
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))',
          gap: 8,
        }}>
          {data.tickers.map(t => <DigestTickerChip
            key={t.ticker}
            t={t}
            isZh={isZh}
            active={activeTicker === t.ticker}
            hasActive={!!activeTicker}
            onClick={() => onTickerClick(t.ticker)}
          />)}
        </div>
      )}
    </div>
  )
}

function DigestTickerChip({
  t, onClick, isZh, active, hasActive,
}: { t: KolDigestTicker; onClick: () => void; isZh: boolean; active: boolean; hasActive: boolean }) {
  const sideColor = t.side === 'long' ? '#16a34a'
                  : t.side === 'short' ? '#dc2626' : '#9ca3af'
  const sideLabel = t.side === 'long'
    ? 'Long'
    : t.side === 'short'
      ? 'Short'
      : 'Mention'
  const actionLabel = t.dominant_action === 'buy'  ? 'BUY'
                    : t.dominant_action === 'sell' ? 'SELL'
                    : sideLabel
  return (
    <button
      onClick={onClick}
      title={`${t.kols.map(k => '@' + k).join(', ')} · ${t.post_count} signals`}
      style={{
        display: 'inline-flex', flexDirection: 'column',
        alignItems: 'flex-start', gap: 4,
        padding: '12px 14px', borderRadius: 12,
        background: active ? `${sideColor}12` : hasActive ? 'color-mix(in oklab, var(--bg-sunk) 92%, transparent)' : 'var(--bg-sunk)',
        border: active ? `2px solid ${sideColor}` : `1px solid ${sideColor}55`,
        boxShadow: active ? `0 10px 24px ${sideColor}22` : 'none',
        cursor: 'pointer', textAlign: 'left',
        minHeight: 108,
        width: '100%',
        transform: active ? 'translateY(-2px)' : 'none',
        opacity: hasActive && !active ? 0.6 : 1,
        transition: 'background 120ms, border-color 120ms, box-shadow 120ms, transform 120ms, opacity 120ms',
      }}
      aria-pressed={active}
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
        <strong style={{ fontSize: 18, color: 'var(--ink)' }}>
          {t.ticker}
        </strong>
        {active && (
          <span style={{
            width: 18,
            height: 18,
            borderRadius: 999,
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            background: sideColor,
            color: '#fff',
            fontSize: 11,
            fontWeight: 800,
          }}>
            ✓
          </span>
        )}
        <span style={{
          fontSize: 10, fontWeight: 700, color: sideColor,
          padding: '2px 6px', borderRadius: 999,
          background: `${sideColor}22`,
        }}>
          {actionLabel}
        </span>
      </div>
      {/* Compact: KOL count + handles in two lines, no "Bullish flow" copy
          (Long/Short label already conveys direction). */}
      <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>
        {t.kol_count} KOL{t.kol_count !== 1 ? 's' : ''} · {(t.max_conviction * 100).toFixed(0)}%
      </div>
      <div style={{
        fontSize: 11, color: 'var(--ink-3)',
        maxWidth: '100%', whiteSpace: 'nowrap',
        overflow: 'hidden', textOverflow: 'ellipsis',
      }}>
        {t.kols.map(k => '@' + k).join(', ')}
      </div>
      {active && (
        <div style={{ fontSize: 11, fontWeight: 700, color: sideColor, marginTop: 2 }}>
          Filtering the page by {t.ticker}
        </div>
      )}
    </button>
  )
}

const CHANGE_COLOR: Record<KolHoldingChange['change_type'], string> = {
  new_position: '#16a34a',
  increased:    '#22c55e',
  decreased:    '#f59e0b',
  closed:       '#dc2626',
}

function WalletCheckWidget({
  isZh,
  dateLocale,
  initialDigest = null,
  initialChanges = null,
  initialItems = null,
  tickerFilter = null,
  days,
}: {
  isZh: boolean
  dateLocale: string
  days: number
  initialDigest?: KolDigest | null
  initialChanges?: KolHoldingChange[] | null
  initialItems?: KolDivergence[] | null
  tickerFilter?: string | null
}) {
  const [digest, setDigest] = useState<KolDigest | null>(initialDigest)
  const [changes, setChanges] = useState<KolHoldingChange[]>(initialChanges ?? [])
  const [items, setItems] = useState<KolDivergence[]>(initialItems ?? [])
  const [loading, setLoading] = useState(
    initialDigest === null || initialChanges === null || initialItems === null,
  )
  // `days` now comes from the parent — no local state needed.

  // genRef lets in-flight fetches detect they were superseded by a newer
  // days/filter change before writing back to state.
  const walletCheckGenRef = useRef(0)

  useEffect(() => {
    const gen = ++walletCheckGenRef.current
    setLoading(true)
    Promise.all([
      swrFetch(
        `kol-digest-${days}`,
        15 * 60_000,
        () => getKolDigest(days),
        fresh => { if (gen === walletCheckGenRef.current) setDigest(fresh) },
      ),
      swrFetch(
        `kol-changes-${days}`,
        30 * 60_000,
        () => getKolChanges({ days }),
        fresh => { if (gen === walletCheckGenRef.current) setChanges(fresh.changes) },
      ),
      swrFetch(
        `kol-divergence-all-${days}`,
        30 * 60_000,
        () => getKolDivergence({ days }),
        fresh => { if (gen === walletCheckGenRef.current) setItems(fresh.items) },
      ),
    ])
      .then(([nextDigest, nextChanges, nextItems]) => {
        if (gen !== walletCheckGenRef.current) return
        setDigest(nextDigest)
        setChanges(nextChanges.changes)
        setItems(nextItems.items)
      })
      .catch(() => {
        if (gen !== walletCheckGenRef.current) return
        setDigest(null)
        setChanges([])
        setItems([])
      })
      .finally(() => { if (gen === walletCheckGenRef.current) setLoading(false) })
  }, [days])

  const rows = useMemo(() => {
    const sourceTickers = digest?.tickers ?? []
    const filteredTickers = tickerFilter
      ? sourceTickers.filter(t => t.ticker === tickerFilter)
      : sourceTickers

    return filteredTickers.map(t => {
      const relatedChanges = changes
        .filter(c => c.ticker === t.ticker)
        .sort((a, b) => +new Date(b.detected_at) - +new Date(a.detected_at))
      const relatedItems = items
        .filter(i => i.ticker === t.ticker)
        .sort((a, b) => +new Date(b.onchain_at) - +new Date(a.onchain_at))
      const divergenceCount = relatedItems.filter(i => i.signal_type === 'divergence').length
      const alignmentCount = relatedItems.filter(i => i.signal_type === 'alignment').length
      const latestChange = relatedChanges[0] ?? null
      const latestMatch = relatedItems[0] ?? null

      const verdict =
        divergenceCount > 0 && alignmentCount === 0
          ? {
              label: 'Mismatch',
              color: '#f59e0b',
              bg: '#f59e0b22',
              note: 'Wallet action disagrees with the public pitch.',
            }
          : alignmentCount > 0 && divergenceCount === 0
            ? {
                label: 'Aligned',
                color: '#22c55e',
                bg: '#22c55e22',
                note: 'Wallet action supports the public pitch.',
              }
            : alignmentCount > 0 && divergenceCount > 0
              ? {
                  label: 'Mixed',
                  color: '#a855f7',
                  bg: '#a855f722',
                  note: 'Different KOLs or time windows point in both directions.',
                }
              : latestChange
                ? {
                    label: 'Watch',
                    color: CHANGE_COLOR[latestChange.change_type],
                    bg: `${CHANGE_COLOR[latestChange.change_type]}22`,
                    note: 'Wallet movement exists, but no clean talk-vs-wallet match yet.',
                  }
                : {
                    label: 'No wallet proof',
                    color: '#9ca3af',
                    bg: '#9ca3af22',
                    note: 'No tracked wallet move linked to this asset in the selected window.',
                  }

      return {
        ticker: t.ticker,
        talkLine: `${t.kol_count} KOLs pushing ${t.side === 'short' ? 'bearish' : t.side === 'long' ? 'bullish' : 'mixed'} flow`,
        speakers: t.kols.map(k => '@' + k).join(', '),
        conviction: `${(t.max_conviction * 100).toFixed(0)}% max conviction`,
        verdict,
        divergenceCount,
        alignmentCount,
        latestChange,
        latestMatch,
      }
    })
  }, [changes, digest, items, tickerFilter])

  const totalMismatch = rows.filter(r => r.verdict.label === 'Mismatch').length
  const totalAligned = rows.filter(r => r.verdict.label === 'Aligned').length

  return (
    <div style={{
      marginBottom: 16, padding: 'clamp(12px, 3vw, 18px)',
      borderRadius: 12, background: 'var(--surface)',
      border: '1px solid var(--line)',
    }}>
      {/* Compact summary — no uppercase header, no redundant label */}
      {!loading && rows.length > 0 && (
        <div style={{ fontSize: 12, color: 'var(--ink-4)', marginBottom: 10 }}>
          {totalAligned > 0 && <span style={{ color: '#22c55e', marginRight: 8 }}>✓ {totalAligned} aligned</span>}
          {totalMismatch > 0 && <span style={{ color: '#f59e0b' }}>⚠ {totalMismatch} mismatch</span>}
          {totalAligned === 0 && totalMismatch === 0 && 'Wallet evidence pending'}
        </div>
      )}

      {!loading && rows.length === 0 && (
        <div style={{
          padding: '12px 16px', borderRadius: 8, background: 'var(--bg-sunk)',
          fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.6,
        }}>
          No wallet overlap for this window — try a wider range.
        </div>
      )}

      {!loading && rows.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {rows.map(row => (
            <div key={row.ticker} style={{
              display: 'grid',
              gridTemplateColumns: 'minmax(0, 1.4fr) minmax(0, 1.4fr) auto',
              gap: 12,
              alignItems: 'start',
              padding: '14px 16px',
              borderRadius: 12,
              background: 'var(--bg-sunk)',
              border: '1px solid var(--line)',
            }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }}>
                  <strong style={{ fontSize: 18 }}>{row.ticker}</strong>
                  <span style={{
                    fontSize: 10, fontWeight: 700, color: row.verdict.color,
                    padding: '3px 8px', borderRadius: 999, background: row.verdict.bg,
                  }}>
                    {row.verdict.label}
                  </span>
                </div>
                <div style={{ fontSize: 13, color: 'var(--ink)', fontWeight: 600, marginBottom: 4 }}>
                  {row.talkLine}
                </div>
                <div style={{ fontSize: 12, color: 'var(--ink-3)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {row.speakers}
                </div>
                <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>
                  {row.conviction}
                </div>
              </div>

              <div style={{ minWidth: 0 }}>
                {/* No "WALLET EVIDENCE" label — context is clear from layout */}
                <div style={{ fontSize: 13, color: 'var(--ink)', fontWeight: 600, marginBottom: 4 }}>
                  {row.latestMatch
                    ? `@${row.latestMatch.handle} ${chainActionLabel(row.latestMatch.onchain_action, isZh).toLowerCase()}`
                    : row.latestChange
                      ? compactChangeCopy(row.latestChange)
                      : 'No tracked move yet'}
                </div>
                <div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5 }}>
                  {/* Drop boilerplate "Wallet action supports/disagrees the public pitch." —
                      the verdict badge already says Aligned / Mismatch. Just show the size. */}
                  {row.latestMatch?.usd_after
                    ? `Size ${formatShortUsd(row.latestMatch.usd_after)}`
                    : row.verdict.label === 'No wallet proof'
                      ? 'No tracked wallet move in this window'
                      : ''}
                </div>
                {(row.divergenceCount > 0 || row.alignmentCount > 0) && (
                  <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 6 }}>
                    {row.alignmentCount > 0 && (
                      <span style={{ fontSize: 11, color: '#22c55e' }}>
                        {row.alignmentCount} alignment
                      </span>
                    )}
                    {row.divergenceCount > 0 && (
                      <span style={{ fontSize: 11, color: '#f59e0b' }}>
                        {row.divergenceCount} divergence
                      </span>
                    )}
                  </div>
                )}
              </div>

              <div style={{ fontSize: 11, color: 'var(--ink-3)', textAlign: 'right', whiteSpace: 'nowrap' }}>
                {row.latestMatch
                  ? new Date(row.latestMatch.onchain_at).toLocaleDateString(dateLocale)
                  : row.latestChange
                    ? new Date(row.latestChange.detected_at).toLocaleDateString(dateLocale)
                    : 'No date'}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

function TickerChips({ tickers, isZh }: { tickers: KolTicker[]; isZh: boolean }) {
  if (!tickers.length) {
    return <span style={{ color: 'var(--ink-3)', fontSize: 12 }}>—</span>
  }
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
      {tickers.map((t, i) => (
        <span
          key={`${t.ticker}-${i}`}
          title={t.quote}
          style={{
            display: 'inline-flex', alignItems: 'center', gap: 4,
            padding: '2px 8px', borderRadius: 999,
            fontSize: 11, fontWeight: 600,
            background: 'var(--bg-sunk)',
            border: `1px solid ${ACTION_COLOR[t.action]}33`,
            color: ACTION_COLOR[t.action],
          }}
        >
          {t.ticker}
          <span style={{ opacity: 0.7, fontWeight: 500 }}>
            {actionLabel(t.action, isZh)}
          </span>
          <span style={{ opacity: 0.55, fontWeight: 500 }}>
            {(t.conviction * 100).toFixed(0)}
          </span>
        </span>
      ))}
    </div>
  )
}

function PostDetail({
  post, onClose, isZh, dateLocale,
}: { post: KolPostDetail; onClose: () => void; isZh: boolean; dateLocale: string }) {
  // Lock body scroll while open
  useEffect(() => {
    const prev = document.body.style.overflow
    document.body.style.overflow = 'hidden'
    return () => { document.body.style.overflow = prev }
  }, [])

  const postSourceLabel = sourceLabel(post.source, isZh)

  const node = (
    <div
      onClick={onClose}
      style={{
        position: 'fixed', inset: 0,
        background: 'rgba(0,0,0,0.82)',
        backdropFilter: 'blur(4px)',
        WebkitBackdropFilter: 'blur(4px)',
        zIndex: 9999, display: 'flex', alignItems: 'flex-start',
        justifyContent: 'center',
        padding: 'clamp(8px, 4vw, 40px) clamp(8px, 3vw, 20px)',
        overflowY: 'auto',
      }}
    >
      <div
        onClick={e => e.stopPropagation()}
        style={{
          background: 'var(--bg)', borderRadius: 14,
          maxWidth: 820, width: '100%',
          padding: 'clamp(16px, 4vw, 28px)',
          border: '1px solid var(--line)',
          boxShadow: '0 24px 80px rgba(0,0,0,0.6)',
        }}
      >
        {/* ── Header: 标题 + 关闭 + 查看原帖 ── */}
        <div style={{
          display: 'flex', justifyContent: 'space-between',
          alignItems: 'flex-start', gap: 12, marginBottom: 12,
        }}>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 6 }}>
              @{post.kol_handle} · {postSourceLabel} · {new Date(post.published_at).toLocaleString(dateLocale)}
            </div>
            <h2 style={{
              margin: 0, fontSize: 'clamp(16px, 4.5vw, 22px)',
              lineHeight: 1.3, wordBreak: 'break-word',
            }}>
              {post.title || post.summary || (post.source === 'twitter' ? '(Tweet)' : '(Untitled)')}
            </h2>
          </div>

          {/* 右上角按钮组 */}
          <div style={{ display: 'flex', gap: 8, flexShrink: 0, alignItems: 'center' }}>
            {post.url && (
              <a
                href={post.url}
                target="_blank"
                rel="noopener noreferrer"
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 4,
                  padding: '5px 12px', borderRadius: 6,
                  background: 'var(--amber)', color: '#000',
                  fontSize: 12, fontWeight: 700, textDecoration: 'none',
                  whiteSpace: 'nowrap',
                }}
              >
                {`Open original ${postSourceLabel}`} ↗
              </a>
            )}
            <button
              onClick={onClose}
              aria-label="Close"
              style={{
                background: 'var(--bg-sunk)', border: '1px solid var(--line)',
                borderRadius: 6, padding: '5px 10px', cursor: 'pointer',
                color: 'var(--ink-2)', fontSize: 14, lineHeight: 1,
              }}
            >✕</button>
          </div>
        </div>

        {/* ── AI 摘要 ── */}
        {post.summary && (
          <div style={{
            margin: '12px 0', padding: 12, borderRadius: 8,
            background: 'var(--bg-sunk)', borderLeft: '3px solid var(--amber)',
            fontSize: 14, lineHeight: 1.6,
          }}>
            <div style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 4,
                          letterSpacing: 1, textTransform: 'uppercase' }}>
              {'AI summary'}
            </div>
            {post.summary}
          </div>
        )}

        {/* ── 喊单标的 ── */}
        {post.tickers.length > 0 && (
          <div style={{ margin: '12px 0' }}>
            <div style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 6,
                          letterSpacing: 1, textTransform: 'uppercase' }}>
              {'Extracted assets'}
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {post.tickers.map((t, i) => (
                <div key={i} style={{
                  padding: 10, borderRadius: 8,
                  background: 'var(--bg-sunk)',
                  borderLeft: `3px solid ${ACTION_COLOR[t.action]}`,
                }}>
                  <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 4 }}>
                    <strong style={{ fontSize: 14 }}>{t.ticker}</strong>
                    <span style={{ color: ACTION_COLOR[t.action], fontSize: 12, fontWeight: 600 }}>
                      {actionLabel(t.action, isZh)}
                    </span>
                    <span style={{ fontSize: 11, color: 'var(--ink-3)' }}>
                      {'Conviction'} {(t.conviction * 100).toFixed(0)}%
                    </span>
                  </div>
                  <div style={{ fontSize: 12, color: 'var(--ink-2)',
                                fontStyle: 'italic', lineHeight: 1.5 }}>
                    &ldquo;{t.quote}&rdquo;
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        {/* ── 原文 ── */}
        <div style={{
          marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--line)',
        }}>
          <div style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 8,
                        letterSpacing: 1, textTransform: 'uppercase' }}>
            {'Source excerpt'}
          </div>
          <div style={{
            whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.7,
            color: 'var(--ink-2)', maxHeight: '40vh', overflowY: 'auto',
            padding: 12, borderRadius: 8, background: 'var(--bg-sunk)',
            wordBreak: 'break-word',
          }}>
            {post.raw_text}
          </div>
        </div>
      </div>
    </div>
  )

  if (typeof document === 'undefined') return null
  return createPortal(node, document.body)
}

type SourceFilter = 'all' | 'substack' | 'blog' | 'podcast' | 'twitter'
const KOL_SERVER_PAGE_SIZE = 50

interface KolPageProps {
  initialPosts?: KolPostSummary[] | null
  initialTotal?: number
  initialDigest?: KolDigest | null
  initialChanges?: KolHoldingChange[] | null
  initialDivergence?: KolDivergence[] | null
}

export default function KolPage({
  initialPosts = null,
  initialTotal = 0,
  initialDigest = null,
  initialChanges = null,
  initialDivergence = null,
}: KolPageProps) {
  const dateLocale = 'en-US'
  const isZh = false  // i18n shelved — passed to helper fns that still take the param
  const [posts,       setPosts]       = useState<KolPostSummary[]>(initialPosts ?? [])
  const [serverTotal, setServerTotal] = useState(initialTotal)
  const [loading,     setLoading]     = useState(initialPosts === null)
  const [err,         setErr]         = useState('')
  const [openPost,    setOpenPost]    = useState<KolPostDetail | null>(null)
  const [sourceFilter,  setSourceFilter]  = useState<SourceFilter>('all')
  const [signalsOnly,   setSignalsOnly]   = useState(false)
  const [tickerFilter,  setTickerFilter]  = useState<string | null>(null)
  const [serverPage,    setServerPage]    = useState(1)
  // Unified time window — controls both DigestWidget and WalletCheckWidget.
  // Default 30d, not 7d: KOL ingestion is daily and sparse, so a 7d window is
  // frequently empty (e.g. 7d=0 while 30d=196) and the first paint showed
  // "No data yet" for modules that actually have data.
  const [kolDays, setKolDays] = useState(30)

  const postsGenRef = useRef(0)

  useEffect(() => {
    const gen = ++postsGenRef.current
    setLoading(true)
    const src = sourceFilter === 'all' ? undefined : sourceFilter
    const cacheKey = `kol-posts-${sourceFilter}-${signalsOnly}-${serverPage}-${tickerFilter ?? ''}-${kolDays}`
    swrFetch(
      cacheKey,
      15 * 60_000, // 15 min TTL — KOL feed is ingested daily
      () => getKolPosts({
        limit: KOL_SERVER_PAGE_SIZE, page: serverPage, source: src, signalsOnly,
        ticker: tickerFilter ?? undefined, days: kolDays,
      }),
      fresh => { if (gen === postsGenRef.current) { setPosts(fresh.items); setServerTotal(fresh.total ?? 0) } },
    )
      .then(r => {
        if (gen !== postsGenRef.current) return
        setPosts(r.items); setServerTotal(r.total ?? 0); setErr('')
      })
      .catch(e => { if (gen === postsGenRef.current) setErr(e instanceof Error ? e.message : ('Failed to load posts')) })
      .finally(() => { if (gen === postsGenRef.current) setLoading(false) })
  }, [sourceFilter, signalsOnly, serverPage, tickerFilter, kolDays])

  // posts are already filtered server-side; no client-side re-filter needed
  const filtered = posts

  const totalServerPages = Math.max(1, Math.ceil(serverTotal / KOL_SERVER_PAGE_SIZE))

  async function openDetail(id: number) {
    try {
      const detail = await getKolPost(id)
      setOpenPost(detail)
    } catch (e) {
      setErr(e instanceof Error ? e.message : ('Failed to load detail'))
    }
  }

  function changeSource(src: SourceFilter) {
    setSourceFilter(src)
    setServerPage(1)
    setTickerFilter(null)
  }

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1 className="page-title">{'KOL Signals'}</h1>
          <PageHint count={`${serverTotal} posts`}>
            Arthur Hayes, Delphi, Bankless, and 22 more — their public calls vs what their wallets actually do.
          </PageHint>
        </div>
        {/* Single time filter controlling both widgets */}
        <div className="nav-tabs" style={{ background: 'var(--bg-sunk)', alignSelf: 'flex-start' }}>
          {WINDOW_OPTIONS.map(d => (
            <button
              key={d}
              onClick={() => { setKolDays(d); setServerPage(1) }}
              className={`nav-tab ${kolDays === d ? 'active' : ''}`}
              style={{ border: 'none', cursor: 'pointer', fontSize: 12 }}
            >
              {windowLabel(d, isZh)}
            </button>
          ))}
        </div>
      </div>

      {/* Source filter — All / Substack / X + Signals only toggle */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}>
        {(['all', 'substack', 'blog', 'podcast', 'twitter'] as SourceFilter[]).map(src => {
          const label = src === 'all' ? 'All' : src === 'substack' ? 'Substack' : src === 'blog' ? 'Blog' : src === 'podcast' ? 'Podcast' : 'X (Twitter)'
          const active = sourceFilter === src
          return (
            <button
              key={src}
              onClick={() => changeSource(src)}
              style={{
                padding: '4px 12px', borderRadius: 6, fontSize: 12, fontWeight: 600,
                cursor: 'pointer', border: '1px solid var(--line)',
                background: active ? 'var(--amber)' : 'var(--surface)',
                color: active ? '#000' : 'var(--ink-2)',
                transition: 'background 0.15s, color 0.15s',
              }}
            >
              {label}
            </button>
          )
        })}
        <button
          onClick={() => { setSignalsOnly(v => !v); setServerPage(1) }}
          title="Exclude noise posts (short tweets, gm, RT). Long-form and directional posts are kept."
          style={{
            marginLeft: 8,
            padding: '4px 12px', borderRadius: 6, fontSize: 12, fontWeight: 600,
            cursor: 'pointer',
            border: `1px solid ${signalsOnly ? '#16a34a88' : 'var(--line)'}`,
            background: signalsOnly ? '#16a34a22' : 'var(--surface)',
            color: signalsOnly ? '#16a34a' : 'var(--ink-3)',
            transition: 'background 0.15s, color 0.15s, border-color 0.15s',
          }}
        >
          {signalsOnly ? '✓ Signals only' : 'Signals only'}
        </button>
      </div>

      <DigestWidget
        isZh={isZh}
        initialDigest={initialDigest}
        activeTicker={tickerFilter}
        days={kolDays}
        onTickerClick={(sym) => {
          setTickerFilter(prev => prev === sym ? null : sym)
          setServerPage(1)
        }}
      />

      <WalletCheckWidget
        isZh={isZh}
        dateLocale={dateLocale}
        initialDigest={initialDigest}
        initialChanges={initialChanges}
        initialItems={initialDivergence}
        tickerFilter={tickerFilter}
        days={kolDays}
      />

      {tickerFilter && (
        <div style={{
          display: 'flex', alignItems: 'center', gap: 8,
          marginBottom: 12, fontSize: 12,
        }}>
          <span style={{ color: 'var(--ink-3)' }}>{'Filtered asset:'}</span>
          <strong>{tickerFilter}</strong>
          <button
            onClick={() => { setTickerFilter(null); setServerPage(1) }}
            style={{
              background: 'var(--bg-sunk)', border: '1px solid var(--line)',
              borderRadius: 4, padding: '2px 8px',
              cursor: 'pointer', fontSize: 11, color: 'var(--ink-2)',
            }}
          >{'Clear ✕'}</button>
        </div>
      )}

      {/* KOL handle filter removed — 25 handles as a horizontal tab bar
          overflows on every screen size and most users filter by asset (ticker),
          not by person. Handle is visible on each post card. */}

      <div style={{ margin: '12px 0 4px', fontSize: 11, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
        All posts · paginated feed
      </div>

      {loading && <KolPostsSkeleton />}
      {err && <div style={{ padding: 20, color: '#dc2626' }}>{`Error: ${err}`}</div>}

      {!loading && !err && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {filtered.length === 0 && (
            <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-3)' }}>
              {'No data yet. Wait for the next feed ingestion run (daily 01:15 UTC).'}
            </div>
          )}
          {filtered.map(p => (
            <div
              key={p.id}
              onClick={() => openDetail(p.id)}
              style={{
                padding: 14, borderRadius: 10,
                background: 'var(--surface)',
                border: '1px solid var(--line)',
                cursor: 'pointer',
                transition: 'border-color 0.15s',
              }}
              onMouseEnter={e => (e.currentTarget.style.borderColor = 'var(--amber)')}
              onMouseLeave={e => (e.currentTarget.style.borderColor = 'var(--line)')}
            >
              {/* 顶部：作者 + 右侧操作区 */}
              <div style={{ display: 'flex', justifyContent: 'space-between',
                            alignItems: 'center', gap: 8, marginBottom: 6,
                            flexWrap: 'wrap' }}>
                <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
                  @{p.kol_handle} · {sourceLabel(p.source, isZh)} · {new Date(p.published_at).toLocaleDateString(dateLocale)}
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
                  {p.tier === 'trade_signal' && (
                    <span style={{
                      fontSize: 10, fontWeight: 700, letterSpacing: '0.04em',
                      padding: '2px 6px', borderRadius: 4,
                      background: '#16a34a22', color: '#16a34a',
                      border: '1px solid #16a34a44',
                      whiteSpace: 'nowrap',
                    }}>
                      SIGNAL
                    </span>
                  )}
                  {p.tier === 'directional' && (
                    <span style={{
                      fontSize: 10, fontWeight: 700, letterSpacing: '0.04em',
                      padding: '2px 6px', borderRadius: 4,
                      background: '#f59e0b22', color: '#f59e0b',
                      border: '1px solid #f59e0b44',
                      whiteSpace: 'nowrap',
                    }}>
                      VIEW
                    </span>
                  )}
                  {p.talks_vs_trades_flag && (
                    <span title="Public stance contradicts revealed position in this post"
                      style={{
                        fontSize: 10, fontWeight: 700,
                        padding: '2px 6px', borderRadius: 4,
                        background: '#ef444422', color: '#ef4444',
                        border: '1px solid #ef444444',
                        whiteSpace: 'nowrap',
                      }}>
                      ⚠ FLIP
                    </span>
                  )}
                  <span style={{ fontSize: 11, color: 'var(--ink-3)' }}>
                    {p.analyzed_at
                      ? ('✓ Analyzed')
                      : ('⏳ Pending')}
                  </span>
                  {p.url && (
                    <a
                      href={p.url}
                      target="_blank"
                      rel="noopener noreferrer"
                      onClick={e => e.stopPropagation()}
                      style={{
                        fontSize: 11, fontWeight: 600,
                        color: 'var(--amber)', textDecoration: 'none',
                        padding: '2px 8px', borderRadius: 4,
                        border: '1px solid color-mix(in srgb, var(--amber) 35%, transparent)',
                        whiteSpace: 'nowrap',
                      }}
                    >
                      {'Source ↗'}
                    </a>
                  )}
                </div>
              </div>
              <div style={{ fontSize: 16, fontWeight: 600, marginBottom: 6,
                            wordBreak: 'break-word' }}>
                {p.title || p.summary || (p.source === 'twitter' ? '(Tweet)' : '(Untitled)')}
              </div>
              {/* Only show summary as a separate line when there's a real title above it */}
              {p.title && p.summary && (
                <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.5,
                              marginBottom: 8 }}>
                  {p.summary}
                </div>
              )}
              <TickerChips tickers={p.tickers} isZh={isZh} />
            </div>
          ))}

          <Pagination
            page={serverPage}
            total={totalServerPages}
            count={serverTotal}
            pageSize={KOL_SERVER_PAGE_SIZE}
            onChange={setServerPage}
            scrollTop={false}
          />
        </div>
      )}

      {openPost && <PostDetail post={openPost} onClose={() => setOpenPost(null)} isZh={isZh} dateLocale={dateLocale} />}
    </div>
  )
}

function KolPostsSkeleton() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 8 }}>
      {Array.from({ length: 5 }).map((_, i) => (
        <div key={i} className="skeleton-card">
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <div className="skeleton sk-line-sm" style={{ width: 160 }} />
            <div className="skeleton sk-line-sm" style={{ width: 56 }} />
          </div>
          <div className="skeleton sk-title sk-w-3q" />
          <div className="skeleton sk-line sk-w-full" />
          <div className="skeleton sk-line" style={{ width: '85%' }} />
          <div style={{ display: 'flex', gap: 6, marginTop: 2 }}>
            <div className="skeleton sk-line-sm" style={{ width: 48 }} />
            <div className="skeleton sk-line-sm" style={{ width: 48 }} />
          </div>
        </div>
      ))}
    </div>
  )
}
