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

from __future__ import annotations

import asyncio
import time

from collections.abc import Callable
from typing import Any, ClassVar

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

from src.core.event_bus import EventBus
from src.core.numbers import safe_float
from src.core.types import JsonDict, NumericLike
from src.integrations.exchange.market_data_runner import MarketDataRunner
from src.notifications.targets import NotificationTargetRegistry
from src.runtime_events.event_types import RuntimeEventType
from src.runtime_events.models import RuntimeEvent
from src.runtime_events.publisher import RuntimeEventPublisher
from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService
from src.telegram.handlers.auto.ui import build_auto_notification_text
from src.trading.diagnostics.formatter import SemanticDiagnosticFormatter
from src.trading.diagnostics.snapshot import SemanticDiagnosticSnapshotBuilder


class AutoTradeRunner:
    _task: ClassVar[asyncio.Task | None] = None
    _bot: ClassVar[Bot | None] = None
    _chat_id: ClassVar[int | None] = None
    _message_id: ClassVar[int | None] = None
    _render_text: ClassVar[Any] = None
    _render_markup: ClassVar[Any] = None
    _current_screen: ClassVar[str | None] = None
    _analysis_interval_seconds = 5
    _ui_interval_seconds = 30
    _last_text: ClassVar[str | None] = None
    _last_semantic_text: ClassVar[str | None] = None
    _last_ui_refresh_at: ClassVar[float] = 0.0
    _last_event_version: ClassVar[int] = 0
    _retry_after_until: ClassVar[float] = 0.0
    _last_screen_state_key: ClassVar[str | None] = None
    _position_aligned_signal_log_interval_seconds = 900
    _last_position_aligned_signal_log_at_by_key: dict[str, float] = {}
    _edit_lock: ClassVar[asyncio.Lock | None] = None

    @classmethod
    def edit_lock(cls) -> asyncio.Lock:
        if cls._edit_lock is None:
            cls._edit_lock = asyncio.Lock()

        return cls._edit_lock

    @classmethod
    def register_screen(
        cls,
        *,
        bot: Bot,
        chat_id: int,
        message_id: int,
        render_text: Callable[[], str],
        render_markup: Callable[[], Any],
    ) -> None:
        cls._bot = bot
        cls._chat_id = chat_id
        cls._message_id = message_id
        cls._render_text = render_text
        cls._render_markup = render_markup
        cls._last_text = None
        cls._last_semantic_text = None
        cls._last_screen_state_key = None

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

    @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._message_id = None
        cls._render_text = None
        cls._render_markup = None
        cls._last_text = None
        cls._last_semantic_text = None
        cls._last_screen_state_key = None

    @classmethod
    def unregister_screen(
        cls,
        *,
        chat_id: int,
        message_id: int,
    ) -> None:
        if cls._chat_id != chat_id or cls._message_id != message_id:
            return

        cls._message_id = None
        cls._render_text = None
        cls._render_markup = None
        cls._last_text = None
        cls._last_semantic_text = None
        cls._last_screen_state_key = None

    @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._bot = None
        cls._chat_id = None
        cls._message_id = None
        cls._render_text = None
        cls._render_markup = None
        cls._current_screen = None
        cls._last_text = None
        cls._last_semantic_text = None
        cls._last_screen_state_key = None

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

    @classmethod
    def start(cls) -> None:
        service = AutoTradeService()

        MarketDataRunner.start(
            symbol_provider=lambda: service.get_state().symbol,
            interval_seconds=1,
            runtime_key="auto",
            screen="auto",
            action="market_data",
            runtime_label="[AUTO]",
        )

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

        cls._last_event_version = EventBus.version()
        cls._task = asyncio.create_task(cls._worker())

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

        if cls._task is None:
            return

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

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

        while True:
            state = service.get_state()

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

            try:
                service.run_cycle()
            except Exception as exc:
                cls._log_refresh_error(
                    "auto_run_cycle_error",
                    {
                        "error": str(exc),
                        "error_type": type(exc).__name__,
                        "symbol": state.symbol,
                        "strategy": state.strategy,
                        "status": state.status,
                    },
                )

            state = service.get_state()

            previous_event_version = cls._last_event_version
            current_event_version = EventBus.version()
            events = EventBus.events_after(previous_event_version)
            has_important_event = bool(events)

            screen_state_key = cls._screen_state_key(state)
            has_screen_state_changed = screen_state_key != cls._last_screen_state_key

            if has_screen_state_changed:
                cls._last_screen_state_key = screen_state_key

            force_refresh = False

            if has_important_event:
                for event_version, event_type, payload in events:
                    if (
                        event_type == "auto_decision_changed"
                        and cls._has_position_opened_event(events)
                    ):
                        continue

                    if event_type in {
                        "paper_position_opened",
                        "paper_position_closed",
                        "paper_position_flipped",
                    }:
                        force_refresh = True

                    try:
                        await cls._handle_important_event(
                            state=state,
                            event_type=event_type,
                            payload=payload,
                        )
                    except Exception as exc:
                        cls._log_refresh_error(
                            "auto_event_handler_error",
                            {
                                "error": str(exc),
                                "error_type": type(exc).__name__,
                                "event_type": event_type,
                                "event_version": event_version,
                            },
                        )

                cls._last_event_version = current_event_version

            try:
                await cls._refresh_screen(
                    force=force_refresh or has_screen_state_changed
                )
            except Exception as exc:
                cls._log_refresh_error(
                    "auto_refresh_loop_error",
                    {
                        "error": str(exc),
                        "error_type": type(exc).__name__,
                    },
                )

            await asyncio.sleep(cls._analysis_interval_seconds)

    @classmethod
    async def process_last_event_now(cls) -> None:
        state = AutoTradeService().get_state()

        previous_event_version = cls._last_event_version
        current_event_version = EventBus.version()
        events = EventBus.events_after(previous_event_version)

        for event_version, event_type, payload in events:
            try:
                await cls._handle_important_event(
                    state=state,
                    event_type=event_type,
                    payload=payload,
                )
            except Exception as exc:
                cls._log_refresh_error(
                    "auto_event_handler_error",
                    {
                        "error": str(exc),
                        "error_type": type(exc).__name__,
                        "event_type": event_type,
                        "event_version": event_version,
                    },
                )

        cls._last_event_version = current_event_version

    @classmethod
    def _has_position_opened_event(cls, events) -> bool:
        for _, event_type, payload in events:
            if event_type == "paper_position_opened":
                return True

        return False

    @classmethod
    async def _handle_important_event(
        cls,
        *,
        state,
        event_type: str | None,
        payload: JsonDict | None,
    ) -> None:
        if not isinstance(payload, dict):
            payload = {}

        if event_type == "auto_decision_changed":
            if payload.get("decision_status") != "READY":
                return

            signal = str(payload.get("signal", "")).upper()
            if signal not in {"BUY", "SELL"}:
                return

            signal_intent = str(payload.get("signal_intent") or "").upper()

            if signal_intent == "REINFORCE_POSITION":
                cls._log_position_aligned_signal_suppressed(
                    state=state,
                    payload=payload,
                    signal=signal,
                )
                return

            cls._publish_strong_signal_event(state=state, payload=payload)
            return

        if event_type in {
            "paper_position_opened",
            "paper_position_closed",
            "paper_position_flipped",
            "paper_flip_blocked",
        }:
            cls._publish_execution_event(
                state=state,
                event_type=str(event_type),
                payload=payload,
            )
            return

    @classmethod
    def _notification_reason_lines(
        cls,
        state,
    ) -> list[str]:
        snapshot = SemanticDiagnosticSnapshotBuilder().build(
            state,
            is_configured=True,
        )

        return SemanticDiagnosticFormatter().build_notification_reason_lines(
            snapshot,
            limit=2,
        )

    @classmethod
    def _is_position_aligned_signal(cls, *, state, signal: str) -> bool:
        position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()

        if position_side == "LONG" and signal == "BUY":
            return True

        if position_side == "SHORT" and signal == "SELL":
            return True

        return False

    @classmethod
    def _build_position_aligned_signal_suppressed_payload(
        cls,
        *,
        symbol: str,
        strategy: str,
        signal: str,
        position_side: str,
        confidence: float,
        repeat_count: int,
        reason: str | None,
    ) -> JsonDict:
        return {
            "symbol": symbol,
            "strategy": strategy,
            "signal": signal,
            "position_side": position_side,
            "confidence": confidence,
            "repeat_count": repeat_count,
            "reason": reason,
            "suppression_interval_seconds": (
                cls._position_aligned_signal_log_interval_seconds
            ),
        }

    @classmethod
    def _log_position_aligned_signal_suppressed(
        cls,
        *,
        state,
        payload: JsonDict,
        signal: str,
    ) -> None:
        position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()
        symbol = str(payload.get("symbol") or state.symbol or "—")
        strategy = str(payload.get("strategy") or state.strategy or "—")
        confidence = safe_float(
            payload.get("confidence")
        )

        if confidence is None:
            confidence = safe_float(state.last_signal_confidence)

        if confidence is None:
            confidence = 0.0

        repeat_count_value = (
            payload.get("repeat_count")
            if payload.get("repeat_count") is not None
            else state.last_signal_repeat_count
        )

        repeat_count = int(safe_float(repeat_count_value) or 0)

        log_key = (
            f"{position_side}:"
            f"{symbol}:"
            f"{strategy}:"
            f"{signal}"
        )

        now = time.monotonic()
        last_logged_at = cls._last_position_aligned_signal_log_at_by_key.get(log_key)

        if (
            last_logged_at is not None
            and now - last_logged_at < cls._position_aligned_signal_log_interval_seconds
        ):
            return

        cls._last_position_aligned_signal_log_at_by_key[log_key] = now

        try:
            JournalService().log_ui_info(
                event_type="auto_position_aligned_signal_suppressed",
                message=(
                    f"Сигнал {signal} совпадает с открытой позицией "
                    f"{position_side}; уведомление подавлено."
                ),
                screen="auto",
                action="signal_notification",
                payload=cls._build_position_aligned_signal_suppressed_payload(
                    symbol=symbol,
                    strategy=strategy,
                    signal=signal,
                    position_side=position_side,
                    confidence=confidence,
                    repeat_count=repeat_count,
                    reason=payload.get("reason") or state.last_signal_reason,
                ),
            )
        except Exception:
            pass

    @classmethod
    def _signal_price_payload(
        cls,
        *,
        state,
        payload: JsonDict,
        signal: str,
    ) -> JsonDict:
        bid_price = safe_float(payload.get("bid_price"))
        ask_price = safe_float(payload.get("ask_price"))
        last_price = safe_float(payload.get("last_price"))

        if bid_price is None:
            bid_price = safe_float(getattr(state, "execution_bid_price", None))

        if ask_price is None:
            ask_price = safe_float(getattr(state, "execution_ask_price", None))

        if last_price is None:
            last_price = safe_float(getattr(state, "execution_last_price", None))

        signal_price = None
        signal_price_role = "last"

        if signal == "BUY":
            signal_price = ask_price or last_price or bid_price
            signal_price_role = "ask"

        elif signal == "SELL":
            signal_price = bid_price or last_price or ask_price
            signal_price_role = "bid"

        else:
            signal_price = last_price or ask_price or bid_price

        return {
            "bid_price": bid_price,
            "ask_price": ask_price,
            "last_price": last_price,
            "signal_price": signal_price,
            "signal_price_role": signal_price_role,
            "signal_price_source": (
                payload.get("price_source")
                or getattr(state, "execution_price_source", None)
            ),
            "signal_price_age_seconds": (
                payload.get("price_age_seconds")
                or getattr(state, "execution_price_age_seconds", None)
            ),
        }

    @classmethod
    def _build_runtime_signal_payload(
        cls,
        *,
        state,
        symbol: str,
        strategy: str,
        signal: str,
        signal_intent: str,
        repeat_count: int,
        confidence: float,
        leverage,
        reason: str,
        position_context: str,
        is_aligned_signal: bool,
        semantic_lines: list[str],
        price_payload: JsonDict,
    ) -> JsonDict:
        return {
            "execution_block_title": getattr(state, "execution_block_title", None),
            "execution_block_message": getattr(state, "execution_block_message", None),
            "execution_block_action": getattr(state, "execution_block_action", None),
            "symbol": symbol,
            "strategy": strategy,
            "signal": signal,
            "signal_intent": signal_intent,
            "repeat_count": repeat_count,
            "confidence": confidence,
            "leverage": leverage,
            "reason": reason,
            "position_context": position_context,
            "position_side": position_context,
            "is_position_aligned_signal": is_aligned_signal,
            "decision_status": state.decision_status,
            "market_score": getattr(state, "market_score", None),
            "market_score_label": getattr(state, "market_score_label", None),
            "semantic_lines": semantic_lines,
            **price_payload,
        }

    @classmethod
    def _publish_strong_signal_event(
        cls,
        *,
        state,
        payload: JsonDict,
    ) -> None:
        signal = str(payload.get("signal", "")).upper()
        symbol = str(payload.get("symbol") or state.symbol or "—")
        strategy = str(payload.get("strategy") or state.strategy or "—")

        repeat_count_value = (
            payload.get("repeat_count")
            if payload.get("repeat_count") is not None
            else state.last_signal_repeat_count
        )

        repeat_count = int(safe_float(repeat_count_value) or 0)

        confidence = safe_float(payload.get("confidence"))

        if confidence is None:
            confidence = safe_float(state.last_signal_confidence)

        if confidence is None:
            confidence = 0.0

        leverage = (
            payload.get("leverage")
            if payload.get("leverage") is not None
            else state.leverage
        )

        reason = str(payload.get("reason") or state.last_signal_reason or "—")
        signal_intent = str(payload.get("signal_intent") or "").upper()
        if signal_intent == "ENTRY_CANDIDATE":
            position_context = "NONE"
        else:
            position_context = str(
                payload.get("position_side")
                or getattr(state, "position_side", "NONE")
                or "NONE"
            ).upper()

        is_aligned_signal = signal_intent == "REINFORCE_POSITION"

        price_payload = cls._signal_price_payload(
            state=state,
            payload=payload,
            signal=signal,
        )

        semantic_lines = cls._notification_reason_lines(state)

        priority = cls._alert_priority(
            confidence=confidence,
            repeat_count=repeat_count,
        )

        RuntimeEventPublisher.publish(
            RuntimeEvent(
                event_type=RuntimeEventType.AUTO_SIGNAL_READY,
                source="auto_trade_runner",
                title=f"Auto strong signal {signal}",
                payload=cls._build_runtime_signal_payload(
                    state=state,
                    symbol=symbol,
                    strategy=strategy,
                    signal=signal,
                    signal_intent=signal_intent,
                    repeat_count=repeat_count,
                    confidence=confidence,
                    leverage=leverage,
                    reason=reason,
                    position_context=position_context,
                    is_aligned_signal=is_aligned_signal,
                    semantic_lines=semantic_lines,
                    price_payload=price_payload,
                ),
                priority=priority.lower(),
                dedupe_key=(
                    f"auto_signal_ready:"
                    f"{position_context}:"
                    f"{symbol}:"
                    f"{strategy}:"
                    f"{signal}:"
                    f"{signal_intent}:"
                    f"{state.decision_status}"
                ),
            )
        )

    @classmethod
    def _build_runtime_execution_payload(
        cls,
        *,
        state,
        payload: JsonDict,
        event_type: str,
        symbol: str,
        side: str,
        old_side: str,
        new_side: str,
        semantic_lines: list[str],
    ) -> JsonDict:
        return {
            **payload,
            "source_event_type": event_type,
            "symbol": symbol,
            "side": side,
            "old_side": old_side,
            "new_side": new_side,
            "leverage": (
                payload.get("leverage")
                if payload.get("leverage") is not None
                else state.leverage
            ),
            "strategy": state.strategy,
            "market_score": getattr(state, "market_score", None),
            "market_score_label": getattr(state, "market_score_label", None),
            "semantic_lines": semantic_lines,
        }

    @classmethod
    def _publish_execution_event(
        cls,
        *,
        state,
        event_type: str,
        payload: JsonDict,
    ) -> None:
        runtime_event_type = cls._runtime_execution_event_type(event_type)
        if runtime_event_type is None:
            return

        symbol = str(payload.get("symbol") or state.symbol or "—")
        side = str(payload.get("side") or getattr(state, "position_side", "—") or "—")
        old_side = str(payload.get("old_side") or "—")
        new_side = str(payload.get("new_side") or side or "—")

        semantic_lines = cls._notification_reason_lines(state)

        RuntimeEventPublisher.publish(
            RuntimeEvent(
                event_type=runtime_event_type,
                source="auto_trade_runner",
                title=cls._execution_event_title(runtime_event_type),
                payload=cls._build_runtime_execution_payload(
                    state=state,
                    payload=payload,
                    event_type=event_type,
                    symbol=symbol,
                    side=side,
                    old_side=old_side,
                    new_side=new_side,
                    semantic_lines=semantic_lines,
                ),
                priority="normal",
                dedupe_key=cls._execution_dedupe_key(
                    runtime_event_type=runtime_event_type,
                    payload=payload,
                ),
            )
        )

    @classmethod
    def _runtime_execution_event_type(cls, event_type: str) -> RuntimeEventType | None:
        mapping = {
            "paper_position_opened": RuntimeEventType.POSITION_OPENED,
            "paper_position_closed": RuntimeEventType.POSITION_CLOSED,
            "paper_position_flipped": RuntimeEventType.POSITION_FLIPPED,
            "paper_flip_blocked": RuntimeEventType.POSITION_FLIP_BLOCKED,
        }
        return mapping.get(event_type)

    @classmethod
    def _execution_event_title(cls, event_type: RuntimeEventType) -> str:
        mapping = {
            RuntimeEventType.POSITION_OPENED: "Paper position opened",
            RuntimeEventType.POSITION_CLOSED: "Paper position closed",
            RuntimeEventType.POSITION_FLIPPED: "Paper position flipped",
            RuntimeEventType.POSITION_FLIP_BLOCKED: "Flip blocked",
        }
        return mapping.get(event_type, "Paper execution event")

    @classmethod
    def _execution_dedupe_key(
        cls,
        *,
        runtime_event_type: RuntimeEventType,
        payload: JsonDict,
    ) -> str:
        return (
            f"{runtime_event_type.value}:"
            f"{payload.get('symbol')}:"
            f"{payload.get('side')}:"
            f"{payload.get('old_side')}:"
            f"{payload.get('new_side')}:"
            f"{payload.get('entry_price')}:"
            f"{payload.get('exit_price')}:"
            f"{payload.get('new_entry_price')}:"
            f"{payload.get('size')}:"
            f"{payload.get('old_size')}:"
            f"{payload.get('new_size')}:"
            f"{payload.get('pnl')}:"
            f"{payload.get('risk_reason')}:"
            f"{payload.get('is_forced')}"
        )

    @classmethod
    def _alert_priority(
        cls,
        *,
        confidence: NumericLike,
        repeat_count: int,
    ) -> str:
        confidence_value = safe_float(confidence) or 0.0

        if confidence_value >= 0.8 and repeat_count >= 3:
            return "HIGH"

        if confidence_value >= 0.6 or repeat_count >= 2:
            return "MEDIUM"

        return "LOW"

    @classmethod
    def _log_refresh_skip(
        cls,
        reason: str,
        payload: JsonDict | None = None,
    ) -> None:
        return

    @classmethod
    def _log_refresh_success(
        cls,
        payload: JsonDict | None = None,
    ) -> None:
        return

    @classmethod
    def _log_refresh_error(
        cls,
        reason: str,
        payload: JsonDict | None = None,
    ) -> None:
        try:
            JournalService().log_error(
                "auto_screen_refresh_error",
                f"Auto screen refresh error: {reason}",
                payload or {},
            )
        except Exception:
            pass

    @classmethod
    def _screen_state_key(
        cls,
        state,
    ) -> str:
        return "|".join(
            str(value)
            for value in [
                getattr(state, "status", None),
                getattr(state, "symbol", None),
                getattr(state, "strategy", None),
                getattr(state, "last_signal", None),
                #getattr(state, "last_signal_repeat_count", None),
                #getattr(state, "last_signal_confidence", None),
                getattr(state, "decision_status", None),
                getattr(state, "decision_reason", None),
                getattr(state, "market_state", None),
                getattr(state, "market_trend", None),
                getattr(state, "market_volatility", None),
                getattr(state, "market_trend_strength", None),
                getattr(state, "market_trend_quality", None),
                getattr(state, "market_phase", None),
                getattr(state, "market_phase_direction", None),

                # Общая оценка рынка влияет на заголовок блока “Рынок”
                # и на adaptive size, поэтому изменение score должно сразу обновлять UI.
                getattr(state, "market_score", None),
                getattr(state, "market_score_label", None),

                getattr(state, "entry_block_reason", None),
                getattr(state, "entry_block_message", None),
                getattr(state, "execution_quality", None),
                getattr(state, "execution_quality_reason", None),
                getattr(state, "execution_semantic_status", None),
                getattr(state, "execution_semantic_message", None),
                getattr(state, "execution_confidence_score", None),
                getattr(state, "adaptive_size_multiplier", None),
                getattr(state, "adaptive_size_reason", None),
                getattr(state, "effective_target_risk_usd", None),
                getattr(state, "position_side", None),
                getattr(state, "entry_price", None),
                getattr(state, "position_size", None),
                #getattr(state, "unrealized_pnl_usd", None),
                getattr(state, "realized_pnl_usd", None),
                getattr(state, "cycle_closed_trades", None),
                getattr(state, "cycle_realized_pnl_usd", None),
                getattr(state, "cycle_winning_trades", None),
                getattr(state, "last_execution_action", None),
                getattr(state, "last_execution_reason", None),
                getattr(state, "execution_block_title", None),
                getattr(state, "execution_block_message", None),
                getattr(state, "execution_block_action", None),
            ]
        )

    @classmethod
    async def _refresh_screen(cls, *, force: bool = False) -> None:
        async with cls.edit_lock():
            now = time.monotonic()

            if now < cls._retry_after_until:
                cls._log_refresh_skip(
                    "retry_after_active",
                    {"retry_after_until": cls._retry_after_until, "now": now},
                )
                return

            if not force and now - cls._last_ui_refresh_at < cls._ui_interval_seconds:
                cls._log_refresh_skip(
                    "ui_interval_not_reached",
                    {
                        "elapsed": round(now - cls._last_ui_refresh_at, 2),
                        "interval": cls._ui_interval_seconds,
                    },
                )
                return

            bot = cls._bot
            chat_id = cls._chat_id
            message_id = cls._message_id
            render_text = cls._render_text
            render_markup = cls._render_markup

            if (
                bot is None
                or chat_id is None
                or message_id is None
                or render_text is None
                or render_markup is None
            ):
                cls._log_refresh_skip("screen_not_registered")
                return

            text = render_text()
            semantic_text = build_auto_notification_text()
            markup = render_markup()

            if (
                bot is not cls._bot
                or chat_id != cls._chat_id
                or message_id != cls._message_id
                or render_text is not cls._render_text
                or render_markup is not cls._render_markup
            ):
                cls._log_refresh_skip("screen_changed_during_render")
                return

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

                cls._last_text = text
                cls._last_semantic_text = semantic_text
                cls._last_ui_refresh_at = now

            except TelegramRetryAfter as exc:
                cls._retry_after_until = time.monotonic() + exc.retry_after + 15
                cls._last_ui_refresh_at = time.monotonic()
                return

            except TelegramBadRequest as exc:
                error_text = str(exc).lower()

                if "message is not modified" in error_text:
                    cls._last_text = text
                    cls._last_semantic_text = semantic_text
                    cls._last_ui_refresh_at = now
                    return

                if "message to edit not found" in error_text:
                    cls._message_id = None
                    cls._render_text = None
                    cls._render_markup = None
                    cls._last_text = None
                    cls._last_semantic_text = None
                    return

                cls._log_refresh_error("telegram_bad_request", {"error": str(exc)})

            except Exception as exc:
                cls._log_refresh_error("unexpected_refresh_error", {"error": str(exc)})