// components.jsx — 공통 컴포넌트
// shared utilities + visual primitives

const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ─── helpers ──────────────────────────────────────────────── */

const cx = (...xs) => xs.filter(Boolean).join(' ');

const fmtKRW = (v) => {
  // "1,234,567" or short "1.2억", "8,400만"
  if (v == null) return '—';
  const abs = Math.abs(v);
  if (abs >= 1e8) return (v / 1e8).toFixed(2).replace(/\.?0+$/,'') + '억';
  if (abs >= 1e4) return (v / 1e4).toFixed(0) + '만';
  return v.toLocaleString('ko-KR');
};
const fmtKRWFull = (v) => v == null ? '—' : v.toLocaleString('ko-KR');
const fmtUSD = (v) => v == null ? '—' : v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const fmtPct = (v, digits = 2) => (v >= 0 ? '+' : '') + v.toFixed(digits) + '%';
const fmtSigned = (v) => (v >= 0 ? '+' : '') + v.toLocaleString('ko-KR');

const relTime = (d) => {
  // d: future or past Date; returns "3시간 14분 후" / "어제 18:30" etc.
  const now = new Date();
  const diff = (d - now) / 1000; // s
  const abs = Math.abs(diff);
  const suffix = diff >= 0 ? '후' : '전';
  if (abs < 60) return `${Math.floor(abs)}초 ${suffix}`;
  if (abs < 3600) {
    const m = Math.floor(abs / 60);
    return `${m}분 ${suffix}`;
  }
  if (abs < 86400) {
    const h = Math.floor(abs / 3600);
    const m = Math.floor((abs % 3600) / 60);
    return m > 0 ? `${h}시간 ${m}분 ${suffix}` : `${h}시간 ${suffix}`;
  }
  const days = Math.floor(abs / 86400);
  return `${days}일 ${suffix}`;
};

const fmtAbsTime = (d, tz = 'KST') => {
  // "2026-05-22 18:30 KST"
  const pad = (n) => String(n).padStart(2, '0');
  const Y = d.getFullYear(), M = pad(d.getMonth()+1), D = pad(d.getDate());
  const h = pad(d.getHours()), m = pad(d.getMinutes());
  return `${Y}-${M}-${D} ${h}:${m} ${tz}`;
};

const fmtTimeShort = (d) => {
  const pad = (n) => String(n).padStart(2, '0');
  return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const fmtTimeFull = (d) => {
  const pad = (n) => String(n).padStart(2, '0');
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
};

/* ─── Badges ──────────────────────────────────────────────── */

function ActionBadge({ action, size = 'md' }) {
  // BUY / SELL / HOLD / UNCERTAIN
  const meta = {
    BUY:       { label: '매수',   sym: '▲', color: 'var(--buy)',       soft: 'var(--buy-soft)' },
    SELL:      { label: '매도',   sym: '▼', color: 'var(--sell)',      soft: 'var(--sell-soft)' },
    HOLD:      { label: '관망',   sym: '◆', color: 'var(--hold)',      soft: 'var(--hold-soft)' },
    UNCERTAIN: { label: '검토',   sym: '⚠', color: 'var(--uncertain)', soft: 'var(--uncertain-soft)' },
  }[action] || {};
  const styleVars = {
    '--c': meta.color, '--cs': meta.soft,
  };
  const cls = size === 'sm' ? 'badge badge-sm' : size === 'lg' ? 'badge badge-lg' : 'badge';
  return (
    <span className={cls} style={styleVars}>
      <span className="badge-sym">{meta.sym}</span>
      <span className="badge-label">{action} <span style={{opacity:.7, marginLeft: 2}}>{meta.label}</span></span>
    </span>
  );
}

function MarketBadge({ market }) {
  const meta = {
    KR:     { label: 'KR',     c: 'var(--m-kr)' },
    NASDAQ: { label: 'NDAQ',   c: 'var(--m-us)' },
    NYSE:   { label: 'NYSE',   c: 'var(--m-us)' },
    AMEX:   { label: 'AMEX',   c: 'var(--m-us)' },
  }[market] || { label: market, c: 'var(--text-3)' };
  return <span className="market-badge mono" style={{'--c': meta.c}}>{meta.label}</span>;
}

function SeverityBadge({ level }) {
  const meta = {
    CRITICAL: { c: 'var(--critical)', soft: 'var(--critical-soft)' },
    ERROR:    { c: 'var(--buy)',      soft: 'var(--buy-soft)' },
    WARN:     { c: 'var(--warn)',     soft: 'var(--warn-soft)' },
    INFO:     { c: 'var(--info)',     soft: 'var(--info-soft)' },
  }[level] || {};
  return <span className="sev-badge mono" style={{'--c': meta.c, '--cs': meta.soft}}>{level}</span>;
}

/* ─── DeltaChip ───────────────────────────────────────────── */
function DeltaChip({ value, suffix = '%', size = 'md', showArrow = true }) {
  const positive = value >= 0;
  const c = positive ? 'var(--buy)' : 'var(--sell)';
  const cs = positive ? 'var(--buy-soft)' : 'var(--sell-soft)';
  const arrow = positive ? '▲' : '▼';
  return (
    <span className={cx('delta-chip', `delta-${size}`)} style={{'--c': c, '--cs': cs}}>
      {showArrow && <span className="delta-arrow">{arrow}</span>}
      <span className="mono tnum">{positive ? '+' : ''}{value.toFixed(2)}{suffix}</span>
    </span>
  );
}

/* ─── IconButton ──────────────────────────────────────────── */
function IconButton({ children, label, onClick, active }) {
  return (
    <button className={cx('icon-btn', active && 'active')} aria-label={label} onClick={onClick}>
      {children}
    </button>
  );
}

/* ─── Segmented (radio) ───────────────────────────────────── */
function Segmented({ value, onChange, options, size = 'md' }) {
  return (
    <div className={cx('seg', `seg-${size}`)}>
      {options.map(opt => {
        const v = typeof opt === 'string' ? opt : opt.value;
        const l = typeof opt === 'string' ? opt : opt.label;
        return (
          <button
            key={v}
            className={cx('seg-btn', v === value && 'active')}
            onClick={() => onChange(v)}
          >{l}</button>
        );
      })}
    </div>
  );
}

/* ─── Sparkline ───────────────────────────────────────────── */
function Sparkline({ data, w = 80, h = 24, color = 'var(--text-2)', fill, strokeWidth = 1.5, fullWidth }) {
  if (!data || data.length < 2) return null;
  const W = fullWidth ? 400 : w;
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * W;
    const y = h - ((v - min) / range) * h;
    return [x, y];
  });
  const d = pts.map((p, i) => (i === 0 ? `M${p[0].toFixed(2)},${p[1].toFixed(2)}` : `L${p[0].toFixed(2)},${p[1].toFixed(2)}`)).join(' ');
  const dFill = fill ? `${d} L${W},${h} L0,${h} Z` : null;
  const sty = fullWidth ? { display: 'block', width: '100%', height: h } : { display: 'block', overflow: 'visible' };
  const gradId = `sp-${Math.random().toString(36).slice(2, 8)}`;
  return (
    <svg width={fullWidth ? '100%' : W} height={h} viewBox={`0 0 ${W} ${h}`} preserveAspectRatio={fullWidth ? 'none' : 'xMidYMid meet'} style={sty}>
      {fill && (
        <defs>
          <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor={fill} stopOpacity="0.4" />
            <stop offset="100%" stopColor={fill} stopOpacity="0" />
          </linearGradient>
        </defs>
      )}
      {dFill && <path d={dFill} fill={`url(#${gradId})`} />}
      <path d={d} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" vectorEffect={fullWidth ? 'non-scaling-stroke' : undefined} />
    </svg>
  );
}

/* ─── GaugeRing ───────────────────────────────────────────── */
function GaugeRing({ pct, color = 'var(--accent)', size = 56, thick = 5, big = false, label }) {
  const r = (size - thick) / 2;
  const c = 2 * Math.PI * r;
  const clamped = Math.max(0, Math.min(100, pct));
  const offset = c * (1 - clamped / 100);
  return (
    <svg width={size} height={size} style={{ display: 'block' }}>
      <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="var(--bg-inset)" strokeWidth={thick} />
      <circle
        cx={size/2} cy={size/2} r={r} fill="none"
        stroke={color} strokeWidth={thick} strokeLinecap="round"
        strokeDasharray={c} strokeDashoffset={offset}
        transform={`rotate(-90 ${size/2} ${size/2})`}
        style={{ transition: 'stroke-dashoffset .6s var(--ease-out)' }}
      />
      {big ? (
        <>
          <text x={size/2} y={size/2} textAnchor="middle" dy="0.06em" fontSize={size*0.22} fontWeight="600" fill="var(--text)" style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>
            {Math.round(clamped)}%
          </text>
          {label && (
            <text x={size/2} y={size/2 + size*0.18} textAnchor="middle" fontSize={size*0.085} fill="var(--text-3)" letterSpacing="0.06em" style={{ fontFamily: 'var(--font-mono)' }}>
              {label}
            </text>
          )}
        </>
      ) : (
        <text x={size/2} y={size/2 + 3} textAnchor="middle" fontSize={size*0.26} fontWeight="700" fill="var(--text)" style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums' }}>
          {Math.round(clamped)}
        </text>
      )}
    </svg>
  );
}

/* ─── SentimentBadge ──────────────────────────────────────── */
function SentimentBadge({ sentiment }) {
  const meta = {
    POSITIVE:     { label: 'POS',    c: 'var(--buy)' },
    NEGATIVE:     { label: 'NEG',    c: 'var(--sell)' },
    NEUTRAL:      { label: 'NEU',    c: 'var(--text-3)' },
    NOT_DETECTED: { label: 'N/D',    c: 'var(--text-4)' },
    SHORT_NEWS:   { label: 'SHORT',  c: 'var(--info)' },
  }[sentiment] || { label: sentiment, c: 'var(--text-3)' };
  return (
    <span className="sent-badge mono" style={{ color: meta.c, borderColor: `color-mix(in oklch, ${meta.c} 30%, transparent)`, background: `color-mix(in oklch, ${meta.c} 10%, transparent)` }}>
      {meta.label}
    </span>
  );
}

function sentimentColor(s) {
  return {
    POSITIVE: 'var(--buy)', NEGATIVE: 'var(--sell)',
    NEUTRAL: 'var(--text-3)', NOT_DETECTED: 'var(--text-4)',
    SHORT_NEWS: 'var(--info)',
  }[s] || 'var(--text-3)';
}

/* ─── LineChart (with markers) ─────────────────────────────── */
function LineChart({
  data,            // [{t: Date, v: number, kr?: number, us?: number}]
  height = 200,
  showMarkers = true,
  markers = [],    // [{t, type:'BUY'|'SELL', symbol}]
  showKRUS = false,
  ariaLabel = '',
}) {
  const containerRef = useRef(null);
  const [w, setW] = useState(640);
  const [hover, setHover] = useState(null);

  useEffect(() => {
    if (!containerRef.current) return;
    const ro = new ResizeObserver((entries) => {
      const cw = entries[0].contentRect.width;
      setW(Math.max(200, cw));
    });
    ro.observe(containerRef.current);
    return () => ro.disconnect();
  }, []);

  if (!data || data.length === 0) return <div ref={containerRef} style={{height}} />;

  const padTop = 16, padBot = 24, padX = 8;
  const innerW = w - padX * 2;
  const innerH = height - padTop - padBot;

  const ts = data.map(d => +d.t);
  const tMin = ts[0], tMax = ts[ts.length - 1];
  const tRange = tMax - tMin || 1;

  const series = showKRUS
    ? [
        { key: 'kr', color: 'var(--m-kr)', label: 'KR', values: data.map(d => d.kr) },
        { key: 'us', color: 'var(--m-us)', label: 'US', values: data.map(d => d.us) },
      ]
    : [
        { key: 'v', color: 'var(--accent)', label: 'TOTAL', values: data.map(d => d.v) },
      ];

  const allVals = series.flatMap(s => s.values).filter(v => v != null);
  const vMin = Math.min(...allVals);
  const vMax = Math.max(...allVals);
  const vPad = (vMax - vMin) * 0.08 || 1;
  const yMin = vMin - vPad, yMax = vMax + vPad;
  const yRange = yMax - yMin || 1;

  const xAt = (t) => padX + ((+t - tMin) / tRange) * innerW;
  const yAt = (v) => padTop + (1 - (v - yMin) / yRange) * innerH;

  // 5 horizontal grid lines
  const gridLines = [0, 0.25, 0.5, 0.75, 1].map(p => {
    const v = yMin + p * yRange;
    return { y: padTop + (1 - p) * innerH, v };
  });

  // pointer move
  const onMove = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const tFrac = (x - padX) / innerW;
    const idx = Math.max(0, Math.min(data.length - 1, Math.round(tFrac * (data.length - 1))));
    setHover(idx);
  };
  const onLeave = () => setHover(null);

  return (
    <div ref={containerRef} className="chart-wrap" style={{ position: 'relative' }}>
      <svg width={w} height={height} viewBox={`0 0 ${w} ${height}`} role="img" aria-label={ariaLabel}
           onMouseMove={onMove} onMouseLeave={onLeave} onTouchMove={(e) => onMove(e.touches[0])} onTouchEnd={onLeave}>
        {/* gridlines */}
        {gridLines.map((g, i) => (
          <line key={i} x1={padX} y1={g.y} x2={w - padX} y2={g.y} stroke="var(--chart-grid)" strokeWidth="1" strokeDasharray={i === 4 ? '' : '2 4'} />
        ))}

        {/* series */}
        {series.map(s => {
          const pts = data.map((d, i) => {
            const v = s.values[i];
            if (v == null) return null;
            return [xAt(d.t), yAt(v)];
          }).filter(Boolean);
          const d = pts.map((p, i) => (i === 0 ? `M${p[0].toFixed(2)},${p[1].toFixed(2)}` : `L${p[0].toFixed(2)},${p[1].toFixed(2)}`)).join(' ');
          const dFill = `${d} L${pts[pts.length-1][0].toFixed(2)},${padTop + innerH} L${pts[0][0].toFixed(2)},${padTop + innerH} Z`;
          return (
            <g key={s.key}>
              <defs>
                <linearGradient id={`grad-${s.key}`} x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0%" stopColor={s.color} stopOpacity="0.22" />
                  <stop offset="100%" stopColor={s.color} stopOpacity="0" />
                </linearGradient>
              </defs>
              <path d={dFill} fill={`url(#grad-${s.key})`} />
              <path d={d} fill="none" stroke={s.color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
            </g>
          );
        })}

        {/* markers */}
        {showMarkers && markers.map((m, i) => {
          const x = xAt(+m.t);
          // place marker just above/below mid line
          const idx = data.findIndex(d => +d.t >= +m.t);
          if (idx < 0) return null;
          const v = data[idx].v ?? data[idx].kr ?? data[idx].us;
          if (v == null) return null;
          const y = yAt(v);
          const isBuy = m.type === 'BUY';
          const offset = isBuy ? -16 : 16;
          const c = isBuy ? 'var(--buy)' : 'var(--sell)';
          return (
            <g key={i} className="marker fade-up">
              <line x1={x} y1={y} x2={x} y2={y + (isBuy ? -10 : 10)} stroke={c} strokeWidth="1" opacity="0.5" />
              <circle cx={x} cy={y + offset} r="9" fill="var(--bg-elev-1)" stroke={c} strokeWidth="1.5" />
              <text x={x} y={y + offset + 3.5} textAnchor="middle" fontSize="9" fill={c} fontWeight="700" style={{fontFamily: 'var(--font-mono)'}}>
                {isBuy ? '▲' : '▼'}
              </text>
            </g>
          );
        })}

        {/* hover crosshair */}
        {hover != null && (
          <g>
            <line x1={xAt(data[hover].t)} y1={padTop} x2={xAt(data[hover].t)} y2={padTop + innerH} stroke="var(--text-3)" strokeWidth="1" strokeDasharray="3 3" opacity="0.7" />
            {series.map(s => {
              const v = s.values[hover];
              if (v == null) return null;
              return (
                <circle key={s.key} cx={xAt(data[hover].t)} cy={yAt(v)} r="5" fill="var(--bg)" stroke={s.color} strokeWidth="2" />
              );
            })}
          </g>
        )}
      </svg>

      {/* hover tooltip */}
      {hover != null && (
        <div className="chart-tip" style={{
          left: Math.min(w - 180, Math.max(0, xAt(data[hover].t) - 90)),
          top: 4,
        }}>
          <div className="chart-tip-date mono">{fmtAbsTime(new Date(data[hover].t))}</div>
          {series.map(s => (
            <div key={s.key} className="chart-tip-row">
              <span className="chart-tip-dot" style={{background: s.color}} />
              <span className="chart-tip-label">{s.label}</span>
              <span className="chart-tip-value mono tnum">{fmtKRWFull(Math.round(s.values[hover]))}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ─── PercentBar (for action distribution) ─────────────────── */
function ActionBar({ counts, total }) {
  // counts: { BUY, SELL, HOLD, UNCERTAIN }
  const t = total || Object.values(counts).reduce((a,b)=>a+b,0) || 1;
  const order = ['BUY', 'SELL', 'HOLD', 'UNCERTAIN'];
  const colors = { BUY: 'var(--buy)', SELL: 'var(--sell)', HOLD: 'var(--hold)', UNCERTAIN: 'var(--uncertain)' };
  return (
    <div className="action-bar">
      {order.map(k => {
        const v = counts[k] || 0;
        if (v === 0) return null;
        const pct = (v / t) * 100;
        return (
          <div key={k} className="action-bar-seg" style={{ flex: pct, background: colors[k] }} title={`${k} ${v}`} />
        );
      })}
    </div>
  );
}

/* ─── Toggle (theme) ───────────────────────────────────────── */
function ThemeToggle({ theme, onChange }) {
  return (
    <button className="theme-toggle" onClick={() => onChange(theme === 'dark' ? 'light' : 'dark')} aria-label="테마 전환">
      {theme === 'dark' ? (
        // moon
        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
        </svg>
      ) : (
        // sun
        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="4" />
          <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
        </svg>
      )}
    </button>
  );
}

/* ─── PaperBadge — globally visible "모의" mode chip ────────── */
function PaperBadge() {
  return <span className="paper-badge mono"><span className="pulse-dot" style={{display:'inline-block', width:6, height:6, borderRadius:99, background: 'var(--warn)', marginRight: 6, verticalAlign: 'middle'}} />PAPER MODE</span>;
}

/* ─── JSONViewer ───────────────────────────────────────────── */
function JSONViewer({ data, collapsed = false }) {
  const [open, setOpen] = useState(!collapsed);
  const str = useMemo(() => JSON.stringify(data, null, 2), [data]);
  const onCopy = (e) => {
    e.stopPropagation();
    navigator.clipboard?.writeText(str);
  };
  return (
    <div className="json-view">
      <div className="json-head">
        <button className="json-toggle mono" onClick={() => setOpen(o => !o)}>
          <span className="json-caret">{open ? '▾' : '▸'}</span> JSON
        </button>
        <button className="json-copy mono" onClick={onCopy} title="복사">copy</button>
      </div>
      {open && <pre className="json-body mono">{syntaxHighlight(str)}</pre>}
    </div>
  );
}

function syntaxHighlight(json) {
  // server-friendly highlight by wrapping spans
  const parts = [];
  const regex = /"(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(\.\d+)?([eE][+-]?\d+)?/g;
  let last = 0;
  let key = 0;
  json.replace(regex, (m, _g1, _g2, _g3, _g4, _g5, idx) => {
    if (idx > last) parts.push(<span key={key++}>{json.slice(last, idx)}</span>);
    let cls = 'jv-num';
    if (m.startsWith('"')) cls = m.endsWith(':') ? 'jv-key' : 'jv-str';
    else if (m === 'true' || m === 'false') cls = 'jv-bool';
    else if (m === 'null') cls = 'jv-null';
    parts.push(<span key={key++} className={cls}>{m}</span>);
    last = idx + m.length;
    return m;
  });
  if (last < json.length) parts.push(<span key={key++}>{json.slice(last)}</span>);
  return parts;
}

/* ─── Export ───────────────────────────────────────────────── */
Object.assign(window, {
  cx, fmtKRW, fmtKRWFull, fmtUSD, fmtPct, fmtSigned,
  relTime, fmtAbsTime, fmtTimeShort, fmtTimeFull,
  ActionBadge, MarketBadge, SeverityBadge, DeltaChip,
  IconButton, Segmented, Sparkline, LineChart, ActionBar,
  ThemeToggle, PaperBadge, JSONViewer,
  GaugeRing, SentimentBadge, sentimentColor,
});
