"""
Контекст выполнения - управление состоянием, флагами и данными пользователя
"""

import logging
from typing import Any, TYPE_CHECKING, ClassVar
from collections.abc import Callable, Awaitable
from dataclasses import dataclass, field
from enum import StrEnum

from aiogram import Bot
from aiogram.types import Message, CallbackQuery

if TYPE_CHECKING:
    from .storage import NavigationStorage


logger = logging.getLogger(__name__)


class EventMode(StrEnum):
    """Режим выполнения в контексте события"""

    MESSAGE = "message"
    CALLBACK = "callback"
    SCHEDULED = "scheduled"
    WEBHOOK = "webhook"


@dataclass
class NavigationState:
    """
    Состояние навигации пользователя

    Note:
        Версионирование: При изменении структуры инкрементируйте VERSION.
        Реализуйте миграцию в from_dict для обратной совместимости.
    """

    VERSION: ClassVar[int] = 1

    current_scene: str
    history: list[str] = field(default_factory=list)
    transitions: list[str] = field(default_factory=list)
    user_input: dict[str, Any] = field(default_factory=dict)
    flags_cache: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict:
        """Сериализация в dict для FSM"""
        return {
            "version": self.VERSION,
            "current_scene": self.current_scene,
            "history": self.history,
            "transitions": self.transitions,
            "user_input": self.user_input,
            "flags_cache": self.flags_cache,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "NavigationState":
        """
        Десериализация из dict с поддержкой миграции

        Проверяет версию данных и выполняет миграцию если необходимо.
        """
        version = data.get("version", 0)

        if version == 0:
            logger.debug("Migrating NavigationState from version 0 to %d", cls.VERSION)

        return cls(
            current_scene=data.get("current_scene", ""),
            history=data.get("history", []),
            transitions=data.get("transitions", []),
            user_input=data.get("user_input", {}),
            flags_cache=data.get("flags_cache", {}),
        )


class ExecutionContext:
    """
    Контекст выполнения хендлера с поддержкой разных режимов

    Поддерживает разные режимы выполнения:
    - MESSAGE: обычное сообщение от пользователя (требует Bot + Message)
    - CALLBACK: callback от пользователя (требует Bot + CallbackQuery)
    - SCHEDULED: запланированное событие (может быть без Bot, без user_id)
    - WEBHOOK: webhook событие (может быть без Bot)

    Warning:
        ExecutionContext не является thread-safe. Каждый запрос должен иметь свой
        собственный экземпляр контекста. В архитектуре aiogram каждый update
        обрабатывается в отдельной задаче, поэтому конкурентный доступ
        к одному контексту не должен происходить.
    """

    def __init__(
        self,
        navigation_manager: "NavigationManager",
        navigation_state: NavigationState,
        scene_id: str,
        bot: Bot | None = None,
        user_id: int | None = None,
        chat_id: int | None = None,
        event: Message | CallbackQuery | None = None,
        event_mode: EventMode = EventMode.MESSAGE,
        event_data: dict[str, Any] | None = None,
        flag_resolver: Callable[[str], Awaitable[Any]] | None = None,
    ) -> None:
        self.navigation_manager = navigation_manager
        self.navigation_state = navigation_state
        self.scene_id = scene_id

        self.bot = bot
        self.user_id = user_id
        self.chat_id = chat_id

        self.event = event

        self.event_mode = event_mode

        self.event_data = event_data or {}

        self._resolved_flags: dict[str, Any] = {}
        self._flag_resolver = flag_resolver

    def get_flag(self, name: str, default: Any = None) -> Any:
        """
        Получить значение флага

        Если флаг не найден в кеше и есть flag_resolver - пытается резолвить лениво.
        ВАЖНО: Lazy resolver работает только в синхронном контексте.
        Для полноценного lazy loading используйте get_flag_async().

        Args:
            name: Имя флага
            default: Значение по умолчанию

        Returns:
            Значение флага или default
        """
        if name in self._resolved_flags:
            return self._resolved_flags[name]

        return default

    async def get_flag_async(self, name: str, default: Any = None) -> Any:
        """
        Асинхронная версия get_flag с поддержкой lazy loading

        Если флаг не найден в кеше и есть flag_resolver - резолвит лениво.

        Args:
            name: Имя флага
            default: Значение по умолчанию

        Returns:
            Значение флага или default
        """
        if name in self._resolved_flags:
            return self._resolved_flags[name]

        if self._flag_resolver:
            try:
                value = await self._flag_resolver(name)
                if value is not None:
                    self._resolved_flags[name] = value
                    return value
            except Exception:
                logger.exception("Error resolving flag '%s'", name)

        return default

    def set_flag(self, name: str, value: Any) -> None:
        """Установить значение флага"""
        self._resolved_flags[name] = value

    def get_all_flags(self) -> dict[str, Any]:
        """Получить все флаги"""
        return self._resolved_flags.copy()

    def get_from_event(self, key: str, default: Any = None) -> Any:
        """Получить данные из event (middleware, dishka container и т.д.)"""
        return self.event_data.get(key, default)

    def get_container(self) -> Any:
        """Получить dishka container из event_data"""
        return self.event_data.get("dishka_container")

    def save_user_input(self, key: str, value: Any) -> None:
        """Сохранить ввод пользователя"""
        self.navigation_state.user_input[key] = value

    def get_user_input(self, key: str, default: Any = None) -> Any:
        """Получить сохранённый ввод"""
        return self.navigation_state.user_input.get(key, default)

    def clear_user_input(self, keys: list[str] | None = None) -> None:
        """Очистить ввод пользователя"""
        if keys is None:
            self.navigation_state.user_input.clear()
        else:
            for key in keys:
                self.navigation_state.user_input.pop(key, None)

    def can_send_message(self) -> bool:
        """
        Проверить, может ли контекст отправить сообщение

        Returns:
            True если bot и user_id доступны для отправки
        """
        return self.bot is not None and self.user_id is not None

    def can_edit_message(self) -> bool:
        """
        Может ли редактировать сообщение

        Returns:
            True если это callback режим с CallbackQuery
        """
        return self.event_mode == EventMode.CALLBACK and isinstance(self.event, CallbackQuery)

    async def save_to_storage(self) -> None:
        """
        Сохранить навигационное состояние в storage

        Note:
            Требует bot_id и user_id. Для SCHEDULED событий без контекста
            пользователя ничего не сохраняется.
        """
        if not self.user_id:
            logger.debug("Cannot save navigation state: no user_id in context")
            return

        bot_id = self.bot.id if self.bot else self.event_data.get("bot_id")
        if not bot_id:
            logger.warning("Cannot save navigation state: no bot_id available")
            return

        await self.navigation_manager.save_state(
            bot_id, self.user_id, self.chat_id or self.user_id, self.navigation_state
        )


class NavigationManager:
    """Менеджер навигации"""

    def __init__(
        self,
        navigation_storage: "NavigationStorage",
        max_history_depth: int = 10,
        prefix: str | None = None,
    ) -> None:
        self.navigation_storage = navigation_storage
        self.max_history_depth = max_history_depth
        self.prefix = prefix

    def _make_bot_id(self, bot_id: int | str) -> str:
        """Создать bot_id с prefix если нужно"""
        if self.prefix:
            return f"{self.prefix}:{bot_id}"
        return str(bot_id)

    async def get_state(self, bot_id: int, user_id: int, chat_id: int) -> NavigationState:
        """Получить состояние навигации из storage"""
        prefixed_bot_id = self._make_bot_id(bot_id)
        state = await self.navigation_storage.get_state(prefixed_bot_id, user_id, chat_id)
        return state or NavigationState(current_scene="")

    async def save_state(self, bot_id: int, user_id: int, chat_id: int, state: NavigationState) -> None:
        """Сохранить состояние навигации в storage"""
        prefixed_bot_id = self._make_bot_id(bot_id)
        await self.navigation_storage.save_state(prefixed_bot_id, user_id, chat_id, state)

    async def navigate_to(
        self,
        bot_id: int,
        user_id: int,
        chat_id: int,
        scene_id: str,
        add_to_history: bool = True,
        transition: str = "send",
    ) -> NavigationState:
        """
        Перейти к сцене

        Warning:
            Циклические переходы (A → B → A) допустимы, но могут привести
            к неожиданному поведению при go_back(). Рекомендуется избегать
            глубоких циклов в навигации.
        """
        state = await self.get_state(bot_id, user_id, chat_id)
        logger.debug("[navigate_to] BEFORE: current_scene=%s, history=%s", state.current_scene, state.history)

        if add_to_history and state.current_scene:
            if len(state.history) >= 3:
                recent = state.history[-3:]
                if all(s == state.current_scene for s in recent):
                    logger.warning(
                        "Potential navigation cycle detected: scene '%s' appears 3+ times in recent history. "
                        "User: %s, Chat: %s",
                        state.current_scene,
                        user_id,
                        chat_id,
                    )

            state.history.append(state.current_scene)
            state.transitions.append(transition)
            if len(state.history) > self.max_history_depth:
                state.history = state.history[-self.max_history_depth :]
                state.transitions = state.transitions[-self.max_history_depth :]

        state.current_scene = scene_id
        logger.debug("[navigate_to] AFTER: current_scene=%s, target=%s", state.current_scene, scene_id)
        await self.save_state(bot_id, user_id, chat_id, state)

        return state

    async def go_back(self, bot_id: int, user_id: int, chat_id: int) -> tuple[NavigationState, str | None, str]:
        """Вернуться на предыдущую сцену, возвращает (state, scene_id, transition)"""
        state = await self.get_state(bot_id, user_id, chat_id)

        if not state.history:
            return state, None, "send"

        previous_scene = state.history.pop()
        previous_transition = state.transitions.pop() if state.transitions else "send"
        state.current_scene = previous_scene
        await self.save_state(bot_id, user_id, chat_id, state)

        return state, previous_scene, previous_transition

    async def initialize(
        self,
        bot_id: int,
        user_id: int,
        chat_id: int,
        initial_scene: str,
    ) -> NavigationState:
        """Инициализировать навигацию для нового пользователя"""
        state = NavigationState(current_scene=initial_scene)
        await self.save_state(bot_id, user_id, chat_id, state)
        return state
