"""
CASCADE Playback System - Buffered Causation Observation

Like a video player for causation events:
- Processing runs at full speed, buffering events to TAPE (JSONL)
- Visualization plays back the tape with smooth animations
- User controls: play, pause, speed (0.25x to 4x), scrub
- No flutter - smooth transitions between states

The model leaves breadcrumbs. We follow at our own pace.

TAPE FILES:
- Model Observatory: logs/cascade_tape_*.jsonl
- Data Unity: logs/unity_tape_*.jsonl

Both use the same playback system!
"""

import json
import time
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from pathlib import Path

# ═══════════════════════════════════════════════════════════════════════════════
# TAPE LOADING - Load events from JSONL tape files
# ═══════════════════════════════════════════════════════════════════════════════

def load_tape_file(tape_path: str) -> List[Dict[str, Any]]:
    """
    Load events from a tape file (JSONL format).
    
    Works with both:
    - Model Observatory tapes: logs/cascade_tape_*.jsonl
    - Data Unity tapes: logs/unity_tape_*.jsonl
    
    Args:
        tape_path: Path to the .jsonl tape file
        
    Returns:
        List of event records in chronological order
    """
    events = []
    with open(tape_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                try:
                    events.append(json.loads(line))
                except json.JSONDecodeError:
                    pass  # Skip malformed lines
    return events


def find_latest_tape(log_dir: str = "./logs", tape_type: str = "unity") -> Optional[str]:
    """
    Find the most recent tape file.
    
    Args:
        log_dir: Directory containing tape files
        tape_type: "unity" for Data Unity tapes, "cascade" for Model Observatory tapes
        
    Returns:
        Path to the latest tape file, or None if not found
    """
    log_path = Path(log_dir)
    if not log_path.exists():
        return None
    
    pattern = f"{tape_type}_tape_*.jsonl"
    tapes = list(log_path.glob(pattern))
    
    if not tapes:
        return None
    
    # Sort by modification time (most recent first)
    tapes.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    return str(tapes[0])


def list_tape_files(log_dir: str = "./logs") -> Dict[str, List[str]]:
    """
    List all available tape files.
    
    Returns:
        Dict with keys "unity", "cascade", and "provenance", each containing list of tape paths
    """
    log_path = Path(log_dir)
    if not log_path.exists():
        return {"unity": [], "cascade": [], "provenance": []}
    
    unity_tapes = sorted(log_path.glob("unity_tape_*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
    cascade_tapes = sorted(log_path.glob("cascade_tape_*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
    provenance_tapes = sorted(log_path.glob("provenance_tape_*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
    
    return {
        "unity": [str(p) for p in unity_tapes],
        "cascade": [str(p) for p in cascade_tapes],
        "provenance": [str(p) for p in provenance_tapes],
    }


# ═══════════════════════════════════════════════════════════════════════════════
# PLAYBACK BUFFER
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class PlaybackEvent:
    """A single event in the playback buffer."""
    timestamp: float
    event_type: str
    data: Dict[str, Any]
    highlight_color: str = "#00aaff"
    duration_ms: int = 500  # How long to show highlight

@dataclass 
class PlaybackBuffer:
    """Buffer of events for playback."""
    events: List[PlaybackEvent] = field(default_factory=list)
    current_index: int = 0
    is_complete: bool = False
    
    def add(self, event: PlaybackEvent):
        self.events.append(event)
    
    def get_events_up_to(self, index: int) -> List[PlaybackEvent]:
        return self.events[:index + 1]
    
    def __len__(self):
        return len(self.events)
    
    @classmethod
    def from_tape(cls, tape_path: str) -> "PlaybackBuffer":
        """Create a PlaybackBuffer from a tape file."""
        buffer = cls()
        records = load_tape_file(tape_path)
        
        for record in records:
            event_data = record.get("event", record)
            buffer.add(PlaybackEvent(
                timestamp=event_data.get("timestamp", 0),
                event_type=event_data.get("event_type", "unknown"),
                data=event_data,
            ))
        
        buffer.is_complete = True
        return buffer


# ═══════════════════════════════════════════════════════════════════════════════
# PLAYBACK HTML TEMPLATE
# ═══════════════════════════════════════════════════════════════════════════════

PLAYBACK_HTML = '''<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CASCADE // CAUSATION PLAYBACK</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
        
        :root {
            --bg: #000;
            --panel: #0a0a0a;
            --surface: #111;
            --border: #1a1a1a;
            --text: #e8e8e8;
            --muted: #555;
            --dim: #333;
            
            --healthy: #00ff88;
            --warning: #ffaa00;
            --critical: #ff3366;
            --info: #00aaff;
            --embed-a: #4488ff;
            --embed-b: #aa44ff;
            --match: #00ff88;
            --compare: #00ffff;
        }
        
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'JetBrains Mono', monospace;
            background: var(--bg);
            color: var(--text);
            height: 100vh;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }
        
        /* === PLAYBACK CONTROLS === */
        .playback-controls {
            background: var(--panel);
            border-bottom: 1px solid var(--border);
            padding: 8px 16px;
            display: flex;
            align-items: center;
            gap: 16px;
        }
        
        .playback-btn {
            background: var(--surface);
            border: 1px solid var(--border);
            color: var(--text);
            padding: 6px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-family: inherit;
            font-size: 12px;
            transition: all 0.2s;
        }
        
        .playback-btn:hover { background: var(--border); }
        .playback-btn.active { background: var(--info); color: #000; }
        .playback-btn.play { background: var(--healthy); color: #000; }
        
        .speed-control {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        .speed-btn {
            background: transparent;
            border: 1px solid var(--dim);
            color: var(--muted);
            padding: 4px 8px;
            border-radius: 3px;
            cursor: pointer;
            font-family: inherit;
            font-size: 10px;
        }
        
        .speed-btn.active { border-color: var(--info); color: var(--info); }
        
        /* === TIMELINE SCRUBBER === */
        .timeline-scrubber {
            flex: 1;
            display: flex;
            align-items: center;
            gap: 12px;
        }
        
        .scrubber-track {
            flex: 1;
            height: 6px;
            background: var(--surface);
            border-radius: 3px;
            position: relative;
            cursor: pointer;
        }
        
        .scrubber-progress {
            height: 100%;
            background: linear-gradient(90deg, var(--embed-a), var(--match));
            border-radius: 3px;
            transition: width 0.1s linear;
        }
        
        .scrubber-head {
            position: absolute;
            top: 50%;
            transform: translate(-50%, -50%);
            width: 14px;
            height: 14px;
            background: var(--text);
            border-radius: 50%;
            cursor: grab;
            box-shadow: 0 0 10px rgba(255,255,255,0.3);
        }
        
        .time-display {
            font-size: 10px;
            color: var(--muted);
            min-width: 100px;
            text-align: right;
        }
        
        /* === MAIN CONTENT === */
        .main-content {
            flex: 1;
            display: grid;
            grid-template-columns: 1fr 320px;
            overflow: hidden;
        }
        
        /* === EVENT VISUALIZATION === */
        .event-canvas {
            position: relative;
            overflow: hidden;
            background: radial-gradient(ellipse at center, #0a0a15 0%, #000 100%);
        }
        
        /* Document columns */
        .doc-column {
            position: absolute;
            top: 60px;
            bottom: 60px;
            width: 45%;
            display: flex;
            flex-direction: column;
            gap: 4px;
            overflow-y: auto;
            padding: 10px;
        }
        
        .doc-column.left { left: 0; }
        .doc-column.right { right: 0; }
        
        .doc-column-label {
            font-size: 10px;
            font-weight: 600;
            letter-spacing: 2px;
            color: var(--muted);
            padding: 8px;
            position: sticky;
            top: 0;
            background: rgba(0,0,0,0.9);
            z-index: 10;
        }
        
        .doc-column.left .doc-column-label { color: var(--embed-a); }
        .doc-column.right .doc-column-label { color: var(--embed-b); }
        
        /* Document items */
        .doc-item {
            padding: 8px 12px;
            background: rgba(20, 20, 30, 0.8);
            border: 1px solid var(--border);
            border-radius: 4px;
            font-size: 10px;
            line-height: 1.4;
            transition: all 0.3s ease;
            position: relative;
            overflow: hidden;
        }
        
        .doc-item.highlight-embed {
            border-color: var(--embed-a);
            box-shadow: 0 0 20px rgba(68, 136, 255, 0.4);
            background: rgba(68, 136, 255, 0.1);
        }
        
        .doc-item.highlight-embed-b {
            border-color: var(--embed-b);
            box-shadow: 0 0 20px rgba(170, 68, 255, 0.4);
            background: rgba(170, 68, 255, 0.1);
        }
        
        .doc-item.highlight-compare {
            border-color: var(--compare);
            box-shadow: 0 0 25px rgba(0, 255, 255, 0.5);
            animation: pulse-compare 0.5s ease;
        }
        
        .doc-item.highlight-match {
            border-color: var(--match);
            box-shadow: 0 0 30px rgba(0, 255, 136, 0.6);
            background: rgba(0, 255, 136, 0.15);
        }
        
        @keyframes pulse-compare {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.02); }
        }
        
        .doc-item-id {
            font-size: 8px;
            color: var(--dim);
            margin-bottom: 4px;
        }
        
        /* Connection lines (SVG overlay) */
        .connection-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 100;
        }
        
        .connection-line {
            fill: none;
            stroke-width: 2;
            opacity: 0;
            transition: opacity 0.3s;
        }
        
        .connection-line.active {
            opacity: 1;
            animation: flow-line 1s linear infinite;
        }
        
        @keyframes flow-line {
            from { stroke-dashoffset: 20; }
            to { stroke-dashoffset: 0; }
        }
        
        /* === EVENT FEED === */
        .event-feed {
            background: var(--panel);
            border-left: 1px solid var(--border);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        
        .feed-header {
            padding: 12px 16px;
            font-size: 9px;
            font-weight: 700;
            letter-spacing: 2px;
            color: var(--muted);
            border-bottom: 1px solid var(--border);
            display: flex;
            justify-content: space-between;
        }
        
        .feed-count { color: var(--healthy); }
        
        .feed-list {
            flex: 1;
            overflow-y: auto;
            padding: 8px;
        }
        
        .feed-item {
            padding: 10px 12px;
            margin-bottom: 4px;
            background: var(--surface);
            border-radius: 4px;
            border-left: 3px solid var(--dim);
            font-size: 10px;
            transition: all 0.3s ease;
            opacity: 0.5;
        }
        
        .feed-item.current {
            opacity: 1;
            border-left-color: var(--info);
            background: rgba(0, 170, 255, 0.1);
        }
        
        .feed-item.played {
            opacity: 0.7;
            border-left-color: var(--healthy);
        }
        
        .feed-item-type {
            font-weight: 600;
            margin-bottom: 4px;
        }
        
        .feed-item-type.embed { color: var(--embed-a); }
        .feed-item-type.compare { color: var(--compare); }
        .feed-item-type.match { color: var(--match); }
        
        .feed-item-detail {
            color: var(--muted);
            font-size: 9px;
        }
        
        /* === STATS BAR === */
        .stats-bar {
            padding: 8px 16px;
            background: var(--surface);
            border-top: 1px solid var(--border);
            display: flex;
            gap: 24px;
            font-size: 10px;
        }
        
        .stat-item {
            display: flex;
            align-items: center;
            gap: 6px;
        }
        
        .stat-label { color: var(--muted); }
        .stat-value { color: var(--text); font-weight: 600; }
        .stat-value.a { color: var(--embed-a); }
        .stat-value.b { color: var(--embed-b); }
        .stat-value.match { color: var(--match); }
    </style>
</head>
<body>
    <!-- Playback Controls -->
    <div class="playback-controls">
        <button class="playback-btn" id="btn-restart" onclick="restart()">⏮</button>
        <button class="playback-btn play" id="btn-play" onclick="togglePlay()">▶</button>
        <button class="playback-btn" id="btn-step" onclick="step()">⏭</button>
        
        <div class="speed-control">
            <span style="font-size: 9px; color: var(--muted);">SPEED</span>
            <button class="speed-btn" onclick="setSpeed(0.25)">0.25x</button>
            <button class="speed-btn" onclick="setSpeed(0.5)">0.5x</button>
            <button class="speed-btn active" onclick="setSpeed(1)">1x</button>
            <button class="speed-btn" onclick="setSpeed(2)">2x</button>
            <button class="speed-btn" onclick="setSpeed(4)">4x</button>
        </div>
        
        <div class="timeline-scrubber">
            <div class="scrubber-track" id="scrubber" onclick="scrub(event)">
                <div class="scrubber-progress" id="progress"></div>
                <div class="scrubber-head" id="scrubber-head"></div>
            </div>
            <div class="time-display" id="time-display">0 / 0 events</div>
        </div>
    </div>
    
    <!-- Main Content -->
    <div class="main-content">
        <!-- Event Visualization -->
        <div class="event-canvas" id="canvas">
            <div class="doc-column left" id="docs-a">
                <div class="doc-column-label">DATASET A</div>
            </div>
            <div class="doc-column right" id="docs-b">
                <div class="doc-column-label">DATASET B</div>
            </div>
            <svg class="connection-layer" id="connections"></svg>
        </div>
        
        <!-- Event Feed -->
        <div class="event-feed">
            <div class="feed-header">
                <span>EVENT SEQUENCE</span>
                <span class="feed-count" id="feed-count">0</span>
            </div>
            <div class="feed-list" id="feed-list"></div>
            <div class="stats-bar">
                <div class="stat-item">
                    <span class="stat-label">Embedded A:</span>
                    <span class="stat-value a" id="stat-embed-a">0</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">Embedded B:</span>
                    <span class="stat-value b" id="stat-embed-b">0</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">Matches:</span>
                    <span class="stat-value match" id="stat-matches">0</span>
                </div>
            </div>
        </div>
    </div>

<script>
// ═══════════════════════════════════════════════════════════════════════════════
// CASCADE PLAYBACK ENGINE
// ═══════════════════════════════════════════════════════════════════════════════

const state = {
    events: [],           // All buffered events
    currentIndex: -1,     // Current playback position
    isPlaying: false,
    speed: 1,             // Playback speed multiplier
    baseInterval: 200,    // Base ms between events at 1x
    
    // Document tracking
    docsA: new Map(),     // id -> element
    docsB: new Map(),
    embedCountA: 0,
    embedCountB: 0,
    matchCount: 0,
    
    // Highlight queue (for smooth transitions)
    activeHighlights: [],
};

// === PLAYBACK CONTROLS ===

function togglePlay() {
    state.isPlaying = !state.isPlaying;
    document.getElementById('btn-play').textContent = state.isPlaying ? '⏸' : '▶';
    document.getElementById('btn-play').classList.toggle('play', !state.isPlaying);
    
    if (state.isPlaying) {
        playNext();
    }
}

function restart() {
    state.currentIndex = -1;
    state.isPlaying = false;
    state.embedCountA = 0;
    state.embedCountB = 0;
    state.matchCount = 0;
    document.getElementById('btn-play').textContent = '▶';
    document.getElementById('btn-play').classList.add('play');
    
    // Clear all highlights
    document.querySelectorAll('.doc-item').forEach(el => {
        el.className = 'doc-item';
    });
    document.querySelectorAll('.feed-item').forEach(el => {
        el.classList.remove('current', 'played');
    });
    
    updateUI();
}

function step() {
    if (state.currentIndex < state.events.length - 1) {
        state.currentIndex++;
        renderEvent(state.events[state.currentIndex]);
        updateUI();
    }
}

function setSpeed(speed) {
    state.speed = speed;
    document.querySelectorAll('.speed-btn').forEach(btn => {
        btn.classList.toggle('active', btn.textContent === speed + 'x');
    });
}

function scrub(event) {
    const track = document.getElementById('scrubber');
    const rect = track.getBoundingClientRect();
    const pct = (event.clientX - rect.left) / rect.width;
    const targetIndex = Math.floor(pct * state.events.length);
    
    // Jump to position
    state.currentIndex = targetIndex - 1;
    
    // Re-render all events up to this point (for accurate state)
    rebuildState();
}

// === PLAYBACK ENGINE ===

function playNext() {
    if (!state.isPlaying) return;
    if (state.currentIndex >= state.events.length - 1) {
        state.isPlaying = false;
        document.getElementById('btn-play').textContent = '▶';
        document.getElementById('btn-play').classList.add('play');
        return;
    }
    
    state.currentIndex++;
    renderEvent(state.events[state.currentIndex]);
    updateUI();
    
    // Schedule next event
    const interval = state.baseInterval / state.speed;
    setTimeout(playNext, interval);
}

function renderEvent(event) {
    const type = event.type;
    const data = event.data;
    
    // Clear previous highlights (smooth transition)
    clearHighlights();
    
    switch (type) {
        case 'embed_a':
            highlightDoc('a', data.id, 'highlight-embed');
            state.embedCountA++;
            break;
            
        case 'embed_b':
            highlightDoc('b', data.id, 'highlight-embed-b');
            state.embedCountB++;
            break;
            
        case 'compare':
            highlightDoc('a', data.idA, 'highlight-compare');
            highlightDoc('b', data.idB, 'highlight-compare');
            drawConnection(data.idA, data.idB, 'compare');
            break;
            
        case 'match':
            highlightDoc('a', data.idA, 'highlight-match');
            highlightDoc('b', data.idB, 'highlight-match');
            drawConnection(data.idA, data.idB, 'match');
            state.matchCount++;
            break;
    }
    
    // Update feed
    const feedItems = document.querySelectorAll('.feed-item');
    feedItems.forEach((item, i) => {
        item.classList.remove('current');
        if (i < state.currentIndex) item.classList.add('played');
        if (i === state.currentIndex) item.classList.add('current');
    });
    
    // Auto-scroll feed
    const currentFeed = feedItems[state.currentIndex];
    if (currentFeed) {
        currentFeed.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
}

function clearHighlights() {
    state.activeHighlights.forEach(el => {
        el.classList.remove('highlight-embed', 'highlight-embed-b', 'highlight-compare', 'highlight-match');
    });
    state.activeHighlights = [];
    
    // Clear connection lines
    document.getElementById('connections').innerHTML = '';
}

function highlightDoc(column, id, className) {
    const map = column === 'a' ? state.docsA : state.docsB;
    const el = map.get(id);
    if (el) {
        el.classList.add(className);
        state.activeHighlights.push(el);
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
}

function drawConnection(idA, idB, type) {
    const elA = state.docsA.get(idA);
    const elB = state.docsB.get(idB);
    if (!elA || !elB) return;
    
    const canvas = document.getElementById('canvas');
    const svg = document.getElementById('connections');
    const canvasRect = canvas.getBoundingClientRect();
    
    const rectA = elA.getBoundingClientRect();
    const rectB = elB.getBoundingClientRect();
    
    const x1 = rectA.right - canvasRect.left;
    const y1 = rectA.top + rectA.height/2 - canvasRect.top;
    const x2 = rectB.left - canvasRect.left;
    const y2 = rectB.top + rectB.height/2 - canvasRect.top;
    
    const color = type === 'match' ? 'var(--match)' : 'var(--compare)';
    
    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    const midX = (x1 + x2) / 2;
    path.setAttribute('d', `M ${x1} ${y1} Q ${midX} ${y1} ${midX} ${(y1+y2)/2} Q ${midX} ${y2} ${x2} ${y2}`);
    path.setAttribute('stroke', color);
    path.setAttribute('stroke-dasharray', '5,5');
    path.classList.add('connection-line', 'active');
    
    svg.appendChild(path);
}

function rebuildState() {
    // Reset counters
    state.embedCountA = 0;
    state.embedCountB = 0;
    state.matchCount = 0;
    
    // Clear highlights
    document.querySelectorAll('.doc-item').forEach(el => {
        el.className = 'doc-item';
    });
    
    // Replay all events up to current position (without animation)
    for (let i = 0; i <= state.currentIndex; i++) {
        const event = state.events[i];
        if (event.type === 'embed_a') state.embedCountA++;
        if (event.type === 'embed_b') state.embedCountB++;
        if (event.type === 'match') state.matchCount++;
    }
    
    // Update UI
    updateUI();
    
    // Render current event with highlight
    if (state.currentIndex >= 0) {
        renderEvent(state.events[state.currentIndex]);
    }
}

function updateUI() {
    // Update progress bar
    const pct = state.events.length > 0 ? ((state.currentIndex + 1) / state.events.length) * 100 : 0;
    document.getElementById('progress').style.width = pct + '%';
    document.getElementById('scrubber-head').style.left = pct + '%';
    
    // Update time display
    document.getElementById('time-display').textContent = 
        `${state.currentIndex + 1} / ${state.events.length} events`;
    
    // Update stats
    document.getElementById('stat-embed-a').textContent = state.embedCountA;
    document.getElementById('stat-embed-b').textContent = state.embedCountB;
    document.getElementById('stat-matches').textContent = state.matchCount;
    document.getElementById('feed-count').textContent = state.events.length;
}

// === DATA LOADING ===

function loadDocuments(docsA, docsB) {
    const containerA = document.getElementById('docs-a');
    const containerB = document.getElementById('docs-b');
    
    // Clear existing (keep label)
    containerA.innerHTML = '<div class="doc-column-label">DATASET A</div>';
    containerB.innerHTML = '<div class="doc-column-label">DATASET B</div>';
    state.docsA.clear();
    state.docsB.clear();
    
    docsA.forEach(doc => {
        const el = document.createElement('div');
        el.className = 'doc-item';
        el.innerHTML = `
            <div class="doc-item-id">${doc.id}</div>
            <div class="doc-item-text">${doc.text.substring(0, 100)}${doc.text.length > 100 ? '...' : ''}</div>
        `;
        containerA.appendChild(el);
        state.docsA.set(doc.id, el);
    });
    
    docsB.forEach(doc => {
        const el = document.createElement('div');
        el.className = 'doc-item';
        el.innerHTML = `
            <div class="doc-item-id">${doc.id}</div>
            <div class="doc-item-text">${doc.text.substring(0, 100)}${doc.text.length > 100 ? '...' : ''}</div>
        `;
        containerB.appendChild(el);
        state.docsB.set(doc.id, el);
    });
}

function loadEvents(events) {
    state.events = events;
    state.currentIndex = -1;
    
    // Build feed list
    const feedList = document.getElementById('feed-list');
    feedList.innerHTML = '';
    
    events.forEach((event, i) => {
        const el = document.createElement('div');
        el.className = 'feed-item';
        
        let typeClass = '';
        let icon = '•';
        let detail = '';
        
        switch (event.type) {
            case 'embed_a':
                typeClass = 'embed';
                icon = '🔵';
                detail = `Embedding: ${event.data.text?.substring(0, 40) || event.data.id}`;
                break;
            case 'embed_b':
                typeClass = 'embed';
                icon = '🟣';
                detail = `Embedding: ${event.data.text?.substring(0, 40) || event.data.id}`;
                break;
            case 'compare':
                typeClass = 'compare';
                icon = '⚡';
                detail = `Comparing ${event.data.idA} ↔ ${event.data.idB}`;
                break;
            case 'match':
                typeClass = 'match';
                icon = '✓';
                detail = `Match! Score: ${(event.data.score * 100).toFixed(1)}%`;
                break;
        }
        
        el.innerHTML = `
            <div class="feed-item-type ${typeClass}">${icon} ${event.type.toUpperCase()}</div>
            <div class="feed-item-detail">${detail}</div>
        `;
        feedList.appendChild(el);
    });
    
    updateUI();
}

// === INITIALIZATION ===
console.log('CASCADE Playback Engine initialized');
updateUI();

// Listen for data from parent
window.addEventListener('message', (event) => {
    const data = event.data;
    if (!data || !data.type) return;
    
    switch (data.type) {
        case 'load_documents':
            loadDocuments(data.docsA || [], data.docsB || []);
            break;
        case 'load_events':
            loadEvents(data.events || []);
            break;
        case 'add_event':
            state.events.push(data.event);
            // Rebuild feed
            loadEvents(state.events);
            break;
    }
});
</script>
</body>
</html>
'''


def get_playback_html():
    """Return the playback visualization HTML."""
    return PLAYBACK_HTML


def build_playback_for_unity(entities_a, entities_b, events, matches=None):
    """
    Build playback visualization with buffered events.
    
    Args:
        entities_a: List of Entity objects from dataset A
        entities_b: List of Entity objects from dataset B  
        events: List of trace events (embed, compare, match)
        matches: Optional list of final matches
        
    Returns:
        HTML string with embedded data
    """
    # Convert increments to document format
    docs_a = []
    for i, increment in enumerate(entities_a):
        text = " ".join(str(v) for v in increment.attributes.values())
        docs_a.append({
            "id": increment.id,
            "text": text,
            "index": i,
        })
    
    docs_b = []
    for i, increment in enumerate(entities_b):
        text = " ".join(str(v) for v in increment.attributes.values())
        docs_b.append({
            "id": increment.id,
            "text": text,
            "index": i,
        })
    
    # Convert events to playback format
    playback_events = []
    for evt in events:
        evt_type = evt.get('event_type', '')
        
        if evt_type == 'document_touched':
            spans = evt.get('spans', [])
            if spans:
                span = spans[0]
                doc_name = span.get('document_name', '')
                # Determine if it's dataset A or B based on source
                is_a = 'sentence1' in doc_name.lower() or doc_name.endswith('_a') or 'database a' in doc_name.lower()
                playback_events.append({
                    'type': 'embed_a' if is_a else 'embed_b',
                    'data': {
                        'id': span.get('document_id', ''),
                        'text': span.get('text', ''),
                    }
                })
        
        elif evt_type == 'association_created':
            assoc = evt.get('association', {})
            if assoc:
                playback_events.append({
                    'type': 'match',
                    'data': {
                        'idA': assoc.get('source', {}).get('document_id', ''),
                        'idB': assoc.get('target', {}).get('document_id', ''),
                        'score': assoc.get('confidence', 0),
                    }
                })
    
    # Inject data into HTML
    html = PLAYBACK_HTML.replace(
        "console.log('CASCADE Playback Engine initialized');",
        f"""console.log('CASCADE Playback Engine initialized');
        
        // Auto-load data
        const initialDocsA = {json.dumps(docs_a)};
        const initialDocsB = {json.dumps(docs_b)};
        const initialEvents = {json.dumps(playback_events)};
        
        setTimeout(() => {{
            loadDocuments(initialDocsA, initialDocsB);
            loadEvents(initialEvents);
        }}, 100);
"""
    )
    
    return html


def build_playback_from_tape(tape_path: str = None) -> str:
    """
    Build playback visualization directly from a tape file.
    
    This is the main entry point for tape-based playback!
    
    Args:
        tape_path: Path to tape file. If None, uses latest unity tape.
        
    Returns:
        HTML string ready for iframe embedding
    """
    # Find tape if not specified
    if tape_path is None:
        tape_path = find_latest_tape(tape_type="unity")
        if tape_path is None:
            # Return empty playback
            return PLAYBACK_HTML
    
    # Load events from tape
    records = load_tape_file(tape_path)
    
    # Extract documents and events from tape
    docs_a = {}  # id -> {id, text, index}
    docs_b = {}
    playback_events = []
    
    for record in records:
        event = record.get("event", {})
        evt_type = event.get("event_type", "")
        spans = event.get("spans", [])
        
        if evt_type == "document_touched" and spans:
            span = spans[0]
            doc_id = span.get("document_id", "")
            doc_name = span.get("document_name", "")
            doc_text = span.get("text", "")
            
            # Determine dataset A vs B by checking doc name patterns
            is_a = any(p in doc_name.lower() for p in ['sentence1', '_a', 'database a', 'table a', 'dataset a'])
            
            doc_entry = {"id": doc_id, "text": doc_text, "index": len(docs_a if is_a else docs_b)}
            
            if is_a:
                if doc_id not in docs_a:
                    docs_a[doc_id] = doc_entry
                playback_events.append({
                    "type": "embed_a",
                    "data": {"id": doc_id, "text": doc_text}
                })
            else:
                if doc_id not in docs_b:
                    docs_b[doc_id] = doc_entry
                playback_events.append({
                    "type": "embed_b", 
                    "data": {"id": doc_id, "text": doc_text}
                })
        
        elif evt_type == "association_created":
            assoc = event.get("association", {})
            if assoc:
                source = assoc.get("source", {})
                target = assoc.get("target", {})
                playback_events.append({
                    "type": "match",
                    "data": {
                        "idA": source.get("document_id", ""),
                        "idB": target.get("document_id", ""),
                        "score": assoc.get("confidence", 0),
                        "reason": assoc.get("reason", ""),
                    }
                })
    
    # Convert to lists
    docs_a_list = list(docs_a.values())
    docs_b_list = list(docs_b.values())
    
    # Get tape metadata for display
    tape_name = Path(tape_path).name
    event_count = len(playback_events)
    
    # Inject data into HTML
    html = PLAYBACK_HTML.replace(
        "console.log('CASCADE Playback Engine initialized');",
        f"""console.log('CASCADE Playback Engine initialized');
        console.log('Loaded from tape: {tape_name}');
        console.log('Events: {event_count}');
        
        // Auto-load data from tape
        const initialDocsA = {json.dumps(docs_a_list)};
        const initialDocsB = {json.dumps(docs_b_list)};
        const initialEvents = {json.dumps(playback_events)};
        
        setTimeout(() => {{
            loadDocuments(initialDocsA, initialDocsB);
            loadEvents(initialEvents);
        }}, 100);
"""
    )
    
    return html


def wrap_playback_in_iframe(html: str) -> str:
    """Wrap playback HTML in an iframe for Gradio."""
    import base64
    encoded = base64.b64encode(html.encode()).decode()
    return f'''
    <iframe 
        src="data:text/html;base64,{encoded}" 
        style="width: 100%; height: 650px; border: none; border-radius: 8px;"
        sandbox="allow-scripts"
    ></iframe>
    '''


def get_playback_iframe(tape_path: str = None) -> str:
    """
    Get ready-to-use playback iframe for Gradio.
    
    This is what you call from the app!
    
    Args:
        tape_path: Path to tape file, or None for latest
        
    Returns:
        HTML iframe string for gr.HTML component
    """
    html = build_playback_from_tape(tape_path)
    return wrap_playback_in_iframe(html)
