"""
Call Event Stream

Subscribes to real-time call lifecycle events from the DVGateway.
Events: call:new, call:ended, conf:join, conf:leave, conf:ended

WebSocket endpoint: /api/v1/ws/callinfo
Protocol: JSON text frames
"""

from __future__ import annotations

import json
import uuid
from datetime import datetime
from typing import Awaitable, Callable

from dvgateway.transport.ws_pool import WsPool, WsSubscription
from dvgateway.types import (
    CallEndedEvent,
    CallEvent,
    CallNewEvent,
    CallSession,
    ConfEndedEvent,
    ConfJoinEvent,
    ConfLeaveEvent,
    Logger,
    StreamDir,
    Unsubscribe,
)

CallEventHandler = Callable[[CallEvent], None | Awaitable[None]]


class CallEventStream:
    """Subscribes to real-time call lifecycle events."""

    def __init__(self, ws_pool: WsPool, logger: Logger) -> None:
        self._ws_pool = ws_pool
        self._logger = logger
        self._handlers: set[CallEventHandler] = set()
        self._subscription_id: str | None = None

    def on(self, handler: CallEventHandler) -> Unsubscribe:
        """Subscribe to all call events. Returns an unsubscribe function."""
        was_empty = len(self._handlers) == 0
        self._handlers.add(handler)

        if was_empty:
            import asyncio
            asyncio.ensure_future(self._start_subscription())

        def unsub() -> None:
            self._handlers.discard(handler)
            if len(self._handlers) == 0:
                self._stop_subscription()

        return unsub

    def on_type(self, event_type: str, handler: Callable[..., None | Awaitable[None]]) -> Unsubscribe:
        """Subscribe to a specific event type."""
        def filtered(event: CallEvent) -> None | Awaitable[None]:
            if event.type == event_type:
                return handler(event)
            return None

        return self.on(filtered)

    async def _start_subscription(self) -> None:
        self._subscription_id = f"callinfo-{uuid.uuid4()}"

        def on_message(data: bytes) -> None:
            self._handle_message(data)

        def on_close() -> None:
            self._logger.debug({}, "Call event stream closed")

        await self._ws_pool.subscribe(WsSubscription(
            id=self._subscription_id,
            path="/api/v1/ws/callinfo",
            params={},
            on_message=on_message,
            on_close=on_close,
        ))

    def _stop_subscription(self) -> None:
        if self._subscription_id:
            self._ws_pool.unsubscribe(self._subscription_id)
            self._subscription_id = None

    def _handle_message(self, data: bytes) -> None:
        try:
            raw: dict[str, object] = json.loads(data.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError):
            self._logger.warn({"data": data.decode("utf-8", errors="replace")}, "Failed to parse call event JSON")
            return

        event = self._normalize_event(raw)
        if not event:
            return

        import asyncio

        for handler in self._handlers:
            try:
                result = handler(event)
                if result is not None and asyncio.iscoroutine(result):
                    asyncio.ensure_future(result)
            except Exception as e:
                self._logger.error({"err": str(e)}, "Call event handler threw an error")

    def _normalize_event(self, raw: dict[str, object]) -> CallEvent | None:
        event_type = raw.get("event")
        timestamp = datetime.now()

        if event_type == "call:new":
            tenant_id = str(raw["tenantId"]) if raw.get("tenantId") else None
            session = CallSession(
                linked_id=str(raw.get("linkedId", "")),
                conf_id=str(raw["confId"]) if raw.get("confId") else None,
                dir=raw.get("dir", "both"),  # type: ignore[arg-type]
                caller=str(raw["caller"]) if raw.get("caller") else None,
                caller_name=str(raw["callerName"]) if raw.get("callerName") else None,
                callee=str(raw["callee"]) if raw.get("callee") else None,
                did=str(raw["did"]) if raw.get("did") else None,
                call_id=str(raw["callid"]) if raw.get("callid") else None,
                agent_number=str(raw["agentNumber"]) if raw.get("agentNumber") else None,
                started_at=timestamp,
                stream_url=str(raw.get("streamUrl", "")),
                tenant_id=tenant_id,
                custom_value_1=str(raw["customValue1"]) if raw.get("customValue1") else None,
                custom_value_2=str(raw["customValue2"]) if raw.get("customValue2") else None,
                custom_value_3=str(raw["customValue3"]) if raw.get("customValue3") else None,
            )
            return CallNewEvent(session=session, tenant_id=tenant_id, timestamp=timestamp)

        if event_type == "call:ended":
            return CallEndedEvent(
                linked_id=str(raw.get("linkedId", "")),
                duration_sec=float(raw.get("duration", 0)),
                timestamp=timestamp,
            )

        if event_type == "conf:join":
            return ConfJoinEvent(
                linked_id=str(raw.get("linkedId", "")),
                conf_id=str(raw.get("confId", "")),
                caller=str(raw["caller"]) if raw.get("caller") else None,
                timestamp=timestamp,
            )

        if event_type == "conf:leave":
            return ConfLeaveEvent(
                linked_id=str(raw.get("linkedId", "")),
                conf_id=str(raw.get("confId", "")),
                timestamp=timestamp,
            )

        if event_type == "conf:ended":
            return ConfEndedEvent(
                conf_id=str(raw.get("confId", "")),
                timestamp=timestamp,
            )

        self._logger.debug({"type": event_type}, "Unknown call event type, ignoring")
        return None
