"""
EventSynchronizer — единая точка управления подписками и scheduled tasks.

Решает проблемы:
- Мёртвый код отписки в reload_schema (проверял _handlers вместо _subscribers)
- self.schema уже обновлена к моменту отписки (итерирует по новой схеме)
- Scheduled tasks не отменяются при удалении EventDefinition
- Task ID от schedule_*() терялись — невозможно отменить
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from ui_router.schema import EventSourceType, ScheduleType

if TYPE_CHECKING:
    from .bus import EventData
    from collections.abc import Awaitable, Callable
    from .dispatcher import EventDispatcher
    from .bus import EventBus, EventScheduler
    from ui_router.schema import EventDefinition, UIRouter

logger = logging.getLogger(__name__)


@dataclass
class EventDiff:
    """Result of comparing events between two schemas."""

    added: list[EventDefinition] = field(default_factory=list)
    removed: list[EventDefinition] = field(default_factory=list)
    modified: list[tuple[EventDefinition, EventDefinition]] = field(default_factory=list)
    unchanged: list[EventDefinition] = field(default_factory=list)

    @property
    def has_changes(self) -> bool:
        return bool(self.added or self.removed or self.modified)


class EventSynchronizer:
    """Manages event subscriptions and scheduled tasks for a single executor.

    Tracks all callbacks registered on EventBus and all task IDs from EventScheduler,
    enabling clean teardown and schema reload without leaked subscriptions or orphaned tasks.
    """

    def __init__(
        self,
        event_bus: EventBus,
        event_scheduler: EventScheduler,
        bot_id: str | int,
        event_filter: Callable[[EventData], Awaitable[bool]] | None = None,
    ) -> None:
        self._event_bus = event_bus
        self._event_scheduler = event_scheduler
        self._bot_id = bot_id
        self._event_filter = event_filter
        self._scheduled_task_ids: dict[str, list[str]] = {}
        self._subscribed_callbacks: dict[str, Callable] = {}

    @staticmethod
    def diff_events(
        old_schema: UIRouter | None,
        new_schema: UIRouter,
    ) -> EventDiff:
        """Compare event definitions between two schemas."""
        diff = EventDiff()

        if old_schema is None:
            diff.added = list(new_schema.events)
            return diff

        old_events = {e.name: e for e in old_schema.events}
        new_events = {e.name: e for e in new_schema.events}

        for name, event in new_events.items():
            if name not in old_events:
                diff.added.append(event)
            elif event != old_events[name]:
                diff.modified.append((old_events[name], event))
            else:
                diff.unchanged.append(event)

        for name, event in old_events.items():
            if name not in new_events:
                diff.removed.append(event)

        return diff

    async def subscribe_handlers(
        self,
        schema: UIRouter,
        dispatcher: EventDispatcher,
    ) -> None:
        """Subscribe dispatcher to all unique event names from schema.event_handlers."""
        event_names = {eh.event_name for eh in schema.event_handlers}

        for event_name in event_names:
            if self._event_filter is not None:
                filter_fn = self._event_filter

                async def filtered_dispatch(
                    event_data: EventData,
                    _dispatch: Callable[..., Awaitable[None]] = dispatcher.dispatch,
                    _filter: Callable[[EventData], Awaitable[bool]] = filter_fn,
                ) -> None:
                    if await _filter(event_data):
                        await _dispatch(event_data)

                callback = filtered_dispatch
            else:
                callback = dispatcher.dispatch

            self._event_bus.subscribe(event_name, callback)
            self._subscribed_callbacks[event_name] = callback

        if event_names:
            logger.debug(
                "Subscribed to %d event names for bot_id=%s",
                len(event_names),
                self._bot_id,
            )

    async def unsubscribe_all(self) -> None:
        """Unsubscribe all previously registered callbacks from EventBus."""
        for event_name, callback in self._subscribed_callbacks.items():
            self._event_bus.unsubscribe(event_name, callback)

        count = len(self._subscribed_callbacks)
        self._subscribed_callbacks.clear()

        if count:
            logger.debug(
                "Unsubscribed %d event callbacks for bot_id=%s",
                count,
                self._bot_id,
            )

    async def schedule_definitions(self, schema: UIRouter) -> None:
        """Schedule cron/interval events from EventDefinitions.

        Only schedules events with source_type=SCHEDULED and schedule_type in {CRON, INTERVAL}.
        ONCE events are created dynamically through SCHEDULE_EVENT actions, not here.
        """
        for event_def in schema.events:
            if event_def.source_type != EventSourceType.SCHEDULED:
                continue
            if event_def.schedule_type is None:
                continue
            if event_def.schedule_type == ScheduleType.ONCE:
                continue

            try:
                task_id = await self._schedule_event(event_def)
                self._scheduled_task_ids.setdefault(event_def.name, []).append(task_id)
                logger.debug(
                    "Scheduled %s event '%s' (task_id=%s) for bot_id=%s",
                    event_def.schedule_type,
                    event_def.name,
                    task_id,
                    self._bot_id,
                )
            except Exception:
                logger.exception(
                    "Failed to schedule event '%s' for bot_id=%s",
                    event_def.name,
                    self._bot_id,
                )

    async def cancel_scheduled_for_event(self, event_name: str) -> None:
        """Cancel all scheduled tasks for a specific event."""
        task_ids = self._scheduled_task_ids.pop(event_name, [])
        for task_id in task_ids:
            try:
                await self._event_scheduler.cancel_scheduled(task_id)
            except Exception:
                logger.exception(
                    "Failed to cancel task %s for event '%s'",
                    task_id,
                    event_name,
                )

    async def cancel_all_scheduled(self) -> None:
        """Cancel all scheduled tasks."""
        all_task_ids = list(self._scheduled_task_ids.items())
        self._scheduled_task_ids.clear()

        for event_name, task_ids in all_task_ids:
            for task_id in task_ids:
                try:
                    await self._event_scheduler.cancel_scheduled(task_id)
                except Exception:
                    logger.exception(
                        "Failed to cancel task %s for event '%s'",
                        task_id,
                        event_name,
                    )

        if all_task_ids:
            total = sum(len(ids) for _, ids in all_task_ids)
            logger.debug(
                "Cancelled %d scheduled tasks for bot_id=%s",
                total,
                self._bot_id,
            )

    async def sync(
        self,
        old_schema: UIRouter | None,
        new_schema: UIRouter,
        new_dispatcher: EventDispatcher,
    ) -> EventDiff:
        """Full synchronization: unsubscribe old, cancel removed/modified, subscribe new, schedule new.

        Args:
            old_schema: Previous schema (None on first init).
            new_schema: New schema to synchronize to.
            new_dispatcher: New EventDispatcher instance for the new schema.

        Returns:
            EventDiff describing what changed.
        """
        diff = self.diff_events(old_schema, new_schema)

        await self.unsubscribe_all()

        for event_def in diff.removed:
            await self.cancel_scheduled_for_event(event_def.name)

        for old_def, _new_def in diff.modified:
            await self.cancel_scheduled_for_event(old_def.name)

        await self.subscribe_handlers(new_schema, new_dispatcher)

        events_to_schedule = [*diff.added, *(new_def for _, new_def in diff.modified)]
        for event_def in events_to_schedule:
            if event_def.source_type != EventSourceType.SCHEDULED:
                continue
            if event_def.schedule_type is None or event_def.schedule_type == ScheduleType.ONCE:
                continue
            try:
                task_id = await self._schedule_event(event_def)
                self._scheduled_task_ids.setdefault(event_def.name, []).append(task_id)
            except Exception:
                logger.exception(
                    "Failed to schedule event '%s' during sync",
                    event_def.name,
                )

        logger.info(
            "Event sync complete for bot_id=%s: +%d -%d ~%d events",
            self._bot_id,
            len(diff.added),
            len(diff.removed),
            len(diff.modified),
        )
        return diff

    async def teardown(self) -> None:
        """Full teardown: unsubscribe all + cancel all scheduled tasks."""
        await self.unsubscribe_all()
        await self.cancel_all_scheduled()
        logger.debug("Event teardown complete for bot_id=%s", self._bot_id)

    async def _schedule_event(self, event_def: EventDefinition) -> str:
        """Schedule a single event definition. Returns task_id."""
        event_data = dict(event_def.expected_data) if event_def.expected_data else {}

        if event_def.schedule_type == ScheduleType.CRON:
            return await self._event_scheduler.schedule_cron(
                event_name=event_def.name,
                cron_expr=event_def.schedule_value or "",
                event_data=event_data,
                bot_id=self._bot_id,
            )
        if event_def.schedule_type == ScheduleType.INTERVAL:
            interval = int(event_def.schedule_value or "0")
            return await self._event_scheduler.schedule_interval(
                event_name=event_def.name,
                interval_seconds=interval,
                event_data=event_data,
                bot_id=self._bot_id,
            )
        msg = f"Unsupported schedule_type for auto-scheduling: {event_def.schedule_type}"
        raise ValueError(msg)
