from __future__ import annotations

import asyncio
import html
import json
import traceback
import re
from dataclasses import dataclass
from typing import Any

from aiogram import Bot, Dispatcher, F
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import BotCommand, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message

from ..schemas import TELEGRAM_ACTIVE_USER_ID_METADATA_KEY
from ..tools.telegram_tools import build_tools as build_telegram_tools
from .telegram_media import format_media_prompt_payload, persist_telegram_media
from .telegram_trigger import should_trigger_agent


@dataclass(slots=True)
class ParsedCommand:
    name: str
    args: str = ""
    tokens: tuple[str, ...] = ()
    mention: str | None = None


ADMIN_COMMANDS = {
    "status",
    "safe",
    "mode",
    "providers",
    "models",
    "skills",
    "reload",
    "provider",
    "model",
    "exec",
    "sessions",
    "end",
    "pair",
    "approve",
    "auth",
    "settings",
    "apikey",
    "reset",
}
PUBLIC_COMMANDS = {"start", "help", "pair", "openclaw"}
TAG_RE = re.compile(r"</?[^>]+>")
MARKDOWNISH_CODE_RE = re.compile(r"```([\s\S]*?)```|`([^`\n]+)`", re.S)
BOLD_RE = re.compile(r"\*\*(.+?)\*\*", re.S)
UNDERLINE_RE = re.compile(r"__(.+?)__", re.S)
ITALIC_STAR_RE = re.compile(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", re.S)
ITALIC_UNDERSCORE_RE = re.compile(r"(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", re.S)
STRIKE_RE = re.compile(r"~~(.+?)~~", re.S)
SPOILER_RE = re.compile(r"\|\|(.+?)\|\|", re.S)


def _split_text_chunks(text: str, max_chars: int) -> list[str]:
    if not text:
        return [""]
    return [text[index : index + max_chars] for index in range(0, len(text), max_chars)] or [""]


def _apply_inline_formatting(escaped_text: str) -> str:
    text = BOLD_RE.sub(r"<b>\1</b>", escaped_text)
    text = UNDERLINE_RE.sub(r"<u>\1</u>", text)
    text = STRIKE_RE.sub(r"<s>\1</s>", text)
    text = SPOILER_RE.sub(r"<tg-spoiler>\1</tg-spoiler>", text)
    text = ITALIC_STAR_RE.sub(r"<i>\1</i>", text)
    text = ITALIC_UNDERSCORE_RE.sub(r"<i>\1</i>", text)
    return text


def _split_html_chunks(html_text: str, max_chars: int = 3500) -> list[str]:
    tag_re = re.compile(r"(<[^>]+>)")
    tokens = [token for token in tag_re.split(html_text) if token]
    chunks: list[str] = []
    current = ""
    stack: list[str] = []

    def close_tags() -> str:
        return "".join(f"</{tag}>" for tag in reversed(stack))

    def open_tags() -> str:
        return "".join(f"<{tag}>" for tag in stack)

    for token in tokens:
        if token.startswith("<") and token.endswith(">"):
            if len(current) + len(token) > max_chars and current:
                chunks.append(current + close_tags())
                current = open_tags()
            current += token
            match = re.match(r"</?([a-zA-Z0-9-]+)", token)
            if match:
                tag = match.group(1)
                if token.startswith("</"):
                    if tag in stack[::-1]:
                        idx = len(stack) - 1 - stack[::-1].index(tag)
                        stack = stack[:idx]
                elif not token.endswith("/>"):
                    stack.append(tag)
            continue

        remaining = token
        while remaining:
            space = max_chars - len(current)
            if space <= 0 and current:
                chunks.append(current + close_tags())
                current = open_tags()
                space = max_chars - len(current)
            piece = remaining[:space] if space > 0 else remaining
            if not piece and current:
                chunks.append(current + close_tags())
                current = open_tags()
                continue
            current += piece
            remaining = remaining[len(piece):]
            if remaining:
                chunks.append(current + close_tags())
                current = open_tags()
    if current:
        chunks.append(current + close_tags())
    return chunks or [""]


def render_markdownish_html_chunks(text: str, max_chars: int = 3500) -> list[str]:
    placeholders: dict[str, str] = {}
    counter = 0

    def replace_code(match: re.Match[str]) -> str:
        nonlocal counter
        key = f"@@CODE{counter}@@"
        counter += 1
        fenced = match.group(1)
        inline = match.group(2)
        if fenced is not None:
            placeholders[key] = f"<pre><code>{html.escape(fenced.strip(chr(10)))}</code></pre>"
        else:
            placeholders[key] = f"<code>{html.escape(inline)}</code>"
        return key

    protected = MARKDOWNISH_CODE_RE.sub(replace_code, text)
    escaped = html.escape(protected)
    formatted = _apply_inline_formatting(escaped)
    for key, value in placeholders.items():
        formatted = formatted.replace(html.escape(key), value)
        formatted = formatted.replace(key, value)
    return _split_html_chunks(formatted, max_chars)


def parse_command(text: str | None) -> ParsedCommand | None:
    if not text:
        return None
    stripped = text.strip()
    if not stripped.startswith("/"):
        return None
    head, *tail = stripped.split(maxsplit=1)
    if len(head) <= 1:
        return None
    body = head[1:]
    name, _, mention = body.partition("@")
    args = tail[0].strip() if tail else ""
    tokens = tuple(args.split()) if args else ()
    return ParsedCommand(name=name.lower(), args=args, tokens=tokens, mention=mention or None)


def build_session_id(chat_id: int | str, thread_id: int | None = None) -> str:
    if thread_id is None:
        return f"telegram:{chat_id}:main"
    return f"telegram:{chat_id}:topic:{thread_id}"


def build_help_text() -> str:
    return "\n".join(
        [
            "<b>OpenClaw Lite Telegram</b>",
            "/start, /help",
            "/openclaw &lt;запрос&gt;",
            "/pair — owner: create/list codes | guest: /pair CODE",
            "/status",
            "/safe on|off",
            "/mode safe|yolo|god",
            "/providers",
            "/models [provider]",
            "/provider &lt;name&gt;",
            "/model &lt;name&gt;",
            "/reset [session_id]",
            "/skills",
            "/sessions",
            "/end [session_id]",
            "/approve [user_id] [chat_id]",
            "/auth",
            "/settings",
            "/apikey &lt;provider&gt; &lt;key&gt;",
            "/reload",
            "/exec &lt;command&gt;",
            "/cancel — отменить ввод из /settings",
            "",
            "Slash-подсказки доступны из меню команд Telegram.",
            "Agent mode запускается через reply на бота или /openclaw; пока сессия не закрыта, следующие сообщения того же пользователя продолжают её автоматически.",
        ]
    )


def build_bot_commands() -> list[BotCommand]:
    return [
        BotCommand(command="start", description="старт и краткая помощь"),
        BotCommand(command="help", description="показать список команд"),
        BotCommand(command="openclaw", description="запустить agent mode"),
        BotCommand(command="status", description="статус текущей сессии"),
        BotCommand(command="provider", description="сменить провайдера сессии"),
        BotCommand(command="model", description="сменить модель сессии"),
        BotCommand(command="providers", description="список доступных провайдеров"),
        BotCommand(command="models", description="список моделей"),
        BotCommand(command="settings", description="настройки провайдеров и ключей"),
        BotCommand(command="apikey", description="задать API key для провайдера"),
        BotCommand(command="reset", description="сбросить текущую сессию"),
        BotCommand(command="end", description="закрыть текущую сессию"),
        BotCommand(command="reload", description="перезагрузить runtime"),
        BotCommand(command="cancel", description="отменить ввод из settings"),
        BotCommand(command="skills", description="список загруженных skills"),
        BotCommand(command="pair", description="привязать доступ по pairing code"),
    ]


def _chunk_inline_buttons(buttons: list[InlineKeyboardButton], row_size: int) -> list[list[InlineKeyboardButton]]:
    return [buttons[index : index + row_size] for index in range(0, len(buttons), row_size)]


def build_settings_keyboard(providers: list[dict[str, Any]], *, current_provider: str, current_model: str) -> InlineKeyboardMarkup:
    rows: list[list[InlineKeyboardButton]] = []
    provider_buttons: list[InlineKeyboardButton] = []
    key_buttons: list[InlineKeyboardButton] = []
    model_buttons: list[InlineKeyboardButton] = []
    for item in providers:
        name = str(item.get("name"))
        if not name:
            continue
        mark = "✅ " if name == current_provider else ""
        provider_buttons.append(InlineKeyboardButton(text=f"{mark}{name}", callback_data=f"settings:provider:{name}"))
        key_mark = "✅" if item.get("has_api_key") else "🔑"
        key_buttons.append(InlineKeyboardButton(text=f"{key_mark} key {name}", callback_data=f"settings:key:{name}"))
        model_mark = "✅ " if name == current_provider else ""
        model_buttons.append(
            InlineKeyboardButton(text=f"{model_mark}model {name}", callback_data=f"settings:model:{name}")
        )
    rows.extend(_chunk_inline_buttons(provider_buttons, 3))
    if key_buttons:
        rows.extend(_chunk_inline_buttons(key_buttons, 2))
    if model_buttons:
        rows.extend(_chunk_inline_buttons(model_buttons, 2))
    rows.append([InlineKeyboardButton(text="Refresh", callback_data="settings:refresh")])
    return InlineKeyboardMarkup(inline_keyboard=rows)


def _strip_html_markup(text: str) -> str:
    return TAG_RE.sub("", text)


def _preview(value: Any, limit: int = 700) -> str:
    text = str(value or "")
    if len(text) > limit:
        text = text[:limit] + "…"
    return html.escape(text)


def _status_text(status: dict[str, Any]) -> str:
    lines = [
        "<b>OpenClaw Lite status</b>",
        f"session: <code>{html.escape(str(status.get('session_id')))}</code>",
        f"provider: <code>{html.escape(str(status.get('provider')))}</code>",
        f"model: <code>{html.escape(str(status.get('model')))}</code>",
        f"mode: <code>{html.escape(str(status.get('security_mode')))}</code>",
        f"closed: <code>{html.escape(str(status.get('closed')))}</code>",
        f"history: <code>{html.escape(str(status.get('history_messages')))}</code>",
    ]
    skills = status.get("skills") or []
    if skills:
        skill_names = ", ".join(item.get("name", "?") for item in skills)
        lines.append(f"skills: <code>{html.escape(skill_names)}</code>")
    return "\n".join(lines)


def _providers_text(providers: list[str]) -> str:
    if not providers:
        return "Провайдеры не настроены."
    return "<b>Providers</b>\n" + "\n".join(f"- <code>{html.escape(name)}</code>" for name in providers)


def _skills_text(skills: list[dict[str, Any]]) -> str:
    if not skills:
        return "<b>Skills</b>\n- none"
    lines = ["<b>Skills</b>"]
    for item in skills:
        flag = "on" if item.get("enabled") else "off"
        lines.append(f"- <code>{html.escape(str(item.get('name')))}</code> [{flag}]")
    return "\n".join(lines)


def _models_text(models: Any) -> str:
    if isinstance(models, dict):
        lines = ["<b>Models</b>"]
        for provider, items in models.items():
            lines.append(f"<code>{html.escape(str(provider))}</code>:")
            for model in items:
                lines.append(f"- <code>{html.escape(str(model))}</code>")
        return "\n".join(lines)
    if isinstance(models, list):
        return "<b>Models</b>\n" + "\n".join(f"- <code>{html.escape(str(item))}</code>" for item in models)
    return f"<b>Models</b>\n<code>{html.escape(str(models))}</code>"


def _sessions_text(items: list[dict[str, Any]]) -> str:
    if not items:
        return "<b>Sessions</b>\n- none"
    lines = ["<b>Sessions</b>"]
    for item in items:
        lines.append(
            "- <code>{}</code> | {}/{} | mode={} | closed={}".format(
                html.escape(str(item.get("session_id"))),
                html.escape(str(item.get("provider"))),
                html.escape(str(item.get("model"))),
                html.escape(str(item.get("security_mode"))),
                html.escape(str(item.get("closed"))),
            )
        )
    return "\n".join(lines)


def _auth_text(payload: dict[str, Any]) -> str:
    lines = ["<b>Auth</b>"]
    lines.append(f"owners: <code>{html.escape(str(payload.get('owner_user_ids')))}</code>")
    lines.append(f"owner chats: <code>{html.escape(str(payload.get('owner_chat_ids')))}</code>")
    lines.append(f"allowed users: <code>{html.escape(str(payload.get('allowed_user_ids')))}</code>")
    lines.append(f"allowed chats: <code>{html.escape(str(payload.get('allowed_chat_ids')))}</code>")
    pending = payload.get("pairing_codes") or []
    if pending:
        lines.append("pending codes:")
        for item in pending:
            lines.append(
                f"- <code>{html.escape(str(item.get('code')))}</code> exp=<code>{html.escape(str(item.get('expires_at')))}</code>"
            )
    return "\n".join(lines)


class TelegramProgressReporter:
    def __init__(self, message: Message, bot: Bot):
        self.message = message
        self.bot = bot
        self.status_message: Message | None = None
        self.last_status: str | None = None
        self.last_assistant_text: str | None = None

    async def _send_rendered(self, text: str) -> None:
        for index, chunk in enumerate(render_markdownish_html_chunks(text, 3500)):
            try:
                if index == 0:
                    await self.message.reply(chunk)
                else:
                    await self.bot.send_message(
                        chat_id=self.message.chat.id,
                        text=chunk,
                        reply_to_message_id=self.message.message_id,
                    )
            except TelegramBadRequest:
                plain = _strip_html_markup(chunk)
                if index == 0:
                    await self.message.reply(plain, parse_mode=None)
                else:
                    await self.bot.send_message(
                        chat_id=self.message.chat.id,
                        text=plain,
                        parse_mode=None,
                        reply_to_message_id=self.message.message_id,
                    )

    def _render_status(self, payload: dict[str, Any]) -> str | None:
        event = payload.get("event")
        if event == "agent_iteration_started":
            return (
                "<b>Agent mode</b>\n"
                f"iteration: <code>{payload.get('iteration')}</code>\n"
                f"provider: <code>{html.escape(str(payload.get('provider')))}</code>\n"
                f"model: <code>{html.escape(str(payload.get('model')))}</code>\n"
                "status: thinking"
            )
        if event == "provider_generate_error" and not payload.get("will_retry"):
            return (
                "<b>Agent mode</b>\n"
                f"iteration: <code>{payload.get('iteration')}</code>\n"
                f"provider: <code>{html.escape(str(payload.get('provider')))}</code>\n"
                "status: provider error\n"
                f"error: {_preview(payload.get('error'), 220)}"
            )
        if event == "tool_call_started":
            return (
                "<b>Agent mode</b>\n"
                f"iteration: <code>{payload.get('iteration')}</code>\n"
                f"tool: <code>{html.escape(str(payload.get('tool')))}</code>\n"
                "status: running tool"
            )
        if event == "tool_call_finished":
            result = payload.get("result") or {}
            error = result.get("error") if isinstance(result, dict) else None
            status = "error handled" if error else "tool finished"
            preview = _preview(error or result)
            return (
                "<b>Agent mode</b>\n"
                f"iteration: <code>{payload.get('iteration')}</code>\n"
                f"tool: <code>{html.escape(str(payload.get('tool')))}</code>\n"
                f"status: {status}\n"
                f"preview: {preview}"
            )
        if event == "provider_retry_scheduled":
            delay = payload.get("delay_sec")
            retry_in = f"{delay:.1f}s" if isinstance(delay, (int, float)) else str(delay)
            return (
                "<b>Agent mode</b>\n"
                f"iteration: <code>{payload.get('iteration')}</code>\n"
                f"provider: <code>{html.escape(str(payload.get('provider')))}</code>\n"
                f"model: <code>{html.escape(str(payload.get('model')))}</code>\n"
                f"attempt: <code>{payload.get('attempt')}</code>/<code>{payload.get('max_attempts')}</code>\n"
                f"status: retry in {html.escape(retry_in)}\n"
                f"error: {_preview(payload.get('error'))}"
            )
        if event == "agent_finished":
            status = "session closed" if payload.get("session_closed") else "waiting next message"
            return (
                "<b>Agent mode</b>\n"
                f"iterations: <code>{payload.get('iteration')}</code>\n"
                f"session closed: <code>{html.escape(str(payload.get('session_closed')))}</code>\n"
                f"status: {status}"
            )
        return None

    async def __call__(self, payload: dict[str, Any]) -> None:
        if payload.get("event") == "assistant_response":
            assistant_text = str(payload.get("text") or "").strip()
            if assistant_text and assistant_text != self.last_assistant_text:
                self.last_assistant_text = assistant_text
                await self._send_rendered(assistant_text)
            return
        text = self._render_status(payload)
        if not text or text == self.last_status:
            return
        self.last_status = text
        if self.status_message is None:
            self.status_message = await self.message.reply(text)
            return
        try:
            await self.status_message.edit_text(text)
        except TelegramBadRequest:
            pass

    async def finish(self, payload: dict[str, Any]) -> None:
        if self.status_message is None:
            return
        text = (
            "<b>Agent mode finished</b>\n"
            f"iterations: <code>{html.escape(str(payload.get('iterations')))}</code>\n"
            f"session closed: <code>{html.escape(str(payload.get('session_closed')))}</code>\n"
            f"mode: <code>{html.escape(str(payload.get('security_mode')))}</code>"
        )
        try:
            await self.status_message.edit_text(text)
        except TelegramBadRequest:
            pass


class TelegramBotInterface:
    def __init__(self, runtime) -> None:
        self.runtime = runtime
        telegram = runtime.config.get("telegram", {}) or {}
        token = telegram.get("bot_token") or ""
        if not token:
            raise RuntimeError("telegram.bot_token is not configured")
        parse_mode_name = str(telegram.get("parse_mode", "HTML")).upper()
        parse_mode = ParseMode.HTML if parse_mode_name == "HTML" else ParseMode.MARKDOWN_V2
        self.bot = Bot(token=token, default=DefaultBotProperties(parse_mode=parse_mode))
        self.dp = Dispatcher()
        self.runtime.telegram_bot = self.bot
        self.pending_inputs: dict[tuple[int, int], dict[str, str]] = {}
        self.runtime.add_tool_registrar(lambda registry: registry.register_many(build_telegram_tools()))
        self._register_routes()

    async def _configure_bot_commands(self) -> None:
        try:
            await self.bot.set_my_commands(build_bot_commands())
        except Exception:
            traceback.print_exc()

    def _user_id(self, message: Message) -> int | None:
        return message.from_user.id if message.from_user else None

    def _chat_id(self, message: Message) -> int:
        return int(message.chat.id)

    def _session_id(self, message: Message) -> str:
        return build_session_id(message.chat.id, getattr(message, "message_thread_id", None))

    def _chat_title(self, message: Message) -> str | None:
        return getattr(message.chat, "title", None)

    def _username(self, message: Message) -> str | None:
        user = message.from_user
        if user is None:
            return None
        return user.username or user.full_name

    def _pending_key(self, message: Message, *, user_id: int | None = None) -> tuple[int, int]:
        resolved_user_id = self._user_id(message) if user_id is None else user_id
        return (self._chat_id(message), resolved_user_id or 0)

    def _set_pending_input(self, message: Message, payload: dict[str, str], *, user_id: int | None = None) -> None:
        self.pending_inputs[self._pending_key(message, user_id=user_id)] = payload

    def _peek_pending_input(self, message: Message, *, user_id: int | None = None) -> dict[str, str] | None:
        return self.pending_inputs.get(self._pending_key(message, user_id=user_id))

    def _pop_pending_input(self, message: Message, *, user_id: int | None = None) -> dict[str, str] | None:
        return self.pending_inputs.pop(self._pending_key(message, user_id=user_id), None)

    async def _render_settings(self, message: Message, *, user_id: int | None = None) -> None:
        session = self.runtime.get_session(
            session_id=self._session_id(message),
            chat_id=str(self._chat_id(message)),
            user_id=self._user_id(message) if user_id is None else user_id,
        )
        providers = self.runtime.provider_summary_payload()
        lines = [
            "<b>LiteClaw Settings</b>",
            f"current provider: <code>{html.escape(session.provider)}</code>",
            f"current model: <code>{html.escape(session.model)}</code>",
            "команды: <code>/provider &lt;name&gt;</code> · <code>/model &lt;name&gt;</code> · <code>/apikey &lt;provider&gt; &lt;key&gt;</code> · <code>/reset</code>",
        ]
        for item in providers:
            lines.append(
                f"- <code>{html.escape(str(item['name']))}</code> | enabled={item.get('enabled')} | key={item.get('has_api_key')} | default_model={html.escape(str(item.get('default_model')))}"
            )
        await message.reply("\n".join(lines), reply_markup=build_settings_keyboard(providers, current_provider=session.provider, current_model=session.model))

    async def _handle_pending_input(self, message: Message) -> bool:
        pending = self._peek_pending_input(message)
        if not pending:
            return False
        raw_text = (message.text or message.caption or "").strip()
        if not raw_text:
            await message.reply("Отправь текстом значение для ожидаемой настройки или /cancel")
            return True
        if raw_text == "/cancel":
            self._pop_pending_input(message)
            await message.reply("Отменено.")
            return True
        self._pop_pending_input(message)
        action = pending.get("action")
        provider = pending.get("provider", "")
        session = self.runtime.get_session(session_id=self._session_id(message), chat_id=str(self._chat_id(message)), user_id=self._user_id(message))
        if action == "api_key":
            payload = self.runtime.set_provider_api_key(provider, raw_text)
            await self._reply_json(message, payload)
            await self._render_settings(message)
            return True
        if action == "model":
            self.runtime.set_provider(session, provider)
            self.runtime.set_provider_default_model(provider, raw_text)
            payload = self.runtime.set_model(session, raw_text)
            await self._reply_json(message, payload)
            await self._render_settings(message)
            return True
        return False

    def _is_reply_to_bot(self, message: Message) -> bool:
        reply = message.reply_to_message
        return bool(reply and reply.from_user and reply.from_user.id == self.bot.id)

    def _has_active_agent_session(self, message: Message) -> bool:
        session = self._find_session_by_id(self._session_id(message))
        if session is None:
            return False
        if session.closed:
            return False
        active_user_id = session.metadata.get(TELEGRAM_ACTIVE_USER_ID_METADATA_KEY)
        return active_user_id == self._user_id(message)

    def _has_media(self, message: Message) -> bool:
        return any(
            getattr(message, attr, None)
            for attr in ["photo", "document", "video", "animation", "audio", "voice", "sticker"]
        )

    async def _build_agent_payload(self, message: Message, *, override_text: str | None = None) -> str:
        base_text = (override_text if override_text is not None else (message.text or message.caption or "")).strip()
        if not self._has_media(message):
            return base_text or "[empty message]"
        try:
            media_payload = await persist_telegram_media(self.bot, message, self.runtime.config)
        except Exception as exc:
            traceback.print_exc()
            fallback = base_text or (message.text or message.caption or "").strip()
            return (fallback + f"\n[saved telegram attachments error: {exc}]").strip()
        return format_media_prompt_payload(
            message_text=base_text or media_payload.get("message_text", ""),
            saved_items=media_payload.get("saved_items", []),
        )

    async def _run_agent_request(self, message: Message, *, text: str) -> None:
        reporter = TelegramProgressReporter(message, self.bot)
        result = await self.runtime.handle_user_message(
            session_id=self._session_id(message),
            chat_id=str(message.chat.id),
            user_id=self._user_id(message),
            text=text,
            transport="telegram",
            transport_state={
                "bot": self.bot,
                "message": message,
                "chat_id": message.chat.id,
                "progress_callback": reporter,
            },
        )
        await reporter.finish(result)
        if str(result.get("text") or "").strip() != str(reporter.last_assistant_text or "").strip():
            await self._reply_long(message, result["text"])

    def _is_owner(self, message: Message | None = None, *, user_id: int | None = None, chat_id: int | None = None) -> bool:
        resolved_user_id = user_id if user_id is not None else (self._user_id(message) if message is not None else None)
        resolved_chat_id = chat_id if chat_id is not None else (self._chat_id(message) if message is not None else None)
        return self.runtime.auth_is_owner(user_id=resolved_user_id, chat_id=resolved_chat_id)

    def _is_authorized(self, message: Message | None = None, *, user_id: int | None = None, chat_id: int | None = None) -> bool:
        resolved_user_id = user_id if user_id is not None else (self._user_id(message) if message is not None else None)
        resolved_chat_id = chat_id if chat_id is not None else (self._chat_id(message) if message is not None else None)
        return self.runtime.auth_is_allowed(user_id=resolved_user_id, chat_id=resolved_chat_id)

    def _register_routes(self) -> None:
        @self.dp.message(F.text | F.caption)
        async def on_message(message: Message) -> None:
            await self.handle_message(message)

        @self.dp.callback_query()
        async def on_callback(callback: CallbackQuery) -> None:
            await self.handle_callback(callback)

    async def _reply_long(self, message: Message, text: str, *, html_mode: bool = False) -> None:
        if html_mode:
            plain_text = _strip_html_markup(text)
            chunks = [html.escape(chunk) for chunk in _split_text_chunks(plain_text, 3500)] or [""]
        else:
            chunks = render_markdownish_html_chunks(text, 3500)
        for index, chunk in enumerate(chunks):
            try:
                if index == 0:
                    await message.reply(chunk)
                else:
                    await self.bot.send_message(chat_id=message.chat.id, text=chunk, reply_to_message_id=message.message_id)
            except TelegramBadRequest:
                plain_chunk = _strip_html_markup(chunk)
                if index == 0:
                    await message.reply(plain_chunk, parse_mode=None)
                else:
                    await self.bot.send_message(chat_id=message.chat.id, text=plain_chunk, parse_mode=None, reply_to_message_id=message.message_id)

    async def _reply_json(self, message: Message, payload: Any) -> None:
        text = json.dumps(payload, ensure_ascii=False, indent=2, default=str)
        chunks = [text[i : i + 3200] for i in range(0, len(text), 3200)] or [""]
        for index, chunk in enumerate(chunks):
            rendered = f"<pre>{html.escape(chunk)}</pre>"
            if index == 0:
                await message.reply(rendered)
            else:
                await self.bot.send_message(chat_id=message.chat.id, text=rendered, reply_to_message_id=message.message_id)

    async def _incoming_payload(self, message: Message) -> str:
        return await self._build_agent_payload(message)

    def _find_session_by_id(self, session_id: str):
        for session in self.runtime.session_store.list_sessions():
            if session.session_id == session_id:
                return session
        return None

    async def handle_message(self, message: Message) -> None:
        try:
            text = message.text or message.caption or ""
            parsed = parse_command(text)
            if parsed is not None:
                handled = await self.handle_command(message, parsed)
                if handled:
                    return
            decision = should_trigger_agent(
                text,
                is_reply_to_bot=self._is_reply_to_bot(message),
                has_active_session=self._has_active_agent_session(message),
            )
            if await self._handle_pending_input(message):
                return
            if not decision.accept:
                return
            if not self._is_authorized(message):
                await message.reply("Доступ запрещён. Используй /pair CODE или добавь user/chat в allowlist.")
                return
            await self._run_agent_request(message, text=await self._incoming_payload(message))
        except Exception as exc:
            traceback.print_exc()
            await message.reply(f"Ошибка Telegram handler: <code>{html.escape(str(exc))}</code>")

    async def _handle_pair_command(self, message: Message, command: ParsedCommand) -> bool:
        user_id = self._user_id(message)
        chat_id = self._chat_id(message)
        if not command.args:
            if self._is_owner(message):
                payload = self.runtime.create_pairing_code(created_by=user_id)
                await message.reply(
                    f"Pairing code: <code>{html.escape(str(payload['code']))}</code>\nexpires_at: <code>{html.escape(str(payload['expires_at']))}</code>"
                )
            else:
                await message.reply("Используй: /pair CODE")
            return True

        first = command.tokens[0].lower() if command.tokens else ""
        if self._is_owner(message) and first in {"create", "new"}:
            ttl = int(command.tokens[1]) if len(command.tokens) >= 2 and command.tokens[1].isdigit() else None
            payload = self.runtime.create_pairing_code(created_by=user_id, ttl_seconds=ttl)
            await message.reply(
                f"Pairing code: <code>{html.escape(str(payload['code']))}</code>\nexpires_at: <code>{html.escape(str(payload['expires_at']))}</code>"
            )
            return True

        if self._is_owner(message) and first in {"list", "show"}:
            await self._reply_long(message, _auth_text(self.runtime.auth_summary()), html_mode=True)
            return True

        try:
            payload = self.runtime.pair_telegram_access(
                command.tokens[0],
                user_id=user_id,
                chat_id=chat_id,
                username=self._username(message),
                chat_title=self._chat_title(message),
            )
        except Exception as exc:
            await message.reply(f"Ошибка pairing: <code>{html.escape(str(exc))}</code>")
            return True
        await self._reply_json(message, payload)
        return True

    async def handle_callback(self, callback: CallbackQuery) -> None:
        data = callback.data or ""
        if not data.startswith("settings:"):
            await callback.answer()
            return
        message = callback.message
        if message is None:
            await callback.answer()
            return
        actor_user_id = callback.from_user.id if callback.from_user else None
        if not self._is_owner(user_id=actor_user_id, chat_id=self._chat_id(message)):
            await callback.answer("Эти кнопки доступны только owner.", show_alert=True)
            return
        parts = data.split(":", 2)
        action = parts[1] if len(parts) > 1 else ""
        value = parts[2] if len(parts) > 2 else ""
        session = self.runtime.get_session(
            session_id=self._session_id(message),
            chat_id=str(self._chat_id(message)),
            user_id=actor_user_id,
        )
        if action == "provider":
            payload = self.runtime.set_provider(session, value)
            await callback.answer(f"Provider: {value}")
            await self._reply_json(message, payload)
            await self._render_settings(message, user_id=actor_user_id)
            return
        if action == "key":
            self._set_pending_input(message, {"action": "api_key", "provider": value}, user_id=actor_user_id)
            await callback.answer()
            await message.reply(f"Пришли следующим сообщением API key для <code>{html.escape(value)}</code> или /cancel")
            return
        if action == "model":
            self._set_pending_input(message, {"action": "model", "provider": value}, user_id=actor_user_id)
            await callback.answer()
            await message.reply(f"Пришли следующим сообщением модель для <code>{html.escape(value)}</code> или /cancel")
            return
        if action == "refresh":
            await callback.answer()
            await self._render_settings(message, user_id=actor_user_id)
            return
        await callback.answer()

    async def handle_command(self, message: Message, command: ParsedCommand) -> bool:
        user_id = self._user_id(message)
        chat_id = self._chat_id(message)

        if command.name in {"start", "help"}:
            await message.reply(build_help_text())
            return True

        if command.name == "pair":
            return await self._handle_pair_command(message, command)

        if command.name == "openclaw":
            if not self._is_authorized(message):
                await message.reply("Команда недоступна до авторизации. Используй /pair CODE.")
                return True
            prompt = command.args.strip()
            if not prompt and not self._has_media(message):
                await message.reply("Используй: /openclaw <запрос>, или отправь файл вместе с /openclaw, или reply на сообщение бота.")
                return True
            await self._run_agent_request(message, text=await self._build_agent_payload(message, override_text=prompt))
            return True

        if not self._is_authorized(message):
            await message.reply("Команда недоступна до авторизации. Используй /pair CODE.")
            return True

        if command.name == "auth":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            await self._reply_long(message, _auth_text(self.runtime.auth_summary()), html_mode=True)
            return True

        if command.name == "settings":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            await self._render_settings(message)
            return True

        if command.name == "apikey":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            if len(command.tokens) < 2:
                await message.reply("Используй: /apikey <provider> <key>")
                return True
            provider = command.tokens[0]
            key = command.args.split(None, 1)[1] if len(command.args.split(None, 1)) == 2 else ""
            payload = self.runtime.set_provider_api_key(provider, key)
            await self._reply_json(message, payload)
            return True

        if command.name == "approve":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            target_user = int(command.tokens[0]) if len(command.tokens) >= 1 and command.tokens[0].lstrip("-").isdigit() else user_id
            target_chat = int(command.tokens[1]) if len(command.tokens) >= 2 and command.tokens[1].lstrip("-").isdigit() else chat_id
            payload = self.runtime.approve_telegram_access(
                user_id=target_user,
                chat_id=target_chat,
                approved_by=user_id,
                username=self._username(message),
                chat_title=self._chat_title(message),
            )
            await self._reply_json(message, payload)
            return True

        session = self.runtime.get_session(
            session_id=self._session_id(message),
            chat_id=str(chat_id),
            user_id=user_id,
        )

        if command.name == "status":
            await message.reply(_status_text(self.runtime.get_session_status(session)))
            return True

        if command.name == "safe":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            if not command.tokens:
                await message.reply("Используй: /safe on или /safe off")
                return True
            token = command.tokens[0].lower()
            mode = "safe" if token == "on" else "god" if token == "off" else None
            if mode is None:
                await message.reply("Используй: /safe on или /safe off")
                return True
            result = self.runtime.set_security_mode(session, mode)
            suffix = "\nПолный unrestricted host-terminal access включён." if mode == "god" else ""
            await message.reply(f"Режим: <code>{html.escape(result['security_mode'])}</code>{suffix}")
            return True

        if command.name == "mode":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            if not command.tokens or command.tokens[0].lower() not in {"safe", "yolo", "god"}:
                await message.reply("Используй: /mode safe|yolo|god")
                return True
            result = self.runtime.set_security_mode(session, command.tokens[0].lower())
            await message.reply(f"Режим: <code>{html.escape(result['security_mode'])}</code>")
            return True

        if command.name == "exec":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            if not command.args:
                await message.reply("Используй: /exec <command>")
                return True
            result = await self.runtime.shell_executor.run(command.args, mode=session.security_mode)
            await self._reply_json(message, result.to_dict())
            return True

        if command.name == "providers":
            await message.reply(_providers_text(self.runtime.list_provider_names()))
            return True

        if command.name == "models":
            provider = command.tokens[0] if command.tokens else None
            await self._reply_long(message, _models_text(await self.runtime.list_models(provider)), html_mode=True)
            return True

        if command.name == "provider":
            if not command.tokens:
                await message.reply("Используй: /provider <name>")
                return True
            result = self.runtime.set_provider(session, command.tokens[0])
            await message.reply(
                f"Provider: <code>{html.escape(result['provider'])}</code>\nModel: <code>{html.escape(result['model'])}</code>"
            )
            return True

        if command.name == "model":
            if not command.args:
                await message.reply("Используй: /model <name>")
                return True
            result = self.runtime.set_model(session, command.args)
            await message.reply(f"Model: <code>{html.escape(result['model'])}</code>")
            return True

        if command.name == "reset":
            target_session_id = command.args.strip() if command.args.strip() and self._is_owner(message) else session.session_id
            if target_session_id != session.session_id and self._find_session_by_id(target_session_id) is None:
                await message.reply("Session not found.")
                return True
            if target_session_id == session.session_id:
                self._pop_pending_input(message)
            result = self.runtime.reset_session(target_session_id)
            await message.reply(f"Session reset: <code>{html.escape(result['session_id'])}</code>")
            return True

        if command.name == "skills":
            await message.reply(_skills_text(self.runtime.list_skills_payload()))
            return True

        if command.name == "sessions":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            await self._reply_long(message, _sessions_text(self.runtime.list_sessions_payload()), html_mode=True)
            return True

        if command.name == "end":
            if command.tokens and command.tokens[0].lower() == "reopen":
                target = self._find_session_by_id(command.tokens[1]) if len(command.tokens) >= 2 else session
                if target is None:
                    await message.reply("Session not found.")
                    return True
                result = self.runtime.reopen_session(target)
                await message.reply(f"Session reopened: <code>{html.escape(result['session_id'])}</code>")
                return True
            target = self._find_session_by_id(command.args) if command.args and self._is_owner(message) else session
            if target is None:
                await message.reply("Session not found.")
                return True
            result = self.runtime.end_session(target)
            await message.reply(f"Session ended: <code>{html.escape(result['session_id'])}</code>")
            return True

        if command.name == "reload":
            if not self._is_owner(message):
                await message.reply("Эта команда доступна только owner.")
                return True
            self.runtime.reload()
            await message.reply("Runtime перезагружен.")
            return True

        return False

    async def run_polling(self) -> None:
        await self._configure_bot_commands()
        await self.bot.delete_webhook(drop_pending_updates=True)
        allowed_updates = self.dp.resolve_used_update_types()
        while True:
            try:
                await self.dp.start_polling(self.bot, allowed_updates=allowed_updates)
                break
            except Exception:
                traceback.print_exc()
                await asyncio.sleep(2)

    async def run(self) -> None:
        try:
            await self.run_polling()
        finally:
            await self.bot.session.close()
