"""
Управление callback_data (inline и cache стратегии)
"""

import json
import hashlib
import logging
from typing import Any, Protocol
from collections.abc import Callable, Awaitable
from abc import ABC, abstractmethod

from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, TelegramObject
from pydantic import BaseModel

from ui_router.exceptions import CallbackDataError, CallbackDecodeError, CallbackExpiredError, ConfigurationError
from ui_router.schema import CallbackDataStrategy


class CacheStorageProtocol(Protocol):
    """Protocol for cache storage implementations (mirrors services.shared.CacheStorageProtocol)."""

    async def get(self, key: str) -> Any | None: ...
    async def set(self, key: str, value: Any, ttl: int | None = None) -> None: ...
    async def delete(self, key: str) -> None: ...


logger = logging.getLogger(__name__)

UI_CALLBACK_DATA_KEY = "ui_call_data"


class UICallbackData(BaseModel):
    is_global: bool
    scene_id: str
    handler_name: str
    params: dict | None = None


class CallbackDataEncoder(ABC):
    """Абстрактный кодировщик callback_data"""

    @abstractmethod
    async def encode(
        self,
        scene_id: str,
        handler_name: str,
        params: dict[str, Any] | None = None,
        is_global: bool = False,
    ) -> str:
        """Закодировать callback_data"""

    @abstractmethod
    async def decode(self, callback_data: str) -> UICallbackData:
        """Декодировать callback_data"""


class InlineCallbackEncoder(CallbackDataEncoder):
    """Inline кодирование (всё в строке)"""

    def __init__(self, separator: str = ":", max_length: int = 64) -> None:
        self.separator = separator
        self.max_length = max_length

    async def encode(
        self,
        scene_id: str,
        handler_name: str,
        params: dict[str, Any] | None = None,
        is_global: bool = False,
    ) -> str:
        """
        Формат: [g|s]:scene_id:handler_name[:param1=val1:param2=val2]
        g - глобальный, s - scene
        """
        prefix = "g" if is_global else "s"
        parts = [prefix, scene_id, handler_name]

        if params:
            for key, value in params.items():
                parts.append(f"{key}={value}")

        result = self.separator.join(parts)

        if len(result) > self.max_length:
            logger.warning(
                "Callback data exceeds max_length (%d). Parameters will be lost. "
                "Consider using 'cache' strategy for long callback data. "
                "Scene: %s, Handler: %s",
                self.max_length,
                scene_id,
                handler_name,
            )
            if params:
                raw = json.dumps(params, sort_keys=True).encode()
                params_hash = hashlib.md5(raw, usedforsecurity=False).hexdigest()[:8]
                result = self.separator.join([prefix, scene_id, handler_name, params_hash])
            else:
                result = self.separator.join([prefix, scene_id, handler_name])

        return result[: self.max_length]

    async def decode(self, callback_data: str) -> UICallbackData:
        """
        Декодировать callback_data

        Выполняет базовую валидацию и санитизацию параметров.
        """
        parts = callback_data.split(self.separator)

        if len(parts) < 3:
            raise CallbackDecodeError(callback_data)

        is_global = parts[0] == "g"
        scene_id = self._sanitize_value(parts[1])
        handler_name = self._sanitize_value(parts[2])

        params = {}
        for part in parts[3:]:
            if "=" in part:
                key, value = part.split("=", 1)
                key = self._sanitize_value(key)
                value = self._sanitize_value(value)
                if key:
                    params[key] = value

        return UICallbackData(
            is_global=is_global,
            scene_id=scene_id,
            handler_name=handler_name,
            params=params,
        )

    @staticmethod
    def _sanitize_value(value: str) -> str:
        """
        Санитизация значения callback параметра

        Удаляет потенциально опасные символы для предотвращения
        path traversal и injection атак.
        """
        dangerous_chars = ["../", "..\\", "<", ">", "|", "&", ";", "$", "`"]
        sanitized = value
        for char in dangerous_chars:
            sanitized = sanitized.replace(char, "")

        return sanitized[:256]


class CacheCallbackEncoder(CallbackDataEncoder):
    """Cache-based кодирование (данные в кеше)"""

    def __init__(
        self,
        cache_storage: CacheStorageProtocol,
        prefix: str = "cbd",
        ttl: int = 3600,
    ) -> None:
        self.cache = cache_storage
        self.prefix = prefix
        self.ttl = ttl

    async def encode(
        self,
        scene_id: str,
        handler_name: str,
        params: dict[str, Any] | None = None,
        is_global: bool = False,
    ) -> str:
        """Сохранить данные в кеш и вернуть ключ"""
        data = UICallbackData(
            is_global=is_global,
            scene_id=scene_id,
            handler_name=handler_name,
            params=params,
        )

        key_data = f"{scene_id}:{handler_name}:{json.dumps(params or {}, sort_keys=True)}"
        key_hash = hashlib.md5(key_data.encode(), usedforsecurity=False).hexdigest()[:12]
        cache_key = f"{self.prefix}:{key_hash}"

        await self.cache.set(cache_key, data.model_dump_json(), ttl=self.ttl)

        return cache_key

    async def decode(self, callback_data: str) -> UICallbackData:
        """Получить данные из кеша"""
        cached = await self.cache.get(callback_data)

        if not cached:
            raise CallbackExpiredError(callback_data)

        return UICallbackData.model_validate_json(cached)


class CallbackDataManager:
    """Менеджер callback_data"""

    def __init__(self, strategy: CallbackDataStrategy, cache_storage: CacheStorageProtocol | None = None) -> None:
        self.strategy = strategy

        if strategy.type == "inline":
            self.encoder = InlineCallbackEncoder(
                separator=strategy.separator,
                max_length=strategy.max_length,
            )
        elif strategy.type == "cache":
            if cache_storage is None:
                msg = "cache_storage required for cache strategy"
                raise ConfigurationError(msg)
            self.encoder = CacheCallbackEncoder(
                cache_storage=cache_storage,
                prefix=strategy.cache_prefix,
                ttl=strategy.ttl,
            )
        else:
            msg = f"Unknown callback strategy: {strategy.type}"
            raise ConfigurationError(msg)

    async def encode(
        self,
        scene_id: str,
        handler_name: str,
        params: dict[str, Any] | None = None,
        is_global: bool = False,
    ) -> str:
        """Закодировать callback_data"""
        return await self.encoder.encode(scene_id, handler_name, params, is_global)

    async def decode(self, callback_data: str) -> UICallbackData:
        """Декодировать callback_data"""
        return await self.encoder.decode(callback_data)


class CallbackManagerOuterMiddleware(BaseMiddleware):
    def __init__(self, manager: CallbackDataManager) -> None:
        self.manager = manager

    async def __call__(
        self,
        handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
        event: CallbackQuery,
        data: dict[str, Any],
    ) -> Any:
        if not isinstance(event, CallbackQuery):
            return await handler(event, data)

        try:
            decoded = await self.manager.decode(event.data)
            if decoded:
                data[UI_CALLBACK_DATA_KEY] = decoded
        except (CallbackDecodeError, CallbackExpiredError):
            pass

        return await handler(event, data)
