"""Small integration module installed into a customer's python-telegram-bot app.

The module contains Telegram glue only. All moderation policy remains on the
private IHM API server.
"""

from __future__ import annotations

import base64
import html
import inspect
import logging
import time
from collections import defaultdict, deque
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Awaitable, Callable

import httpx
from telegram import (
    ChatAdministratorRights,
    ChatPermissions,
    InlineKeyboardButton,
    InlineKeyboardMarkup,
    KeyboardButton,
    KeyboardButtonRequestChat,
    ReplyKeyboardMarkup,
    ReplyKeyboardRemove,
    Update,
)
from telegram.constants import ChatMemberStatus, ChatType, ParseMode
from telegram.ext import (
    Application,
    CallbackQueryHandler,
    ChatMemberHandler,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    MessageReactionHandler,
    filters,
)


logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class AdapterSettings:
    api_key: str
    base_url: str = "https://ihm.thebeautifulmusic.ru"
    timeout_seconds: int = 55


class IHMAPIClient:
    def __init__(self, settings: AdapterSettings):
        self.settings = settings

    async def request(self, method: str, path: str, payload: dict | None = None) -> dict:
        headers = {"Authorization": f"Bearer {self.settings.api_key}"}
        async with httpx.AsyncClient(timeout=self.settings.timeout_seconds) as client:
            response = await client.request(
                method,
                self.settings.base_url.rstrip("/") + path,
                json=payload,
                headers=headers,
            )
            response.raise_for_status()
            return response.json()

    async def technical(self, event: str, details: str, level: str = "error") -> None:
        try:
            await self.request(
                "POST",
                "/v1/technical-events",
                {"level": level, "event": event, "details": details[:4000]},
            )
        except Exception:
            logger.exception("Unable to send IHM technical event")


class IHMAdapter:
    ROLE_CONFIG = (
        ("questionnaire", "канал с анкетами"),
        ("alerts", "канал важных уведомлений"),
    )

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://ihm.thebeautifulmusic.ru",
        should_moderate: Callable[[Update, ContextTypes.DEFAULT_TYPE], bool | Awaitable[bool]] | None = None,
    ):
        self.client = IHMAPIClient(AdapterSettings(api_key=api_key, base_url=base_url))
        self.should_moderate = should_moderate
        self.bindings: dict[str, Any] | None = None
        self.registered_bot_id = 0
        self.recent_messages: dict[tuple[int, int], deque[tuple[float, int]]] = defaultdict(
            lambda: deque(maxlen=1000)
        )
        self.linked_channel_ids: dict[int, int] = {}

    async def _allowed_by_host_bot(
        self, update: Update, context: ContextTypes.DEFAULT_TYPE
    ) -> bool:
        """Optional hook for the host bot's captcha/allowlist state."""
        if self.should_moderate is None:
            return True
        result = self.should_moderate(update, context)
        if inspect.isawaitable(result):
            result = await result
        return bool(result)

    def install(self, application: Application) -> None:
        application.add_handler(CommandHandler("iloveyou", self.iloveyou), group=-100)
        application.add_handler(
            MessageHandler(filters.StatusUpdate.CHAT_SHARED, self.chat_shared), group=-100
        )
        application.add_handler(
            MessageHandler(
                filters.ChatType.PRIVATE & filters.Regex(r"^Готово$"),
                self.finish_group_selection,
            ),
            group=-100,
        )
        application.add_handler(
            CallbackQueryHandler(self.incident_callback, pattern=r"^ihm:"), group=-100
        )
        application.add_handler(
            ChatMemberHandler(self.my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER), group=-100
        )
        application.add_handler(MessageReactionHandler(self.reaction), group=90)
        application.add_handler(
            MessageHandler(
                filters.UpdateType.MESSAGE & filters.ChatType.GROUPS,
                self.message,
            ),
            group=90,
        )
        application.add_handler(
            MessageHandler(
                filters.UpdateType.EDITED_MESSAGE & filters.ChatType.GROUPS,
                self.edited_message,
            ),
            group=90,
        )

    async def ensure_registered(self, bot) -> None:
        me = await bot.get_me()
        if self.registered_bot_id == me.id:
            return
        await self.client.request(
            "POST", "/v1/register", {"bot_id": me.id, "bot_username": me.username or ""}
        )
        self.registered_bot_id = me.id

    @staticmethod
    def _rights(channel: bool = True) -> ChatAdministratorRights:
        return ChatAdministratorRights(
            is_anonymous=False,
            can_manage_chat=True,
            can_delete_messages=not channel,
            can_manage_video_chats=False,
            can_restrict_members=not channel,
            can_promote_members=False,
            can_change_info=False,
            can_invite_users=True,
            can_post_stories=False,
            can_edit_stories=False,
            can_delete_stories=False,
            can_post_messages=channel,
            can_edit_messages=False,
            can_pin_messages=False,
            can_manage_topics=False,
            can_manage_direct_messages=False,
            can_manage_tags=False,
        )

    def _channel_keyboard(self, role_index: int) -> ReplyKeyboardMarkup:
        request = KeyboardButtonRequestChat(
            request_id=9100 + role_index,
            chat_is_channel=True,
            user_administrator_rights=self._rights(channel=True),
            bot_administrator_rights=self._rights(channel=True),
            request_title=True,
            request_username=True,
            request_photo=False,
        )
        return ReplyKeyboardMarkup(
            [[KeyboardButton("Выбрать канал", request_chat=request)]],
            resize_keyboard=True,
            one_time_keyboard=True,
        )

    def _group_keyboard(self, request_id: int) -> ReplyKeyboardMarkup:
        request = KeyboardButtonRequestChat(
            request_id=request_id,
            chat_is_channel=False,
            user_administrator_rights=self._rights(channel=False),
            bot_administrator_rights=self._rights(channel=False),
            request_title=True,
            request_username=True,
            request_photo=False,
        )
        return ReplyKeyboardMarkup(
            [
                [KeyboardButton("Добавить группу", request_chat=request)],
                [KeyboardButton("Готово")],
            ],
            resize_keyboard=True,
            one_time_keyboard=False,
        )

    async def iloveyou(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        message = update.effective_message
        if not message or not update.effective_user:
            return
        if update.effective_chat.type != ChatType.PRIVATE:
            await message.reply_text("Настройка /iloveyou выполняется в личных сообщениях с ботом.")
            return
        try:
            await self.ensure_registered(context.bot)
            existing = await self.get_bindings()
            if existing and int(existing.get("configured_by", 0)) not in {
                0,
                update.effective_user.id,
            }:
                await message.reply_text(
                    "Каналы уже настроены другим администратором. "
                    "Повторную привязку может выполнить только он."
                )
                return
        except Exception as exc:
            await self.client.technical("adapter_registration_failed", str(exc))
            await message.reply_text("Не удалось подключиться к API. Владелец сети уже получил ошибку.")
            return
        context.user_data["ihm_setup"] = {
            "role_index": 0,
            "channels": {},
            "moderated_groups": [],
            "stage": "channels",
        }
        await message.reply_text(
            "<b>Настройка IHM-модерации</b>\n\n"
            "Сначала выбери канал, куда будут приходить анкеты нарушителей. "
            "Ты должен быть его администратором.",
            parse_mode=ParseMode.HTML,
            reply_markup=self._channel_keyboard(0),
        )

    async def _selected_chat(
        self,
        update: Update,
        context: ContextTypes.DEFAULT_TYPE,
        role: str,
        expect_channel: bool,
    ) -> dict:
        shared = update.effective_message.chat_shared
        chat = await context.bot.get_chat(shared.chat_id)
        is_channel = chat.type == ChatType.CHANNEL
        if is_channel != expect_channel:
            raise ValueError("wrong_chat_type")
        user_member = await context.bot.get_chat_member(chat.id, update.effective_user.id)
        if user_member.status not in {ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER}:
            raise ValueError("selected_user_is_not_channel_admin")
        bot_member = await context.bot.get_chat_member(chat.id, context.bot.id)
        if bot_member.status != ChatMemberStatus.ADMINISTRATOR:
            raise ValueError("bot_is_not_channel_admin")
        link = f"https://t.me/{chat.username}" if chat.username else ""
        if not link:
            invite = await context.bot.create_chat_invite_link(
                chat.id, name="IHM Control"
            )
            link = invite.invite_link
        return {
            "chat_id": chat.id,
            "title": chat.title or f"Чат {chat.id}",
            "username": chat.username or "",
            "invite_link": link,
            "role": role,
        }

    async def chat_shared(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        message = update.effective_message
        state = context.user_data.get("ihm_setup") if update.effective_user else None
        if not message or not message.chat_shared or not isinstance(state, dict):
            return
        if state.get("stage") == "groups":
            expected_request_id = int(state.get("group_request_id", 9200))
            if message.chat_shared.request_id != expected_request_id:
                return
            try:
                selected = await self._selected_chat(
                    update, context, "moderated_group", expect_channel=False
                )
            except Exception as exc:
                await self.client.technical("group_binding_failed", str(exc))
                await message.reply_text(
                    "Не получилось добавить группу. Бот и настраивающий пользователь должны "
                    "быть администраторами группы. Боту нужны права удаления сообщений, "
                    "ограничения участников и приглашения пользователей.",
                    reply_markup=self._group_keyboard(expected_request_id),
                )
                return
            groups = state.setdefault("moderated_groups", [])
            if not any(int(group["chat_id"]) == int(selected["chat_id"]) for group in groups):
                groups.append(selected)
            state["group_request_id"] = 9200 + len(groups)
            await message.reply_text(
                f"✅ Группа добавлена: {selected['title']}\n\n"
                "Можно добавить ещё одну группу или нажать «Готово».",
                reply_markup=self._group_keyboard(state["group_request_id"]),
            )
            return
        role_index = int(state.get("role_index", 0))
        if role_index < 0 or role_index >= len(self.ROLE_CONFIG):
            return
        expected_request_id = 9100 + role_index
        if message.chat_shared.request_id != expected_request_id:
            return
        role, label = self.ROLE_CONFIG[role_index]
        try:
            selected = await self._selected_chat(
                update, context, role, expect_channel=True
            )
        except Exception as exc:
            await self.client.technical("channel_binding_failed", f"{role}: {exc}")
            await message.reply_text(
                "Не получилось привязать канал. Проверь, что бот добавлен администратором "
                "с правами публикации и приглашения пользователей, затем выбери его ещё раз.",
                reply_markup=self._channel_keyboard(role_index),
            )
            return
        state["channels"][role] = selected
        role_index += 1
        state["role_index"] = role_index
        if role_index < len(self.ROLE_CONFIG):
            _, next_label = self.ROLE_CONFIG[role_index]
            await message.reply_text(
                f"✅ Привязан {label}: {selected['title']}\n\nТеперь выбери {next_label}.",
                reply_markup=self._channel_keyboard(role_index),
            )
            return
        state["stage"] = "groups"
        state["group_request_id"] = 9200
        await message.reply_text(
            f"✅ Привязан {label}: {selected['title']}\n\n"
            "Теперь добавь группы, в которых должна работать модерация. "
            "Можно выбрать несколько групп, а затем нажать «Готово».",
            reply_markup=self._group_keyboard(9200),
        )

    async def finish_group_selection(
        self, update: Update, context: ContextTypes.DEFAULT_TYPE
    ) -> None:
        message = update.effective_message
        state = context.user_data.get("ihm_setup") if update.effective_user else None
        if not message or not isinstance(state, dict) or state.get("stage") != "groups":
            return
        groups = list(state.get("moderated_groups") or [])
        if not groups:
            await message.reply_text(
                "Сначала добавь хотя бы одну группу для модерации.",
                reply_markup=self._group_keyboard(int(state.get("group_request_id", 9200))),
            )
            return
        payload = {
            "configured_by": update.effective_user.id,
            "questionnaire": state["channels"]["questionnaire"],
            "alerts": state["channels"]["alerts"],
            "moderated_groups": groups,
        }
        for value in payload.values():
            if isinstance(value, dict):
                value.pop("role", None)
            elif isinstance(value, list):
                for item in value:
                    item.pop("role", None)
        try:
            await self.client.request("POST", "/v1/bindings", payload)
            self.bindings = payload
        except Exception as exc:
            await self.client.technical("binding_save_failed", str(exc))
            await message.reply_text(
                "Каналы выбраны, но API не сохранило настройки. Владелец сети получил ошибку.",
                reply_markup=ReplyKeyboardRemove(),
            )
            return
        context.user_data.pop("ihm_setup", None)
        await message.reply_text(
            "✅ <b>IHM-модерация подключена</b>\n\n"
            f"Канал анкет, важные уведомления и группы сохранены: {len(groups)}. "
            "Технические логи в эти каналы отправляться не будут.",
            parse_mode=ParseMode.HTML,
            reply_markup=ReplyKeyboardRemove(),
        )

    async def get_bindings(self) -> dict | None:
        if self.bindings:
            return self.bindings
        try:
            result = await self.client.request("GET", "/v1/bindings")
            binding = result.get("binding")
            if not binding:
                return None
            self.bindings = {
                "configured_by": binding.get("configured_by", 0),
                "questionnaire": {
                    "chat_id": binding["questionnaire_chat_id"],
                    "title": binding["questionnaire_title"],
                },
                "alerts": {"chat_id": binding["alerts_chat_id"], "title": binding["alerts_title"]},
                "moderated_groups": binding.get("moderated_groups") or [],
            }
            return self.bindings
        except Exception as exc:
            await self.client.technical("binding_read_failed", str(exc))
            return None

    async def my_chat_member(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        changed = update.my_chat_member
        if not changed or changed.chat.type not in {
            ChatType.CHANNEL,
            ChatType.GROUP,
            ChatType.SUPERGROUP,
        }:
            return
        try:
            await self.ensure_registered(context.bot)
            me = await context.bot.get_me()
            await self.client.request(
                "POST",
                "/v1/channel-events",
                {
                    "bot_id": me.id,
                    "bot_username": me.username or "",
                    "chat_id": changed.chat.id,
                    "title": changed.chat.title or "",
                    "username": changed.chat.username or "",
                    "old_status": changed.old_chat_member.status,
                    "new_status": changed.new_chat_member.status,
                    "actor_id": changed.from_user.id if changed.from_user else 0,
                },
            )
        except Exception as exc:
            await self.client.technical("channel_event_failed", str(exc))

    def _remember_message(self, chat_id: int, user_id: int, message_id: int) -> None:
        now = time.time()
        queue = self.recent_messages[(chat_id, user_id)]
        queue.append((now, message_id))
        while queue and now - queue[0][0] > 600:
            queue.popleft()

    @staticmethod
    def _is_moderated_group(bindings: dict, chat_id: int) -> bool:
        return any(
            int(group.get("chat_id", 0)) == int(chat_id)
            for group in bindings.get("moderated_groups", [])
        )

    async def _linked_channel_id(self, bot, chat_id: int) -> int:
        if chat_id in self.linked_channel_ids:
            return self.linked_channel_ids[chat_id]
        linked_id = 0
        try:
            full_chat = await bot.get_chat(chat_id)
            linked_id = int(getattr(full_chat, "linked_chat_id", 0) or 0)
        except Exception:
            pass
        self.linked_channel_ids[chat_id] = linked_id
        return linked_id

    async def _image_base64(self, bot, message) -> str:
        media = None
        if message.photo:
            media = message.photo[-1]
        elif message.document and str(message.document.mime_type or "").startswith("image/"):
            media = message.document
        if not media or int(media.file_size or 0) > 12 * 1024 * 1024:
            return ""
        telegram_file = await bot.get_file(media.file_id)
        data = bytes(await telegram_file.download_as_bytearray())
        return base64.b64encode(data).decode("ascii")

    async def _profile_payload(self, bot, user) -> tuple[dict, dict]:
        bio = ""
        personal = {"chat_id": 0, "title": "", "member_count": None}
        avatar = ""
        try:
            full = await bot.get_chat(user.id)
            bio = getattr(full, "bio", "") or ""
            personal_chat = getattr(full, "personal_chat", None)
            if personal_chat:
                count = None
                try:
                    count = await bot.get_chat_member_count(personal_chat.id)
                except Exception:
                    pass
                personal = {
                    "chat_id": personal_chat.id,
                    "title": personal_chat.title or "",
                    "member_count": count,
                }
        except Exception:
            pass
        try:
            photos = await bot.get_user_profile_photos(user.id, limit=1)
            if photos.photos:
                photo = photos.photos[0][-1]
                if int(photo.file_size or 0) <= 8 * 1024 * 1024:
                    telegram_file = await bot.get_file(photo.file_id)
                    data = bytes(await telegram_file.download_as_bytearray())
                    avatar = base64.b64encode(data).decode("ascii")
        except Exception:
            pass
        return (
            {
                "user_id": user.id,
                "first_name": user.first_name or "",
                "last_name": user.last_name or "",
                "bio": bio,
                "avatar_base64": avatar,
                "avatar_ocr_text": "",
            },
            personal,
        )

    async def _moderate_message(
        self, update: Update, context: ContextTypes.DEFAULT_TYPE, edited: bool
    ) -> None:
        message = update.effective_message
        user = update.effective_user
        chat = update.effective_chat
        if not message or not user or user.is_bot or not chat:
            return
        if not await self._allowed_by_host_bot(update, context):
            return
        try:
            await self.ensure_registered(context.bot)
            bindings = await self.get_bindings()
            if not bindings or not self._is_moderated_group(bindings, chat.id):
                return
            self._remember_message(chat.id, user.id, message.message_id)
            profile, personal = await self._profile_payload(context.bot, user)
            image = await self._image_base64(context.bot, message)
            linked_id = await self._linked_channel_id(context.bot, chat.id)
            reply_sender = getattr(message.reply_to_message, "sender_chat", None)
            payload = {
                "event_type": "edited_message" if edited else "message",
                "chat_id": chat.id,
                "chat_title": chat.title or "",
                "profile": profile,
                "personal_channel": personal,
                "message": {
                    "message_id": message.message_id,
                    "text": message.text or message.caption or "",
                    "image_base64": image,
                    "image_ocr_text": "",
                    "reply_to_linked_channel": bool(reply_sender and reply_sender.id == linked_id),
                },
            }
            result = await self.client.request("POST", "/v1/check", payload)
            if result.get("violation"):
                await self._apply_violation(context, message, user, result, bindings)
        except Exception as exc:
            await self.client.technical("message_moderation_failed", str(exc))

    async def message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        await self._moderate_message(update, context, edited=False)

    async def edited_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        await self._moderate_message(update, context, edited=True)

    async def reaction(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        reaction = update.message_reaction
        user = getattr(reaction, "user", None) if reaction else None
        chat = getattr(reaction, "chat", None) if reaction else None
        if not reaction or not user or user.is_bot or not chat:
            return
        if not reaction.new_reaction:
            return
        if not await self._allowed_by_host_bot(update, context):
            return
        try:
            await self.ensure_registered(context.bot)
            bindings = await self.get_bindings()
            if not bindings or not self._is_moderated_group(bindings, chat.id):
                return
            profile, personal = await self._profile_payload(context.bot, user)
            result = await self.client.request(
                "POST",
                "/v1/check",
                {
                    "event_type": "reaction",
                    "chat_id": chat.id,
                    "chat_title": chat.title or "",
                    "profile": profile,
                    "personal_channel": personal,
                    "message": {"message_id": reaction.message_id, "text": ""},
                },
            )
            if result.get("violation"):
                await self._apply_violation(context, None, user, result, bindings, chat_id=chat.id)
        except Exception as exc:
            await self.client.technical("reaction_moderation_failed", str(exc))

    async def _important_alert(self, bot, bindings: dict, text: str) -> None:
        try:
            await bot.send_message(bindings["alerts"]["chat_id"], text)
        except Exception as exc:
            await self.client.technical("important_alert_delivery_failed", str(exc))

    async def _delete_recent(self, bot, chat_id: int, user_id: int, minutes: int) -> None:
        threshold = time.time() - minutes * 60
        ids = [mid for ts, mid in self.recent_messages[(chat_id, user_id)] if ts >= threshold]
        for message_id in ids:
            try:
                await bot.delete_message(chat_id, message_id)
            except Exception:
                pass

    async def _apply_violation(
        self,
        context: ContextTypes.DEFAULT_TYPE,
        message,
        user,
        result: dict,
        bindings: dict,
        chat_id: int | None = None,
    ) -> None:
        chat_id = int(chat_id or message.chat_id)
        state = await self._capture_member_state(context.bot, chat_id, user.id)
        incident_id = str(result.get("incident_id") or "")
        if incident_id:
            try:
                await self.client.request(
                    "PUT",
                    f"/v1/incidents/{incident_id}/telegram-state",
                    {"state": state},
                )
            except Exception as exc:
                await self.client.technical("punishment_state_save_failed", str(exc))
        try:
            if result.get("action") == "ban":
                await context.bot.ban_chat_member(chat_id, user.id, revoke_messages=True)
            else:
                until = datetime.now(timezone.utc) + timedelta(
                    seconds=int(result.get("mute_seconds") or 365 * 24 * 3600)
                )
                await context.bot.restrict_chat_member(
                    chat_id, user.id, ChatPermissions.no_permissions(), until_date=until
                )
            if message and result.get("delete_message"):
                try:
                    await message.delete()
                except Exception:
                    pass
            if result.get("delete_recent_minutes"):
                await self._delete_recent(
                    context.bot, chat_id, user.id, int(result["delete_recent_minutes"])
                )
        except Exception as exc:
            await self._important_alert(
                context.bot,
                bindings,
                f"Бот не смог применить наказание в чате {chat_id}. Проверь его права администратора.",
            )
            await self.client.technical("punishment_failed", str(exc))
        if result.get("form_required"):
            markers = ", ".join(html.escape(str(x)) for x in result.get("markers", [])[:8])
            name = html.escape(" ".join(x for x in [user.first_name, user.last_name] if x))
            text = (
                "⛔ <b>Анкета модерации</b>\n\n"
                f"Пользователь: {name}\n"
                f"ID: <code>{user.id}</code>\n"
                f"Категория: {html.escape(result.get('category', ''))}\n"
                f"Причина: {html.escape(result.get('reason', ''))}\n"
                f"Маркеры: {markers or 'нет'}\n"
                f"Наказание: {'бан' if result.get('action') == 'ban' else 'мут на 1 год'}"
            )
            if message:
                original = (message.text or message.caption or "").strip()
                if original:
                    text += f"\n\n<b>Сообщение:</b>\n{html.escape(original[:1800])}"
            incident = result.get("incident_id", "")
            keyboard = InlineKeyboardMarkup(
                [
                    [
                        InlineKeyboardButton("Пропустить", callback_data=f"ihm:skip:{incident}"),
                        InlineKeyboardButton("Забанить", callback_data=f"ihm:ban:{incident}"),
                    ],
                    [InlineKeyboardButton("Снять наказание", callback_data=f"ihm:unmute:{incident}")],
                ]
            )
            try:
                await context.bot.send_message(
                    bindings["questionnaire"]["chat_id"],
                    text,
                    parse_mode=ParseMode.HTML,
                    reply_markup=keyboard,
                )
            except Exception as exc:
                await self._important_alert(
                    context.bot,
                    bindings,
                    "Бот не смог отправить анкету. Проверь права публикации в канале анкет.",
                )
                await self.client.technical("questionnaire_delivery_failed", str(exc))

    @staticmethod
    async def _capture_member_state(bot, chat_id: int, user_id: int) -> dict:
        try:
            member = await bot.get_chat_member(chat_id, user_id)
            state: dict[str, Any] = {"status": str(member.status)}
            permission_names = inspect.signature(ChatPermissions).parameters
            permissions = {}
            for name in permission_names:
                value = getattr(member, name, None)
                if isinstance(value, bool):
                    permissions[name] = value
            if permissions:
                state["permissions"] = permissions
            until = getattr(member, "until_date", None)
            if isinstance(until, datetime):
                state["until_date"] = int(until.timestamp())
            return state
        except Exception:
            return {}

    @staticmethod
    async def _restore_member_state(bot, chat_id: int, user_id: int, state: dict) -> None:
        # A ban must be removed before Telegram accepts restored restrictions.
        try:
            await bot.unban_chat_member(chat_id, user_id, only_if_banned=True)
        except Exception:
            pass
        prior_status = str((state or {}).get("status", ""))
        saved_permissions = (state or {}).get("permissions")
        if prior_status == str(ChatMemberStatus.RESTRICTED) and isinstance(saved_permissions, dict):
            valid_names = set(inspect.signature(ChatPermissions).parameters)
            clean = {
                key: bool(value)
                for key, value in saved_permissions.items()
                if key in valid_names and isinstance(value, bool)
            }
            permissions = ChatPermissions(**clean)
            until_timestamp = int((state or {}).get("until_date") or 0)
            until_date = (
                datetime.fromtimestamp(until_timestamp, timezone.utc)
                if until_timestamp > int(time.time())
                else None
            )
            await bot.restrict_chat_member(
                chat_id,
                user_id,
                permissions,
                until_date=until_date,
                use_independent_chat_permissions=True,
            )
            return
        await bot.restrict_chat_member(
            chat_id,
            user_id,
            ChatPermissions.all_permissions(),
            use_independent_chat_permissions=True,
        )

    async def incident_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        query = update.callback_query
        if not query or not query.data or not update.effective_user:
            return
        try:
            _, decision, incident_id = query.data.split(":", 2)
            result = await self.client.request(
                "POST",
                f"/v1/incidents/{incident_id}/decision",
                {"decision": decision, "decided_by": update.effective_user.id},
            )
            chat_id, user_id = int(result["chat_id"]), int(result["user_id"])
            if decision == "ban":
                await context.bot.ban_chat_member(chat_id, user_id, revoke_messages=True)
            else:
                await self._restore_member_state(
                    context.bot,
                    chat_id,
                    user_id,
                    result.get("telegram_state") or {},
                )
            actor = "@" + update.effective_user.username if update.effective_user.username else str(update.effective_user.id)
            labels = {"skip": "пропущен, дальнейшие проверки отключены", "ban": "забанен", "unmute": "наказание снято"}
            await query.answer("Решение сохранено")
            await query.edit_message_text(
                (query.message.text_html or html.escape(query.message.text or ""))
                + f"\n\nРешение: {labels[decision]}\nВердикт вынес: {html.escape(actor)}",
                parse_mode=ParseMode.HTML,
            )
        except Exception as exc:
            await query.answer("Не удалось применить решение", show_alert=True)
            await self.client.technical("incident_decision_failed", str(exc))


def install_ihm_adapter(
    application: Application,
    api_key: str,
    base_url: str = "https://ihm.thebeautifulmusic.ru",
    should_moderate: Callable[[Update, ContextTypes.DEFAULT_TYPE], bool | Awaitable[bool]] | None = None,
) -> IHMAdapter:
    adapter = IHMAdapter(
        api_key=api_key,
        base_url=base_url,
        should_moderate=should_moderate,
    )
    adapter.install(application)
    return adapter
