// Icons + formatters + chart primitives shared across screens.
// Heroicons-style 1.6 stroke; sized via currentColor.

const Icon = ({ d, size = 18, fill, stroke = 1.6, ...p }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill={fill || "none"}
    stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" {...p}>
    {typeof d === 'string' ? <path d={d} /> : d}
  </svg>
);

const IconDash    = (p) => <Icon {...p} d={<><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></>} />;
const IconBot     = (p) => <Icon {...p} d={<><rect x="4" y="7" width="16" height="12" rx="3"/><circle cx="9" cy="13" r="1.2" fill="currentColor"/><circle cx="15" cy="13" r="1.2" fill="currentColor"/><path d="M12 3v4M8 19v2M16 19v2"/></>} />;
const IconWallet  = (p) => <Icon {...p} d={<><rect x="3" y="6" width="18" height="13" rx="2.5"/><path d="M3 10h18M17 14h.5"/></>} />;
const IconClock   = (p) => <Icon {...p} d={<><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>} />;
const IconUser    = (p) => <Icon {...p} d={<><circle cx="12" cy="8" r="4"/><path d="M4 20c1.5-4 5-6 8-6s6.5 2 8 6"/></>} />;
const IconShield  = (p) => <Icon {...p} d={<><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z"/></>} />;
const IconBolt    = (p) => <Icon {...p} d="M13 2L4 14h7l-1 8 9-12h-7l1-8z" />;
const IconArrowUp = (p) => <Icon {...p} d="M7 14l5-5 5 5"/>;
const IconArrowDn = (p) => <Icon {...p} d="M7 10l5 5 5-5"/>;
const IconArrowRt = (p) => <Icon {...p} d="M5 12h14M13 6l6 6-6 6"/>;
const IconPlus    = (p) => <Icon {...p} d="M12 5v14M5 12h14"/>;
const IconMinus   = (p) => <Icon {...p} d="M5 12h14"/>;
const IconCheck   = (p) => <Icon {...p} d="M5 12l4 4 10-10"/>;
const IconX       = (p) => <Icon {...p} d="M6 6l12 12M18 6L6 18"/>;
const IconSearch  = (p) => <Icon {...p} d={<><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></>} />;
const IconChart   = (p) => <Icon {...p} d={<><path d="M4 19V5M4 19h16"/><path d="M8 15l3-4 3 2 5-7"/></>} />;
const IconSettings= (p) => <Icon {...p} d={<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></>} />;
const IconUpload  = (p) => <Icon {...p} d={<><path d="M12 16V4M7 9l5-5 5 5"/><path d="M4 20h16"/></>} />;
const IconID      = (p) => <Icon {...p} d={<><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="12" r="2"/><path d="M14 10h4M14 14h4M5 17c.7-1.4 2.2-2.3 4-2.3s3.3.9 4 2.3"/></>} />;
const IconQueue   = (p) => <Icon {...p} d={<><rect x="3" y="4" width="18" height="4" rx="1.5"/><rect x="3" y="10" width="18" height="4" rx="1.5"/><rect x="3" y="16" width="18" height="4" rx="1.5"/></>} />;
const IconPower   = (p) => <Icon {...p} d={<><path d="M12 3v9"/><path d="M7 6a8 8 0 1 0 10 0"/></>} />;
const IconKey     = (p) => <Icon {...p} d={<><circle cx="8" cy="15" r="4"/><path d="M11 12l8-8M16 7l3 3"/></>} />;
const IconExchange= (p) => <Icon {...p} d={<><path d="M3 8h14l-3-3M21 16H7l3 3"/></>} />;
const IconRadar   = (p) => <Icon {...p} d={<><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></>} />;
const IconGoogle  = ({ size = 18 }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
    <path fill="#4285F4" d="M22.5 12.3c0-.8-.1-1.6-.2-2.3H12v4.5h5.9a5 5 0 0 1-2.2 3.3v2.7h3.5c2-1.9 3.3-4.6 3.3-8.2z"/>
    <path fill="#34A853" d="M12 23c3 0 5.5-1 7.3-2.7l-3.5-2.7c-1 .7-2.3 1.1-3.8 1.1a6.6 6.6 0 0 1-6.2-4.5H2.2v2.8A11 11 0 0 0 12 23z"/>
    <path fill="#FBBC05" d="M5.8 14.2a6.6 6.6 0 0 1 0-4.2V7.2H2.2a11 11 0 0 0 0 9.8l3.6-2.8z"/>
    <path fill="#EA4335" d="M12 5.5c1.7 0 3.2.6 4.4 1.7l3.2-3.2A11 11 0 0 0 12 1 11 11 0 0 0 2.2 7.2l3.6 2.8A6.6 6.6 0 0 1 12 5.5z"/>
  </svg>
);

/* ---------- Formatting helpers (USD) ---------- */
const USD = (n, opts = {}) => {
  const { sign = false, decimals = 2 } = opts;
  const num = Number(n || 0);
  const s = (sign && num > 0 ? '+' : '') + '$' + Math.abs(num).toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  return num < 0 ? '-' + s.replace('-', '') : s;
};
const fmtNum = (n, d = 4) => Number(n || 0).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
const fmtPct = (n, d = 2) => (n > 0 ? '+' : '') + Number(n || 0).toFixed(d) + '%';
const fmtDate = (ts) => new Date(ts).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
const fmtDateTime = (ts) => new Date(ts).toLocaleString('en-GB', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: 'short' });

/* ---------- useSpring: critically-damped-ish spring ---------- */
function useSpring(initial, { stiffness = 220, damping = 26, mass = 1 } = {}) {
  const [val, setVal] = React.useState(initial);
  const ref = React.useRef({ v: initial, vel: 0, target: initial, raf: 0 });
  const set = React.useCallback((target) => {
    ref.current.target = target;
    if (ref.current.raf) return;
    const step = () => {
      const s = ref.current;
      const force = -stiffness * (s.v - s.target);
      const damp = -damping * s.vel;
      const accel = (force + damp) / mass;
      s.vel += accel * (1 / 60);
      s.v += s.vel * (1 / 60);
      if (Math.abs(s.vel) < 0.001 && Math.abs(s.v - s.target) < 0.001) {
        s.v = s.target; s.vel = 0; s.raf = 0;
        setVal(s.v); return;
      }
      setVal(s.v);
      s.raf = requestAnimationFrame(step);
    };
    ref.current.raf = requestAnimationFrame(step);
  }, [stiffness, damping, mass]);
  React.useEffect(() => () => cancelAnimationFrame(ref.current.raf), []);
  return [val, set];
}

/* ---------- MorphNumber: digit-by-digit spring animation ----------
   Each digit is a vertical strip 0-9; the target digit is revealed by
   translating the strip. New digits added on the left (e.g. 999 -> 1000)
   fade in from a tiny scale. Requires a numeric `size` so the strip
   geometry can be computed.                                         */
function MorphNumber({ value, decimals = 2, prefix = '', suffix = '', size = 18, weight = 'inherit', color = 'inherit' }) {
  if (value == null || !Number.isFinite(value)) value = 0;
  const negative = value < 0;
  const abs = Math.abs(value);
  const str = abs.toFixed(decimals);
  const [intPart, decPart] = str.split('.');
  const intWithSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  const chars = (negative ? '-' : '') + (decPart ? intWithSep + '.' + decPart : intWithSep);
  return (
    <span style={{ display: 'inline-flex', alignItems: 'baseline', fontFamily: 'var(--font-num)',
      fontVariantNumeric: 'tabular-nums', fontWeight: weight, color, fontSize: size, lineHeight: 1,
      letterSpacing: '-0.02em' }}>
      {prefix && <span style={{ marginRight: 2 }}>{prefix}</span>}
      {chars.split('').map((c, i) => {
        if (/[0-9]/.test(c)) return <MorphDigit key={`${chars.length}-${i}`} d={+c} size={size} />;
        const w = c === '.' || c === ',' ? size * 0.28 : size * 0.5;
        return <span key={`s-${i}`} style={{ display: 'inline-block', width: w, textAlign: 'center' }}>{c}</span>;
      })}
      {suffix && <span style={{ marginLeft: 4, opacity: 0.65, fontSize: size * 0.6 }}>{suffix}</span>}
    </span>
  );
}

function MorphDigit({ d, size }) {
  const [y, setY] = useSpring(-d * size);
  React.useEffect(() => { setY(-d * size); }, [d, size]);
  return (
    <span style={{ display: 'inline-block', height: size, width: size * 0.58, overflow: 'hidden',
      position: 'relative', textAlign: 'center' }}>
      <span style={{ display: 'block', transform: `translateY(${y}px)`, willChange: 'transform' }}>
        {[0,1,2,3,4,5,6,7,8,9].map(n => (
          <span key={n} style={{ display: 'block', height: size, lineHeight: `${size}px` }}>{n}</span>
        ))}
      </span>
    </span>
  );
}

/* ---------- Sparkline (with springy stroke-dasharray reveal) ---------- */
function Sparkline({ data, color = 'currentColor', fill = true, height = 64 }) {
  const w = 200, h = height;
  const pathRef = React.useRef(null);
  if (!data || !data.length) return null;
  const min = Math.min(...data), max = Math.max(...data), rng = max - min || 1;
  const pts = data.map((v, i) => [(i / Math.max(1, data.length - 1)) * w, h - ((v - min) / rng) * (h - 8) - 4]);
  const path = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
  const area = path + ` L${w},${h} L0,${h} Z`;
  const gradId = React.useMemo(() => 'g' + Math.random().toString(36).slice(2, 7), []);
  React.useEffect(() => {
    const p = pathRef.current; if (!p) return;
    const len = p.getTotalLength();
    p.style.strokeDasharray = len;
    p.style.strokeDashoffset = len;
    // Force layout so the transition applies cleanly when path changes.
    p.getBoundingClientRect();
    p.style.transition = 'stroke-dashoffset 700ms cubic-bezier(.2,.7,.3,1)';
    p.style.strokeDashoffset = 0;
  }, [path]);
  return (
    <svg className="spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      <defs>
        <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.25"/>
          <stop offset="60%" stopColor={color} stopOpacity="0.06"/>
          <stop offset="100%" stopColor={color} stopOpacity="0"/>
        </linearGradient>
      </defs>
      {fill && <path d={area} fill={`url(#${gradId})`} />}
      <path ref={pathRef} d={path} fill="none" stroke={color} strokeWidth="1.8" strokeLinejoin="round" strokeLinecap="round"/>
    </svg>
  );
}

/* ---------- LineChart: edge-to-edge, fill + line + hover crosshair ----------
   Minimal by design — no grid, no axis labels, no baseline. The prototype
   relies on the change pill above and the live value to communicate scale;
   chart's job is to show the SHAPE of the move. Hover crosshair surfaces the
   exact value on demand without cluttering the resting state.            */
function LineChart({ data, color = 'var(--accent)', height = 280, baseline = null, labels = null }) {
  const w = 800, h = height;
  const pathRef = React.useRef(null);
  const svgRef = React.useRef(null);
  const [hover, setHover] = React.useState(null);
  const gradId = React.useMemo(() => 'lc' + Math.random().toString(36).slice(2, 7), []);

  // Edge-to-edge: no horizontal padding, minimal vertical breathing room
  // for the stroke + endpoint dot. baseline + labels are kept in the API
  // for backward compat but no longer rendered.
  const padTop = 8, padBot = 8, padL = 0, padR = 0;
  const innerW = w - padL - padR;
  const innerH = h - padTop - padBot;
  const hasData = data && data.length > 0;

  const minV = hasData ? Math.min(...data) : 0;
  const maxV = hasData ? Math.max(...data) : 1;
  const rng = (maxV - minV) || 1;
  const xAt = i => padL + (i / Math.max(1, (data || [0]).length - 1)) * innerW;
  const yAt = v => padTop + innerH - ((v - minV) / rng) * innerH;
  const pts = hasData ? data.map((v, i) => [xAt(i), yAt(v)]) : [];
  const path = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
  const area = hasData ? path + ` L${pts[pts.length-1][0]},${h - padBot} L${pts[0][0]},${h - padBot} Z` : '';

  // Springy reveal — re-runs whenever the path string changes (e.g. on
  // bucket switch), so the new line draws itself in instead of cutting.
  React.useEffect(() => {
    const p = pathRef.current; if (!p || !path) return;
    const len = p.getTotalLength();
    p.style.strokeDasharray = len;
    p.style.strokeDashoffset = len;
    p.getBoundingClientRect();
    p.style.transition = 'stroke-dashoffset 900ms cubic-bezier(.2,.7,.3,1)';
    p.style.strokeDashoffset = 0;
  }, [path]);

  if (!hasData) return null;

  const onMove = (e) => {
    const r = svgRef.current.getBoundingClientRect();
    const x = ((e.clientX - r.left) / r.width) * w;
    const t = Math.max(0, Math.min(1, (x - padL) / innerW));
    setHover(Math.round(t * (data.length - 1)));
  };

  return (
    <svg ref={svgRef} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none"
         style={{ width: '100%', height, display: 'block', cursor: 'crosshair', overflow: 'visible' }}
         onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
      <defs>
        <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.22"/>
          <stop offset="100%" stopColor={color} stopOpacity="0"/>
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#${gradId})`}/>
      <path ref={pathRef} d={path} fill="none" stroke={color} strokeWidth="2"
            strokeLinejoin="round" strokeLinecap="round"/>
      <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="3.5" fill={color}/>
      {hover != null && pts[hover] && (
        <g style={{ pointerEvents: 'none' }}>
          <line x1={pts[hover][0]} x2={pts[hover][0]} y1={padTop} y2={h - padBot}
                stroke={color} strokeOpacity="0.35" strokeWidth="1"/>
          <circle cx={pts[hover][0]} cy={pts[hover][1]} r="5" fill={color} stroke="var(--bg)" strokeWidth="2"/>
          {(() => {
            const tipW = 88, tipH = 26;
            const tipX = Math.min(w - tipW - 2, Math.max(2, pts[hover][0] - tipW / 2));
            const tipY = Math.max(2, pts[hover][1] - tipH - 12);
            // Hard-coded near-black tooltip so the text reads on both light
            // and dark themes — using var(--ink) flips with the theme and
            // breaks contrast.
            return (
              <g transform={`translate(${tipX}, ${tipY})`}>
                <rect width={tipW} height={tipH} rx="6" fill="rgba(10,10,12,0.92)"/>
                <text x={tipW / 2} y="17" textAnchor="middle" fontSize="11" fill="white" fontFamily="var(--font-num)" fontWeight="600" letterSpacing="-0.01em">
                  {Number(data[hover]).toFixed(4)}
                </text>
              </g>
            );
          })()}
        </g>
      )}
    </svg>
  );
}

/* ---------- KPI card ---------- */
function KPI({ label, value, delta, sub, pos, neg }) {
  return (
    <div className="card kpi">
      <div className="kpi-label">{label}</div>
      <div className="kpi-value num">{value}</div>
      {delta && <div className={'kpi-delta ' + (neg ? 'neg' : pos ? 'pos' : '')}>{pos && <IconArrowUp size={12}/>}{neg && <IconArrowDn size={12}/>}{delta}</div>}
      {sub && <div className="tiny muted">{sub}</div>}
    </div>
  );
}

/* ---------- FeeRow utility ---------- */
function FeeRow({ label, v, hint }) {
  return (
    <div className="row" style={{ justifyContent: 'space-between', padding: '10px 0', borderBottom: '1px solid var(--line-2)' }}>
      <div>
        <div style={{ fontSize: 13.5, fontWeight: 500 }}>{label}</div>
        {hint && <div className="tiny muted">{hint}</div>}
      </div>
      <div className="num" style={{ fontWeight: 600 }}>{v}</div>
    </div>
  );
}

/* ---------- ToggleSwitch ---------- */
function ToggleSwitch({ on, onChange, disabled }) {
  return (
    <button onClick={() => !disabled && onChange(!on)} disabled={disabled}
      style={{
        width: 48, height: 28, borderRadius: 999,
        background: on ? 'var(--pos)' : 'var(--line)',
        border: 0, padding: 3, cursor: disabled ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.5 : 1,
        position: 'relative', transition: 'background .2s'
      }}>
      <div style={{
        width: 22, height: 22, borderRadius: '50%', background: 'white',
        boxShadow: '0 1px 3px rgba(0,0,0,.2)',
        transform: on ? 'translateX(20px)' : 'translateX(0)',
        transition: 'transform .2s'
      }}/>
    </button>
  );
}

/* ---------- useApi hook: load on mount + refetch helper ---------- */
function useApi(path, deps = []) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [reloadKey, setReloadKey] = React.useState(0);
  React.useEffect(() => {
    let alive = true;
    setLoading(true);
    setError(null);
    if (!path) { setLoading(false); return; }
    api.get(path)
      .then(d => { if (alive) { setData(d); setLoading(false); } })
      .catch(e => { if (alive) { setError(e); setLoading(false); } });
    return () => { alive = false; };
  }, [path, reloadKey, ...deps]);
  return [data, { loading, error, reload: () => setReloadKey(k => k + 1) }];
}

Object.assign(window, {
  Icon, IconDash, IconBot, IconWallet, IconClock, IconUser, IconShield, IconBolt,
  IconArrowUp, IconArrowDn, IconArrowRt, IconPlus, IconMinus, IconCheck, IconX,
  IconSearch, IconChart, IconSettings, IconUpload, IconID, IconQueue, IconPower,
  IconKey, IconExchange, IconRadar, IconGoogle,
  USD, fmtNum, fmtPct, fmtDate, fmtDateTime,
  Sparkline, LineChart, KPI, FeeRow, ToggleSwitch, useApi,
  useSpring, MorphNumber,
});
