# app/src/trading/debug/runner.py

from __future__ import annotations

import asyncio
import time
from collections.abc import Callable
from typing import ClassVar, Protocol

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

from src.core.telegram_errors import (
    is_message_not_modified,
    is_message_to_edit_not_found,
)
from src.integrations.exchange.market_data_runner import MarketDataRunner
from src.notifications.targets import NotificationTargetRegistry
from src.trading.debug.service import DebugTradeService


class RenderText(Protocol):
    def __call__(self) -> str: ...


class RenderMarkup(Protocol):
    def __call__(self) -> InlineKeyboardMarkup | None: ...
    


class DebugTradeRunner:
    _task: ClassVar[asyncio.Task[None] | None] = None

    _bot: ClassVar[Bot | None] = None
    _chat_id: ClassVar[int | None] = None
    _message_id: ClassVar[int | None] = None

    _text_renderer: ClassVar[RenderText | None] = None
    _markup_renderer: ClassVar[RenderMarkup | None] = None

    _current_screen: ClassVar[str | None] = None

    _interval_seconds: ClassVar[int] = 5
    _market_interval_seconds: ClassVar[int] = 1

    _last_text: ClassVar[str | None] = None
    _last_refresh_at: ClassVar[float] = 0.0
    _retry_after_until: ClassVar[float] = 0.0

    @classmethod
    def register_screen(
        cls,
        *,
        bot: Bot,
        chat_id: int,
        message_id: int,
        render_text: RenderText,
        render_markup: RenderMarkup,
    ) -> None:
        cls._bot = bot
        cls._chat_id = chat_id
        cls._message_id = message_id
        cls._text_renderer = render_text
        cls._markup_renderer = render_markup
        cls._last_text = None

        NotificationTargetRegistry.set_default_chat(
            bot=bot,
            chat_id=chat_id,
        )

    @classmethod
    def _reset_screen(cls) -> None:
        cls._message_id = None
        cls._text_renderer = None
        cls._markup_renderer = None
        cls._last_text = None

    @classmethod
    def _reset_runtime(cls) -> None:
        cls._bot = None
        cls._chat_id = None
        cls._current_screen = None
        cls._reset_screen()

    @classmethod
    def _is_screen_ready(cls) -> bool:
        return (
            cls._bot is not None
            and cls._chat_id is not None
            and cls._message_id is not None
            and cls._text_renderer is not None
            and cls._markup_renderer is not None
        )

    @classmethod
    async def delete_registered_screen(
        cls,
        *,
        bot: Bot,
        chat_id: int,
    ) -> None:
        if cls._chat_id is None or cls._message_id is None:
            return

        if cls._chat_id != chat_id:
            return

        try:
            await bot.delete_message(
                chat_id=cls._chat_id,
                message_id=cls._message_id,
            )
        except Exception:
            pass

        cls._reset_screen()

    @classmethod
    async def detach_screen(
        cls,
        *,
        delete_message: bool = False,
        bot: Bot | None = None,
        chat_id: int | None = None,
        keep_message_id: int | None = None,
    ) -> None:
        if (
            delete_message
            and bot is not None
            and cls._chat_id is not None
            and cls._message_id is not None
            and cls._chat_id == chat_id
            and cls._message_id != keep_message_id
        ):
            try:
                await bot.delete_message(
                    chat_id=cls._chat_id,
                    message_id=cls._message_id,
                )
            except Exception:
                pass

        cls._reset_runtime()

    @classmethod
    def set_current_screen(cls, screen: str) -> None:
        cls._current_screen = screen

    @classmethod
    def start(cls) -> None:
        service = DebugTradeService()
        state = service.get_state()

        state.status = "RUNNING"

        MarketDataRunner.start(
            symbol_provider=lambda: DebugTradeService().get_state().symbol,
            interval_seconds=cls._market_interval_seconds,
            runtime_key="debug_auto",
            screen="debug_auto",
            action="market_data",
            runtime_label="[DEBUG]",
        )

        cls._last_text = None

        if cls._task is not None and not cls._task.done():
            return

        cls._task = asyncio.create_task(cls._worker())

    @classmethod
    def stop(cls) -> None:
        MarketDataRunner.stop("debug_auto")

        if cls._task is None:
            return

        cls._task.cancel()
        cls._task = None

    @classmethod
    async def _worker(cls) -> None:
        service = DebugTradeService()

        while True:
            state = service.get_state()

            if state.status == "OFF":
                cls._task = None
                MarketDataRunner.stop("debug_auto")
                break

            service.process()
            await cls.refresh_screen(force=False)

            await asyncio.sleep(cls._interval_seconds)

    @classmethod
    async def refresh_screen(
        cls,
        *,
        force: bool = False,
    ) -> None:
        if cls._current_screen != "debug_auto":
            return

        now = time.monotonic()

        if now < cls._retry_after_until:
            return

        if (
            not force
            and now - cls._last_refresh_at < cls._interval_seconds
        ):
            return

        if not cls._is_screen_ready():
            return

        bot = cls._bot
        chat_id = cls._chat_id
        message_id = cls._message_id
        text_renderer = cls._text_renderer
        markup_renderer = cls._markup_renderer

        if (
            bot is None
            or chat_id is None
            or message_id is None
            or text_renderer is None
            or markup_renderer is None
        ):
            return

        text = text_renderer()

        if text == cls._last_text:
            return

        try:
            await bot.edit_message_text(
                chat_id=chat_id,
                message_id=message_id,
                text=text,
                reply_markup=markup_renderer(),
            )

            cls._last_text = text
            cls._last_refresh_at = now

        except TelegramRetryAfter as exc:
            cls._retry_after_until = time.monotonic() + exc.retry_after + 5

        except TelegramBadRequest as exc:
            if is_message_not_modified(exc):
                cls._last_text = text
                cls._last_refresh_at = now
                return

            if is_message_to_edit_not_found(exc):
                cls._reset_screen()
                return

        except Exception:
            pass