// Step-by-step solve replay with virtual cube state
function SolveReview({ solve, onClose }) {
  const moves = Array.isArray(solve.moves) ? solve.moves : [];
  const totalSteps = moves.length;
  const [step, setStep] = React.useState(0);

  const hostRef = React.useRef(null);
  const playerRef = React.useRef(null);
  const [playerReady, setPlayerReady] = React.useState(!!window.TwistyPlayer);
  const moveItemsRef = React.useRef({});

  // Alg to display at the current step: scramble + first N solve moves
  const algAtStep = React.useMemo(() => {
    const scramble = solve.scramble || '';
    const applied = moves.slice(0, step).map(m => m.move).filter(Boolean).join(' ');
    return [scramble, applied].filter(Boolean).join(' ');
  }, [solve.scramble, moves, step]);

  // Wait for TwistyPlayer CDN module
  React.useEffect(() => {
    if (window.TwistyPlayer) { setPlayerReady(true); return; }
    const t = setInterval(() => {
      if (window.TwistyPlayer) { setPlayerReady(true); clearInterval(t); }
    }, 100);
    return () => clearInterval(t);
  }, []);

  // Create TwistyPlayer once when ready
  React.useEffect(() => {
    if (!playerReady || !hostRef.current) return;
    if (playerRef.current) { playerRef.current.remove(); playerRef.current = null; }
    const player = new window.TwistyPlayer({
      puzzle: '3x3x3',
      alg: '',
      experimentalSetupAnchor: 'start',
      background: 'none',
      controlPanel: 'none',
      hintFacelets: 'none',
      visualization: '3D',
    });
    player.style.width = '100%';
    player.style.height = '280px';
    hostRef.current.appendChild(player);
    // Set AFTER connecting to DOM — TwistyPlayer (web component) processes
    // experimentalSetupAlg in connectedCallback, so constructor options are ignored.
    player.alg = '';
    player.experimentalSetupAlg = algAtStep;
    playerRef.current = player;
    return () => { player.remove(); playerRef.current = null; };
  }, [playerReady]);

  // Update cube state when step changes
  React.useEffect(() => {
    if (!playerRef.current) return;
    playerRef.current.alg = '';
    playerRef.current.experimentalSetupAlg = algAtStep;
  }, [algAtStep]);

  // Scroll the active move chip into view
  React.useEffect(() => {
    const el = moveItemsRef.current[step - 1];
    if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
  }, [step]);

  // Keyboard navigation
  React.useEffect(() => {
    function onKey(e) {
      if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
        e.preventDefault(); setStep(s => Math.max(0, s - 1));
      } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
        e.preventDefault(); setStep(s => Math.min(totalSteps, s + 1));
      } else if (e.key === 'Escape') {
        onClose();
      } else if (e.key === 'Home') {
        e.preventDefault(); setStep(0);
      } else if (e.key === 'End') {
        e.preventDefault(); setStep(totalSteps);
      }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [totalSteps, onClose]);

  const currentMove = step > 0 ? moves[step - 1] : null;

  // Find where exec phase begins (for separator in move list)
  const firstExecIdx = moves.findIndex(m => m.phase === 'executing');
  const hasMemoMoves = moves.some(m => m.phase === 'memo');

  // Wide/slice moves (Fw, Rw, M, x, y, z, etc.) aren't tracked by consumer smart cubes.
  // Their inner-slice component is silently dropped, causing the visualization to diverge
  // from the physical cube state.
  const wideMovesInScramble = React.useMemo(() => {
    if (!solve.scramble) return [];
    return solve.scramble.split(' ').filter(m => /[a-z]|^[MESxyz]/.test(m));
  }, [solve.scramble]);

  return (
    <div style={revStyles.overlay} onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={revStyles.modal}>
        {/* Header row */}
        <div style={revStyles.header}>
          <div style={revStyles.headerLeft}>
            <span style={revStyles.title}>Solve Review</span>
            <span style={revStyles.stepCounter}>{step} / {totalSteps}</span>
            {currentMove && (
              <span style={{
                ...revStyles.phaseChip,
                ...(currentMove.phase === 'memo' ? revStyles.phaseChipMemo : revStyles.phaseChipExec),
              }}>
                {currentMove.phase === 'memo' ? 'MEMO' : 'EXEC'}
              </span>
            )}
          </div>
          <div style={revStyles.headerRight}>
            {solve.total != null && (
              <span style={revStyles.timeStat}>{solve.dnf ? 'DNF' : formatTime(solve.total)}</span>
            )}
            {solve.memo != null && (
              <span style={{...revStyles.timeStat, color: 'oklch(70% 0.14 80)'}}>M {formatTime(solve.memo)}</span>
            )}
            {solve.exec != null && (
              <span style={{...revStyles.timeStat, color: 'oklch(70% 0.12 200)'}}>E {formatTime(solve.exec)}</span>
            )}
            <button style={revStyles.closeBtn} onClick={onClose}>✕ Close</button>
          </div>
        </div>

        {/* Scramble */}
        {solve.scramble && (
          <div style={revStyles.scrambleRow}>
            <span style={revStyles.scrambleLabel}>SCRAMBLE</span>
            <div style={revStyles.scrambleMoves}>
              {solve.scramble.split(' ').map((m, i) => (
                <span key={i} style={{
                  ...revStyles.scrambleMove,
                  ...(m.includes("'") ? revStyles.scramblePrime : {}),
                  ...(m.includes('2') ? revStyles.scrambleDouble : {}),
                }}>{m}</span>
              ))}
            </div>
          </div>
        )}

        {/* Wide-move warning */}
        {wideMovesInScramble.length > 0 && (
          <div style={revStyles.notice}>
            Scramble contains wide/slice moves ({wideMovesInScramble.join(' ')}).
            Smart cubes only track outer faces — the inner-slice component is not reported,
            so the end state may appear unsolved even if the physical cube was correct.
          </div>
        )}

        {/* Cube visualization */}
        <div style={revStyles.playerShell}>
          {playerReady ? (
            <div ref={hostRef} style={{ width: '100%', height: '280px' }} />
          ) : (
            <div style={revStyles.playerFallback}>Loading cube display…</div>
          )}
        </div>

        {/* Step info + controls */}
        <div style={revStyles.controlsSection}>
          <div style={revStyles.currentMoveRow}>
            {step === 0 ? (
              <span style={revStyles.stepLabel}>Scrambled state · no solve moves applied yet</span>
            ) : step === totalSteps ? (
              <span style={revStyles.stepLabel}>End of solve · {totalSteps} moves applied</span>
            ) : (
              <>
                <span style={revStyles.stepLabel}>After move {step}:</span>
                <span style={revStyles.currentMoveName}>{currentMove?.move || '?'}</span>
                {currentMove?.elapsed != null && (
                  <span style={revStyles.currentMoveTime}>@ {formatTime(currentMove.elapsed)}</span>
                )}
              </>
            )}
          </div>

          <div style={revStyles.navRow}>
            <button style={revStyles.navBtn} onClick={() => setStep(0)} disabled={step === 0} title="Go to start (Home)">⏮</button>
            <button style={revStyles.navBtn} onClick={() => setStep(s => Math.max(0, s - 1))} disabled={step === 0} title="Previous (←)">◀ Prev</button>
            <input
              type="range" min={0} max={totalSteps} value={step}
              onChange={e => setStep(Number(e.target.value))}
              style={revStyles.slider}
            />
            <button style={revStyles.navBtn} onClick={() => setStep(s => Math.min(totalSteps, s + 1))} disabled={step === totalSteps} title="Next (→)">Next ▶</button>
            <button style={revStyles.navBtn} onClick={() => setStep(totalSteps)} disabled={step === totalSteps} title="Go to end (End)">⏭</button>
          </div>
        </div>

        {/* Move list */}
        <div style={revStyles.moveListWrap}>
          <div style={revStyles.moveListHeader}>
            <span style={revStyles.moveListTitle}>MOVES</span>
            <span style={revStyles.moveListCounts}>
              {hasMemoMoves && (
                <span style={{color: 'oklch(65% 0.14 80)', marginRight: '8px'}}>
                  MEMO {moves.filter(m => m.phase === 'memo').length}
                </span>
              )}
              <span style={{color: 'oklch(65% 0.12 200)'}}>
                EXEC {moves.filter(m => m.phase !== 'memo').length}
              </span>
            </span>
          </div>
          <div style={revStyles.moveList}>
            {moves.length === 0 ? (
              <span style={revStyles.noMoves}>No moves recorded for this solve.</span>
            ) : (
              moves.map((m, i) => {
                const isActive = i === step - 1;
                const isPast = i < step;
                const isMemo = m.phase === 'memo';
                const isFirstExec = i === firstExecIdx && hasMemoMoves;
                return (
                  <React.Fragment key={i}>
                    {isFirstExec && (
                      <span style={revStyles.phaseSep}>/ EXEC /</span>
                    )}
                    <span
                      ref={el => moveItemsRef.current[i] = el}
                      style={{
                        ...revStyles.moveChip,
                        ...(isMemo ? revStyles.moveChipMemo : revStyles.moveChipExec),
                        ...(isActive ? revStyles.moveChipActive : {}),
                        ...(isPast && !isActive ? revStyles.moveChipPast : {}),
                        ...(!isPast ? revStyles.moveChipFuture : {}),
                      }}
                      onClick={() => setStep(i + 1)}
                      title={`#${i + 1} ${m.move}${m.elapsed != null ? ` @ ${formatTime(m.elapsed)}` : ''} (${m.phase || '?'})`}
                    >
                      {m.move || '?'}
                    </span>
                  </React.Fragment>
                );
              })
            )}
          </div>
        </div>

        <div style={revStyles.keyHint}>← → arrow keys to step · click any move to jump · Esc to close</div>
      </div>
    </div>
  );
}

const revStyles = {
  overlay: {
    position: 'fixed', inset: 0, zIndex: 1000,
    background: 'oklch(0% 0 0 / 0.85)',
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    padding: '20px',
  },
  modal: {
    background: 'oklch(12% 0.01 250)',
    border: '1px solid oklch(22% 0.01 250)',
    borderRadius: '12px',
    width: '100%', maxWidth: '860px',
    maxHeight: '92vh', overflowY: 'auto',
    display: 'flex', flexDirection: 'column', gap: '0',
    boxShadow: '0 24px 64px oklch(0% 0 0 / 0.6)',
  },
  header: {
    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
    padding: '16px 20px',
    borderBottom: '1px solid oklch(18% 0.01 250)',
    flexShrink: 0,
  },
  headerLeft: { display: 'flex', alignItems: 'center', gap: '12px' },
  headerRight: { display: 'flex', alignItems: 'center', gap: '12px' },
  title: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '13px', fontWeight: '700', letterSpacing: '0.08em',
    color: 'oklch(75% 0.01 250)',
  },
  stepCounter: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '12px', color: 'oklch(45% 0.01 250)',
    padding: '2px 8px', borderRadius: '4px',
    background: 'oklch(18% 0.01 250)',
  },
  phaseChip: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '10px', fontWeight: '700', letterSpacing: '0.10em',
    padding: '2px 7px', borderRadius: '4px',
  },
  phaseChipMemo: { background: 'oklch(20% 0.10 80)', color: 'oklch(70% 0.18 80)' },
  phaseChipExec: { background: 'oklch(18% 0.08 200)', color: 'oklch(70% 0.12 200)' },
  timeStat: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '13px', fontWeight: '500',
    color: 'oklch(80% 0.01 250)',
  },
  closeBtn: {
    padding: '6px 12px', borderRadius: '6px',
    border: '1px solid oklch(28% 0.01 250)',
    background: 'oklch(18% 0.01 250)',
    color: 'oklch(55% 0.01 250)', fontSize: '12px',
    cursor: 'pointer', fontFamily: "'Inter', sans-serif",
  },
  scrambleRow: {
    display: 'flex', alignItems: 'flex-start', gap: '12px',
    padding: '12px 20px',
    borderBottom: '1px solid oklch(16% 0.01 250)',
    background: 'oklch(11% 0.01 250)',
  },
  scrambleLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '9px', fontWeight: '700', letterSpacing: '0.12em',
    color: 'oklch(38% 0.01 250)', marginTop: '3px', flexShrink: 0,
  },
  scrambleMoves: { display: 'flex', flexWrap: 'wrap', gap: '2px 1px' },
  scrambleMove: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '14px', fontWeight: '500',
    color: 'oklch(72% 0.01 250)',
    padding: '1px 4px', borderRadius: '3px',
  },
  scramblePrime: { color: 'oklch(70% 0.14 165)' },
  scrambleDouble: { color: 'oklch(70% 0.10 200)' },
  playerShell: {
    background: 'oklch(9% 0.01 250)',
    minHeight: '280px',
    borderBottom: '1px solid oklch(16% 0.01 250)',
  },
  playerFallback: {
    height: '280px', display: 'flex',
    alignItems: 'center', justifyContent: 'center',
    color: 'oklch(40% 0.01 250)', fontSize: '13px',
    fontFamily: "'Inter', sans-serif",
  },
  controlsSection: {
    padding: '14px 20px',
    borderBottom: '1px solid oklch(16% 0.01 250)',
    display: 'flex', flexDirection: 'column', gap: '10px',
  },
  currentMoveRow: {
    display: 'flex', alignItems: 'center', gap: '10px', minHeight: '24px',
  },
  stepLabel: {
    fontSize: '12px', color: 'oklch(45% 0.01 250)',
    fontFamily: "'Inter', sans-serif",
  },
  currentMoveName: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '20px', fontWeight: '500', color: 'oklch(88% 0.01 250)',
  },
  currentMoveTime: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '12px', color: 'oklch(42% 0.01 250)',
  },
  navRow: {
    display: 'flex', alignItems: 'center', gap: '8px',
  },
  navBtn: {
    padding: '6px 12px', borderRadius: '6px',
    border: '1px solid oklch(25% 0.01 250)',
    background: 'oklch(17% 0.01 250)',
    color: 'oklch(65% 0.01 250)', fontSize: '12px',
    cursor: 'pointer', fontFamily: "'Inter', sans-serif",
    fontWeight: '500', transition: 'all 0.1s', flexShrink: 0,
  },
  slider: {
    flex: 1, cursor: 'pointer',
    accentColor: 'oklch(65% 0.18 165)',
  },
  moveListWrap: {
    padding: '14px 20px',
    borderBottom: '1px solid oklch(16% 0.01 250)',
  },
  moveListHeader: {
    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
    marginBottom: '10px',
  },
  moveListTitle: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '9px', fontWeight: '700', letterSpacing: '0.12em',
    color: 'oklch(38% 0.01 250)',
  },
  moveListCounts: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '11px',
  },
  moveList: {
    display: 'flex', flexWrap: 'wrap', gap: '4px 3px',
    minHeight: '32px',
  },
  noMoves: {
    fontSize: '12px', color: 'oklch(38% 0.01 250)',
    fontFamily: "'Inter', sans-serif",
  },
  moveChip: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '13px', fontWeight: '500',
    padding: '3px 7px', borderRadius: '5px',
    cursor: 'pointer', transition: 'all 0.1s',
    userSelect: 'none',
  },
  moveChipMemo: {
    background: 'oklch(19% 0.08 80)', color: 'oklch(58% 0.12 80)',
    border: '1px solid oklch(26% 0.08 80)',
  },
  moveChipExec: {
    background: 'oklch(17% 0.06 200)', color: 'oklch(55% 0.10 200)',
    border: '1px solid oklch(24% 0.06 200)',
  },
  moveChipActive: {
    background: 'oklch(65% 0.18 165)', color: 'oklch(10% 0.01 250)',
    border: '1px solid oklch(65% 0.18 165)',
    transform: 'scale(1.1)',
  },
  moveChipPast: {
    opacity: 0.7,
  },
  moveChipFuture: {
    opacity: 0.35,
  },
  phaseSep: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: '9px', letterSpacing: '0.10em',
    color: 'oklch(35% 0.01 250)',
    alignSelf: 'center', padding: '0 2px',
  },
  keyHint: {
    padding: '10px 20px',
    fontSize: '11px', color: 'oklch(32% 0.01 250)',
    fontFamily: "'Inter', sans-serif",
    textAlign: 'center',
  },
  notice: {
    padding: '8px 20px',
    fontSize: '11px', lineHeight: 1.5,
    color: 'oklch(65% 0.14 80)',
    background: 'oklch(16% 0.06 80)',
    borderBottom: '1px solid oklch(22% 0.08 80)',
    fontFamily: "'Inter', sans-serif",
  },
};

Object.assign(window, { SolveReview });
