"""
Event system для UI Router

Архитектура:
- EventBus: pub/sub шина для распространения событий
- EventScheduler: независимый планировщик отложенных событий
- EventDispatcher: обработчик событий с контекстом (в router.py)
"""

import asyncio
import logging
from typing import Any, Protocol, Literal
from collections.abc import Callable, Awaitable
from datetime import datetime, timedelta
from dataclasses import dataclass, field

from ui_router.exceptions import EventSchedulingError
from ui_router.schema import EventSourceType, ScheduleType
from ui_router.integrations._utils import parse_delay


logger = logging.getLogger(__name__)


@dataclass
class EventData:
    """Данные события"""

    event_name: str
    source_type: EventSourceType
    data: dict[str, Any]
    bot_id: str | int
    user_id: int | None = None
    chat_id: int | None = None
    timestamp: datetime = field(default_factory=datetime.now)


@dataclass
class ScheduledTask:
    """Описание запланированной задачи"""

    task_id: str
    event_name: str
    event_data: dict[str, Any]
    bot_id: str | int
    schedule_type: ScheduleType
    user_id: int | None = None
    chat_id: int | None = None
    schedule_value: str | None = None

    status: Literal["pending", "running", "completed", "failed", "cancelled"] = "pending"
    created_at: datetime = field(default_factory=datetime.now)
    next_run_at: datetime | None = None
    last_run_at: datetime | None = None

    error: str | None = None


class EventBus:
    """
    Шина событий (только pub/sub).

    Отвечает только за распространение событий подписчикам.
    НЕ управляет планировщиком.
    НЕ обрабатывает события сам.
    """

    def __init__(self) -> None:
        self._subscribers: dict[str, list[Callable[[EventData], Awaitable[None]]]] = {}

    def subscribe(
        self,
        event_name: str,
        callback: Callable[[EventData], Awaitable[None]],
    ) -> None:
        """
        Подписаться на событие

        Args:
            event_name: Имя события для подписки
            callback: Async функция для вызова при событии
        """
        if event_name not in self._subscribers:
            self._subscribers[event_name] = []
        self._subscribers[event_name].append(callback)

    def unsubscribe(self, event_name: str, callback: Callable[[EventData], Awaitable[None]]) -> None:
        """
        Отписаться от события

        Args:
            event_name: Имя события
            callback: Функция для удаления из подписчиков
        """
        if event_name in self._subscribers:
            try:
                self._subscribers[event_name].remove(callback)
            except ValueError:
                logger.warning("Callback not found in subscribers for event '%s'", event_name)

    async def emit(self, event: EventData) -> None:
        """
        Эмитить событие всем подписчикам

        Вызывает всех подписчиков синхронно. Если callback вызывает исключение,
        оно логируется, но не прерывает обработку остальных подписчиков.

        Args:
            event: Данные события для эмита
        """
        if event.event_name not in self._subscribers:
            logger.debug("No subscribers for event '%s'", event.event_name)
            return

        for callback in self._subscribers[event.event_name]:
            try:
                await callback(event)
            except Exception:
                logger.exception("Error in event callback for '%s'", event.event_name)


class EventScheduler(Protocol):
    """
    Интерфейс для планировщика событий.

    Реализуется пользователем через taskiq, APScheduler, Celery, etc.
    НЕ зависит от EventBus.

    Note:
        Protocol использует структурную типизацию (duck typing).
    """

    async def schedule_once(
        self,
        event_name: str,
        delay: str,
        event_data: dict[str, Any],
        bot_id: str | int,
        user_id: int | None = None,
        chat_id: int | None = None,
    ) -> str:
        """
        Запланировать одноразовое событие

        Args:
            event_name: Имя события
            delay: Задержка ("10m", "1h", "30s")
            event_data: Данные события (словарь)
            bot_id: ID бота
            user_id: ID пользователя (опционально, для фоновых задач может быть None)
            chat_id: ID чата (опционально)

        Returns:
            task_id: ID задачи для отмены
        """
        ...

    async def schedule_cron(
        self,
        event_name: str,
        cron_expr: str,
        event_data: dict[str, Any],
        bot_id: str | int,
        user_id: int | None = None,
        chat_id: int | None = None,
    ) -> str:
        """
        Запланировать повторяющееся событие (cron)

        Args:
            event_name: Имя события
            cron_expr: Cron выражение
            event_data: Данные события
            bot_id: ID бота
            user_id: ID пользователя (опционально)
            chat_id: ID чата (опционально)

        Returns:
            task_id: ID задачи
        """
        ...

    async def schedule_interval(
        self,
        event_name: str,
        interval_seconds: int,
        event_data: dict[str, Any],
        bot_id: str | int,
        user_id: int | None = None,
        chat_id: int | None = None,
    ) -> str:
        """
        Запланировать периодическое событие (интервал)

        Args:
            event_name: Имя события
            interval_seconds: Интервал в секундах
            event_data: Данные события
            bot_id: ID бота
            user_id: ID пользователя (опционально)
            chat_id: ID чата (опционально)

        Returns:
            task_id: ID задачи
        """
        ...

    async def cancel_scheduled(self, task_id: str) -> bool:
        """
        Отменить запланированное событие

        Args:
            task_id: ID задачи для отмены

        Returns:
            True если задача была отменена, False если не найдена
        """
        ...


class InMemoryEventScheduler:
    """
    Простой in-memory scheduler для разработки/тестирования.

    НЕ зависит от EventBus.
    Вызывает callback напрямую при срабатывании события.

    В продакшене используйте taskiq, APScheduler, или Celery.
    """

    def __init__(
        self,
        event_callback: Callable[[EventData], Awaitable[None]],
    ) -> None:
        """
        Инициализация scheduler

        Args:
            event_callback: Callback для вызова при срабатывании события.
            Обычно это EventBus.emit или EventDispatcher.dispatch_scheduled
        """
        self.event_callback = event_callback
        self._tasks: dict[str, asyncio.Task] = {}
        self._task_counter = 0

    def _generate_task_id(self) -> str:
        """Генерация уникального ID задачи"""
        self._task_counter += 1
        return f"task_{self._task_counter}"

    @staticmethod
    def _parse_delay(delay: str) -> timedelta:
        return parse_delay(delay)

    async def schedule_once(
        self,
        event_name: str,
        delay: str,
        event_data: dict[str, Any],
        bot_id: str | int,
        user_id: int | None = None,
        chat_id: int | None = None,
    ) -> str:
        """
        Запланировать одноразовое событие

        Args:
            event_name: Имя события
            delay: Задержка ("10m", "1h", "30s")
            event_data: Данные события (словарь, НЕ EventData!)
            bot_id: ID бота
            user_id: ID пользователя (опционально)
            chat_id: ID чата (опционально)

        Returns:
            task_id: ID задачи для отмены
        """
        task_id = self._generate_task_id()
        delay_td = self._parse_delay(delay)

        async def delayed_task() -> None:
            try:
                await asyncio.sleep(delay_td.total_seconds())

                event = EventData(
                    event_name=event_name,
                    source_type=EventSourceType.SCHEDULED,
                    data=event_data,
                    bot_id=bot_id,
                    user_id=user_id,
                    chat_id=chat_id,
                )

                await self.event_callback(event)
            except asyncio.CancelledError:
                logger.debug("Task %s was cancelled", task_id)
            except Exception:
                logger.exception("Error executing scheduled task %s", task_id)
            finally:
                self._tasks.pop(task_id, None)

        task = asyncio.create_task(delayed_task())
        self._tasks[task_id] = task

        logger.debug(
            "Scheduled event '%s' with delay %s, task_id: %s, user_id: %s",
            event_name,
            delay,
            task_id,
            user_id,
        )

        return task_id

    async def schedule_cron(
        self,
        event_name: str,
        cron_expr: str,
        event_data: dict[str, Any],
        bot_id: str | int,
        user_id: int | None = None,
        chat_id: int | None = None,
    ) -> str:
        """
        Запланировать повторяющееся событие (упрощённо)

        Note:
            InMemoryEventScheduler не поддерживает cron.
            Для production используйте APScheduler или taskiq-scheduler.
        """
        msg = "InMemoryEventScheduler does not support cron. Use APScheduler or taskiq for cron scheduling"
        raise EventSchedulingError(msg)

    async def schedule_interval(
        self,
        event_name: str,
        interval_seconds: int,
        event_data: dict[str, Any],
        bot_id: str | int,
        user_id: int | None = None,
        chat_id: int | None = None,
    ) -> str:
        """
        Запланировать периодическое событие (упрощённо)

        Note:
            InMemoryEventScheduler не поддерживает interval.
            Для production используйте APScheduler или taskiq для interval scheduling.
        """
        msg = "InMemoryEventScheduler does not support interval. Use APScheduler or taskiq for interval scheduling"
        raise EventSchedulingError(msg)

    async def cancel_scheduled(self, task_id: str) -> bool:
        """
        Отменить задачу

        Args:
            task_id: ID задачи для отмены

        Returns:
            True если задача была отменена, False если не найдена
        """
        if task_id in self._tasks:
            self._tasks[task_id].cancel()
            del self._tasks[task_id]
            logger.debug("Cancelled scheduled task %s", task_id)
            return True

        logger.warning("Task %s not found for cancellation", task_id)
        return False
