# app/src/telegram/live/runner.py

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from collections.abc import Callable

from aiogram import Bot
from aiogram.types import InlineKeyboardMarkup
from aiogram.exceptions import TelegramBadRequest


@dataclass(slots=True)
class LiveScreen:
    screen: str
    bot: Bot
    chat_id: int
    message_id: int
    render_text: Callable[[], str]
    render_markup: Callable[[], InlineKeyboardMarkup | None]
    interval_seconds: int = 5


@dataclass(slots=True)
class StaticScreen:
    screen: str
    bot: Bot
    chat_id: int
    message_id: int


class ScreenRegistry:
    _screens: dict[str, list[StaticScreen]] = {}

    @classmethod
    def register_screen(cls, static_screen: StaticScreen) -> None:
        screens = cls._screens.setdefault(static_screen.screen, [])
        screens[:] = [
            item
            for item in screens
            if not (
                item.chat_id == static_screen.chat_id
                and item.message_id == static_screen.message_id
            )
        ]
        screens.append(static_screen)

    @classmethod
    def unregister_message(cls, *, chat_id: int, message_id: int) -> None:
        empty_screens: list[str] = []

        for screen, screens in cls._screens.items():
            screens[:] = [
                item
                for item in screens
                if not (item.chat_id == chat_id and item.message_id == message_id)
            ]

            if not screens:
                empty_screens.append(screen)

        for screen in empty_screens:
            cls._screens.pop(screen, None)

    @classmethod
    async def delete_screen(cls, *, screen: str, bot: Bot, chat_id: int) -> None:
        screens = cls._screens.get(screen, [])
        remaining: list[StaticScreen] = []

        for static_screen in screens:
            if static_screen.chat_id != chat_id:
                remaining.append(static_screen)
                continue

            try:
                await bot.delete_message(
                    chat_id=static_screen.chat_id,
                    message_id=static_screen.message_id,
                )
            except Exception:
                pass

        if remaining:
            cls._screens[screen] = remaining
        else:
            cls._screens.pop(screen, None)

    @classmethod
    async def delete_chat_screens(
        cls,
        *,
        bot: Bot,
        chat_id: int,
        keep_message_id: int | None = None,
    ) -> None:
        empty_screens: list[str] = []

        for screen, screens in cls._screens.items():
            remaining: list[StaticScreen] = []

            for static_screen in screens:
                if static_screen.chat_id != chat_id:
                    remaining.append(static_screen)
                    continue

                if keep_message_id is not None and static_screen.message_id == keep_message_id:
                    remaining.append(static_screen)
                    continue

                try:
                    await bot.delete_message(
                        chat_id=static_screen.chat_id,
                        message_id=static_screen.message_id,
                    )
                except Exception:
                    pass

            cls._screens[screen] = remaining

            if not remaining:
                empty_screens.append(screen)

        for screen in empty_screens:
            cls._screens.pop(screen, None)


class LiveScreenRunner:
    _screens: dict[str, list[LiveScreen]] = {}
    _tasks: dict[str, asyncio.Task] = {}

    @classmethod
    def register_screen(cls, live_screen: LiveScreen) -> None:
        screens = cls._screens.setdefault(live_screen.screen, [])
        screens[:] = [
            item
            for item in screens
            if not (
                item.chat_id == live_screen.chat_id
                and item.message_id == live_screen.message_id
            )
        ]
        screens.append(live_screen)

    @classmethod
    def unregister_message(cls, *, chat_id: int, message_id: int) -> None:
        empty_screens: list[str] = []

        for screen, screens in cls._screens.items():
            screens[:] = [
                item
                for item in screens
                if not (item.chat_id == chat_id and item.message_id == message_id)
            ]

            if not screens:
                empty_screens.append(screen)

        for screen in empty_screens:
            cls._screens.pop(screen, None)
            cls.stop(screen)

    @classmethod
    async def delete_screen(cls, *, screen: str, bot: Bot, chat_id: int) -> None:
        screens = cls._screens.get(screen, [])
        remaining: list[LiveScreen] = []

        for live_screen in screens:
            if live_screen.chat_id != chat_id:
                remaining.append(live_screen)
                continue

            try:
                await bot.delete_message(
                    chat_id=live_screen.chat_id,
                    message_id=live_screen.message_id,
                )
            except Exception:
                pass

        if remaining:
            cls._screens[screen] = remaining
        else:
            cls._screens.pop(screen, None)
            cls.stop(screen)

    @classmethod
    async def delete_chat_screens(
        cls,
        *,
        bot: Bot,
        chat_id: int,
        keep_message_id: int | None = None,
    ) -> None:
        empty_screens: list[str] = []

        for screen, screens in cls._screens.items():
            remaining: list[LiveScreen] = []

            for live_screen in screens:
                if live_screen.chat_id != chat_id:
                    remaining.append(live_screen)
                    continue

                if keep_message_id is not None and live_screen.message_id == keep_message_id:
                    remaining.append(live_screen)
                    continue

                try:
                    await bot.delete_message(
                        chat_id=live_screen.chat_id,
                        message_id=live_screen.message_id,
                    )
                except Exception:
                    pass

            cls._screens[screen] = remaining

            if not remaining:
                empty_screens.append(screen)

        for screen in empty_screens:
            cls._screens.pop(screen, None)
            cls.stop(screen)

    @classmethod
    def start(cls, screen: str) -> None:
        task = cls._tasks.get(screen)

        if task is not None and not task.done():
            return

        cls._tasks[screen] = asyncio.create_task(cls._worker(screen))

    @classmethod
    def stop(cls, screen: str) -> None:
        task = cls._tasks.get(screen)

        if task is None:
            return

        task.cancel()
        cls._tasks.pop(screen, None)

    @classmethod
    async def _worker(cls, screen: str) -> None:
        while True:
            if screen not in cls._screens:
                cls._tasks.pop(screen, None)
                return

            await cls._refresh_screen(screen)
            await asyncio.sleep(cls._screen_interval(screen))

    @classmethod
    def _screen_interval(cls, screen: str) -> int:
        screens = cls._screens.get(screen, [])
        if not screens:
            return 5

        return screens[0].interval_seconds

    @classmethod
    async def _refresh_screen(cls, screen: str) -> None:
        screens = cls._screens.get(screen, [])
        if not screens:
            cls._screens.pop(screen, None)
            return

        alive_screens: list[LiveScreen] = []

        for live_screen in screens:
            try:
                await live_screen.bot.edit_message_text(
                    chat_id=live_screen.chat_id,
                    message_id=live_screen.message_id,
                    text=live_screen.render_text(),
                    reply_markup=live_screen.render_markup(),
                )
                alive_screens.append(live_screen)

            except TelegramBadRequest as exc:
                if "message is not modified" in str(exc).lower():
                    alive_screens.append(live_screen)
                    continue

            except Exception:
                pass

        if alive_screens:
            cls._screens[screen] = alive_screens
        else:
            cls._screens.pop(screen, None)