// scenes-7.jsx — Caching internals II: prefix hashing, invalidation, TTL/eviction

function Block({ x, y, w, label, sub, col, o = 1, dim = false }) {
  return (
    <div style={{
      position:'absolute', left:x, top:y, width:w, height:62, opacity:o,
      background: col + (dim ? '14' : '22'), border:`1.6px solid ${col}${dim?'66':''}`, borderRadius:11,
      display:'flex', flexDirection:'column', justifyContent:'center', paddingLeft:16, transition:'none',
    }}>
      <div style={{ fontFamily:FONTS.mono, fontSize:19, color: COLORS.ink, fontWeight:600 }}>{label}</div>
      {sub && <div style={{ fontFamily:FONTS.sans, fontSize:14, color: COLORS.inkDim }}>{sub}</div>}
    </div>
  );
}

function HashChip({ x, y, hash, state, o = 1 }) {
  // state: 'idle'|'hit'|'miss'|'bad'
  const col = state==='hit'?COLORS.green : state==='miss'?COLORS.blue : state==='bad'?COLORS.coral : COLORS.inkDim;
  if (o <= 0.01) return null;
  return (
    <div style={{
      position:'absolute', left:x, top:y, transform:'translate(-50%,-50%)', opacity:o,
      display:'flex', alignItems:'center', gap:7, padding:'5px 11px', borderRadius:999,
      background: col+'1f', border:`1.4px solid ${col}`, fontFamily:FONTS.mono, fontSize:16, color:col, whiteSpace:'nowrap',
      boxShadow: state==='hit'||state==='bad' ? `0 0 14px ${col}55` : 'none',
    }}>
      <span style={{ fontSize:13, opacity:0.7 }}>#</span>{hash}
      {state==='hit' && <span style={{ fontSize:13 }}>✓</span>}
      {state==='bad' && <span style={{ fontSize:13 }}>✕</span>}
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// SCENE — PREFIX MATCHING & INVALIDATION  (length 25s)
// ════════════════════════════════════════════════════════════════════════════
function ScenePrefixMatch() {
  const { localTime: lt } = useSprite();
  const setup = ramp(lt, 0.2, 1.0);

  const blocks = [
    { label:'system prompt', sub:'static',  hash:'a3f9' },
    { label:'tool defs',     sub:'static',  hash:'7b1c' },
    { label:'history 1–4',   sub:'settled', hash:'e602' },
    { label:'new message',   sub:'changes', hash:'5d8a' },
  ];
  const bw = 250, bx0 = 150, gap = 22, rowAy = 300, rowBy = 560;
  const bxOf = (i) => bx0 + i*(bw+gap);

  // ── PHASE 1 (0–9): hashing + longest-prefix match ──────────────────────────
  // Call A blocks appear & hash, then stored.
  const aAt = (i) => 1.2 + i*0.5;
  // Call B appears ~6, compares; blocks 0..2 hit, block3 miss.
  const bShown = lt > 6.0;
  const matchAt = (i) => 6.6 + i*0.5;        // when comparison resolves per block

  // ── PHASE 2 (10–17): causal invalidation cascade ───────────────────────────
  const invalidate = lt > 10.6;              // flip block 0
  const cascadeAt = (i) => 11.4 + i*0.5;     // chips turn red left→right

  // ── PHASE 3 (17.5–25): TTL / eviction ──────────────────────────────────────
  const showTTL = lt > 17.6;

  const aState = (i) => {
    if (invalidate) return i===0 ? 'bad' : (lt > cascadeAt(i) ? 'bad' : 'hit');
    if (!bShown) return 'idle';
    return 'hit';
  };
  const bState = (i) => {
    if (!bShown || lt < matchAt(i)) return 'idle';
    if (invalidate && lt > cascadeAt(i)) return 'bad';
    return i < 3 ? 'hit' : 'miss';
  };

  return (
    <>
      <Bg accent={COLORS.green} />
      <Eyebrow lt={lt} a={0.4} b={25} n="12" label="Prefix match · invalidation · TTL" color={COLORS.green} />

      {/* ROW A label */}
      <div style={{ position:'absolute', left: bx0, top: rowAy - 44, opacity:setup, fontFamily:FONTS.mono, fontSize:18, letterSpacing:'0.12em', color:COLORS.inkDim, whiteSpace:'nowrap' }}>
        CALL 1 — cached prefix
      </div>
      {blocks.map((b,i)=>(
        <React.Fragment key={'a'+i}>
          <Block x={bxOf(i)} y={rowAy} w={bw} label={b.label} sub={b.sub}
            col={aState(i)==='bad'?COLORS.coral:(i<3?COLORS.green:COLORS.blue)} o={ramp(lt, aAt(i)-0.2, aAt(i)+0.5)} />
          <HashChip x={bxOf(i)+bw/2} y={rowAy-2} hash={i===0&&invalidate?'c14e':b.hash} state={aState(i)} o={ramp(lt, aAt(i), aAt(i)+0.4)} />
        </React.Fragment>
      ))}

      {/* chain arrows between hashes (cumulative prefix) */}
      {blocks.slice(0,3).map((_,i)=>(
        <div key={'arr'+i} style={{ position:'absolute', left: bxOf(i)+bw-2, top: rowAy-2, transform:'translateY(-50%)',
          width: gap+4, height:2, background: (invalidate&&lt>cascadeAt(i+1))?COLORS.coral:COLORS.inkFaint,
          opacity: ramp(lt, aAt(i)+0.3, aAt(i)+0.7) }} />
      ))}

      {/* ROW B */}
      {bShown && (
        <>
          <div style={{ position:'absolute', left: bx0, top: rowBy - 44, opacity:ramp(lt,6.0,6.6), fontFamily:FONTS.mono, fontSize:18, letterSpacing:'0.12em', color:COLORS.inkDim, whiteSpace:'nowrap' }}>
            CALL 2 — same prefix, new tail
          </div>
          {blocks.map((b,i)=>(
            <React.Fragment key={'b'+i}>
              <Block x={bxOf(i)} y={rowBy} w={bw} label={b.label} sub={i<3?'reused':'recomputed'}
                col={bState(i)==='bad'?COLORS.coral:bState(i)==='hit'?COLORS.green:bState(i)==='miss'?COLORS.blue:COLORS.inkFaint}
                o={ramp(lt, 6.2+i*0.18, 6.9+i*0.18)} dim={bState(i)==='idle'} />
              <HashChip x={bxOf(i)+bw/2} y={rowBy+64} hash={b.hash} state={bState(i)} o={ramp(lt, matchAt(i), matchAt(i)+0.4)} />
            </React.Fragment>
          ))}
          {/* vertical match links A<->B */}
          {blocks.map((_,i)=>{
            const st = bState(i);
            if (st==='idle') return null;
            const col = st==='bad'?COLORS.coral:st==='hit'?COLORS.green:COLORS.blue;
            return <div key={'v'+i} style={{ position:'absolute', left: bxOf(i)+bw/2, top: rowAy+62, width:2, height: rowBy-rowAy-62,
              transform:'translateX(-50%)', background: col, opacity: 0.5*ramp(lt, matchAt(i), matchAt(i)+0.4) }} />;
          })}
        </>
      )}

      {/* "longest matching prefix" bracket under blocks 0-2 (phase1 only) */}
      {bShown && !invalidate && (() => {
        const o = pulse(lt, 8.4, 10.4, 0.4);
        if (o<=0.01) return null;
        const x1 = bxOf(0), x2 = bxOf(2)+bw;
        return (
          <div style={{ position:'absolute', left:x1, top: rowBy+96, width:x2-x1, opacity:o, textAlign:'center' }}>
            <div style={{ height:10, borderBottom:`3px solid ${COLORS.green}`, borderLeft:`3px solid ${COLORS.green}`, borderRight:`3px solid ${COLORS.green}`, borderRadius:'0 0 8px 8px' }} />
            <div style={{ marginTop:8, fontFamily:FONTS.sans, fontSize:21, fontWeight:600, color:COLORS.green }}>longest matching prefix → served from cache</div>
          </div>
        );
      })()}

      {/* invalidation cascade callout */}
      {invalidate && (
        <div style={{ position:'absolute', left: bxOf(0)+bw/2, top: rowAy-92, transform:'translate(-50%,0)', opacity:pulse(lt,10.8,17.2,0.4), textAlign:'center', width:300 }}>
          <div style={{ fontFamily:FONTS.sans, fontSize:19, fontWeight:600, color:COLORS.coral }}>one token changed</div>
          <div style={{ fontFamily:FONTS.sans, fontSize:15, color:COLORS.inkDim }}>→ hash changes → all downstream invalid</div>
        </div>
      )}

      {/* TTL / eviction panel */}
      {showTTL && (() => {
        const o = pulse(lt, 17.8, 25, 0.45);
        const evict = clamp(ramp(lt, 21.0, 23.2), 0, 1);   // entry decays
        const cx = 1640, cy = 470;
        const remain = 1 - evict;
        return (
          <div style={{ opacity:o }}>
            <CacheStore x={cx} y={cy} scale={0.92} fill={0.2 + 0.6*remain} glow={0.5*remain} label="cache entry" />
            {/* TTL ring */}
            <svg width="120" height="120" viewBox="0 0 120 120" style={{ position:'absolute', left:cx-60, top:cy-150 }}>
              <circle cx="60" cy="60" r="50" fill="none" stroke={COLORS.inkFaint+'55'} strokeWidth="8" />
              <circle cx="60" cy="60" r="50" fill="none" stroke={evict>0.95?COLORS.coral:COLORS.green} strokeWidth="8"
                strokeDasharray={2*Math.PI*50} strokeDashoffset={2*Math.PI*50*evict} strokeLinecap="round" transform="rotate(-90 60 60)" />
              <text x="60" y="68" textAnchor="middle" fontFamily={FONTS.mono} fontSize="20" fill={COLORS.ink}>
                {evict>0.95?'evicted':`${Math.ceil((1-evict)*5)}m`}
              </text>
            </svg>
            <div style={{ position:'absolute', left:cx, top:cy+150, transform:'translate(-50%,0)', textAlign:'center', width:380 }}>
              <div style={{ fontFamily:FONTS.sans, fontSize:20, color:COLORS.inkDim, lineHeight:1.4 }}>
                Entries live a short <b style={{color:COLORS.green}}>TTL</b> (~minutes), then LRU-evict.
              </div>
            </div>
          </div>
        );
      })()}

      <Caption lt={lt} a={1.2} b={4.0}>
        The cache key is a <b>hash of the token prefix</b>, built up block by block.
      </Caption>
      <Caption lt={lt} a={4.2} b={6.0} color={COLORS.inkDim} sub>
        each block's hash folds in every token before it
      </Caption>
      <Caption lt={lt} a={6.2} b={10.2} color={COLORS.green}>
        A new call is matched to the <b>longest identical prefix</b> — those blocks are reused.
      </Caption>
      <Caption lt={lt} a={10.8} b={14.0} color={COLORS.coral}>
        Attention is <b>causal</b>: change one early token and every hash after it breaks.
      </Caption>
      <Caption lt={lt} a={14.2} b={17.4}>
        So put <b>static content first</b> — keep the prefix byte-for-byte identical.
      </Caption>
      <Caption lt={lt} a={17.8} b={21.0}>
        Caches aren't permanent — each entry carries a <b style={{color:COLORS.green}}>time-to-live</b>.
      </Caption>
      <Caption lt={lt} a={21.4} b={25} color={COLORS.coral}>
        Idle too long and it's evicted — the next call pays full prefill again.
      </Caption>
    </>
  );
}

Object.assign(window, { ScenePrefixMatch, Block, HashChip });
