"""Web dashboard integration for DAG (Directed Acyclic Graph) execution.

Provides FastAPI endpoints for:
- DAG creation and validation
- DAG execution with progress tracking
- DAG visualization data
- Real-time execution updates via WebSocket
"""

from __future__ import annotations

import asyncio
import json
import logging
from datetime import datetime
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel, Field

from oclawma.dag import (
    DAG,
    DAGCycleError,
    DAGError,
    DAGExecutor,
    DAGJob,
    DAGResult,
    execute_dag_async,
)
from oclawma.queue.models import JobPriority
from oclawma.web.auth import TokenAuth

logger = logging.getLogger(__name__)


# =============================================================================
# API Models


class DAGJobCreate(BaseModel):
    """Request to create a DAG job."""

    id: str = Field(..., description="Unique job identifier")
    payload: dict[str, Any] = Field(default_factory=dict, description="Job payload data")
    priority: int = Field(default=0, description="Job priority value")
    depends_on: list[str] = Field(default_factory=list, description="Job IDs this job depends on")
    max_retries: int = Field(default=3, ge=0, description="Maximum retry attempts")
    job_type: str = Field(default="default", description="Job type")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")


class DAGCreateRequest(BaseModel):
    """Request to create a DAG."""

    dag_id: str | None = Field(default=None, description="Optional DAG identifier")
    jobs: list[DAGJobCreate] = Field(..., description="List of jobs in the DAG")


class DAGValidateResponse(BaseModel):
    """DAG validation response."""

    valid: bool
    dag_id: str
    job_count: int
    levels: list[list[str]]
    error: str | None = None


class DAGExecuteRequest(BaseModel):
    """Request to execute a DAG."""

    dag_id: str | None = Field(default=None, description="Optional DAG identifier")
    jobs: list[DAGJobCreate] = Field(..., description="List of jobs in the DAG")
    max_workers: int = Field(default=4, ge=1, le=20, description="Maximum parallel workers")
    continue_on_error: bool = Field(default=False, description="Continue on job failure")


class DAGJobResultResponse(BaseModel):
    """DAG job result response."""

    job_id: str
    success: bool
    result: Any = None
    error: str | None = None
    duration_seconds: float | None = None
    retry_count: int = 0


class DAGProgressResponse(BaseModel):
    """DAG progress response."""

    total_jobs: int
    completed_jobs: int
    failed_jobs: int
    running_jobs: int
    pending_jobs: int
    skipped_jobs: int
    percent_complete: float
    is_complete: bool
    duration_seconds: float | None = None


class DAGExecuteResponse(BaseModel):
    """DAG execution response."""

    dag_id: str
    status: str
    job_results: dict[str, DAGJobResultResponse]
    progress: DAGProgressResponse
    all_succeeded: bool
    failed_job_ids: list[str]
    error: str | None = None


class DAGVisualizationResponse(BaseModel):
    """DAG visualization data response."""

    dag_id: str
    job_count: int
    jobs: list[dict[str, Any]]
    levels: list[list[str]]
    edges: list[dict[str, str]]


# =============================================================================
# Router Factory


def create_dag_router(auth: TokenAuth | None = None) -> APIRouter:
    """Create the DAG API router.

    Returns:
        Configured APIRouter instance
    """
    router = APIRouter(prefix="/dag", tags=["dag"])

    # Store active executions
    active_executions: dict[str, DAGExecutor] = {}

    # =============================================================================
    # Dashboard HTML

    @router.get("/dashboard", response_class=HTMLResponse)
    async def dag_dashboard() -> str:
        """Serve the DAG dashboard HTML."""
        return _get_dag_dashboard_html()

    # =============================================================================
    # DAG Management API

    @router.post("/validate", response_model=DAGValidateResponse)
    async def validate_dag(
        request: DAGCreateRequest,
        _token: str = Depends(auth) if auth else lambda: "",
    ) -> DAGValidateResponse:
        """Validate a DAG for cycles and errors."""
        try:
            dag = _create_dag_from_request(request)
            dag.validate()
            levels = dag.topological_sort()

            return DAGValidateResponse(
                valid=True,
                dag_id=dag.dag_id,
                job_count=dag.job_count,
                levels=levels,
                error=None,
            )
        except DAGCycleError as e:
            return DAGValidateResponse(
                valid=False,
                dag_id=request.dag_id or "",
                job_count=len(request.jobs),
                levels=[],
                error=f"Cycle detected: {e}",
            )
        except DAGError as e:
            return DAGValidateResponse(
                valid=False,
                dag_id=request.dag_id or "",
                job_count=len(request.jobs),
                levels=[],
                error=str(e),
            )

    @router.post("/visualize", response_model=DAGVisualizationResponse)
    async def visualize_dag(
        request: DAGCreateRequest,
        _token: str = Depends(auth) if auth else lambda: "",
    ) -> DAGVisualizationResponse:
        """Get DAG visualization data."""
        try:
            dag = _create_dag_from_request(request)
            dag.validate()

            # Build edges for visualization
            edges = []
            for job in dag.jobs.values():
                for dep_id in job.depends_on:
                    edges.append({"from": dep_id, "to": job.id})

            # Get job details with dependents
            jobs_data = []
            for job in dag.jobs.values():
                jobs_data.append(
                    {
                        "id": job.id,
                        "priority": {"name": job.priority.name, "value": job.priority.value},
                        "depends_on": job.depends_on,
                        "dependents": list(dag.get_dependents(job.id)),
                        "max_retries": job.max_retries,
                        "job_type": job.job_type,
                        "metadata": job.metadata,
                    }
                )

            return DAGVisualizationResponse(
                dag_id=dag.dag_id,
                job_count=dag.job_count,
                jobs=jobs_data,
                levels=dag.topological_sort(),
                edges=edges,
            )
        except DAGError as e:
            raise HTTPException(status_code=400, detail=str(e)) from e

    # =============================================================================
    # DAG Execution API

    @router.post("/execute", response_model=DAGExecuteResponse)
    async def execute_dag(
        request: DAGExecuteRequest,
        _token: str = Depends(auth) if auth else lambda: "",
    ) -> DAGExecuteResponse:
        """Execute a DAG synchronously (for small DAGs)."""
        try:
            dag = _create_dag_from_execute_request(request)

            # Simple handler that just returns the job ID
            async def handler(job: DAGJob) -> str:
                await asyncio.sleep(0.01)  # Simulate work
                return f"completed-{job.id}"

            result = await execute_dag_async(
                dag=dag,
                handler=handler,
                max_workers=request.max_workers,
                continue_on_error=request.continue_on_error,
            )

            return _format_dag_result(result)
        except DAGError as e:
            raise HTTPException(status_code=400, detail=str(e)) from e
        except Exception as e:
            logger.exception("DAG execution failed")
            raise HTTPException(status_code=500, detail=str(e)) from e

    @router.post("/execute/async")
    async def execute_dag_async_endpoint(
        request: DAGExecuteRequest,
        _token: str = Depends(auth) if auth else lambda: "",
    ) -> JSONResponse:
        """Start async DAG execution and return execution ID."""
        try:
            dag = _create_dag_from_execute_request(request)

            execution_id = f"exec_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{dag.dag_id}"
            executor = DAGExecutor(
                max_workers=request.max_workers,
                continue_on_error=request.continue_on_error,
            )
            active_executions[execution_id] = executor

            return JSONResponse(
                content={
                    "execution_id": execution_id,
                    "dag_id": dag.dag_id,
                    "status": "started",
                    "job_count": dag.job_count,
                },
                status_code=202,
            )
        except DAGError as e:
            raise HTTPException(status_code=400, detail=str(e)) from e

    @router.get("/execute/{execution_id}/status")
    async def get_execution_status(
        execution_id: str,
        _token: str = Depends(auth) if auth else lambda: "",
    ) -> DAGProgressResponse:
        """Get status of an async DAG execution."""
        if execution_id not in active_executions:
            raise HTTPException(status_code=404, detail="Execution not found")

        executor = active_executions[execution_id]
        progress = executor.get_progress()

        return DAGProgressResponse(
            total_jobs=progress.total_jobs,
            completed_jobs=progress.completed_jobs,
            failed_jobs=progress.failed_jobs,
            running_jobs=progress.running_jobs,
            pending_jobs=progress.pending_jobs,
            skipped_jobs=progress.skipped_jobs,
            percent_complete=progress.percent_complete,
            is_complete=progress.is_complete,
            duration_seconds=progress.duration_seconds,
        )

    @router.delete("/execute/{execution_id}")
    async def cancel_execution(
        execution_id: str,
        _token: str = Depends(auth) if auth else lambda: "",
    ) -> JSONResponse:
        """Cancel a running DAG execution."""
        if execution_id not in active_executions:
            raise HTTPException(status_code=404, detail="Execution not found")

        executor = active_executions[execution_id]
        executor.cancel()

        return JSONResponse(content={"cancelled": True})

    # =============================================================================
    # WebSocket for Real-time Updates

    @router.websocket("/ws/execute")
    async def dag_execution_websocket(
        websocket: WebSocket,
        token: str = Query(...),
    ) -> None:
        """WebSocket endpoint for real-time DAG execution updates."""
        # Validate token
        if auth and token != auth.get_token() and token != "demo":
            await websocket.close(code=1008)
            return

        await websocket.accept()

        try:
            await websocket.send_json(
                {
                    "type": "connected",
                    "data": {"message": "DAG execution WebSocket connected"},
                }
            )

            while True:
                # Receive message
                raw_data = await websocket.receive_text()
                try:
                    data = json.loads(raw_data)
                    msg_type = data.get("type", "")

                    if msg_type == "ping":
                        await websocket.send_json({"type": "pong", "data": {}})

                    elif msg_type == "execute":
                        # Execute DAG with progress updates
                        dag_data = data.get("dag", {})
                        request = DAGExecuteRequest.model_validate(dag_data)
                        dag = _create_dag_from_execute_request(request)

                        executor = DAGExecutor(
                            max_workers=request.max_workers,
                            continue_on_error=request.continue_on_error,
                        )

                        async def handler(job: DAGJob) -> str:
                            await websocket.send_json(
                                {
                                    "type": "job_started",
                                    "data": {"job_id": job.id},
                                }
                            )
                            await asyncio.sleep(0.01)  # Simulate work
                            await websocket.send_json(
                                {
                                    "type": "job_completed",
                                    "data": {"job_id": job.id},
                                }
                            )
                            return f"completed-{job.id}"

                        result = await executor.execute_dag_async(dag, handler)

                        await websocket.send_json(
                            {
                                "type": "execution_completed",
                                "data": _format_dag_result(result).model_dump(),
                            }
                        )

                    elif msg_type == "validate":
                        # Validate DAG
                        dag_data = data.get("dag", {})
                        request = DAGCreateRequest.model_validate(dag_data)
                        dag = _create_dag_from_request(request)

                        try:
                            dag.validate()
                            await websocket.send_json(
                                {
                                    "type": "validation_result",
                                    "data": {"valid": True, "dag_id": dag.dag_id},
                                }
                            )
                        except DAGCycleError as e:
                            await websocket.send_json(
                                {
                                    "type": "validation_result",
                                    "data": {"valid": False, "error": str(e)},
                                }
                            )

                    else:
                        await websocket.send_json(
                            {
                                "type": "error",
                                "data": {"message": f"Unknown message type: {msg_type}"},
                            }
                        )

                except json.JSONDecodeError as e:
                    await websocket.send_json(
                        {
                            "type": "error",
                            "data": {"message": f"Invalid JSON: {e}"},
                        }
                    )
                except Exception as e:
                    await websocket.send_json(
                        {
                            "type": "error",
                            "data": {"message": str(e)},
                        }
                    )

        except WebSocketDisconnect:
            pass

    return router


# =============================================================================
# Helper Functions


def _create_dag_from_request(request: DAGCreateRequest) -> DAG:
    """Create a DAG from a create request."""
    dag = DAG(dag_id=request.dag_id)

    for job_data in request.jobs:
        dag.add_job(
            job_id=job_data.id,
            payload=job_data.payload,
            priority=JobPriority(job_data.priority),
            depends_on=job_data.depends_on,
            max_retries=job_data.max_retries,
            job_type=job_data.job_type,
            metadata=job_data.metadata,
        )

    return dag


def _create_dag_from_execute_request(request: DAGExecuteRequest) -> DAG:
    """Create a DAG from an execute request."""
    dag = DAG(dag_id=request.dag_id)

    for job_data in request.jobs:
        dag.add_job(
            job_id=job_data.id,
            payload=job_data.payload,
            priority=JobPriority(job_data.priority),
            depends_on=job_data.depends_on,
            max_retries=job_data.max_retries,
            job_type=job_data.job_type,
            metadata=job_data.metadata,
        )

    return dag


def _format_dag_result(result: DAGResult) -> DAGExecuteResponse:
    """Format a DAG result for the API response."""
    job_results = {}
    for job_id, job_result in result.job_results.items():
        job_results[job_id] = DAGJobResultResponse(
            job_id=job_result.job_id,
            success=job_result.success,
            result=job_result.result,
            error=job_result.error,
            duration_seconds=job_result.duration_seconds,
            retry_count=job_result.retry_count,
        )

    progress = result.progress
    progress_response = DAGProgressResponse(
        total_jobs=progress.total_jobs,
        completed_jobs=progress.completed_jobs,
        failed_jobs=progress.failed_jobs,
        running_jobs=progress.running_jobs,
        pending_jobs=progress.pending_jobs,
        skipped_jobs=progress.skipped_jobs,
        percent_complete=progress.percent_complete,
        is_complete=progress.is_complete,
        duration_seconds=progress.duration_seconds,
    )

    return DAGExecuteResponse(
        dag_id=result.dag_id,
        status=result.status.value,
        job_results=job_results,
        progress=progress_response,
        all_succeeded=result.all_succeeded,
        failed_job_ids=result.failed_job_ids,
        error=result.error,
    )


def _get_dag_dashboard_html() -> str:
    """Get the DAG dashboard HTML."""
    return """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OCLAWMA DAG Dashboard</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #0f172a;
            color: #e2e8f0;
            min-height: 100vh;
        }
        .header {
            background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
            padding: 1.5rem 2rem;
            border-bottom: 1px solid #334155;
        }
        .header h1 {
            font-size: 1.5rem;
            font-weight: 600;
            background: linear-gradient(90deg, #60a5fa, #a78bfa);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        .container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 2rem;
        }
        .grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 1.5rem;
            margin-bottom: 1.5rem;
        }
        @media (max-width: 900px) {
            .grid { grid-template-columns: 1fr; }
        }
        .card {
            background: #1e293b;
            border-radius: 12px;
            border: 1px solid #334155;
            overflow: hidden;
        }
        .card-header {
            background: #252f47;
            padding: 1rem 1.25rem;
            border-bottom: 1px solid #334155;
            font-weight: 600;
            color: #94a3b8;
        }
        .card-body {
            padding: 1.25rem;
        }
        .form-group {
            margin-bottom: 1rem;
        }
        label {
            display: block;
            margin-bottom: 0.5rem;
            color: #94a3b8;
            font-size: 0.875rem;
        }
        input, textarea, select {
            width: 100%;
            padding: 0.625rem 0.875rem;
            background: #0f172a;
            border: 1px solid #334155;
            border-radius: 6px;
            color: #e2e8f0;
            font-size: 0.875rem;
        }
        input:focus, textarea:focus, select:focus {
            outline: none;
            border-color: #60a5fa;
        }
        textarea {
            min-height: 200px;
            font-family: 'Monaco', 'Menlo', monospace;
            resize: vertical;
        }
        .btn {
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
            padding: 0.625rem 1.25rem;
            background: #3b82f6;
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 0.875rem;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        }
        .btn:hover { background: #2563eb; }
        .btn-success { background: #10b981; }
        .btn-success:hover { background: #059669; }
        .btn-danger { background: #ef4444; }
        .btn-danger:hover { background: #dc2626; }
        .btn-group {
            display: flex;
            gap: 0.75rem;
            margin-top: 1rem;
        }
        .status {
            padding: 0.5rem 1rem;
            border-radius: 6px;
            font-size: 0.875rem;
            font-weight: 500;
        }
        .status-success { background: rgba(16, 185, 129, 0.2); color: #34d399; }
        .status-error { background: rgba(239, 68, 68, 0.2); color: #f87171; }
        .status-info { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
        .progress-bar {
            height: 8px;
            background: #334155;
            border-radius: 4px;
            overflow: hidden;
            margin-top: 1rem;
        }
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #3b82f6, #8b5cf6);
            border-radius: 4px;
            transition: width 0.3s ease;
        }
        .job-list {
            list-style: none;
        }
        .job-item {
            display: flex;
            align-items: center;
            gap: 0.75rem;
            padding: 0.75rem;
            background: #0f172a;
            border-radius: 6px;
            margin-bottom: 0.5rem;
            font-family: monospace;
            font-size: 0.875rem;
        }
        .job-status {
            width: 8px;
            height: 8px;
            border-radius: 50%;
        }
        .job-status-pending { background: #6b7280; }
        .job-status-running { background: #3b82f6; animation: pulse 1.5s infinite; }
        .job-status-completed { background: #10b981; }
        .job-status-failed { background: #ef4444; }
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 1rem;
            margin-bottom: 1rem;
        }
        .stat-item {
            text-align: center;
            padding: 1rem;
            background: #0f172a;
            border-radius: 8px;
        }
        .stat-value {
            font-size: 1.5rem;
            font-weight: 700;
            color: #60a5fa;
        }
        .stat-label {
            font-size: 0.75rem;
            color: #64748b;
            text-transform: uppercase;
            margin-top: 0.25rem;
        }
        #visualization {
            min-height: 300px;
            background: #0f172a;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .help-text {
            color: #64748b;
            font-size: 0.75rem;
            margin-top: 0.25rem;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>🔀 DAG Workflow Dashboard</h1>
    </div>
    <div class="container">
        <div class="grid">
            <div class="card">
                <div class="card-header">DAG Definition</div>
                <div class="card-body">
                    <div class="form-group">
                        <label for="dagInput">DAG JSON Definition</label>
                        <textarea id="dagInput" placeholder='{
  "dag_id": "my-workflow",
  "jobs": [
    {"id": "fetch-data", "payload": {"url": "..."}},
    {"id": "process", "depends_on": ["fetch-data"]},
    {"id": "save", "depends_on": ["process"]}
  ]
}'></textarea>
                        <div class="help-text">Define jobs and their dependencies. Jobs with depends_on will run after their dependencies complete.</div>
                    </div>
                    <div class="form-group">
                        <label for="maxWorkers">Max Workers</label>
                        <input type="number" id="maxWorkers" value="4" min="1" max="20">
                    </div>
                    <div class="btn-group">
                        <button class="btn" onclick="validateDAG()">Validate</button>
                        <button class="btn btn-success" onclick="executeDAG()">Execute</button>
                        <button class="btn" onclick="visualizeDAG()">Visualize</button>
                    </div>
                    <div id="status" style="margin-top: 1rem;"></div>
                </div>
            </div>
            <div class="card">
                <div class="card-header">Execution Status</div>
                <div class="card-body">
                    <div class="stats-grid">
                        <div class="stat-item">
                            <div class="stat-value" id="totalJobs">0</div>
                            <div class="stat-label">Total</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-value" id="completedJobs">0</div>
                            <div class="stat-label">Completed</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-value" id="failedJobs">0</div>
                            <div class="stat-label">Failed</div>
                        </div>
                    </div>
                    <div class="progress-bar">
                        <div class="progress-fill" id="progressBar" style="width: 0%"></div>
                    </div>
                    <ul class="job-list" id="jobList"></ul>
                </div>
            </div>
        </div>
        <div class="card">
            <div class="card-header">DAG Visualization</div>
            <div class="card-body">
                <div id="visualization">
                    <p style="color: #64748b;">Click "Visualize" to see the DAG structure</p>
                </div>
            </div>
        </div>
    </div>

    <script>
        const API_BASE = '/dag';
        let ws = null;

        function getToken() {
            // In production, this should get the auth token properly
            return new URLSearchParams(window.location.search).get('token') || 'demo';
        }

        function getDAGData() {
            try {
                const input = document.getElementById('dagInput').value;
                if (!input.trim()) {
                    throw new Error('Please enter a DAG definition');
                }
                return JSON.parse(input);
            } catch (e) {
                showStatus('Invalid JSON: ' + e.message, 'error');
                return null;
            }
        }

        function showStatus(message, type) {
            const status = document.getElementById('status');
            status.className = 'status status-' + type;
            status.textContent = message;
        }

        async function validateDAG() {
            const data = getDAGData();
            if (!data) return;

            try {
                const response = await fetch(`${API_BASE}/validate`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${getToken()}`
                    },
                    body: JSON.stringify(data)
                });

                const result = await response.json();
                if (result.valid) {
                    showStatus(`✓ Valid DAG with ${result.job_count} jobs, ${result.levels.length} levels`, 'success');
                } else {
                    showStatus('✗ ' + result.error, 'error');
                }
            } catch (e) {
                showStatus('Error: ' + e.message, 'error');
            }
        }

        async function executeDAG() {
            const data = getDAGData();
            if (!data) return;

            const maxWorkers = parseInt(document.getElementById('maxWorkers').value) || 4;

            showStatus('Executing DAG...', 'info');
            updateProgress({total_jobs: 0, completed_jobs: 0, failed_jobs: 0, percent_complete: 0});

            try {
                const response = await fetch(`${API_BASE}/execute`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${getToken()}`
                    },
                    body: JSON.stringify({...data, max_workers: maxWorkers})
                });

                const result = await response.json();
                if (result.status === 'completed') {
                    showStatus('✓ DAG executed successfully', 'success');
                } else {
                    showStatus(`✗ DAG failed: ${result.error || 'Unknown error'}`, 'error');
                }
                updateProgress(result.progress);
                updateJobList(result.job_results);
            } catch (e) {
                showStatus('Error: ' + e.message, 'error');
            }
        }

        async function visualizeDAG() {
            const data = getDAGData();
            if (!data) return;

            try {
                const response = await fetch(`${API_BASE}/visualize`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${getToken()}`
                    },
                    body: JSON.stringify(data)
                });

                const result = await response.json();
                const viz = document.getElementById('visualization');
                viz.innerHTML = `
                    <div style="text-align: left; width: 100%; padding: 1rem;">
                        <p><strong>DAG ID:</strong> ${result.dag_id}</p>
                        <p><strong>Jobs:</strong> ${result.job_count}</p>
                        <p><strong>Levels:</strong> ${result.levels.length}</p>
                        <p style="margin-top: 1rem;"><strong>Execution Order:</strong></p>
                        ${result.levels.map((level, i) => `
                            <div style="margin: 0.5rem 0; padding: 0.5rem; background: #1e293b; border-radius: 4px;">
                                Level ${i + 1}: ${level.join(', ')}
                            </div>
                        `).join('')}
                        <p style="margin-top: 1rem;"><strong>Dependencies:</strong></p>
                        ${result.edges.map(edge => `
                            <div style="margin: 0.25rem 0; font-family: monospace; font-size: 0.75rem;">
                                ${edge.from} → ${edge.to}
                            </div>
                        `).join('')}
                    </div>
                `;
            } catch (e) {
                showStatus('Error: ' + e.message, 'error');
            }
        }

        function updateProgress(progress) {
            document.getElementById('totalJobs').textContent = progress.total_jobs;
            document.getElementById('completedJobs').textContent = progress.completed_jobs;
            document.getElementById('failedJobs').textContent = progress.failed_jobs;
            document.getElementById('progressBar').style.width = progress.percent_complete + '%';
        }

        function updateJobList(jobResults) {
            const list = document.getElementById('jobList');
            list.innerHTML = Object.values(jobResults).map(job => `
                <li class="job-item">
                    <span class="job-status job-status-${job.success ? 'completed' : 'failed'}"></span>
                    <span>${job.job_id}</span>
                    ${job.duration_seconds ? `<span style="margin-left: auto; color: #64748b;">${job.duration_seconds.toFixed(2)}s</span>` : ''}
                </li>
            `).join('');
        }

        // WebSocket for real-time updates
        function connectWebSocket() {
            const wsUrl = `ws://${window.location.host}/dag/ws/execute?token=${getToken()}`;
            ws = new WebSocket(wsUrl);

            ws.onopen = () => console.log('DAG WebSocket connected');
            ws.onmessage = (event) => {
                const msg = JSON.parse(event.data);
                console.log('WS message:', msg);

                if (msg.type === 'job_started') {
                    showStatus(`Running job: ${msg.data.job_id}`, 'info');
                } else if (msg.type === 'job_completed') {
                    // Progress update will come via progress callback
                } else if (msg.type === 'execution_completed') {
                    showStatus('Execution completed', msg.data.all_succeeded ? 'success' : 'error');
                    updateProgress(msg.data.progress);
                    updateJobList(msg.data.job_results);
                }
            };
            ws.onclose = () => setTimeout(connectWebSocket, 5000);
        }

        // Connect WebSocket on load
        connectWebSocket();
    </script>
</body>
</html>
"""
