// balance-viz.jsx — four balance visualizations:
//   01 Constellation · 06 Postcards · 07 Typographic · 08 Feed
// Plus shared utilities: Odometer, useTween, SettledConfetti.

// ─────────────────────────────────────────────────────────────
// SHARED · Odometer — digit-by-digit roll, Indian grouping
// ─────────────────────────────────────────────────────────────
function Odometer({ value, color = 'var(--ink)' }) {
  const v = Math.max(0, Math.round(value));
  const digits = String(v).split('');
  return (
    <span
      style={{
        display: 'inline-flex',
        alignItems: 'baseline',
        fontVariantNumeric: 'tabular-nums',
        color,
      }}
      aria-label={`₹${v.toLocaleString('en-IN')}`}
    >
      {formatINGroups(digits).map((ch, i) =>
        ch === ',' ? (
          <span
            key={`c${i}`}
            style={{
              opacity: 0.55,
              transform: 'translateY(-0.05em)',
              margin: '0 -0.04em',
              fontWeight: 500,
            }}
          >
            ,
          </span>
        ) : (
          <Digit key={`d${i}-${ch}`} digit={parseInt(ch, 10)} delay={i * 35} />
        )
      )}
    </span>
  );
}

function Digit({ digit, delay = 0 }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.style.transform = 'translateY(0%)';
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      el.style.transition = 'none';
      el.style.transform = `translateY(${-digit * 10}%)`;
      return;
    }
    const t = setTimeout(() => {
      el.style.transition = 'transform 780ms cubic-bezier(0.23, 1, 0.32, 1)';
      el.style.transform = `translateY(${-digit * 10}%)`;
    }, 40 + delay);
    return () => clearTimeout(t);
  }, [digit, delay]);

  return (
    <span
      style={{
        display: 'inline-block',
        height: '1em',
        overflow: 'hidden',
        verticalAlign: 'baseline',
        lineHeight: 1,
      }}
    >
      <span
        ref={ref}
        style={{
          display: 'block',
          lineHeight: 1,
          willChange: 'transform',
          transform: 'translateY(0%)',
        }}
      >
        {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((d) => (
          <span key={d} style={{ display: 'block', height: '1em', lineHeight: 1 }}>
            {d}
          </span>
        ))}
      </span>
    </span>
  );
}

function formatINGroups(digits) {
  const n = digits.join('');
  const formatted = Number(n).toLocaleString('en-IN');
  return Array.from(formatted);
}

// ─────────────────────────────────────────────────────────────
// SHARED · useTween — rAF spring tween
// ─────────────────────────────────────────────────────────────
function useTween(target, duration = 760) {
  const [v, setV] = React.useState(target);
  const fromRef = React.useRef(target);
  React.useEffect(() => {
    const reduce =
      typeof window !== 'undefined' &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      fromRef.current = target;
      setV(target);
      return;
    }
    const from = fromRef.current;
    const to = target;
    const t0 = performance.now();
    let raf;
    const step = (now) => {
      const k = Math.min(1, (now - t0) / duration);
      const eased = bezierY(k, 0.34, 1.56, 0.64, 1);
      const value = from + (to - from) * eased;
      setV(value);
      if (k < 1) raf = requestAnimationFrame(step);
      else fromRef.current = to;
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [target, duration]);
  return v;
}
function bezierY(t, p1x, p1y, p2x, p2y) {
  let s = t;
  for (let i = 0; i < 6; i++) {
    const x = 3 * (1 - s) * (1 - s) * s * p1x + 3 * (1 - s) * s * s * p2x + s * s * s;
    const dx = 3 * (1 - s) * (1 - s) * p1x + 6 * (1 - s) * s * (p2x - p1x) + 3 * s * s * (1 - p2x);
    if (Math.abs(dx) < 1e-6) break;
    s -= (x - t) / dx;
    s = Math.max(0, Math.min(1, s));
  }
  return 3 * (1 - s) * (1 - s) * s * p1y + 3 * (1 - s) * s * s * p2y + s * s * s;
}

// ─────────────────────────────────────────────────────────────
// 01 · CONSTELLATION — orbital nodes around "you"
// Size = amount, distance = recency, color = direction.
// No left/right split, no fulcrum metaphor. Reads as a map of your
// money-social-graph right now.
// ─────────────────────────────────────────────────────────────
function BalanceConstellation({ items, owed, owe, allSettled }) {
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setMounted(true), 220);
    return () => clearTimeout(t);
  }, []);

  const W = 340, H = 210;
  const cx = W / 2, cy = H / 2 + 4;

  // Map "4d", "8d", "2h" → days (rough)
  const ageDays = (age) => {
    if (!age) return 1;
    const n = parseFloat(age) || 1;
    if (/h$/.test(age)) return Math.max(0.2, n / 24);
    return n;
  };

  const amounts = items.map((i) => i.amount);
  const maxAmt = Math.max(1, ...amounts);
  const maxAge = Math.max(1, ...items.map((i) => ageDays(i.age)));

  // Light per-id jitter so the layout doesn't feel mechanical.
  const jitter = (id, span) => {
    let h = 0;
    for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0;
    return ((Math.abs(h) % 1000) / 1000 - 0.5) * span;
  };

  const n = Math.max(1, items.length);
  const nodes = items.map((it, i) => {
    const angle = (i / n) * Math.PI * 2 - Math.PI * 0.62 + jitter(it.id, 0.32);
    // recency → ring distance (recent = closer)
    const dist = 60 + (ageDays(it.age) / maxAge) * 42 + jitter(it.id + 'd', 8);
    // amount → radius (₹100 → 11, max → 22)
    const r = 11 + (it.amount / maxAmt) * 11;
    const x = cx + dist * Math.cos(angle) * 1.18; // ellipse stretch
    const y = cy + dist * Math.sin(angle) * 0.78;
    return { ...it, x, y, r, angle };
  });

  return (
    <div className="bv-const">
      <div className="bv-const-head">
        <span className="bv-const-eyebrow">balance · today</span>
        <span className="bv-const-meta">
          <span className="dot teal" /> ₹{owed.toLocaleString('en-IN')}
          <span className="sep">·</span>
          <span className="dot coral" /> ₹{owe.toLocaleString('en-IN')}
        </span>
      </div>

      <svg
        className="bv-const-svg"
        viewBox={`0 0 ${W} ${H}`}
        preserveAspectRatio="xMidYMid meet"
        aria-hidden="true"
      >
        {/* dashed orbit rings (decorative) */}
        <ellipse cx={cx} cy={cy} rx={74} ry={50} fill="none"
          stroke="var(--ink-mute)" strokeDasharray="2 5" strokeWidth="0.7" opacity="0.22" />
        <ellipse cx={cx} cy={cy} rx={116} ry={82} fill="none"
          stroke="var(--ink-mute)" strokeDasharray="2 5" strokeWidth="0.6" opacity="0.14" />

        {/* tethers */}
        {!allSettled && nodes.map((nd, i) => {
          const color = nd.side === 'teal' ? 'var(--teal)' : 'var(--coral)';
          return (
            <line
              key={`l-${nd.id}`}
              x1={cx} y1={cy} x2={nd.x} y2={nd.y}
              stroke={color}
              strokeWidth="0.8"
              strokeDasharray="2 4"
              opacity={mounted ? 0.22 : 0}
              style={{ transition: `opacity 360ms ease ${300 + i * 90}ms` }}
            />
          );
        })}

        {/* central "you" */}
        <g transform={`translate(${cx} ${cy})`}>
          {allSettled && (
            <circle r="28" fill="var(--butter)" opacity="0.18">
              <animate attributeName="r" values="22;30;22" dur="3.6s" repeatCount="indefinite" />
              <animate attributeName="opacity" values="0.10;0.28;0.10" dur="3.6s" repeatCount="indefinite" />
            </circle>
          )}
          <g transform="rotate(-6)">
            <rect x="-15" y="-15" width="30" height="30" rx="7" fill="var(--ink)" />
            <text
              x="0" y="0"
              textAnchor="middle"
              dominantBaseline="central"
              fontFamily="Bricolage Grotesque"
              fontWeight="600"
              fontSize="16"
              fill="var(--paper)"
              transform="rotate(6)"
            >A</text>
          </g>
          <text
            x="0" y="34"
            textAnchor="middle"
            fontFamily="JetBrains Mono"
            fontSize="8.5"
            letterSpacing="1.4"
            fill="var(--ink-mute)"
          >YOU</text>
        </g>

        {/* nodes */}
        {!allSettled && nodes.map((nd, i) => {
          const color = nd.side === 'teal' ? 'var(--teal)' : 'var(--coral)';
          const bobDelay = (Math.abs(nd.x + nd.y) % 1200);
          return (
            <g
              key={nd.id}
              style={{
                opacity: mounted ? 1 : 0,
                transform: mounted ? 'translate(0,0)' : `translate(${(cx - nd.x) * 0.4}px, ${(cy - nd.y) * 0.4}px)`,
                transition: `opacity 380ms cubic-bezier(.23,1,.32,1) ${260 + i * 110}ms, transform 540ms cubic-bezier(0.34, 1.56, 0.64, 1) ${260 + i * 110}ms`,
              }}
            >
              <g style={{
                animation: `constBob 5.2s ease-in-out ${bobDelay}ms infinite`,
                transformOrigin: `${nd.x}px ${nd.y}px`,
              }}>
                {/* paper-rim halo so node sits on cream nicely */}
                <circle cx={nd.x} cy={nd.y} r={nd.r + 2.4} fill="var(--paper)" opacity="0.9" />
                <circle cx={nd.x} cy={nd.y} r={nd.r} fill={color} />
                <text
                  x={nd.x} y={nd.y}
                  textAnchor="middle"
                  dominantBaseline="central"
                  fontFamily="Bricolage Grotesque"
                  fontWeight="600"
                  fontSize={nd.r * 0.95}
                  fill="var(--paper)"
                  style={{ letterSpacing: '-0.04em' }}
                >{nd.who[0]}</text>
                {/* amount label */}
                <text
                  x={nd.x}
                  y={nd.y + nd.r + 11}
                  textAnchor="middle"
                  fontFamily="JetBrains Mono"
                  fontWeight="500"
                  fontSize="8.5"
                  fill="var(--ink-soft)"
                  letterSpacing="0.4"
                >₹{nd.amount.toLocaleString('en-IN')}</text>
                {/* tiny age caption below amount */}
                <text
                  x={nd.x}
                  y={nd.y + nd.r + 21}
                  textAnchor="middle"
                  fontFamily="JetBrains Mono"
                  fontSize="7.5"
                  fill="var(--ink-mute)"
                  letterSpacing="1.2"
                  style={{ textTransform: 'uppercase' }}
                >{nd.age}</text>
              </g>
            </g>
          );
        })}
      </svg>

      {allSettled && (
        <div className="bv-const-empty">all settled · enjoy the chai</div>
      )}
      {!allSettled && (
        <div className="bv-const-footer">
          <span>{items.length} open</span>
          <span className="sep">·</span>
          <span className="muted">closer = newer · bigger = ₹ heavier</span>
        </div>
      )}

      <style>{`
        @keyframes constBob {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-2.4px); }
        }
        @media (prefers-reduced-motion: reduce) {
          @keyframes constBob { 0%,100% { transform: none; } }
        }
      `}</style>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// 06 · POSTCARDS — fanned stack, swipeable
// Each open balance is its own card. The stack IS the viz.
// ─────────────────────────────────────────────────────────────
function BalancePostcards({ items, owed, owe, allSettled }) {
  const [idx, setIdx] = React.useState(0);
  const [drag, setDrag] = React.useState(null);
  React.useEffect(() => { setIdx(0); }, [items.length]);

  // entrance
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setMounted(true), 120);
    return () => clearTimeout(t);
  }, []);

  if (allSettled) {
    return (
      <div className="bv-cards">
        <div className="bv-cards-eyebrow">your balances</div>
        <div className="bv-cards-stack bv-cards-stack-empty">
          <div className="bv-postcard bv-postcard-empty">
            <div className="bv-pc-stripe butter" />
            <div className="bv-pc-body">
              <div className="bv-pc-eyebrow">all settled</div>
              <div className="bv-pc-amt">₹0</div>
              <div className="bv-pc-name">nothing open · enjoy the chai</div>
            </div>
          </div>
        </div>
        <div className="bv-cards-meta">
          <span className="muted">no postcards in the pile</span>
        </div>
      </div>
    );
  }

  function onPointerDown(e) {
    if (e.target.closest('button')) return;
    setDrag({ startX: e.clientX, dx: 0 });
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
  }
  function onPointerMove(e) {
    if (!drag) return;
    setDrag({ ...drag, dx: e.clientX - drag.startX });
  }
  function onPointerUp() {
    if (!drag) return;
    if (Math.abs(drag.dx) > 44) {
      const dir = drag.dx > 0 ? -1 : 1;
      setIdx((i) => Math.max(0, Math.min(items.length - 1, i + dir)));
    }
    setDrag(null);
  }

  const cur = items[idx];

  return (
    <div className="bv-cards">
      <div className="bv-cards-eyebrow">
        <span>your balances</span>
        <span className="muted">
          {String(idx + 1).padStart(2, '0')} / {String(items.length).padStart(2, '0')}
        </span>
      </div>

      <div
        className="bv-cards-stack"
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
      >
        {items.map((it, i) => {
          const offset = i - idx;
          const live = mounted ? (drag && offset === 0 ? drag.dx : 0) : 0;
          // mount slide-in offset for fan reveal
          const entryX = mounted ? 0 : (offset >= 0 ? 24 : -24);
          const entryO = mounted ? 1 : 0;
          // visual transform based on offset
          const tx = offset * 18 + live + entryX;
          const ty = Math.abs(offset) * 8;
          const rot = offset * 4 + (offset === 0 ? live * 0.045 : 0);
          const scale = 1 - Math.min(Math.abs(offset), 3) * 0.04;
          const baseOp = Math.max(0, 1 - Math.abs(offset) * 0.26);
          const opacity = (Math.abs(offset) > 3 ? 0 : baseOp) * entryO;
          const z = items.length - Math.abs(offset);
          const owedSide = it.side === 'teal';
          return (
            <div
              key={it.id}
              className="bv-postcard"
              style={{
                transform: `translate(-50%, 0) translateX(${tx}px) translateY(${ty}px) rotate(${rot}deg) scale(${scale})`,
                opacity,
                zIndex: z,
                pointerEvents: offset === 0 ? 'auto' : 'none',
                transition: drag ? 'none' : 'transform 460ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 320ms ease',
              }}
            >
              <div className={`bv-pc-stripe ${owedSide ? 'teal' : 'coral'}`}>
                {it.glyph && <span className="bv-pc-glyph">{it.glyph}</span>}
              </div>
              <div className="bv-pc-body">
                <div className="bv-pc-eyebrow">
                  {owedSide ? "they owe you" : "you owe"}
                </div>
                <div className="bv-pc-amt">
                  <span className="cur">₹</span>{it.amount.toLocaleString('en-IN')}
                </div>
                <div className="bv-pc-name">
                  {it.who}
                  <span className="bv-pc-age">{it.age}</span>
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div className="bv-cards-meta">
        <div className="bv-cards-dots">
          {items.map((it, i) => (
            <button
              key={it.id}
              className={`bv-dot ${i === idx ? 'on' : ''} ${it.side}`}
              onClick={() => setIdx(i)}
              aria-label={`go to ${it.who}`}
            />
          ))}
        </div>
        <span className="muted bv-cards-hint">swipe ›</span>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// 07 · TYPOGRAPHIC — no chart, just type
// Dominant number towers; smaller number tucks under as meta; hairline; mono summary.
// ─────────────────────────────────────────────────────────────
function BalanceTypographic({ items, owed, owe, allSettled }) {
  if (allSettled) {
    return (
      <div className="bv-type">
        <div className="bv-type-eyebrow">balance · today</div>
        <div className="bv-type-big" style={{ color: 'var(--ink)' }}>
          <span className="cur">₹</span>0
        </div>
        <div className="bv-type-rule" />
        <div className="bv-type-summary">
          <span className="muted">all settled</span>
          <span className="muted">· 0 open</span>
        </div>
      </div>
    );
  }

  const owedBigger = owed >= owe;
  const big = owedBigger ? owed : owe;
  const small = owedBigger ? owe : owed;
  const bigLabel = owedBigger ? "you're owed" : "you owe";
  const smallLabel = owedBigger ? 'you owe' : "you're owed";
  const bigColor = owedBigger ? 'var(--teal)' : 'var(--ink)';
  const net = Math.abs(owed - owe);
  const open = items.length;
  const dirSign = owedBigger ? '+' : '−';

  return (
    <div className="bv-type">
      <div className="bv-type-eyebrow">{bigLabel}</div>
      <div className="bv-type-big" style={{ color: bigColor }}>
        <span className="cur">₹</span>
        <Odometer value={big} color={bigColor} />
      </div>
      <div className="bv-type-small">
        {smallLabel}{' '}
        <span className="amt">₹{small.toLocaleString('en-IN')}</span>
      </div>
      <div className="bv-type-rule" />
      <div className="bv-type-summary">
        <span>NET <span className={`net ${owedBigger ? 'teal' : 'coral'}`}>{dirSign} ₹{net.toLocaleString('en-IN')}</span></span>
        <span className="muted">· {open} OPEN</span>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// 08 · FEED — Since-you-last-opened
// 2–3 event chips up top, hairline, net + count at bottom.
// ─────────────────────────────────────────────────────────────
function BalanceFeed({ items, events = [], owed, owe, allSettled }) {
  const net = owed - owe;
  const open = items.length;

  return (
    <div className="bv-feed">
      <div className="bv-feed-eyebrow">since yesterday</div>

      <div className="bv-feed-list">
        {events.length === 0 ? (
          <div className="bv-feed-empty">
            <span className="bv-feed-dot ink">·</span>
            <span>nothing new. quiet day.</span>
          </div>
        ) : (
          events.map((ev, i) => (
            <FeedRow key={ev.id} ev={ev} i={i} />
          ))
        )}
      </div>

      <div className="bv-feed-rule" />

      <div className="bv-feed-bottom">
        {allSettled ? (
          <>
            <span className="bv-feed-net">
              <span className="bv-feed-net-eb">net</span>
              <span className="bv-feed-net-val">₹0</span>
            </span>
            <span className="bv-feed-count muted">all settled</span>
          </>
        ) : (
          <>
            <span className="bv-feed-net">
              <span className="bv-feed-net-eb">net</span>
              <span className={`bv-feed-net-val ${net >= 0 ? 'teal' : 'coral'}`}>
                {net >= 0 ? '+' : '−'} ₹{Math.abs(net).toLocaleString('en-IN')}
              </span>
            </span>
            <span className="bv-feed-count">{open} OPEN <span className="muted">›</span></span>
          </>
        )}
      </div>
    </div>
  );
}

function FeedRow({ ev, i }) {
  // entry animation
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setMounted(true), 140 + i * 90);
    return () => clearTimeout(t);
  }, [i]);

  const tone = ev.tone || 'ink';
  // text rendering: support inline amount/who emphasis
  return (
    <div
      className="bv-feed-row"
      style={{
        opacity: mounted ? 1 : 0,
        transform: mounted ? 'translateX(0)' : 'translateX(10px)',
        transition: 'opacity 320ms cubic-bezier(.23,1,.32,1), transform 380ms cubic-bezier(0.34, 1.56, 0.64, 1)',
      }}
    >
      <span className={`bv-feed-dot ${tone}`}>
        {ev.glyph ? (
          <span className="gly">{ev.glyph}</span>
        ) : (
          <span className="ini">{(ev.who || '·')[0]}</span>
        )}
      </span>
      <span className="bv-feed-text">
        {ev.text}
        {ev.amount != null && (
          <span className={`bv-feed-amt ${ev.dir === 'owe' ? 'coral' : 'teal'}`}>
            {ev.dir === 'owe' ? ' +' : ' '}₹{ev.amount.toLocaleString('en-IN')}
          </span>
        )}
      </span>
      <span className="bv-feed-ago">{ev.ago}</span>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// SettledConfetti — keeps existing behavior
// ─────────────────────────────────────────────────────────────
function SettledConfetti({ show }) {
  if (!show) return null;
  const bits = Array.from({ length: 14 }, (_, i) => i);
  return (
    <div
      style={{
        position: 'absolute',
        left: 0, right: 0, top: '50%',
        height: 0, pointerEvents: 'none', overflow: 'visible',
        zIndex: 3,
      }}
    >
      {bits.map((i) => {
        const angle = (i / bits.length) * Math.PI * 2;
        const dx = Math.cos(angle) * (60 + (i % 3) * 20);
        const dy = Math.sin(angle) * (28 + (i % 3) * 10);
        const colors = ['var(--butter)', 'var(--teal)', 'var(--coral-pale)'];
        const color = colors[i % colors.length];
        return (
          <span
            key={i}
            style={{
              position: 'absolute',
              left: '50%', top: 0,
              width: 6, height: 6,
              marginLeft: -3, marginTop: -3,
              background: color,
              borderRadius: i % 2 ? 999 : 2,
              opacity: 0,
              animation: `confetti 1100ms cubic-bezier(0.23,1,0.32,1) forwards`,
              animationDelay: `${i * 18}ms`,
              ['--dx']: `${dx}px`,
              ['--dy']: `${dy}px`,
            }}
          />
        );
      })}
      <style>{`
        @keyframes confetti {
          0%   { opacity: 0; transform: translate(0,0) scale(0.4); }
          15%  { opacity: 1; }
          100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(1) rotate(140deg); }
        }
      `}</style>
    </div>
  );
}

Object.assign(window, {
  Odometer,
  BalanceConstellation,
  BalancePostcards,
  BalanceTypographic,
  BalanceFeed,
  SettledConfetti,
});
