"""WebSocket连接管理器 - 管理多个并发WebSocket连接"""

import asyncio
import dataclasses
import logging
import typing
import uuid
from datetime import datetime

import pydantic

from ...api.http.context import ExecutionContext, PrincipalContext

logger = logging.getLogger(__name__)
_SESSION_FILTER_UNSET = object()
_DEFAULT_SEND_QUEUE_SIZE = 100


@dataclasses.dataclass(frozen=True, slots=True)
class WebSocketScope:
    """Trusted runtime placement carried by every WebSocket connection."""

    instance_uuid: str
    workspace_uuid: str
    placement_generation: int

    def __post_init__(self) -> None:
        if not self.instance_uuid.strip() or not self.workspace_uuid.strip():
            raise ValueError('WebSocket scope requires an instance and Workspace')
        if self.placement_generation <= 0:
            raise ValueError('WebSocket scope requires a positive placement generation')

    @classmethod
    def from_context(cls, context: typing.Any) -> 'WebSocketScope':
        return cls(
            instance_uuid=str(getattr(context, 'instance_uuid', '')),
            workspace_uuid=str(getattr(context, 'workspace_uuid', '')),
            placement_generation=int(getattr(context, 'placement_generation', 0)),
        )


def is_valid_session_id(value: str) -> bool:
    """Accept only canonical random UUIDs for client conversation identifiers."""
    try:
        parsed = uuid.UUID(value)
    except (ValueError, TypeError, AttributeError):
        return False
    return parsed.version == 4 and str(parsed) == value


class WebSocketConnection(pydantic.BaseModel):
    """单个WebSocket连接"""

    model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)

    connection_id: str = pydantic.Field(default_factory=lambda: str(uuid.uuid4()))
    """连接唯一ID"""

    instance_uuid: str
    """Owning LangBot instance."""

    workspace_uuid: str
    """Owning Workspace."""

    placement_generation: int
    """Workspace placement generation captured at connect time."""

    pipeline_uuid: str
    """关联的流水线UUID"""

    session_type: str  # 'person' or 'group'
    """会话类型"""

    session_id: str | None = None
    """Optional client conversation identifier used by embed widgets."""

    websocket: typing.Any = pydantic.Field(exclude=True)
    """WebSocket连接对象 (quart.websocket)"""

    created_at: datetime = pydantic.Field(default_factory=datetime.now)
    """连接创建时间"""

    last_active: datetime = pydantic.Field(default_factory=datetime.now)
    """最后活跃时间"""

    send_queue: asyncio.Queue = pydantic.Field(
        default_factory=lambda: asyncio.Queue(maxsize=_DEFAULT_SEND_QUEUE_SIZE),
        exclude=True,
    )
    """发送消息队列"""

    is_active: bool = True
    """连接是否活跃"""

    metadata: dict = pydantic.Field(default_factory=dict)
    """连接元数据（可存储额外信息）"""

    trigger_principal: PrincipalContext | None = None
    """Authenticated principal that opened this dashboard connection."""

    @property
    def scope(self) -> WebSocketScope:
        return WebSocketScope(
            instance_uuid=self.instance_uuid,
            workspace_uuid=self.workspace_uuid,
            placement_generation=self.placement_generation,
        )

    @property
    def execution_context(self) -> ExecutionContext:
        """Return the storage/runtime context captured for this connection."""

        return ExecutionContext(
            instance_uuid=self.instance_uuid,
            workspace_uuid=self.workspace_uuid,
            placement_generation=self.placement_generation,
            pipeline_uuid=self.pipeline_uuid,
            trigger_principal=self.trigger_principal,
        )


class WebSocketConnectionManager:
    """WebSocket连接管理器 - 支持多连接并发"""

    def __init__(self):
        self.connections: dict[str, WebSocketConnection] = {}
        """所有活跃连接 {connection_id: connection}"""

        self.pipeline_connections: dict[tuple[str, str, int, str], set[str]] = {}
        """Scoped pipeline to connection mapping."""

        self.session_connections: dict[tuple[str, str, int, str], set[str]] = {}
        """Scoped session-type to connection mapping."""

        self._lock = asyncio.Lock()
        """线程锁，保护并发访问"""

    async def add_connection(
        self,
        websocket: typing.Any,
        scope: WebSocketScope,
        pipeline_uuid: str,
        session_type: str,
        metadata: dict | None = None,
        trigger_principal: PrincipalContext | None = None,
        session_id: str | None = None,
        send_queue_size: int = _DEFAULT_SEND_QUEUE_SIZE,
        max_connections: int = 1024,
        max_connections_per_workspace: int = 32,
    ) -> WebSocketConnection:
        """Register a WebSocket connection and its optional embed session."""
        try:
            send_queue_size = max(int(send_queue_size), 1)
        except (TypeError, ValueError):
            send_queue_size = _DEFAULT_SEND_QUEUE_SIZE
        max_connections = max(int(max_connections), 1)
        max_connections_per_workspace = max(
            min(int(max_connections_per_workspace), max_connections),
            1,
        )
        async with self._lock:
            if len(self.connections) >= max_connections:
                raise RuntimeError(f'WebSocket connection capacity reached ({max_connections})')
            workspace_connection_count = sum(
                1
                for connection in self.connections.values()
                if connection.instance_uuid == scope.instance_uuid
                and connection.workspace_uuid == scope.workspace_uuid
                and connection.placement_generation == scope.placement_generation
            )
            if workspace_connection_count >= max_connections_per_workspace:
                raise RuntimeError(f'Workspace WebSocket connection capacity reached ({max_connections_per_workspace})')
            connection = WebSocketConnection(
                instance_uuid=scope.instance_uuid,
                workspace_uuid=scope.workspace_uuid,
                placement_generation=scope.placement_generation,
                pipeline_uuid=pipeline_uuid,
                session_type=session_type,
                session_id=session_id,
                websocket=websocket,
                metadata=metadata or {},
                trigger_principal=trigger_principal,
                send_queue=asyncio.Queue(maxsize=send_queue_size),
            )

            self.connections[connection.connection_id] = connection

            # 更新流水线映射
            pipeline_key = self._pipeline_key(scope, pipeline_uuid)
            if pipeline_key not in self.pipeline_connections:
                self.pipeline_connections[pipeline_key] = set()
            self.pipeline_connections[pipeline_key].add(connection.connection_id)

            # 更新会话类型映射
            session_key = self._session_key(scope, session_type)
            if session_key not in self.session_connections:
                self.session_connections[session_key] = set()
            self.session_connections[session_key].add(connection.connection_id)

            logger.debug(
                f'WebSocket connection established: {connection.connection_id} '
                f'(workspace={scope.workspace_uuid}, generation={scope.placement_generation}, '
                f'pipeline={pipeline_uuid}, session_type={session_type})'
            )

            return connection

    async def close_scope(self, scope: WebSocketScope) -> None:
        """Close and forget every live connection for one runtime placement."""

        async with self._lock:
            connection_ids = [
                connection_id for connection_id, connection in self.connections.items() if connection.scope == scope
            ]
        for connection_id in connection_ids:
            connection = self.connections.get(connection_id)
            if connection is None:
                continue
            close = getattr(connection.websocket, 'close', None)
            if close is not None:
                try:
                    result = close()
                    if asyncio.iscoroutine(result):
                        await result
                except Exception:
                    logger.debug(
                        'Failed to close WebSocket connection %s',
                        connection_id,
                        exc_info=True,
                    )
            await self.remove_connection(connection_id)

    async def remove_connection(self, connection_id: str):
        """移除WebSocket连接"""
        async with self._lock:
            if connection_id not in self.connections:
                return

            connection = self.connections[connection_id]
            connection.is_active = False

            # 从流水线映射中移除
            pipeline_key = self._pipeline_key(connection.scope, connection.pipeline_uuid)
            if pipeline_key in self.pipeline_connections:
                self.pipeline_connections[pipeline_key].discard(connection_id)
                if not self.pipeline_connections[pipeline_key]:
                    del self.pipeline_connections[pipeline_key]

            # 从会话类型映射中移除
            session_key = self._session_key(connection.scope, connection.session_type)
            if session_key in self.session_connections:
                self.session_connections[session_key].discard(connection_id)
                if not self.session_connections[session_key]:
                    del self.session_connections[session_key]

            del self.connections[connection_id]

            logger.debug(f'WebSocket connection disconnected: {connection_id}')

    @staticmethod
    def _pipeline_key(scope: WebSocketScope, pipeline_uuid: str) -> tuple[str, str, int, str]:
        return (
            scope.instance_uuid,
            scope.workspace_uuid,
            scope.placement_generation,
            pipeline_uuid,
        )

    @staticmethod
    def _session_key(scope: WebSocketScope, session_type: str) -> tuple[str, str, int, str]:
        return (
            scope.instance_uuid,
            scope.workspace_uuid,
            scope.placement_generation,
            session_type,
        )

    async def get_connection(
        self,
        connection_id: str,
        *,
        scope: WebSocketScope,
    ) -> WebSocketConnection | None:
        """Get a connection only when it belongs to the expected placement."""

        connection = self.connections.get(connection_id)
        if connection is None or connection.scope != scope:
            return None
        return connection

    async def get_connection_by_session_id(
        self,
        session_id: str,
        *,
        scope: WebSocketScope,
        pipeline_uuid: str | None = None,
    ) -> WebSocketConnection | None:
        """Get an active embed connection by its stable browser session identifier."""
        candidates: typing.Iterable[WebSocketConnection]
        if pipeline_uuid is not None:
            candidates = await self.get_connections_by_pipeline(pipeline_uuid, scope=scope)
        else:
            candidates = self.connections.values()
        for connection in candidates:
            if (
                connection.session_id == session_id
                and connection.is_active
                and connection.scope == scope
                and (pipeline_uuid is None or connection.pipeline_uuid == pipeline_uuid)
            ):
                return connection
        return None

    async def get_connections_by_pipeline(
        self,
        pipeline_uuid: str,
        *,
        scope: WebSocketScope,
    ) -> list[WebSocketConnection]:
        """获取指定流水线的所有连接"""
        connection_ids = self.pipeline_connections.get(self._pipeline_key(scope, pipeline_uuid), set())
        return [self.connections[cid] for cid in connection_ids if cid in self.connections]

    async def get_connections_by_session_type(
        self,
        session_type: str,
        *,
        scope: WebSocketScope,
    ) -> list[WebSocketConnection]:
        """获取指定会话类型的所有连接"""
        connection_ids = self.session_connections.get(self._session_key(scope, session_type), set())
        return [self.connections[cid] for cid in connection_ids if cid in self.connections]

    async def broadcast_to_pipeline(
        self,
        pipeline_uuid: str,
        message: dict,
        *,
        scope: WebSocketScope,
        session_type: str | None = None,
        session_id: typing.Any = _SESSION_FILTER_UNSET,
    ):
        """Broadcast a message to matching connections for one pipeline.

        Args:
            pipeline_uuid: Pipeline identifier.
            message: Serialized message to enqueue.
            session_type: Optional session-type filter.
            session_id: Embed conversation filter. Omit it to broadcast across
                conversations; pass ``None`` to target non-embed connections.
        """
        connections = await self.get_connections_by_pipeline(pipeline_uuid, scope=scope)

        if session_type is not None:
            connections = [conn for conn in connections if conn.session_type == session_type]

        if session_id is not _SESSION_FILTER_UNSET:
            connections = [conn for conn in connections if conn.session_id == session_id]

        tasks = []
        for conn in connections:
            tasks.append(self.send_to_connection(conn.connection_id, message))
        if tasks:
            await asyncio.gather(*tasks, return_exceptions=True)

    async def send_to_connection(self, connection_id: str, message: dict):
        """向指定连接发送消息"""
        connection = self.connections.get(connection_id)
        if not connection or not connection.is_active:
            logger.warning(f'Attempt to send message to invalid connection: {connection_id}')
            return

        try:
            try:
                connection.send_queue.put_nowait(message)
            except asyncio.QueueFull:
                # A slow or disconnected browser must not backpressure every
                # other connection or retain an unbounded response stream.
                try:
                    connection.send_queue.get_nowait()
                except asyncio.QueueEmpty:
                    pass
                connection.send_queue.put_nowait(message)
                logger.warning(
                    'WebSocket send queue full; dropped oldest message for connection %s',
                    connection_id,
                )
            connection.last_active = datetime.now()
        except Exception as e:
            logger.error(f'Failed to send message to connection {connection_id}: {e}')
            await self.remove_connection(connection_id)

    async def update_activity(self, connection_id: str):
        """更新连接活跃时间"""
        connection = self.connections.get(connection_id)
        if connection:
            connection.last_active = datetime.now()

    def get_stats(self, *, scope: WebSocketScope) -> dict:
        """Return connection statistics for one trusted placement."""

        scoped_connections = [connection for connection in self.connections.values() if connection.scope == scope]
        pipelines: dict[str, int] = {}
        session_types: dict[str, int] = {}
        for connection in scoped_connections:
            pipelines[connection.pipeline_uuid] = pipelines.get(connection.pipeline_uuid, 0) + 1
            session_types[connection.session_type] = session_types.get(connection.session_type, 0) + 1
        return {
            'total_connections': len(scoped_connections),
            'pipelines': len(pipelines),
            'connections_by_pipeline': pipelines,
            'connections_by_session_type': session_types,
        }


# 全局连接管理器实例
ws_connection_manager = WebSocketConnectionManager()
