"""
FastAPI WebSocket Server for the Console Dashboard.

Provides real-time streaming of:
- Console log entries
- Reasoning loop steps
- Agent activity updates
- Cortex AI interactions
"""

import asyncio
import json
import logging
from contextlib import asynccontextmanager
from typing import Any, Dict, List, Optional
import os
import uvicorn

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
from pathlib import Path

from .handlers import ConnectionManager
from .broadcaster import ConsoleBroadcaster
from .types import WebSocketMessage, WebSocketMessageType
from src.codex_graphrag.contracts import VALID_ACTIONS, error_payload, validate_discrepancy_event
from src.codex_graphrag.core import (
    build_index,
    search_index,
    recommend_workflow,
    review_workflow,
    ingest_discrepancy_event,
)
from src.codex_graphrag.closure_engine import run_closure_loop, get_closure_metrics, trace_discrepancy, propose_fix
from src.codex_graphrag.trust_policy import explain_trust_event, get_retention_status, get_trust_metrics

logger = logging.getLogger(__name__)


class ConsoleServer:
    """
    WebSocket server for the Console Dashboard.

    Manages the FastAPI application and WebSocket connections.
    """

    def __init__(
        self,
        host: str = "0.0.0.0",
        port: int = 8080,
        redis_url: Optional[str] = None,
    ):
        self.host = host
        self.port = port
        self.redis_url = redis_url
        self._app: Optional[FastAPI] = None
        self._server: Optional[uvicorn.Server] = None
        self._server_task: Optional[asyncio.Task] = None
        self._running = False

        # Initialize broadcaster and connection manager
        self.broadcaster = ConsoleBroadcaster.get_instance(redis_url)
        self.connection_manager = ConnectionManager(self.broadcaster)

    def create_app(self) -> FastAPI:
        """Create the FastAPI application."""

        @asynccontextmanager
        async def lifespan(app: FastAPI):
            # Startup
            logger.info(f"Console server starting on {self.host}:{self.port}")
            self._running = True
            yield
            # Shutdown
            logger.info("Console server shutting down")
            self._running = False

        app = FastAPI(
            title="DataBridge Console",
            description="Real-time agent activity streaming",
            version="1.0.0",
            lifespan=lifespan,
        )

        # Mount static files for dashboard
        dashboard_path = Path(__file__).parent.parent.parent / "console_dashboard"
        if dashboard_path.exists():
            app.mount("/static", StaticFiles(directory=str(dashboard_path)), name="static")

        # Register routes
        self._register_routes(app)

        self._app = app
        return app

    def _register_routes(self, app: FastAPI) -> None:
        """Register HTTP and WebSocket routes."""

        @app.get("/", response_class=HTMLResponse)
        async def root():
            """Serve the dashboard or redirect info."""
            dashboard_path = Path(__file__).parent.parent.parent / "console_dashboard" / "index.html"
            if dashboard_path.exists():
                return HTMLResponse(content=dashboard_path.read_text())
            return HTMLResponse(content="""
                <html>
                    <head><title>DataBridge Console</title></head>
                    <body>
                        <h1>DataBridge Console Server</h1>
                        <p>WebSocket endpoint: <code>ws://localhost:{port}/ws/console</code></p>
                        <p>Dashboard not found. Place index.html in console_dashboard/</p>
                    </body>
                </html>
            """.format(port=self.port))

        @app.get("/health")
        async def health():
            """Health check endpoint."""
            return JSONResponse({
                "status": "healthy",
                "connections": self.connection_manager.get_connection_count(),
                "broadcaster": self.broadcaster.get_stats(),
            })

        @app.get("/stats")
        async def stats():
            """Get server statistics."""
            return JSONResponse({
                "connections": self.connection_manager.get_stats(),
                "broadcaster": self.broadcaster.get_stats(),
            })

        @app.get("/connections")
        async def connections():
            """List active connections."""
            return JSONResponse({
                "connections": [
                    conn.model_dump()
                    for conn in self.connection_manager.get_connections()
                ]
            })

        @app.get("/agents")
        async def agents(include_static: bool = True):
            """List known agents from observed runtime events and static registries."""
            return JSONResponse(self._build_agent_inventory(include_static=include_static))

        @app.post("/api/rag/action")
        async def rag_action(payload: dict):
            """Run real Codex GraphRAG actions for dashboard testing."""
            action = str(payload.get("action", "")).strip().lower()
            docs_root = str(payload.get("docs_root", "docs/Codex"))
            index_path = str(payload.get("index_path", "data/codex_graphrag/index.json"))
            valid_actions = list(VALID_ACTIONS)

            try:
                if action == "build":
                    return JSONResponse(build_index(docs_root=docs_root, index_path=index_path))

                if action == "search":
                    query = str(payload.get("query", "")).strip()
                    if not query:
                        return JSONResponse(
                            error_payload(
                                code="missing_query",
                                message="query is required for action='search'",
                                action=action,
                                details={"required_field": "query"},
                            ),
                            status_code=400,
                        )
                    return JSONResponse(
                        search_index(
                            index_path=index_path,
                            query=query,
                            k=int(payload.get("k", 10)),
                            expand_depth=int(payload.get("expand_depth", 1)),
                            category=str(payload.get("category", "")),
                            domain=str(payload.get("domain", "")),
                            persona=str(payload.get("persona", "balanced")),
                            response_mode=str(payload.get("response_mode", "plain")),
                            include_evidence=bool(payload.get("include_evidence", True)),
                            include_guardrails=bool(payload.get("include_guardrails", False)),
                        )
                    )

                if action == "workflow":
                    goal = str(payload.get("goal", "")).strip()
                    if not goal:
                        return JSONResponse(
                            error_payload(
                                code="missing_goal",
                                message="goal is required for action='workflow'",
                                action=action,
                                details={"required_field": "goal"},
                            ),
                            status_code=400,
                        )
                    return JSONResponse(
                        recommend_workflow(
                            index_path=index_path,
                            goal=goal,
                            max_steps=int(payload.get("max_steps", 10)),
                            domain=str(payload.get("domain", "")),
                            persona=str(payload.get("persona", "balanced")),
                            response_mode=str(payload.get("response_mode", "plain")),
                            include_evidence=bool(payload.get("include_evidence", True)),
                            include_guardrails=bool(payload.get("include_guardrails", False)),
                        )
                    )

                if action == "review":
                    goal = str(payload.get("goal", "")).strip()
                    if not goal:
                        return JSONResponse(
                            error_payload(
                                code="missing_goal",
                                message="goal is required for action='review'",
                                action=action,
                                details={"required_field": "goal"},
                            ),
                            status_code=400,
                        )
                    return JSONResponse(
                        review_workflow(
                            index_path=index_path,
                            goal=goal,
                            max_steps=int(payload.get("max_steps", 10)),
                            domain=str(payload.get("domain", "")),
                            review_mode=str(payload.get("review_mode", "balanced")),
                            persona=str(payload.get("persona", "balanced")),
                            response_mode=str(payload.get("response_mode", "plain")),
                            include_evidence=bool(payload.get("include_evidence", True)),
                            include_guardrails=bool(payload.get("include_guardrails", True)),
                        )
                    )

                if action == "ingest_discrepancy":
                    event = payload.get("event")
                    validation_error = validate_discrepancy_event(
                        event,
                        action=action,
                        strict_contract=bool(payload.get("strict_contract", True)),
                    )
                    if validation_error:
                        return JSONResponse(validation_error, status_code=400)
                    return JSONResponse(
                        ingest_discrepancy_event(
                            event=event,
                            index_path=index_path,
                            docs_root=docs_root,
                            runtime_events_path=str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json")),
                            auto_build=bool(payload.get("auto_build", True)),
                            tenant_id=str(payload.get("tenant_id", event.get("tenant_id", "default"))),
                            ingest_mode=str(payload.get("ingest_mode", "dual")),
                            dedupe_window_seconds=int(payload.get("dedupe_window_seconds", 900)),
                            policy_mode=str(payload.get("policy_mode", "shadow")),
                        )
                    )

                if action == "closure_loop":
                    event = payload.get("event")
                    validation_error = validate_discrepancy_event(
                        event,
                        action=action,
                        strict_contract=bool(payload.get("strict_contract", True)),
                    )
                    if validation_error:
                        return JSONResponse(validation_error, status_code=400)
                    return JSONResponse(
                        run_closure_loop(
                            event=event,
                            index_path=index_path,
                            docs_root=docs_root,
                            runtime_events_path=str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json")),
                            auto_ingest=bool(payload.get("auto_ingest", True)),
                            allow_cortex=bool(payload.get("allow_cortex", False)),
                            tenant_id=str(payload.get("tenant_id", event.get("tenant_id", "default"))),
                            ingest_mode=str(payload.get("ingest_mode", "dual")),
                            dedupe_window_seconds=int(payload.get("dedupe_window_seconds", 900)),
                            policy_mode=str(payload.get("policy_mode", "shadow")),
                        )
                    )

                if action == "closure_metrics":
                    return JSONResponse(get_closure_metrics(metrics_path=str(payload.get("metrics_path", "data/codex_graphrag/closure_metrics.json"))))

                if action == "trust_metrics":
                    return JSONResponse(
                        get_trust_metrics(
                            runtime_events_path=str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json")),
                            closure_metrics_path=str(payload.get("metrics_path", "data/codex_graphrag/closure_metrics.json")),
                        )
                    )

                if action == "retention_status":
                    return JSONResponse(
                        get_retention_status(
                            runtime_events_path=str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json")),
                        )
                    )

                if action == "trust_explain":
                    event_key = str(payload.get("event_key", "")).strip()
                    if not event_key:
                        return JSONResponse(
                            error_payload(
                                code="missing_event_key",
                                message="event_key is required for action='trust_explain'",
                                action=action,
                                details={"required_field": "event_key"},
                            ),
                            status_code=400,
                        )
                    out = explain_trust_event(
                        event_key=event_key,
                        runtime_events_path=str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json")),
                    )
                    if out.get("status") == "error":
                        return JSONResponse(out, status_code=404)
                    return JSONResponse(out)

                if action == "propose_fix":
                    event = payload.get("event")
                    validation_error = validate_discrepancy_event(
                        event,
                        action=action,
                        strict_contract=bool(payload.get("strict_contract", True)),
                    )
                    if validation_error:
                        return JSONResponse(validation_error, status_code=400)
                    trace = trace_discrepancy(event, index_path=index_path)
                    fix_result = propose_fix(
                        trace,
                        index_path=index_path,
                        event=event,
                        allow_cortex=bool(payload.get("allow_cortex", False)),
                    )
                    return JSONResponse(fix_result)

                return JSONResponse(
                    error_payload(
                        code="invalid_action",
                        message=f"Invalid action '{action}'",
                        action=action,
                        valid_actions=valid_actions,
                    ),
                    status_code=400,
                )
            except Exception as exc:
                logger.exception("RAG action failed")
                return JSONResponse(
                    error_payload(
                        code="internal_error",
                        message=str(exc),
                        action=action,
                        retriable=True,
                    ),
                    status_code=500,
                )

        @app.post("/api/rag/events/discrepancy")
        async def ingest_discrepancy_endpoint(payload: dict):
            event = dict(payload or {})
            strict = bool(event.pop("strict_contract", True))
            tenant_id = str(event.get("tenant_id", "default"))
            validation_error = validate_discrepancy_event(event, action="ingest_discrepancy", strict_contract=strict)
            if validation_error:
                return JSONResponse(validation_error, status_code=400)
            result = ingest_discrepancy_event(
                event=event,
                index_path=str(payload.get("index_path", "data/codex_graphrag/index.json")),
                docs_root=str(payload.get("docs_root", "docs/Codex")),
                runtime_events_path=str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json")),
                auto_build=bool(payload.get("auto_build", True)),
                tenant_id=tenant_id,
                ingest_mode=str(payload.get("ingest_mode", "dual")),
                dedupe_window_seconds=int(payload.get("dedupe_window_seconds", 900)),
                policy_mode=str(payload.get("policy_mode", "shadow")),
            )
            return JSONResponse(result)

        @app.post("/api/rag/events/discrepancy/batch")
        async def ingest_discrepancy_batch_endpoint(payload: dict):
            events = payload.get("events", [])
            if not isinstance(events, list):
                return JSONResponse(
                    error_payload(
                        code="invalid_batch",
                        message="events must be a JSON array",
                        action="ingest_discrepancy_batch",
                    ),
                    status_code=400,
                )
            docs_root = str(payload.get("docs_root", "docs/Codex"))
            index_path = str(payload.get("index_path", "data/codex_graphrag/index.json"))
            runtime_events_path = str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json"))
            ingest_mode = str(payload.get("ingest_mode", "dual"))
            dedupe_window_seconds = int(payload.get("dedupe_window_seconds", 900))
            auto_build = bool(payload.get("auto_build", True))
            strict = bool(payload.get("strict_contract", True))
            results: List[Dict[str, Any]] = []
            ok_count = 0
            error_count = 0
            for idx_row, event in enumerate(events):
                validation_error = validate_discrepancy_event(event, action="ingest_discrepancy", strict_contract=strict)
                if validation_error:
                    results.append(
                        {
                            "index": idx_row,
                            "status": "error",
                            "error": validation_error,
                        }
                    )
                    error_count += 1
                    continue
                out = ingest_discrepancy_event(
                    event=event,
                    index_path=index_path,
                    docs_root=docs_root,
                    runtime_events_path=runtime_events_path,
                    auto_build=auto_build,
                    tenant_id=str(event.get("tenant_id", "default")),
                    ingest_mode=ingest_mode,
                    dedupe_window_seconds=dedupe_window_seconds,
                    policy_mode=str(payload.get("policy_mode", "shadow")),
                )
                if out.get("status") in {"ok", "partial_ok"}:
                    ok_count += 1
                else:
                    error_count += 1
                results.append({"index": idx_row, "result": out})
            return JSONResponse(
                {
                    "status": "ok" if error_count == 0 else "partial_ok",
                    "count": len(events),
                    "ok_count": ok_count,
                    "error_count": error_count,
                    "results": results,
                }
            )

        @app.post("/api/rag/events/reset")
        async def reset_discrepancy_events_endpoint(payload: dict):
            runtime_events_path = str(payload.get("runtime_events_path", "data/codex_graphrag/runtime_discrepancy_events.json"))
            metrics_path = str(payload.get("metrics_path", "data/codex_graphrag/closure_metrics.json"))
            reset_summary: Dict[str, Any] = {"status": "ok", "files": {}}
            try:
                runtime_file = Path(runtime_events_path)
                runtime_file.parent.mkdir(parents=True, exist_ok=True)
                runtime_file.write_text("[]\n", encoding="utf-8")
                reset_summary["files"]["runtime_events"] = runtime_events_path

                metrics_file = Path(metrics_path)
                metrics_file.parent.mkdir(parents=True, exist_ok=True)
                metrics_file.write_text(
                    json.dumps(
                        {
                            "total_runs": 0,
                            "reconciliation_pass_rate": 0.0,
                            "avg_elapsed_seconds": 0.0,
                            "last_updated": None,
                            "runs": [],
                        },
                        indent=2,
                    ) + "\n",
                    encoding="utf-8",
                )
                reset_summary["files"]["closure_metrics"] = metrics_path
                return JSONResponse(reset_summary)
            except Exception as exc:
                logger.exception("Failed to reset discrepancy runtime files")
                return JSONResponse(
                    error_payload(
                        code="reset_failed",
                        message=str(exc),
                        action="reset_discrepancy_events",
                        retriable=True,
                    ),
                    status_code=500,
                )

        @app.get("/api/rag/events/discrepancy/{event_key}")
        async def get_discrepancy_event(event_key: str, runtime_events_path: str = "data/codex_graphrag/runtime_discrepancy_events.json"):
            p = Path(runtime_events_path)
            if not p.exists():
                return JSONResponse(
                    error_payload(
                        code="not_found",
                        message="runtime events file not found",
                        action="get_discrepancy_event",
                    ),
                    status_code=404,
                )
            try:
                payload = json.loads(p.read_text(encoding="utf-8"))
            except Exception:
                payload = []
            if not isinstance(payload, list):
                payload = []
            for row in payload:
                if isinstance(row, dict) and str(row.get("event_key", "")) == str(event_key):
                    return JSONResponse({"status": "ok", "event": row})
            return JSONResponse(
                error_payload(
                    code="not_found",
                    message=f"event_key '{event_key}' not found",
                    action="get_discrepancy_event",
                ),
                status_code=404,
            )

        @app.get("/api/rag/trust/metrics")
        async def get_trust_metrics_endpoint(
            runtime_events_path: str = "data/codex_graphrag/runtime_discrepancy_events.json",
            metrics_path: str = "data/codex_graphrag/closure_metrics.json",
        ):
            return JSONResponse(
                get_trust_metrics(
                    runtime_events_path=runtime_events_path,
                    closure_metrics_path=metrics_path,
                )
            )

        @app.get("/api/rag/trust/explain/{event_key}")
        async def get_trust_explain_endpoint(
            event_key: str,
            runtime_events_path: str = "data/codex_graphrag/runtime_discrepancy_events.json",
        ):
            out = explain_trust_event(
                event_key=event_key,
                runtime_events_path=runtime_events_path,
            )
            if out.get("status") == "error":
                return JSONResponse(out, status_code=404)
            return JSONResponse(out)

        @app.get("/api/rag/trust/retention/status")
        async def get_retention_status_endpoint(
            runtime_events_path: str = "data/codex_graphrag/runtime_discrepancy_events.json",
        ):
            return JSONResponse(
                get_retention_status(
                    runtime_events_path=runtime_events_path,
                )
            )

        @app.websocket("/ws/console")
        async def websocket_console(
            websocket: WebSocket,
            channels: str = Query(default="console,reasoning,agents,cortex"),
        ):
            """Main console WebSocket endpoint."""
            await self._handle_websocket(websocket, channels.split(","))

        @app.websocket("/ws/console/{session_id}")
        async def websocket_console_session(
            websocket: WebSocket,
            session_id: str,
        ):
            """Session-specific console WebSocket endpoint."""
            await self._handle_websocket(websocket, ["console", "reasoning", "agents", "cortex"], session_id)

        @app.websocket("/ws/agent/{agent_id}")
        async def websocket_agent(
            websocket: WebSocket,
            agent_id: str,
        ):
            """Agent-specific WebSocket endpoint."""
            await self._handle_websocket(websocket, ["agents"], filter_agent=agent_id)

        @app.websocket("/ws/reasoning/{conversation_id}")
        async def websocket_reasoning(
            websocket: WebSocket,
            conversation_id: str,
        ):
            """Conversation-specific reasoning WebSocket endpoint."""
            await self._handle_websocket(websocket, ["reasoning", "cortex"], filter_conversation=conversation_id)

    async def _handle_websocket(
        self,
        websocket: WebSocket,
        channels: list,
        filter_conversation: Optional[str] = None,
        filter_agent: Optional[str] = None,
    ) -> None:
        """Handle a WebSocket connection."""
        await websocket.accept()

        # Get client IP
        client_ip = "unknown"
        if websocket.client:
            client_ip = websocket.client.host

        # Register connection
        connection = await self.connection_manager.connect(websocket, client_ip)

        try:
            # Subscribe to requested channels
            subscribe_msg = WebSocketMessage(
                type=WebSocketMessageType.SUBSCRIBE,
                payload={
                    "channels": channels,
                    "conversation_id": filter_conversation,
                    "agent_id": filter_agent,
                },
            )
            response = await self.connection_manager.handle_message(
                connection, subscribe_msg.model_dump_json()
            )
            if response:
                await connection.send(response)

            # Handle incoming messages
            while True:
                data = await websocket.receive_text()
                response = await self.connection_manager.handle_message(connection, data)
                if response:
                    await connection.send(response)

        except WebSocketDisconnect:
            logger.info(f"Client {connection.connection_id} disconnected")
        except Exception as e:
            logger.error(f"WebSocket error: {e}")
        finally:
            await self.connection_manager.disconnect(connection.connection_id)

    def _build_agent_inventory(self, include_static: bool = True) -> Dict[str, Any]:
        agents: Dict[str, Dict[str, Any]] = {}

        for record in self._collect_observed_agents():
            agents[record["agent_id"]] = record

        if include_static:
            for record in self._collect_langgraph_agents():
                existing = agents.get(record["agent_id"])
                if existing:
                    existing["source"] = "observed+langgraph"
                    if not existing.get("capabilities"):
                        existing["capabilities"] = record.get("capabilities", [])
                    if not existing.get("description"):
                        existing["description"] = record.get("description", "")
                else:
                    agents[record["agent_id"]] = record

            for record in self._collect_planner_agents():
                existing = agents.get(record["agent_id"])
                if existing:
                    if existing.get("source") == "observed":
                        existing["source"] = "observed+planner"
                    if not existing.get("capabilities"):
                        existing["capabilities"] = record.get("capabilities", [])
                    if not existing.get("description"):
                        existing["description"] = record.get("description", "")
                else:
                    agents[record["agent_id"]] = record

            for record in self._collect_orchestrator_agents():
                existing = agents.get(record["agent_id"])
                if existing:
                    if "orchestrator" not in str(existing.get("source", "")):
                        existing["source"] = f"{existing.get('source', 'unknown')}+orchestrator"
                    if not existing.get("capabilities"):
                        existing["capabilities"] = record.get("capabilities", [])
                    if not existing.get("description"):
                        existing["description"] = record.get("description", "")
                else:
                    agents[record["agent_id"]] = record

        result = sorted(
            agents.values(),
            key=lambda row: (row.get("agent_name", row.get("agent_id", "")).lower()),
        )
        return {"count": len(result), "agents": result}

    def _collect_observed_agents(self, limit: int = 500) -> List[Dict[str, Any]]:
        observed: Dict[str, Dict[str, Any]] = {}
        for message in self.broadcaster.get_history(limit=limit):
            if message.type not in (
                WebSocketMessageType.AGENT_STATUS,
                WebSocketMessageType.AGENT_REGISTER,
                WebSocketMessageType.AGENT_MESSAGE,
            ):
                continue

            record = self._normalize_agent_record(dict(message.payload or {}), source="observed")
            if not record:
                continue
            record["last_seen"] = message.timestamp.isoformat()
            observed[record["agent_id"]] = record

        return list(observed.values())

    def _collect_langgraph_agents(self) -> List[Dict[str, Any]]:
        try:
            from src.langgraph_agents.orchestrator_graph import list_available_agents
            agents = list_available_agents()
        except Exception:
            return []

        out: List[Dict[str, Any]] = []
        for row in agents:
            if not isinstance(row, dict):
                continue
            out_row = self._normalize_agent_record(
                {
                    "agent_id": row.get("name"),
                    "agent_name": row.get("name"),
                    "status": "idle",
                    "current_task": row.get("phase"),
                    "capabilities": row.get("tools", []),
                    "description": row.get("description", ""),
                },
                source="langgraph",
            )
            if out_row:
                out.append(out_row)
        return out

    def _collect_planner_agents(self) -> List[Dict[str, Any]]:
        try:
            from src.agents.planner_agent import DEFAULT_AGENTS
        except Exception:
            return []

        out: List[Dict[str, Any]] = []
        for agent in DEFAULT_AGENTS:
            row = agent.to_dict() if hasattr(agent, "to_dict") else {}
            out_row = self._normalize_agent_record(
                {
                    "agent_id": row.get("name"),
                    "agent_name": row.get("name"),
                    "status": "idle",
                    "capabilities": row.get("capabilities", []),
                    "description": row.get("description", ""),
                },
                source="planner",
            )
            if out_row:
                out.append(out_row)
        return out

    def _collect_orchestrator_agents(self) -> List[Dict[str, Any]]:
        url = os.getenv("ORCHESTRATOR_URL", "").strip()
        api_key = os.getenv("ORCHESTRATOR_API_KEY", "").strip()
        if not url:
            return []

        try:
            from src.orchestrator.mcp_impl import OrchestratorClient

            client = OrchestratorClient(base_url=url, api_key=api_key, timeout=3)
            payload = client.list_agents(status="active")
            rows = payload.get("agents", []) if isinstance(payload, dict) else []
        except Exception:
            return []

        out: List[Dict[str, Any]] = []
        for row in rows:
            if not isinstance(row, dict):
                continue
            out_row = self._normalize_agent_record(
                {
                    "agent_id": row.get("agent_id") or row.get("id"),
                    "agent_name": row.get("name") or row.get("agent_name"),
                    "status": row.get("status", "active"),
                    "current_task": row.get("current_task"),
                    "capabilities": row.get("capabilities", []),
                    "description": row.get("description", ""),
                },
                source="orchestrator",
            )
            if out_row:
                out.append(out_row)
        return out

    def _normalize_agent_record(self, payload: Dict[str, Any], source: str) -> Optional[Dict[str, Any]]:
        agent_id = str(
            payload.get("agent_id")
            or payload.get("id")
            or payload.get("name")
            or payload.get("agent_name")
            or ""
        ).strip()
        if not agent_id:
            return None

        agent_name = str(payload.get("agent_name") or payload.get("name") or agent_id).strip()
        status = str(payload.get("status") or "idle").strip().lower()
        current_task = payload.get("current_task") or payload.get("task")
        caps = payload.get("capabilities", [])
        if not isinstance(caps, list):
            caps = []
        description = str(payload.get("description") or "").strip()

        out = {
            "agent_id": agent_id,
            "agent_name": agent_name,
            "status": status,
            "current_task": current_task,
            "capabilities": caps,
            "description": description,
            "source": source,
        }
        if payload.get("last_activity"):
            last_activity = payload.get("last_activity")
            out["last_activity"] = (
                last_activity.isoformat()
                if hasattr(last_activity, "isoformat")
                else str(last_activity)
            )
        return out

    async def start(self) -> None:
        """Start the server."""
        if self._running:
            logger.warning("Server already running")
            return

        app = self.create_app()
        config = uvicorn.Config(
            app,
            host=self.host,
            port=self.port,
            log_level="info",
        )
        self._server = uvicorn.Server(config)
        self._server_task = asyncio.create_task(self._server.serve())
        self._running = True

        logger.info(f"Console server started on http://{self.host}:{self.port}")

    async def stop(self) -> None:
        """Stop the server."""
        if not self._running:
            return

        if self._server:
            self._server.should_exit = True
            if self._server_task:
                try:
                    await asyncio.wait_for(self._server_task, timeout=5.0)
                except asyncio.TimeoutError:
                    self._server_task.cancel()

        self._running = False
        self._server = None
        self._server_task = None

        logger.info("Console server stopped")

    @property
    def is_running(self) -> bool:
        """Check if server is running."""
        return self._running

    def get_status(self) -> dict:
        """Get server status."""
        return {
            "running": self._running,
            "host": self.host,
            "port": self.port,
            "url": f"http://{self.host}:{self.port}" if self._running else None,
            "connections": self.connection_manager.get_connection_count() if self._running else 0,
            "broadcaster": self.broadcaster.get_stats() if self._running else None,
        }


# Singleton instance
_server_instance: Optional[ConsoleServer] = None


def get_server(
    host: str = "0.0.0.0",
    port: int = 8080,
    redis_url: Optional[str] = None,
) -> ConsoleServer:
    """Get or create the singleton server instance."""
    global _server_instance
    if _server_instance is None:
        _server_instance = ConsoleServer(host, port, redis_url)
    return _server_instance


def reset_server() -> None:
    """Reset the singleton server instance."""
    global _server_instance
    _server_instance = None
