"""
WebSocket display backend - broadcasts frame buffer to connected web clients.

Supports streaming protocols:
- Full frame (JSON) - default for compatibility
- Binary streaming - efficient binary protocol
- Diff streaming - only sends changed lines

TODO: Transform to a true streaming backend with:
- Proper WebSocket message streaming (currently sends full buffer each frame)
- Connection pooling and backpressure handling
- Binary protocol for efficiency (instead of JSON)
- Client management with proper async handling
- Mark for deprecation if replaced by a new streaming implementation

Current implementation: Simple broadcast of text frames to all connected clients.
"""

import asyncio
import base64
import json
import threading
import time
from enum import IntFlag

from engine.display.streaming import (
    MessageType,
    compress_frame,
    compute_diff,
    encode_binary_message,
    encode_diff_message,
)


class StreamingMode(IntFlag):
    """Streaming modes for WebSocket display."""

    JSON = 0x01  # Full JSON frames (default, compatible)
    BINARY = 0x02  # Binary compression
    DIFF = 0x04  # Differential updates


try:
    import websockets
except ImportError:
    websockets = None


def get_monitor():
    """Get the performance monitor."""
    try:
        from engine.effects.performance import get_monitor as _get_monitor

        return _get_monitor()
    except Exception:
        return None


class WebSocketDisplay:
    """WebSocket display backend - broadcasts to HTML Canvas clients."""

    width: int = 80
    height: int = 24

    def __init__(
        self,
        host: str = "0.0.0.0",
        port: int = 8765,
        http_port: int = 8766,
        streaming_mode: StreamingMode = StreamingMode.JSON,
    ):
        self.host = host
        self.port = port
        self.http_port = http_port
        self.width = 80
        self.height = 24
        self._clients: set = set()
        self._server_running = False
        self._http_running = False
        self._server_thread: threading.Thread | None = None
        self._http_thread: threading.Thread | None = None
        self._available = True
        self._max_clients = 10
        self._client_connected_callback = None
        self._client_disconnected_callback = None
        self._command_callback = None
        self._controller = None  # Reference to UI panel or pipeline controller
        self._frame_delay = 0.0
        self._httpd = None  # HTTP server instance

        # Streaming configuration
        self._streaming_mode = streaming_mode
        self._last_buffer: list[str] = []
        self._client_capabilities: dict = {}  # Track client capabilities

        try:
            import websockets as _ws

            self._available = _ws is not None
        except ImportError:
            self._available = False

    def is_available(self) -> bool:
        """Check if WebSocket support is available."""
        return self._available

    def init(self, width: int, height: int, reuse: bool = False) -> None:
        """Initialize display with dimensions and start server.

        Args:
            width: Terminal width in characters
            height: Terminal height in rows
            reuse: If True, skip starting servers (assume already running)
        """
        self.width = width
        self.height = height

        if not reuse or not self._server_running:
            self.start_server()
            self.start_http_server()

    def show(self, buffer: list[str], border: bool = False) -> None:
        """Broadcast buffer to all connected clients using streaming protocol."""
        t0 = time.perf_counter()

        # Get metrics for border display
        fps = 0.0
        frame_time = 0.0
        monitor = get_monitor()
        if monitor:
            stats = monitor.get_stats()
            avg_ms = stats.get("pipeline", {}).get("avg_ms", 0) if stats else 0
            frame_count = stats.get("frame_count", 0) if stats else 0
            if avg_ms and frame_count > 0:
                fps = 1000.0 / avg_ms
                frame_time = avg_ms

        # Apply border if requested
        if border:
            from engine.display import render_border

            buffer = render_border(buffer, self.width, self.height, fps, frame_time)

        if not self._clients:
            self._last_buffer = buffer
            return

        # Send to each client based on their capabilities
        disconnected = set()
        for client in list(self._clients):
            try:
                client_id = id(client)
                client_mode = self._client_capabilities.get(
                    client_id, StreamingMode.JSON
                )

                if client_mode & StreamingMode.DIFF:
                    self._send_diff_frame(client, buffer)
                elif client_mode & StreamingMode.BINARY:
                    self._send_binary_frame(client, buffer)
                else:
                    self._send_json_frame(client, buffer)
            except Exception:
                disconnected.add(client)

        for client in disconnected:
            self._clients.discard(client)
            if self._client_disconnected_callback:
                self._client_disconnected_callback(client)

        self._last_buffer = buffer

        elapsed_ms = (time.perf_counter() - t0) * 1000
        if monitor:
            chars_in = sum(len(line) for line in buffer)
            monitor.record_effect("websocket_display", elapsed_ms, chars_in, chars_in)

    def _send_json_frame(self, client, buffer: list[str]) -> None:
        """Send frame as JSON."""
        frame_data = {
            "type": "frame",
            "width": self.width,
            "height": self.height,
            "lines": buffer,
        }
        message = json.dumps(frame_data)
        asyncio.run(client.send(message))

    def _send_binary_frame(self, client, buffer: list[str]) -> None:
        """Send frame as compressed binary."""
        compressed = compress_frame(buffer)
        message = encode_binary_message(
            MessageType.FULL_FRAME, self.width, self.height, compressed
        )
        encoded = base64.b64encode(message).decode("utf-8")
        asyncio.run(client.send(encoded))

    def _send_diff_frame(self, client, buffer: list[str]) -> None:
        """Send frame as diff."""
        diff = compute_diff(self._last_buffer, buffer)

        if not diff.changed_lines:
            return

        diff_payload = encode_diff_message(diff)
        message = encode_binary_message(
            MessageType.DIFF_FRAME, self.width, self.height, diff_payload
        )
        encoded = base64.b64encode(message).decode("utf-8")
        asyncio.run(client.send(encoded))

    def set_streaming_mode(self, mode: StreamingMode) -> None:
        """Set the default streaming mode for new clients."""
        self._streaming_mode = mode

    def get_streaming_mode(self) -> StreamingMode:
        """Get the current streaming mode."""
        return self._streaming_mode

    def clear(self) -> None:
        """Broadcast clear command to all clients."""
        if self._clients:
            clear_data = {"type": "clear"}
            message = json.dumps(clear_data)
            for client in list(self._clients):
                try:
                    asyncio.run(client.send(message))
                except Exception:
                    pass

    def cleanup(self) -> None:
        """Stop the servers."""
        self.stop_server()
        self.stop_http_server()

    async def _websocket_handler(self, websocket):
        """Handle WebSocket connections."""
        if len(self._clients) >= self._max_clients:
            await websocket.close()
            return

        self._clients.add(websocket)
        if self._client_connected_callback:
            self._client_connected_callback(websocket)

        try:
            async for message in websocket:
                try:
                    data = json.loads(message)
                    msg_type = data.get("type")

                    if msg_type == "resize":
                        self.width = data.get("width", 80)
                        self.height = data.get("height", 24)
                    elif msg_type == "command" and self._command_callback:
                        # Forward commands to the pipeline controller
                        command = data.get("command", {})
                        self._command_callback(command)
                    elif msg_type == "state_request":
                        # Send current state snapshot
                        state = self._get_state_snapshot()
                        if state:
                            response = {"type": "state", "state": state}
                            await websocket.send(json.dumps(response))
                except json.JSONDecodeError:
                    pass
        except Exception:
            pass
        finally:
            self._clients.discard(websocket)
            if self._client_disconnected_callback:
                self._client_disconnected_callback(websocket)

    async def _run_websocket_server(self):
        """Run the WebSocket server."""
        if not websockets:
            return
        async with websockets.serve(self._websocket_handler, self.host, self.port):
            while self._server_running:
                await asyncio.sleep(0.1)

    async def _run_http_server(self):
        """Run simple HTTP server for the client."""
        import os
        from http.server import HTTPServer, SimpleHTTPRequestHandler

        # Find the project root by locating 'engine' directory in the path
        websocket_file = os.path.abspath(__file__)
        parts = websocket_file.split(os.sep)
        if "engine" in parts:
            engine_idx = parts.index("engine")
            project_root = os.sep.join(parts[:engine_idx])
            client_dir = os.path.join(project_root, "client")
        else:
            # Fallback: go up 4 levels from websocket.py
            # websocket.py: .../engine/display/backends/websocket.py
            # We need: .../client
            client_dir = os.path.join(
                os.path.dirname(
                    os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
                ),
                "client",
            )

        class Handler(SimpleHTTPRequestHandler):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, directory=client_dir, **kwargs)

            def log_message(self, format, *args):
                pass

        httpd = HTTPServer((self.host, self.http_port), Handler)
        # Store reference for shutdown
        self._httpd = httpd
        # Serve requests continuously
        httpd.serve_forever()

    def _run_async(self, coro):
        """Run coroutine in background."""
        try:
            asyncio.run(coro)
        except Exception as e:
            print(f"WebSocket async error: {e}")

    def start_server(self):
        """Start the WebSocket server in a background thread."""
        if not self._available:
            return
        if self._server_thread is not None:
            return

        self._server_running = True
        self._server_thread = threading.Thread(
            target=self._run_async, args=(self._run_websocket_server(),), daemon=True
        )
        self._server_thread.start()

    def stop_server(self):
        """Stop the WebSocket server."""
        self._server_running = False
        self._server_thread = None

    def start_http_server(self):
        """Start the HTTP server in a background thread."""
        if not self._available:
            return
        if self._http_thread is not None:
            return

        self._http_running = True

        self._http_running = True
        self._http_thread = threading.Thread(
            target=self._run_async, args=(self._run_http_server(),), daemon=True
        )
        self._http_thread.start()

    def stop_http_server(self):
        """Stop the HTTP server."""
        self._http_running = False
        if hasattr(self, "_httpd") and self._httpd:
            self._httpd.shutdown()
        self._http_thread = None

    def client_count(self) -> int:
        """Return number of connected clients."""
        return len(self._clients)

    def get_ws_port(self) -> int:
        """Return WebSocket port."""
        return self.port

    def get_http_port(self) -> int:
        """Return HTTP port."""
        return self.http_port

    def set_frame_delay(self, delay: float) -> None:
        """Set delay between frames in seconds."""
        self._frame_delay = delay

    def get_frame_delay(self) -> float:
        """Get delay between frames."""
        return self._frame_delay

    def set_client_connected_callback(self, callback) -> None:
        """Set callback for client connections."""
        self._client_connected_callback = callback

    def set_client_disconnected_callback(self, callback) -> None:
        """Set callback for client disconnections."""
        self._client_disconnected_callback = callback

    def set_command_callback(self, callback) -> None:
        """Set callback for incoming command messages from clients."""
        self._command_callback = callback

    def set_controller(self, controller) -> None:
        """Set controller (UI panel or pipeline) for state queries and command execution."""
        self._controller = controller

    def broadcast_state(self, state: dict) -> None:
        """Broadcast state update to all connected clients.

        Args:
            state: Dictionary containing state data to send to clients
        """
        if not self._clients:
            return

        message = json.dumps({"type": "state", "state": state})

        disconnected = set()
        for client in list(self._clients):
            try:
                asyncio.run(client.send(message))
            except Exception:
                disconnected.add(client)

        for client in disconnected:
            self._clients.discard(client)
            if self._client_disconnected_callback:
                self._client_disconnected_callback(client)

    def _get_state_snapshot(self) -> dict | None:
        """Get current state snapshot from controller."""
        if not self._controller:
            return None

        try:
            # Expect controller to have methods we need
            state = {}

            # Get stages info if UIPanel
            if hasattr(self._controller, "stages"):
                state["stages"] = {
                    name: {
                        "enabled": ctrl.enabled,
                        "params": ctrl.params,
                        "selected": ctrl.selected,
                    }
                    for name, ctrl in self._controller.stages.items()
                }

            # Get current preset
            if hasattr(self._controller, "_current_preset"):
                state["preset"] = self._controller._current_preset
            if hasattr(self._controller, "_presets"):
                state["presets"] = self._controller._presets

            # Get selected stage
            if hasattr(self._controller, "selected_stage"):
                state["selected_stage"] = self._controller.selected_stage

            return state
        except Exception:
            return None

    def get_dimensions(self) -> tuple[int, int]:
        """Get current dimensions.

        Returns:
            (width, height) in character cells
        """
        return (self.width, self.height)
