"""
mizan.channels - Real-time WebSocket communication.

Type-safe bidirectional messaging between Django and React via WebSockets.
Hooks are auto-generated with full TypeScript types.

## Basic Usage

```python
# channels.py
from pydantic import BaseModel
from mizan import channels

class ChatChannel(channels.ReactChannel):

    class Params(BaseModel):
        room: str

    class ReactMessage(BaseModel):
        text: str

    class DjangoMessage(BaseModel):
        user: str
        text: str
        timestamp: datetime

    def authorize(self, params: Params) -> bool:
        return self.user.is_authenticated

    def group(self, params: Params) -> str:
        return f'chat_{params.room}'

    def receive(self, params: Params, msg: ReactMessage) -> DjangoMessage | None:
        return self.DjangoMessage(
            user=self.user.email,
            text=msg.text,
            timestamp=now(),
        )

channels.register(ChatChannel, 'chat')
```

```python
# asgi.py
from mizan import channels

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": channels.get_websocket_application(),
})
```

## Frontend Usage (auto-generated)

```tsx
import { useChatChannel } from '@/api/generated.channels'

function Chat({ room }) {
    const chat = useChatChannel({ room })

    chat.status     // 'connecting' | 'connected' | 'disconnected'
    chat.messages   // DjangoMessage[]
    chat.send({ text: 'Hello' })  // ReactMessage
}
```

## Server Push

```python
await ChatChannel.push(room='general', message=ChatChannel.DjangoMessage(...))
```
"""

import logging
from typing import TYPE_CHECKING, Any, ClassVar, Type

from pydantic import BaseModel

if TYPE_CHECKING:
    from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
    from ninja import NinjaAPI


logger = logging.getLogger(__name__)


# =============================================================================
# Base Classes
# =============================================================================


class ReactChannel:
    """
    Base class for WebSocket channels.

    Define nested Pydantic classes for typed messaging:
    - Params: Query parameters for subscribing (optional)
    - ReactMessage: Messages from browser to server (optional)
    - DjangoMessage: Messages from server to browser (optional)

    Implement required methods:
    - authorize(): Permission check for connection
    - group(): Which group to broadcast to

    Optionally implement:
    - receive(): Handle incoming ReactMessage, return DjangoMessage to broadcast
    - on_connect(): Called after successful connection
    - on_disconnect(): Called when connection closes
    """

    # Nested classes (optional, defined by subclasses)
    Params: ClassVar[Type[BaseModel] | None] = None
    ReactMessage: ClassVar[Type[BaseModel] | None] = None
    DjangoMessage: ClassVar[Type[BaseModel] | None] = None

    # Set by the framework when handling a connection
    user: "AbstractBaseUser | AnonymousUser"
    _channel_layer: Any = None
    _channel_name: str = ""
    _registered_name: ClassVar[str] = ""
    _params_dict: dict = {}
    _groups: set[str]

    def __init__(self):
        self._groups = set()
        self._params_dict = {}

    def authorize(self, params: BaseModel | None = None) -> bool:
        """
        Permission check. Return True to allow connection, False to reject.

        Override this to implement custom authorization logic.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} must implement authorize()"
        )

    def group(self, params: BaseModel | None = None) -> str:
        """
        Return the group name for broadcasting.

        Messages returned from receive() are broadcast to this group.
        """
        raise NotImplementedError(f"{self.__class__.__name__} must implement group()")

    def receive(self, params: BaseModel | None, msg: BaseModel) -> BaseModel | None:
        """
        Handle incoming ReactMessage.

        Return a DjangoMessage to broadcast to the group, or None to skip.
        Override this to implement message handling.
        """
        return None

    async def on_connect(self, params: BaseModel | None = None) -> None:
        """Called after successful connection and group join."""
        pass

    async def on_disconnect(self) -> None:
        """Called when the connection closes."""
        pass

    # -------------------------------------------------------------------------
    # Internal Methods (used by the consumer)
    # -------------------------------------------------------------------------

    async def _join_group(self, group_name: str) -> None:
        """Join a channel layer group."""
        if self._channel_layer:
            await self._channel_layer.group_add(group_name, self._channel_name)
            self._groups.add(group_name)

    async def _leave_group(self, group_name: str) -> None:
        """Leave a channel layer group."""
        if self._channel_layer and group_name in self._groups:
            await self._channel_layer.group_discard(group_name, self._channel_name)
            self._groups.discard(group_name)

    async def _leave_all_groups(self) -> None:
        """Leave all joined groups."""
        for group_name in list(self._groups):
            await self._leave_group(group_name)

    async def _broadcast(self, group_name: str, message: BaseModel) -> None:
        """Broadcast a message to a group."""
        if self._channel_layer:
            await self._channel_layer.group_send(
                group_name,
                {
                    "type": "channel.message",
                    "channel": self._registered_name,
                    "params": self._params_dict,
                    "data": message.model_dump(mode="json"),
                    "message_type": message.__class__.__name__,
                },
            )

    # -------------------------------------------------------------------------
    # Class Methods for Server Push
    # -------------------------------------------------------------------------

    @classmethod
    async def push(cls, message: BaseModel, **params) -> None:
        """
        Push a message from server code (views, tasks, signals).

        Usage:
            await ChatChannel.push(
                room='general',
                message=ChatChannel.DjangoMessage(user='system', text='Hello')
            )
        """
        from channels.layers import get_channel_layer

        channel_layer = get_channel_layer()
        if not channel_layer:
            logger.warning(
                f"No channel layer configured, cannot push to {cls.__name__}"
            )
            return

        # Build params model if defined
        params_obj = None
        if cls.Params:
            params_obj = cls.Params(**params)

        # Get group name
        instance = cls()
        group_name = instance.group(params_obj)

        # Send to group
        await channel_layer.group_send(
            group_name,
            {
                "type": "channel.message",
                "channel": cls._registered_name,
                "params": params,
                "data": message.model_dump(mode="json"),
                "message_type": message.__class__.__name__,
            },
        )


# =============================================================================
# Registry
# =============================================================================

_registry: dict[str, Type[ReactChannel]] = {}


def register(channel_class: Type[ReactChannel], name: str) -> None:
    """
    Register a channel.

    Args:
        channel_class: The ReactChannel subclass to register
        name: URL-friendly name (used in subscriptions)
    """
    if name in _registry:
        raise ValueError(f"Channel '{name}' is already registered")

    channel_class._registered_name = name

    # Validate the channel class
    if not hasattr(channel_class, "authorize"):
        raise ValueError(f"{channel_class.__name__} must implement authorize()")
    if not hasattr(channel_class, "group"):
        raise ValueError(f"{channel_class.__name__} must implement group()")

    _registry[name] = channel_class
    logger.debug(f"Registered channel: {name} -> {channel_class.__name__}")


def get_channel(name: str) -> Type[ReactChannel] | None:
    """Get a registered channel class by name."""
    return _registry.get(name)


def get_registered_channels() -> dict[str, Type[ReactChannel]]:
    """Get all registered channel classes."""
    return dict(_registry)


# =============================================================================
# WebSocket Consumer
# =============================================================================


def get_websocket_application():
    """
    Get the WebSocket application for ASGI.

    Usage in asgi.py:
        from mizan import channels

        application = ProtocolTypeRouter({
            "http": get_asgi_application(),
            "websocket": channels.get_websocket_application(),
        })
    """
    try:
        from channels.routing import URLRouter
        from channels.auth import AuthMiddlewareStack
        from django.urls import path
    except ImportError:
        raise ImportError(
            "django-channels is required for WebSocket support. "
            "Install it with: pip install channels channels-redis"
        )

    from .connection import DjangoReactConsumer

    return AuthMiddlewareStack(
        URLRouter(
            [
                path("ws/", DjangoReactConsumer.as_asgi()),
            ]
        )
    )


# =============================================================================
# Schema Export (for TypeScript generation)
# =============================================================================


def get_channels_schema() -> dict:
    """
    Get schema for all registered channels (for TypeScript generation).

    Returns a dict suitable for the frontend code generator.
    """
    schema = {"channels": {}}

    for name, channel_class in _registry.items():
        channel_schema = {
            "name": name,
            "params": None,
            "reactMessage": None,
            "djangoMessage": None,
        }

        # Extract Params schema
        if hasattr(channel_class, "Params") and channel_class.Params:
            channel_schema["params"] = channel_class.Params.model_json_schema()

        # Extract ReactMessage schema
        if hasattr(channel_class, "ReactMessage") and channel_class.ReactMessage:
            channel_schema[
                "reactMessage"
            ] = channel_class.ReactMessage.model_json_schema()

        # Extract DjangoMessage schema
        if hasattr(channel_class, "DjangoMessage") and channel_class.DjangoMessage:
            channel_schema[
                "djangoMessage"
            ] = channel_class.DjangoMessage.model_json_schema()

        schema["channels"][name] = channel_schema

    return schema


def _register_channel_schema_endpoint(
    api: "NinjaAPI",
    path: str,
    operation_id: str,
    summary: str,
    input_cls: type | None,
    output_cls: type,
) -> None:
    """Register a dummy endpoint for schema generation (avoids closure issues)."""
    if input_cls is not None:

        def endpoint(request, data):
            pass

        endpoint.__annotations__ = {"data": input_cls}
    else:

        def endpoint(request):
            pass

    api.post(path, response=output_cls, operation_id=operation_id, summary=summary)(
        endpoint
    )


def get_channels_openapi_schema() -> dict:
    """
    Get OpenAPI schema for all registered channels.

    Uses Django Ninja's schema generation for robust Pydantic→OpenAPI conversion.
    This schema is consumed by openapi-typescript for type generation.
    """
    from ninja import NinjaAPI
    from pydantic import BaseModel

    # Create temporary Ninja API for schema generation only
    schema_api = NinjaAPI(
        title="mizan Channels",
        version="1.0.0",
        description="Auto-generated schema for mizan channels",
        docs_url=None,
        openapi_url=None,
    )

    # Store dynamically created classes
    schema_classes: dict[str, type] = {}
    channel_metadata: list[dict] = []

    for name, channel_class in _registry.items():
        pascal_name = name.replace("_", " ").title().replace(" ", "")

        channel_meta = {
            "name": name,
            "pascalName": pascal_name,
            "hasParams": False,
            "hasReactMessage": False,
            "hasDjangoMessage": False,
        }

        # Register Params type
        if hasattr(channel_class, "Params") and channel_class.Params:
            params_name = f"{pascal_name}Params"
            schema_classes[params_name] = type(params_name, (channel_class.Params,), {})
            channel_meta["hasParams"] = True
            channel_meta["paramsType"] = params_name

            # Create dummy endpoint to include in schema
            _register_channel_schema_endpoint(
                api=schema_api,
                path=f"/channels/{name}/params",
                operation_id=f"{name}Params",
                summary=f"{pascal_name} channel params",
                input_cls=schema_classes[params_name],
                output_cls=BaseModel,
            )

        # Register ReactMessage type
        if hasattr(channel_class, "ReactMessage") and channel_class.ReactMessage:
            react_name = f"{pascal_name}ReactMessage"
            schema_classes[react_name] = type(
                react_name, (channel_class.ReactMessage,), {}
            )
            channel_meta["hasReactMessage"] = True
            channel_meta["reactMessageType"] = react_name

            _register_channel_schema_endpoint(
                api=schema_api,
                path=f"/channels/{name}/react",
                operation_id=f"{name}ReactMessage",
                summary=f"{pascal_name} React→Django message",
                input_cls=schema_classes[react_name],
                output_cls=BaseModel,
            )

        # Register DjangoMessage type
        if hasattr(channel_class, "DjangoMessage") and channel_class.DjangoMessage:
            django_name = f"{pascal_name}DjangoMessage"
            schema_classes[django_name] = type(
                django_name, (channel_class.DjangoMessage,), {}
            )
            channel_meta["hasDjangoMessage"] = True
            channel_meta["djangoMessageType"] = django_name

            _register_channel_schema_endpoint(
                api=schema_api,
                path=f"/channels/{name}/django",
                operation_id=f"{name}DjangoMessage",
                summary=f"{pascal_name} Django→React message",
                input_cls=None,
                output_cls=schema_classes[django_name],
            )

        channel_metadata.append(channel_meta)

    # Get OpenAPI schema from Ninja
    # path_prefix="" avoids URL reverse() — this API is never mounted
    schema = schema_api.get_openapi_schema(path_prefix="")

    # Add channel metadata extension
    schema["x-mizan-channels"] = channel_metadata

    return schema


# =============================================================================
# Schema Endpoint (for TypeScript generation)
# =============================================================================

_schema_router = None


def _get_schema_router():
    """Get the Ninja router for the channels schema endpoint."""
    global _schema_router
    if _schema_router is None:
        from ninja import Router

        _schema_router = Router(tags=["channels"])

        @_schema_router.get("/schema/")
        def channels_schema(request):
            """Get schema for all registered channels (for TypeScript generation)."""
            return get_channels_schema()

    return _schema_router


def get_urls():
    """Get URL patterns for channels schema endpoint."""
    from ninja import NinjaAPI

    api = NinjaAPI(urls_namespace="django_react_channels")
    api.add_router("/", _get_schema_router())
    return api.urls


def __getattr__(name):
    if name == "urls":
        return get_urls()
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


# =============================================================================
# Core Registry Extension
# =============================================================================


class _ChannelsExtension:
    """
    Plugs the channel registry into mizan_core.registry as the 'channels'
    extension. Schema output goes under schema['channels'] in the unified
    registry export consumed by codegen.
    """

    def all(self) -> dict:
        return dict(_registry)

    def schema(self) -> dict:
        out: dict[str, Any] = {}
        for name, channel_class in _registry.items():
            channel_schema: dict[str, Any] = {
                "name": name,
                "type": "channel",
                "bidirectional": False,
            }
            if getattr(channel_class, "Params", None):
                channel_schema["params"] = channel_class.Params.model_json_schema()
            if getattr(channel_class, "ReactMessage", None):
                channel_schema["react_message"] = channel_class.ReactMessage.model_json_schema()
                channel_schema["bidirectional"] = True
            if getattr(channel_class, "DjangoMessage", None):
                channel_schema["django_message"] = channel_class.DjangoMessage.model_json_schema()
            out[name] = channel_schema
        return out

    def clear(self) -> None:
        _registry.clear()


from mizan_core.registry import register_extension as _register_extension
_register_extension("channels", _ChannelsExtension())


# =============================================================================
# Exports
# =============================================================================

__all__ = [
    # URLs
    "urls",
    # Base class
    "ReactChannel",
    # Registration
    "register",
    "get_channel",
    "get_registered_channels",
    # ASGI application
    "get_websocket_application",
    # Schema export
    "get_channels_schema",
]
