// Reusable presentational components. Formatters come from boot.js (window).
const { useState, useEffect, useRef, useMemo } = React;

// ---- Identicon: deterministic block from address ----
function hashAddr(a) {
  let h = 0;
  for (let i = 0; i < (a || '').length; i++) h = (h * 31 + a.charCodeAt(i)) | 0;
  return h;
}
// Asset token chip: bundled PNG logo (src/public/icons, by base ticker)
// with a hashed colored-badge fallback when no logo exists. Shows just the
// base ticker (ETH, not ETH/USD) to save horizontal space.
function CoinChip({ symbol, base, size = 20, showText = true }) {
  const sym = symbol || base || '?';
  const tag = ((base || sym).replace(/\/.*$/, '') || '?').toUpperCase();
  const [err, setErr] = useState(false);
  const hue = Math.abs(hashAddr(sym)) % 360;
  return (
    <span className="coin-chip" title={sym}>
      {err ? (
        <span className="coin-dot" style={{
          width: size, height: size,
          background: `oklch(0.7 0.15 ${hue})`,
          color: `oklch(0.22 0.05 ${hue})`,
          fontSize: Math.max(8, size * 0.4),
        }}>{tag.slice(0, 4)}</span>
      ) : (
        <img className="coin-img" alt={tag}
          src={'/icons/' + tag.toLowerCase() + '.png'}
          style={{ width: size, height: size }}
          onError={() => setErr(true)} />
      )}
      {showText && <span className="coin-sym">{tag}</span>}
    </span>
  );
}

function Identicon({ addr, size = 26, className = '' }) {
  const cells = useMemo(() => {
    const h = hashAddr(addr || '');
    const hue1 = Math.abs(h) % 360;
    const hue2 = (hue1 + 60 + (Math.abs(h >> 8) % 80)) % 360;
    const cs = [];
    for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) cs.push((h >> (r * 4 + c)) & 1);
    return { cs, hue1, hue2 };
  }, [addr]);
  const px = size / 4;
  return (
    <div className={'identicon ' + className}
      style={{ width: size, height: size, borderRadius: size * 0.24 }}>
      {cells.cs.map((b, i) => (
        <span key={i} style={{
          width: px, height: px,
          background: b ? `oklch(0.75 0.17 ${cells.hue1})` : `oklch(0.5 0.13 ${cells.hue2})`,
        }} />
      ))}
    </div>
  );
}

// ---- AddressBlock ----
// Known reporter -> moniker only (no address noise).
// Plain wallet  -> full address.
//
// Naming/colour rules
// - If the address is `tellorvaloper...` OR the caller passes
//   as="validator", the moniker is resolved through window.VAL_MON
//   (byOper for valoper, byAcct for the derived tellor1) and the
//   name is tinted violet to mark it as a validator.
// - Otherwise (default = reporter context), the lookup goes
//   window.MON first and the name stays in the default text colour,
//   even when the underlying key is also registered as a validator.
function AddressBlock({ addr, moniker, to, navigate, onClick, as }) {
  if (!addr) return <span className="muted">—</span>;
  const isValoperAddr = addr.startsWith('tellorvaloper');
  const showAsValidator = isValoperAddr || as === 'validator';
  const valMap = window.VAL_MON || { byOper: {}, byAcct: {} };
  let name = moniker;
  if (!name) {
    if (showAsValidator) {
      name = (isValoperAddr ? valMap.byOper[addr] : valMap.byAcct[addr])
        || window.MON[addr] || null;
    } else {
      name = window.MON[addr] || null;
    }
  }
  const inner = (
    <span className="addr-text">
      {name
        ? <span className={'addr-name' + (showAsValidator ? ' addr-name-validator' : '')}>{name}</span>
        : <span className="addr-hash">{addr}</span>}
    </span>
  );
  if (to && navigate) {
    return <a className="addr-block" {...navLink(navigate, to)}>{inner}</a>;
  }
  if (onClick) return <button className="addr-block" type="button" onClick={onClick}>{inner}</button>;
  return <span className="addr-block">{inner}</span>;
}

// ---- Pills ----
function StatusPill({ status }) {
  const map = {
    active: { label: 'Active', cls: 'pill-ok' },
    jailed: { label: 'Jailed', cls: 'pill-bad' },
    locked: { label: 'Locked', cls: 'pill-warn' },
    idle:   { label: 'Not reported recently', cls: 'pill-idle' },
    inactive: { label: 'Inactive', cls: 'pill-idle' },
  };
  const s = map[status] || map.active;
  return <span className={'pill ' + s.cls}><i className="pill-dot" />{s.label}</span>;
}

const EV_META = {
  create_reporter: { label: 'Create', glyph: '+', tone: 'mint' },
  select_reporter: { label: 'Select Reporter', glyph: '→', tone: 'sky' },
  switch_reporter: { label: 'Switch Reporter', glyph: '⇄', tone: 'amber' },
  switch_in: { label: 'Reporter Switch In', glyph: '⇨', tone: 'mint' },
  switch_out: { label: 'Reporter Switch Out', glyph: '⇦', tone: 'red' },
  delegate: { label: 'Delegate', glyph: '↑', tone: 'mint' },
  undelegate: { label: 'Undelegate', glyph: '↓', tone: 'red' },
  redelegate: { label: 'Redelegate', glyph: '⇄', tone: 'violet' },
};
function TxBadge({ kind }) {
  const m = EV_META[kind] || { label: kind, glyph: '•', tone: 'mut' };
  return (
    <span className={'tx-badge tone-' + m.tone}>
      <i className="tx-glyph">{m.glyph}</i><span>{m.label}</span>
    </span>
  );
}

// ---- Sparkline ----
function Sparkline({ data, width = 120, height = 32, stroke = 'var(--accent)', fill = true }) {
  if (!data || data.length < 2) return <svg width={width} height={height} className="sparkline" />;
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const stepX = width / (data.length - 1);
  const pts = data.map((v, i) => [i * stepX, height - ((v - min) / range) * (height - 4) - 2]);
  const path = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
  const area = path + ` L${width} ${height} L0 ${height} Z`;
  const last = pts[pts.length - 1];
  const trend = data[data.length - 1] >= data[0];
  return (
    <svg viewBox={`0 0 ${width} ${height}`} width={width} height={height} className="sparkline">
      {fill && <path d={area} fill={trend ? 'url(#sparkUp)' : 'url(#sparkDown)'} opacity="0.7" />}
      <path d={path} fill="none" stroke={stroke} strokeWidth="1.4" strokeLinejoin="round" strokeLinecap="round" />
      <circle cx={last[0]} cy={last[1]} r="2" fill={stroke} />
    </svg>
  );
}

function ChartDefs() {
  return (
    <svg width="0" height="0" style={{ position: 'absolute' }} aria-hidden="true">
      <defs>
        <linearGradient id="sparkUp" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.35" />
          <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
        </linearGradient>
        <linearGradient id="sparkDown" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="var(--danger)" stopOpacity="0.25" />
          <stop offset="100%" stopColor="var(--danger)" stopOpacity="0" />
        </linearGradient>
        <linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.30" />
          <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
        </linearGradient>
      </defs>
    </svg>
  );
}

// ---- Area chart with crosshair tooltip ----
function AreaChart({ points, valueFn = (p) => p.value, fmtVal = fmt, label, height = 220 }) {
  const wrapRef = useRef(null);
  const [hover, setHover] = useState(null);
  const data = useMemo(() =>
    (points || [])
      .map((p) => ({ t: new Date(p.block_time).getTime(), v: Number(valueFn(p)) || 0 }))
      .filter((d) => d.t > 0)
      .sort((a, b) => a.t - b.t),
    [points, valueFn]);

  if (data.length < 2)
    return <div className="muted small" style={{ padding: 18 }}>Not enough history yet for {label} (need ≥2 snapshots).</div>;

  const W = 720, H = height, P = { t: 18, r: 24, b: 34, l: 58 };
  const xs = data.map((d) => d.t), ys = data.map((d) => d.v);
  const x0 = Math.min(...xs), x1 = Math.max(...xs);
  const y0 = Math.min(...ys), y1 = Math.max(...ys);
  const yPad = (y1 - y0) * 0.12 || 1;
  const yLo = Math.max(0, y0 - yPad), yHi = y1 + yPad;
  const sx = (t) => P.l + ((t - x0) / ((x1 - x0) || 1)) * (W - P.l - P.r);
  const sy = (v) => H - P.b - ((v - yLo) / ((yHi - yLo) || 1)) * (H - P.t - P.b);
  const path = data.map((d, i) => (i ? 'L' : 'M') + sx(d.t).toFixed(1) + ' ' + sy(d.v).toFixed(1)).join(' ');
  const area = path + ` L${sx(x1).toFixed(1)} ${H - P.b} L${sx(x0).toFixed(1)} ${H - P.b} Z`;
  const ticks = [yHi, (yHi + yLo) / 2, yLo];
  const onMove = (e) => {
    const r = wrapRef.current.getBoundingClientRect();
    const mx = ((e.clientX - r.left) / r.width) * W;
    let best = data[0], bd = Infinity;
    for (const d of data) { const dist = Math.abs(sx(d.t) - mx); if (dist < bd) { bd = dist; best = d; } }
    setHover({ x: sx(best.t), y: sy(best.v), t: best.t, v: best.v });
  };
  const fmtDay = (t) => new Date(t).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });

  return (
    <div className="chart" ref={wrapRef} onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
      <div className="chart-label">{label}</div>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" preserveAspectRatio="none" className="chart-svg">
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={P.l} x2={W - P.r} y1={sy(t)} y2={sy(t)} stroke="var(--grid)" strokeDasharray={i === 1 ? '2 4' : '0'} />
            <text x={P.l - 10} y={sy(t) + 4} textAnchor="end" className="chart-axis">{fmtVal(t)}</text>
          </g>
        ))}
        {[x0, (x0 + x1) / 2, x1].map((t, i) => (
          <text key={i} x={sx(t)} y={H - P.b + 18} textAnchor={i === 0 ? 'start' : i === 2 ? 'end' : 'middle'} className="chart-axis">{fmtDay(t)}</text>
        ))}
        <path d={area} fill="url(#areaFill)" />
        <path d={path} fill="none" stroke="var(--accent)" strokeWidth="1.6" strokeLinejoin="round" />
        {hover && (
          <g>
            <line x1={hover.x} x2={hover.x} y1={P.t} y2={H - P.b} stroke="var(--accent)" strokeDasharray="2 3" opacity="0.6" />
            <circle cx={hover.x} cy={hover.y} r="4" fill="var(--bg)" stroke="var(--accent)" strokeWidth="1.6" />
          </g>
        )}
      </svg>
      {hover && (() => {
        // Flip the tooltip below the point when the point sits near the
        // top of the chart (default position would clip outside the card).
        const flip = hover.y / H < 0.28;
        const xPct = hover.x / W * 100;
        const ty = flip ? '24%' : '-130%';
        const tx = xPct < 14 ? '0%' : xPct > 86 ? '-100%' : '-50%';
        return (
          <div className="chart-tip" style={{
            left: xPct + '%', top: (hover.y / H * 100) + '%',
            transform: `translate(${tx}, ${ty})`,
          }}>
            <div className="chart-tip-v">{fmtVal(hover.v)}</div>
            <div className="chart-tip-t muted small">
              {new Date(hover.t).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })}
            </div>
          </div>
        );
      })()}
    </div>
  );
}

function StatCard({ label, value, sub, spark, accent }) {
  return (
    <div className="stat-card">
      <div className="stat-label">{label}</div>
      <div className="stat-value">{value}</div>
      <div className="stat-bottom">
        {sub && <div className="stat-sub muted small">{sub}</div>}
        {spark && spark.length > 1 && <Sparkline data={spark} width={90} height={28} stroke={accent || 'var(--accent)'} />}
      </div>
    </div>
  );
}

// ---- Multi-select dropdown ----
function MultiSelect({ options, value, onChange, label }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState('');
  const ref = useRef(null);
  useEffect(() => {
    const close = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, []);
  const filt = options.filter((o) => o.label.toLowerCase().includes(q.toLowerCase()));
  const toggle = (k) => {
    const s = new Set(value);
    s.has(k) ? s.delete(k) : s.add(k);
    onChange([...s]);
  };
  return (
    <div className="dd" ref={ref}>
      <button className="dd-btn" type="button" onClick={() => setOpen((o) => !o)}>
        <span>{label}</span>
        {value.length > 0 && <span className="dd-count">{value.length}</span>}
        <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 4l3 3 3-3" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" /></svg>
      </button>
      {open && (
        <div className="dd-panel">
          <input className="dd-search" placeholder="Filter…" value={q} onChange={(e) => setQ(e.target.value)} autoFocus />
          <div className="dd-list">
            {filt.length === 0 && <div className="muted small" style={{ padding: 8 }}>No match</div>}
            {filt.map((o) => (
              <label key={o.key} className="dd-opt">
                <input type="checkbox" checked={value.includes(o.key)} onChange={() => toggle(o.key)} />
                <span>{o.label}</span>
              </label>
            ))}
          </div>
          <div className="dd-foot">
            <button onClick={() => onChange([])}>Clear</button>
            <button className="primary" onClick={() => setOpen(false)}>Apply</button>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, {
  hashAddr, Identicon, CoinChip, AddressBlock, StatusPill, TxBadge, Sparkline,
  ChartDefs, AreaChart, StatCard, MultiSelect, EV_META,
});
