"""Minimal stdlib WebSocket server for live dashboard events.

Implements RFC 6455 WebSocket protocol over http.server, enabling
real-time event streaming from StateEmitter/EventBus to browser
clients via /ws/events. Stdlib-only (ADR-022).
"""

from __future__ import annotations

import base64
import hashlib
import json
import struct
import threading
import time
from typing import Any

from orxaq.enterprise.events import Event, EventBus
from orxaq.enterprise.state import StateEmitter

# RFC 6455 magic GUID for WebSocket handshake.
_WS_MAGIC = "258EAFA5-E914-47DA-95CA-5AB5B0653C24"

# Frame opcodes.
OP_TEXT = 0x1
OP_CLOSE = 0x8
OP_PING = 0x9
OP_PONG = 0xA


def compute_accept_key(key: str) -> str:
    """Compute Sec-WebSocket-Accept from client key per RFC 6455."""
    digest = hashlib.sha1((key.strip() + _WS_MAGIC).encode()).digest()  # noqa: S324
    return base64.b64encode(digest).decode()


def encode_frame(data: bytes, opcode: int = OP_TEXT) -> bytes:
    """Encode a WebSocket frame (server to client, no masking)."""
    frame = bytearray()
    frame.append(0x80 | opcode)  # FIN + opcode

    length = len(data)
    if length < 126:
        frame.append(length)
    elif length < 65536:
        frame.append(126)
        frame.extend(struct.pack("!H", length))
    else:
        frame.append(127)
        frame.extend(struct.pack("!Q", length))

    frame.extend(data)
    return bytes(frame)


def decode_frame(data: bytes) -> tuple[int, bytes, int]:
    """Decode a WebSocket frame. Returns (opcode, payload, bytes_consumed).

    Returns (-1, b"", 0) if the buffer is incomplete.
    """
    if len(data) < 2:
        return -1, b"", 0

    opcode = data[0] & 0x0F
    masked = bool(data[1] & 0x80)
    length = data[1] & 0x7F
    offset = 2

    if length == 126:
        if len(data) < 4:
            return -1, b"", 0
        length = struct.unpack("!H", data[2:4])[0]
        offset = 4
    elif length == 127:
        if len(data) < 10:
            return -1, b"", 0
        length = struct.unpack("!Q", data[2:10])[0]
        offset = 10

    if masked:
        if len(data) < offset + 4 + length:
            return -1, b"", 0
        mask = data[offset : offset + 4]
        offset += 4
        payload = bytearray(data[offset : offset + length])
        for i in range(length):
            payload[i] ^= mask[i % 4]
        return opcode, bytes(payload), offset + length

    if len(data) < offset + length:
        return -1, b"", 0
    return opcode, data[offset : offset + length], offset + length


class WebSocketClient:
    """A single connected WebSocket client."""

    def __init__(self, wfile: Any, client_id: str) -> None:
        self.wfile = wfile
        self.client_id = client_id
        self.connected = True
        self._lock = threading.Lock()

    def send(self, message: str) -> bool:
        """Send a text message. Returns False if connection is dead."""
        if not self.connected:
            return False
        try:
            frame = encode_frame(message.encode("utf-8"))
            with self._lock:
                self.wfile.write(frame)
                self.wfile.flush()
            return True
        except (OSError, BrokenPipeError):
            self.connected = False
            return False

    def send_json(self, data: dict[str, Any]) -> bool:
        """Send a JSON-encoded message."""
        return self.send(json.dumps(data, default=str))

    def close(self) -> None:
        """Send close frame."""
        if not self.connected:
            return
        try:
            frame = encode_frame(b"", OP_CLOSE)
            with self._lock:
                self.wfile.write(frame)
                self.wfile.flush()
        except (OSError, BrokenPipeError):
            pass
        self.connected = False


class WebSocketHub:
    """Manages WebSocket clients and broadcasts events.

    Bridges the EventBus and StateEmitter to connected WebSocket
    clients, sending real-time JSON updates matching the Mission
    Control dashboard schema.
    """

    def __init__(
        self,
        event_bus: EventBus | None = None,
        state_emitter: StateEmitter | None = None,
    ) -> None:
        self._clients: dict[str, WebSocketClient] = {}
        self._lock = threading.Lock()
        self._next_id = 0
        self._event_bus = event_bus or EventBus()
        self._state_emitter = state_emitter or StateEmitter()

        # Subscribe to all events on the bus.
        self._event_bus.subscribe("*", self._on_event)

        # Start heartbeat thread for periodic state snapshots.
        self._running = True
        self._heartbeat_interval = 5.0
        self._heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True)
        self._heartbeat_thread.start()

    def add_client(self, wfile: Any) -> WebSocketClient:
        """Register a new WebSocket client and send initial state."""
        with self._lock:
            client_id = f"ws-{self._next_id}"
            self._next_id += 1
        client = WebSocketClient(wfile, client_id)
        with self._lock:
            self._clients[client_id] = client

        # Send initial state snapshot.
        client.send_json(
            {
                "type": "state_snapshot",
                "data": self._state_emitter.snapshot(),
            }
        )
        return client

    def remove_client(self, client: WebSocketClient) -> None:
        """Unregister a WebSocket client."""
        with self._lock:
            self._clients.pop(client.client_id, None)

    def broadcast(self, message: dict[str, Any]) -> int:
        """Send a message to all connected clients. Returns send count."""
        text = json.dumps(message, default=str)
        dead: list[str] = []
        count = 0
        with self._lock:
            clients = list(self._clients.values())
        for client in clients:
            if client.send(text):
                count += 1
            else:
                dead.append(client.client_id)
        if dead:
            with self._lock:
                for cid in dead:
                    self._clients.pop(cid, None)
        return count

    def client_count(self) -> int:
        """Number of connected clients."""
        with self._lock:
            return len(self._clients)

    def shutdown(self) -> None:
        """Stop the hub and close all clients."""
        self._running = False
        with self._lock:
            for client in self._clients.values():
                client.close()
            self._clients.clear()

    def _on_event(self, event: Event) -> None:
        """Forward EventBus events to all WebSocket clients."""
        self.broadcast(
            {
                "type": "event",
                "data": event.to_dict(),
            }
        )

    def _heartbeat_loop(self) -> None:
        """Send state snapshots periodically."""
        while self._running:
            time.sleep(self._heartbeat_interval)
            if not self._running:
                break
            with self._lock:
                if not self._clients:
                    continue
            self.broadcast(
                {
                    "type": "state_snapshot",
                    "data": self._state_emitter.snapshot(),
                }
            )

    @property
    def event_bus(self) -> EventBus:
        """The underlying event bus."""
        return self._event_bus

    @property
    def state_emitter(self) -> StateEmitter:
        """The underlying state emitter."""
        return self._state_emitter


def is_websocket_upgrade(headers: Any) -> bool:
    """Check if the request headers indicate a WebSocket upgrade."""
    connection = (headers.get("Connection") or "").lower()
    upgrade = (headers.get("Upgrade") or "").lower()
    return "upgrade" in connection and upgrade == "websocket"


def handle_websocket_upgrade(
    handler: Any,
    hub: WebSocketHub,
) -> None:
    """Upgrade an HTTP connection to WebSocket and run the read loop.

    Call this from do_GET when the path is /ws/events and the
    request contains the Upgrade: websocket header.
    """
    key = handler.headers.get("Sec-WebSocket-Key", "")
    if not key:
        handler.send_error(400, "Missing Sec-WebSocket-Key")
        return

    # Send upgrade response.
    accept = compute_accept_key(key)
    handler.send_response(101, "Switching Protocols")
    handler.send_header("Upgrade", "websocket")
    handler.send_header("Connection", "Upgrade")
    handler.send_header("Sec-WebSocket-Accept", accept)
    handler.end_headers()

    # Register client.
    client = hub.add_client(handler.wfile)

    # Read loop -- process incoming frames until close.
    try:
        buf = b""
        rfile = handler.rfile
        while client.connected:
            try:
                chunk = rfile.read1(4096) if hasattr(rfile, "read1") else rfile.read(4096)
            except (OSError, ConnectionResetError):
                break
            if not chunk:
                break
            buf += chunk

            while buf:
                opcode, payload, consumed = decode_frame(buf)
                if opcode == -1:
                    break
                buf = buf[consumed:]

                if opcode == OP_CLOSE:
                    client.close()
                    break
                elif opcode == OP_PING:
                    try:
                        pong = encode_frame(payload, OP_PONG)
                        handler.wfile.write(pong)
                        handler.wfile.flush()
                    except (OSError, BrokenPipeError):
                        client.connected = False
                elif opcode == OP_TEXT:
                    _handle_client_message(client, hub, payload)
    finally:
        hub.remove_client(client)


def _handle_client_message(
    client: WebSocketClient,
    hub: WebSocketHub,
    payload: bytes,
) -> None:
    """Process a text message from a WebSocket client."""
    try:
        msg = json.loads(payload.decode("utf-8"))
    except (ValueError, UnicodeDecodeError):
        return

    msg_type = msg.get("type", "")

    if msg_type == "get_state":
        client.send_json(
            {
                "type": "state_snapshot",
                "data": hub.state_emitter.snapshot(),
            }
        )
    elif msg_type == "get_history":
        topic = msg.get("topic")
        limit = msg.get("limit", 50)
        events = hub.event_bus.history(topic=topic, limit=limit)
        client.send_json(
            {
                "type": "event_history",
                "data": [e.to_dict() for e in events],
            }
        )
