"""
CASCADE Document Space - Information Flow Visualization

Visual representation of all informational wealth systems observed at any given time.
Shows datasets, processing activities, events, and discovered relationships.

This is NOT just A/B entity columns - it's a live view of information flowing
through the system: Ingest → Embed → Compare → Match → Export
"""

# ═══════════════════════════════════════════════════════════════════════════════
# INFORMATION FLOW VISUALIZATION
# ═══════════════════════════════════════════════════════════════════════════════

DOCUMENT_SPACE_HTML = '''<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CASCADE // INFORMATION SPACE</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
        
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        :root {
            --bg: #08080c;
            --panel: #0d0d12;
            --surface: #12121a;
            --border: #1a1a24;
            --text: #e8e8e8;
            --muted: #666;
            --dim: #333;
            --dataset-a: #4aa8ff;
            --dataset-b: #ff8844;
            --activity: #aa66ff;
            --match: #00ff88;
            --event: #ffaa00;
        }
        
        body {
            font-family: 'JetBrains Mono', monospace;
            background: var(--bg);
            color: var(--text);
            height: 100vh;
            overflow: hidden;
        }
        
        /* Main layout: top status, center flow visualization, bottom events */
        .container {
            display: grid;
            grid-template-rows: auto 1fr auto;
            height: 100vh;
        }
        
        /* Top status bar */
        .status-bar {
            background: var(--panel);
            border-bottom: 1px solid var(--border);
            padding: 10px 16px;
            display: flex;
            align-items: center;
            gap: 20px;
        }
        
        .status-title {
            font-size: 11px;
            font-weight: 600;
            color: var(--text);
            letter-spacing: 1px;
        }
        
        .status-phase {
            padding: 4px 10px;
            font-size: 9px;
            font-weight: 700;
            letter-spacing: 1px;
            border-radius: 2px;
            background: var(--surface);
            color: var(--muted);
        }
        
        .status-phase.active {
            background: var(--activity);
            color: #000;
            animation: pulse 1.5s infinite;
        }
        
        .status-phase.complete {
            background: var(--match);
            color: #000;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.7; }
        }
        
        .status-counts {
            display: flex;
            gap: 16px;
            margin-left: auto;
            font-size: 10px;
        }
        
        .status-count {
            display: flex;
            align-items: center;
            gap: 6px;
        }
        
        .count-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
        }
        
        .count-dot.a { background: var(--dataset-a); }
        .count-dot.b { background: var(--dataset-b); }
        .count-dot.match { background: var(--match); }
        .count-dot.event { background: var(--event); }
        
        .count-value {
            font-weight: 700;
            color: var(--text);
        }
        
        .count-label {
            color: var(--muted);
        }
        
        /* Center: Flow visualization */
        .flow-area {
            display: grid;
            grid-template-columns: 180px 1fr 180px;
            padding: 16px;
            gap: 16px;
            overflow: hidden;
        }
        
        /* Data columns (A and B) */
        .data-column {
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 6px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        
        .column-header {
            padding: 10px 12px;
            border-bottom: 1px solid var(--border);
            font-size: 10px;
            font-weight: 700;
            letter-spacing: 1px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .column-header.a { color: var(--dataset-a); }
        .column-header.b { color: var(--dataset-b); }
        
        .column-count {
            font-size: 11px;
            opacity: 0.8;
        }
        
        .column-records {
            flex: 1;
            overflow-y: auto;
            padding: 6px;
        }
        
        .record-bar {
            height: 4px;
            margin-bottom: 2px;
            border-radius: 2px;
            opacity: 0.4;
            transition: all 0.3s;
        }
        
        .record-bar.a { background: var(--dataset-a); }
        .record-bar.b { background: var(--dataset-b); }
        
        .record-bar.processing {
            opacity: 1;
            box-shadow: 0 0 10px currentColor;
            animation: record-pulse 0.5s ease-in-out infinite;
        }
        
        .record-bar.matched {
            opacity: 1;
            background: var(--match) !important;
        }
        
        .record-bar.done {
            opacity: 0.6;
        }
        
        @keyframes record-pulse {
            0%, 100% { transform: scaleX(1); }
            50% { transform: scaleX(1.05); }
        }
        
        /* Center flow canvas */
        .flow-center {
            background: var(--panel);
            border: 1px solid var(--border);
            border-radius: 6px;
            position: relative;
            overflow: hidden;
        }
        
        /* Activity pipeline */
        .pipeline {
            position: absolute;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 12px;
            z-index: 10;
        }
        
        .pipeline-stage {
            padding: 6px 12px;
            background: var(--surface);
            border: 1px solid var(--border);
            border-radius: 4px;
            font-size: 9px;
            font-weight: 600;
            color: var(--muted);
            display: flex;
            align-items: center;
            gap: 6px;
        }
        
        .pipeline-stage.active {
            border-color: var(--activity);
            color: var(--activity);
            animation: stage-pulse 1s infinite;
        }
        
        .pipeline-stage.complete {
            border-color: var(--match);
            color: var(--match);
        }
        
        @keyframes stage-pulse {
            0%, 100% { box-shadow: 0 0 0 0 rgba(170,102,255,0.4); }
            50% { box-shadow: 0 0 10px 2px rgba(170,102,255,0.4); }
        }
        
        .pipeline-arrow {
            color: var(--dim);
            font-size: 10px;
        }
        
        /* Connection SVG */
        .connections-svg {
            position: absolute;
            inset: 0;
            pointer-events: none;
        }
        
        .connection-line {
            stroke: var(--match);
            stroke-width: 1.5;
            fill: none;
            opacity: 0;
            stroke-dasharray: 100;
            animation: draw-connection 0.5s ease-out forwards;
        }
        
        @keyframes draw-connection {
            to {
                opacity: 0.6;
                stroke-dashoffset: 0;
            }
        }
        
        /* Center stats display */
        .center-stats {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            text-align: center;
        }
        
        .big-number {
            font-size: 56px;
            font-weight: 200;
            color: var(--match);
            line-height: 1;
        }
        
        .big-label {
            font-size: 10px;
            letter-spacing: 2px;
            color: var(--muted);
            margin-top: 4px;
        }
        
        /* Match pills */
        .matches-row {
            position: absolute;
            bottom: 16px;
            left: 16px;
            right: 16px;
            display: flex;
            gap: 6px;
            overflow-x: auto;
            padding: 4px;
        }
        
        .match-pill {
            display: flex;
            align-items: center;
            gap: 4px;
            padding: 4px 10px;
            background: rgba(0, 255, 136, 0.1);
            border: 1px solid rgba(0, 255, 136, 0.3);
            border-radius: 12px;
            font-size: 9px;
            white-space: nowrap;
            animation: pill-pop 0.3s ease-out;
        }
        
        @keyframes pill-pop {
            0% { transform: scale(0); opacity: 0; }
            100% { transform: scale(1); opacity: 1; }
        }
        
        .match-pill .score {
            color: var(--match);
            font-weight: 700;
        }
        
        .match-pill .ids {
            color: var(--muted);
        }
        
        /* Bottom: Live event feed */
        .event-feed {
            background: var(--panel);
            border-top: 1px solid var(--border);
            padding: 10px 16px;
            display: flex;
            gap: 12px;
            overflow-x: auto;
            max-height: 80px;
        }
        
        .event-chip {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 6px 10px;
            background: var(--surface);
            border-radius: 4px;
            font-size: 9px;
            white-space: nowrap;
            animation: chip-slide 0.3s ease-out;
            flex-shrink: 0;
        }
        
        @keyframes chip-slide {
            from { transform: translateX(-20px); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }
        
        .event-chip .icon {
            font-size: 11px;
        }
        
        .event-chip .text {
            color: var(--text);
            max-width: 150px;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        
        .event-chip .time {
            color: var(--dim);
            font-size: 8px;
        }
        
        .event-chip.embed { border-left: 2px solid var(--dataset-a); }
        .event-chip.match { border-left: 2px solid var(--match); }
        .event-chip.activity { border-left: 2px solid var(--activity); }
        
        /* Progress bar */
        .progress-track {
            position: absolute;
            bottom: 70px;
            left: 16px;
            right: 16px;
            height: 3px;
            background: var(--surface);
            border-radius: 2px;
        }
        
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, var(--dataset-a), var(--activity), var(--match));
            border-radius: 2px;
            transition: width 0.3s;
        }
        
        .empty-state {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100%;
            color: var(--dim);
        }
        
        .empty-state .icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; }
        .empty-state .text { font-size: 11px; text-align: center; }
    </style>
</head>
<body>
    <div class="container">
        <!-- Status bar -->
        <div class="status-bar">
            <span class="status-title">INFORMATION SPACE</span>
            <span class="status-phase" id="phase">READY</span>
            <div class="status-counts">
                <div class="status-count">
                    <span class="count-dot a"></span>
                    <span class="count-value" id="countA">0</span>
                    <span class="count-label">Dataset A</span>
                </div>
                <div class="status-count">
                    <span class="count-dot b"></span>
                    <span class="count-value" id="countB">0</span>
                    <span class="count-label">Dataset B</span>
                </div>
                <div class="status-count">
                    <span class="count-dot match"></span>
                    <span class="count-value" id="countMatches">0</span>
                    <span class="count-label">Matches</span>
                </div>
                <div class="status-count">
                    <span class="count-dot event"></span>
                    <span class="count-value" id="countEvents">0</span>
                    <span class="count-label">Events</span>
                </div>
            </div>
        </div>
        
        <!-- Flow area -->
        <div class="flow-area">
            <!-- Dataset A column -->
            <div class="data-column">
                <div class="column-header a">
                    <span>📂 DATASET A</span>
                    <span class="column-count" id="colCountA">0</span>
                </div>
                <div class="column-records" id="recordsA"></div>
            </div>
            
            <!-- Center flow -->
            <div class="flow-center">
                <!-- Pipeline stages -->
                <div class="pipeline">
                    <div class="pipeline-stage" id="stageIngest">📥 INGEST</div>
                    <span class="pipeline-arrow">→</span>
                    <div class="pipeline-stage" id="stageEmbed">🧠 EMBED</div>
                    <span class="pipeline-arrow">→</span>
                    <div class="pipeline-stage" id="stageCompare">⚡ COMPARE</div>
                    <span class="pipeline-arrow">→</span>
                    <div class="pipeline-stage" id="stageMatch">✓ MATCH</div>
                </div>
                
                <!-- Connection lines -->
                <svg class="connections-svg" id="connectionsSvg"></svg>
                
                <!-- Center statistics -->
                <div class="center-stats">
                    <div class="big-number" id="bigMatchCount">0</div>
                    <div class="big-label">MATCHES FOUND</div>
                </div>
                
                <!-- Progress bar -->
                <div class="progress-track">
                    <div class="progress-fill" id="progressFill" style="width: 0%"></div>
                </div>
                
                <!-- Match pills -->
                <div class="matches-row" id="matchesRow"></div>
            </div>
            
            <!-- Dataset B column -->
            <div class="data-column">
                <div class="column-header b">
                    <span>📂 DATASET B</span>
                    <span class="column-count" id="colCountB">0</span>
                </div>
                <div class="column-records" id="recordsB"></div>
            </div>
        </div>
        
        <!-- Event feed -->
        <div class="event-feed" id="eventFeed">
            <div class="empty-state">
                <span class="text">Events appear here as processing occurs</span>
            </div>
        </div>
    </div>
    
    <script>
        // State
        const state = {
            docsA: [],
            docsB: [],
            matches: [],
            events: [],
            phase: 'ready',
            progress: 0,
            processingA: -1,
            processingB: -1
        };
        
        // Update functions
        function loadDocuments(docsA, docsB) {
            state.docsA = docsA || [];
            state.docsB = docsB || [];
            document.getElementById('countA').textContent = state.docsA.length;
            document.getElementById('countB').textContent = state.docsB.length;
            document.getElementById('colCountA').textContent = state.docsA.length;
            document.getElementById('colCountB').textContent = state.docsB.length;
            renderRecords();
            
            if (state.docsA.length > 0) {
                setStage('stageIngest', 'complete');
            }
        }
        
        function setPhase(phase, progress = 0) {
            state.phase = phase;
            state.progress = progress;
            
            const phaseEl = document.getElementById('phase');
            phaseEl.textContent = phase.toUpperCase();
            phaseEl.className = 'status-phase' + (phase === 'complete' ? ' complete' : phase !== 'ready' ? ' active' : '');
            
            document.getElementById('progressFill').style.width = (progress * 100) + '%';
            
            // Update pipeline stages
            if (phase === 'embedding_a' || phase === 'embedding_b') {
                setStage('stageEmbed', 'active');
            } else if (phase === 'comparing') {
                setStage('stageEmbed', 'complete');
                setStage('stageCompare', 'active');
            } else if (phase === 'complete') {
                setStage('stageCompare', 'complete');
                setStage('stageMatch', 'complete');
            }
        }
        
        function setStage(stageId, status) {
            const el = document.getElementById(stageId);
            if (el) {
                el.className = 'pipeline-stage' + (status === 'active' ? ' active' : status === 'complete' ? ' complete' : '');
            }
        }
        
        function setProcessing(aIndex, bIndex) {
            state.processingA = aIndex;
            state.processingB = bIndex;
            renderRecords();
        }
        
        // Performance: debounce render updates
        let renderPending = false;
        function scheduleRender() {
            if (!renderPending) {
                renderPending = true;
                requestAnimationFrame(() => {
                    renderPending = false;
                    renderRecords();
                });
            }
        }
        
        function addMatch(docAId, docBId, score) {
            state.matches.push({ docAId, docBId, score });
            
            // Update counts (cheap)
            document.getElementById('countMatches').textContent = state.matches.length;
            document.getElementById('bigMatchCount').textContent = state.matches.length;
            
            // PERFORMANCE: Only show first 50 match pills
            const matchesRow = document.getElementById('matchesRow');
            if (matchesRow.children.length < 50) {
                const pill = document.createElement('div');
                pill.className = 'match-pill';
                const aIdx = state.docsA.findIndex(d => d.id === docAId);
                const bIdx = state.docsB.findIndex(d => d.id === docBId);
                pill.innerHTML = '<span class="score">' + (score * 100).toFixed(0) + '%</span><span class="ids">' + (aIdx + 1) + '↔' + (bIdx + 1) + '</span>';
                matchesRow.appendChild(pill);
            } else if (matchesRow.children.length === 50) {
                // Add overflow indicator
                const more = document.createElement('div');
                more.className = 'match-pill';
                more.style.background = 'rgba(255,255,255,0.1)';
                more.innerHTML = '<span class="ids">+more...</span>';
                matchesRow.appendChild(more);
            }
            
            // PERFORMANCE: Only draw connections for first 100 matches
            if (state.matches.length <= 100) {
                const aIdx = state.docsA.findIndex(d => d.id === docAId);
                const bIdx = state.docsB.findIndex(d => d.id === docBId);
                drawConnection(aIdx, bIdx, score);
            }
            
            // Debounced render update
            scheduleRender();
        }
        
        function addEvent(eventType, text, timestamp) {
            state.events.push({ eventType, text, timestamp });
            document.getElementById('countEvents').textContent = state.events.length;
            
            const feed = document.getElementById('eventFeed');
            // Remove empty state
            const empty = feed.querySelector('.empty-state');
            if (empty) empty.remove();
            
            const chip = document.createElement('div');
            chip.className = 'event-chip ' + (eventType.includes('embed') ? 'embed' : eventType.includes('match') || eventType.includes('association') ? 'match' : 'activity');
            const icon = eventType.includes('embed') ? '🧠' : eventType.includes('match') || eventType.includes('association') ? '🔗' : '⚡';
            const timeStr = timestamp ? new Date(timestamp * 1000).toLocaleTimeString() : '';
            chip.innerHTML = '<span class="icon">' + icon + '</span><span class="text">' + text.substring(0, 50) + '</span><span class="time">' + timeStr + '</span>';
            
            feed.insertBefore(chip, feed.firstChild);
            
            // Keep only last 20 events visible
            while (feed.children.length > 20) {
                feed.removeChild(feed.lastChild);
            }
        }
        
        function drawConnection(aIdx, bIdx, score) {
            const svg = document.getElementById('connectionsSvg');
            const rect = svg.getBoundingClientRect();
            
            // Calculate Y positions
            const aY = 80 + Math.min(aIdx * 5, rect.height - 150);
            const bY = 80 + Math.min(bIdx * 5, rect.height - 150);
            
            const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            const midX = rect.width / 2;
            path.setAttribute('d', 'M 0 ' + aY + ' C ' + midX + ' ' + aY + ', ' + midX + ' ' + bY + ', ' + rect.width + ' ' + bY);
            path.setAttribute('class', 'connection-line');
            path.style.opacity = Math.min(0.3 + score * 0.5, 0.8);
            
            svg.appendChild(path);
        }
        
        function renderRecords() {
            const matchedA = new Set(state.matches.map(m => m.docAId));
            const matchedB = new Set(state.matches.map(m => m.docBId));
            
            // Render A
            const recA = document.getElementById('recordsA');
            recA.innerHTML = state.docsA.slice(0, 200).map((d, i) => {
                let cls = 'record-bar a';
                if (matchedA.has(d.id)) cls += ' matched';
                else if (i === state.processingA) cls += ' processing';
                else if (i < state.processingA) cls += ' done';
                return '<div class="' + cls + '"></div>';
            }).join('');
            
            // Render B
            const recB = document.getElementById('recordsB');
            recB.innerHTML = state.docsB.slice(0, 200).map((d, i) => {
                let cls = 'record-bar b';
                if (matchedB.has(d.id)) cls += ' matched';
                else if (i === state.processingB) cls += ' processing';
                else if (i < state.processingB) cls += ' done';
                return '<div class="' + cls + '"></div>';
            }).join('');
        }
        
        // Expose API
        window.CASCADE_DOCSPACE = { 
            loadDocuments, 
            addMatch, 
            addEvent,
            setPhase, 
            setProcessing, 
            state 
        };
        
        console.log('CASCADE Information Space initialized');
    </script>
</body>
</html>
'''


def get_document_space_html():
    """Return the Document Space (Information Flow) visualization HTML."""
    return DOCUMENT_SPACE_HTML


def build_document_space_for_unity(entities_a, entities_b, matches, events):
    """
    Build Document Space visualization with data from Data Unity.
    
    NOW INCLUDES: Live events, activity stages, and information flow!
    
    Args:
        entities_a: List of Entity objects from dataset A
        entities_b: List of Entity objects from dataset B
        matches: List of match tuples (entity_a, entity_b, score)
        events: List of processing events (NOW USED!)
        
    Returns:
        HTML string with embedded data and events
    """
    import json
    import traceback
    
    print(f"[DOCSPACE BUILD] entities_a type: {type(entities_a)}, len: {len(entities_a) if entities_a else 0}")
    print(f"[DOCSPACE BUILD] entities_b type: {type(entities_b)}, len: {len(entities_b) if entities_b else 0}")
    print(f"[DOCSPACE BUILD] matches type: {type(matches)}, len: {len(matches) if matches else 0}")
    print(f"[DOCSPACE BUILD] events type: {type(events)}, len: {len(events) if events else 0}")
    
    try:
        # Convert increments to document format
        docs_a = []
        for i, increment in enumerate(entities_a or []):
            try:
                text = " ".join(str(v) for v in increment.attributes.values())
                docs_a.append({
                    "id": increment.id,
                    "text": text[:200],  # Truncate for display
                    "index": i,
                })
            except Exception as e:
                print(f"[DOCSPACE BUILD] Error processing entity_a[{i}]: {e}")
                docs_a.append({"id": str(i), "text": f"Error: {e}", "index": i})
        
        docs_b = []
        for i, increment in enumerate(entities_b or []):
            try:
                text = " ".join(str(v) for v in increment.attributes.values())
                docs_b.append({
                    "id": increment.id,
                    "text": text[:200],
                    "index": i,
                })
            except Exception as e:
                print(f"[DOCSPACE BUILD] Error processing entity_b[{i}]: {e}")
                docs_b.append({"id": str(i), "text": f"Error: {e}", "index": i})
        
        # Convert matches
        match_data = []
        for m in (matches or []):
            try:
                if hasattr(m, 'increment_a_id'):
                    match_data.append({
                        "docA": m.increment_a_id,
                        "docB": m.increment_b_id,
                        "score": float(m.similarity),
                    })
                elif hasattr(m, 'entity_a_id'):
                    match_data.append({
                        "docA": m.entity_a_id,
                        "docB": m.entity_b_id,
                        "score": float(m.similarity),
                    })
                elif isinstance(m, (list, tuple)) and len(m) >= 2:
                    match_data.append({
                        "docA": m[0].id if hasattr(m[0], 'id') else str(m[0]),
                        "docB": m[1].id if hasattr(m[1], 'id') else str(m[1]),
                        "score": float(m[2]) if len(m) > 2 else 0.5,
                    })
            except Exception as e:
                print(f"[DOCSPACE BUILD] Error processing match: {e}")
        
        # Convert events to displayable format
        event_data = []
        for evt in (events or []):
            try:
                if isinstance(evt, dict):
                    event_data.append({
                        "type": evt.get('event_type', 'unknown'),
                        "text": evt.get('activity_name') or evt.get('progress_message') or str(evt.get('spans', [{}])[0].get('text', ''))[:100],
                        "timestamp": evt.get('timestamp', 0),
                    })
                elif hasattr(evt, 'event_type'):
                    event_data.append({
                        "type": evt.event_type.value if hasattr(evt.event_type, 'value') else str(evt.event_type),
                        "text": getattr(evt, 'activity_name', '') or getattr(evt, 'progress_message', '') or '',
                        "timestamp": getattr(evt, 'timestamp', 0),
                    })
            except Exception as e:
                print(f"[DOCSPACE BUILD] Error processing event: {e}")
        
        print(f"[DOCSPACE BUILD] Converted {len(docs_a)} A, {len(docs_b)} B, {len(match_data)} matches, {len(event_data)} events")
        
        # Serialize to JSON
        docs_a_json = json.dumps(docs_a)
        docs_b_json = json.dumps(docs_b)
        match_json = json.dumps(match_data)
        events_json = json.dumps(event_data)
        
        # Determine phase based on data state
        if match_data:
            phase = "complete"
            progress = 1.0
        elif docs_a and docs_b:
            phase = "comparing"
            progress = 0.5
        elif docs_a or docs_b:
            phase = "embedding"
            progress = 0.25
        else:
            phase = "ready"
            progress = 0
        
        # Inject data into HTML
        html = DOCUMENT_SPACE_HTML.replace(
            "console.log('CASCADE Information Space initialized');",
            f"""console.log('CASCADE Information Space initialized');
            
            // Auto-load data from Unity pipeline
            const initialDocsA = {docs_a_json};
            const initialDocsB = {docs_b_json};
            const initialMatches = {match_json};
            const initialEvents = {events_json};
            
            console.log('Loading', initialDocsA.length, 'A docs,', initialDocsB.length, 'B docs,', initialMatches.length, 'matches,', initialEvents.length, 'events');
            
            // Load documents
            loadDocuments(initialDocsA, initialDocsB);
            
            // Set phase
            setPhase('{phase}', {progress});
            
            // Add matches
            initialMatches.forEach(m => addMatch(m.docA, m.docB, m.score));
            
            // Add events
            initialEvents.slice(-30).forEach(e => addEvent(e.type, e.text, e.timestamp));
            """
        )
        
        print(f"[DOCSPACE BUILD] Final HTML length: {len(html)}")
        return html
        
    except Exception as e:
        print(f"[DOCSPACE BUILD ERROR] {type(e).__name__}: {e}")
        print(f"[DOCSPACE BUILD ERROR] Traceback:\n{traceback.format_exc()}")
        return f'''<!DOCTYPE html>
<html><body style="background:#1a0000;color:#ff4444;padding:20px;font-family:monospace;">
<h3>Document Space Build Error</h3>
<pre>{type(e).__name__}: {e}</pre>
<pre>{traceback.format_exc()}</pre>
</body></html>'''
