from __future__ import annotations

import html
import asyncio
import secrets
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import quote_plus

from aiogram import Bot, F, Router
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.filters import Command, CommandObject, CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, FSInputFile, InlineKeyboardButton, InputMediaPhoto, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder

from app.bot.ui.panel import (
    PANEL_SECTION_ADMIN,
    PANEL_SECTION_HOME,
    PANEL_SECTION_REFERRAL,
    PANEL_SECTION_SUBSCRIPTION,
    PANEL_SECTION_SUPPORT,
    PANEL_SECTION_SUPPORT_TICKET,
    PANEL_SECTION_TERMS,
    PanelContext,
    build_panel_keyboard,
    render_panel_caption,
)
from app.config import Settings
from app.services.bot_config_service import BotConfigService, BotConfigSnapshot, StaticBotConfigService
from app.services.payment_service import (
    FULFILLED_PAYMENT_STATUS,
    PENDING_PAYMENT_STATUS,
    REJECTED_PAYMENT_STATUS,
    REVIEW_PAYMENT_STATUS,
    CreatedPaymentOrder,
    PaymentPlan,
    PaymentService,
    StoredPaymentOrder,
)
from app.services.remnawave_client import RemnawaveApiError
from app.services.support_ticket_service import (
    AdminSupportTicketsPage,
    ANSWERED_SUPPORT_TICKET_STATUS,
    CLOSED_SUPPORT_TICKET_STATUS,
    OPEN_SUPPORT_TICKET_STATUS,
    SupportTicketService,
)
from app.services.sync_service import (
    AdminTelegramUserProfile,
    AdminTelegramUsersPage,
    CachedUserView,
    GuideView,
    PromoCodeView,
    SyncService,
)
from app.utils.formatters import (
    build_help_text,
    format_bytes,
    format_datetime,
    format_datetime_with_days_left,
    format_issued_access,
    format_traffic_limit,
    format_user_card,
)


class SupportTicketStates(StatesGroup):
    waiting_for_message = State()
    waiting_for_note = State()


class SupportTicketAdminStates(StatesGroup):
    waiting_for_note = State()


class ReferralCodeStates(StatesGroup):
    waiting_for_code = State()


class ManualPaymentStates(StatesGroup):
    waiting_for_receipt = State()


class AdminLookupStates(StatesGroup):
    waiting_for_identifier = State()


class AdminBroadcastStates(StatesGroup):
    waiting_for_text = State()


class AdminConfigStates(StatesGroup):
    waiting_for_value = State()


class AdminPromoStates(StatesGroup):
    waiting_for_code = State()
    waiting_for_plan = State()
    waiting_for_discount = State()
    waiting_for_expires = State()


class AdminGuideStates(StatesGroup):
    waiting_for_title = State()
    waiting_for_description = State()
    waiting_for_body = State()
    waiting_for_photos = State()


def build_router(
    *,
    settings: Settings,
    sync_service: SyncService,
    support_ticket_service: SupportTicketService,
    payment_service: PaymentService,
    bot_config_service: StaticBotConfigService,
) -> Router:
    router = Router(name="main")
    project_root = Path(__file__).resolve().parents[3]
    default_start_image_path = project_root / "assets" / "main.png"

    async def get_runtime_config() -> BotConfigSnapshot:
        return await bot_config_service.get_snapshot()

    def resolve_start_image_path(raw_path: str) -> Path | None:
        if not raw_path.strip():
            return default_start_image_path if default_start_image_path.is_file() else None

        image_path = Path(raw_path).expanduser()
        if not image_path.is_absolute():
            image_path = project_root / image_path

        return image_path if image_path.is_file() else None

    def get_start_image_path(config: BotConfigSnapshot) -> Path | None:
        if not config.bot_start_image_enabled:
            return None

        return resolve_start_image_path(config.bot_start_image_path)

    def payment_callback(action: str, value: str | None = None) -> str:
        return f"payment:{action}:{value}" if value else f"payment:{action}"

    def guides_callback(action: str, value: str | None = None) -> str:
        return f"guides:{action}:{value}" if value else f"guides:{action}"

    async def send_branded(
        bot_instance: Bot,
        *,
        chat_id: int,
        text: str,
        reply_markup=None,
    ) -> None:
        """Send a message with brand image if enabled and caption fits."""
        config = await get_runtime_config()
        start_image_path = get_start_image_path(config)
        if start_image_path is not None and len(text) <= 1024:
            await bot_instance.send_photo(
                chat_id=chat_id,
                photo=FSInputFile(str(start_image_path)),
                caption=text,
                reply_markup=reply_markup,
            )
            return
        await bot_instance.send_message(
            chat_id=chat_id,
            text=text,
            reply_markup=reply_markup,
        )

    async def answer_branded(
        message: Message,
        text: str,
        reply_markup=None,
    ) -> None:
        """Answer message with brand image if enabled and caption fits."""
        config = await get_runtime_config()
        start_image_path = get_start_image_path(config)
        if start_image_path is not None and len(text) <= 1024:
            await message.answer_photo(
                FSInputFile(str(start_image_path)),
                caption=text,
                reply_markup=reply_markup,
            )
            return
        await message.answer(text, reply_markup=reply_markup)

    async def show_branded_view(
        callback: CallbackQuery,
        *,
        text: str,
        reply_markup,
    ) -> None:
        """Show a branded text view — new message with image if possible."""
        if callback.message is None:
            await callback.answer()
            return

        config = await get_runtime_config()
        start_image_path = get_start_image_path(config)
        if start_image_path is not None and len(text) <= 1024:
            await callback.message.answer_photo(
                FSInputFile(str(start_image_path)),
                caption=text,
                reply_markup=reply_markup,
            )
            return

        try:
            if callback.message.photo or callback.message.document:
                await callback.message.answer(text, reply_markup=reply_markup)
            else:
                await callback.message.edit_text(text, reply_markup=reply_markup)
        except TelegramBadRequest as exc:
            if "message is not modified" not in str(exc).lower():
                raise

    async def ensure_sender(message: Message) -> bool:
        if message.from_user is None:
            return False
        if await sync_service.is_user_blocked(message.from_user.id):
            await message.answer(
                "\u26d4 Ваш аккаунт заблокирован. Обратитесь в поддержку."
            )
            return False
        return True

    async def clear_interactive_states(state: FSMContext) -> None:
        if await state.get_state() in {
            SupportTicketStates.waiting_for_message.state,
            SupportTicketStates.waiting_for_note.state,
            SupportTicketAdminStates.waiting_for_note.state,
            ReferralCodeStates.waiting_for_code.state,
            ManualPaymentStates.waiting_for_receipt.state,
            AdminLookupStates.waiting_for_identifier.state,
            AdminBroadcastStates.waiting_for_text.state,
            AdminConfigStates.waiting_for_value.state,
            AdminPromoStates.waiting_for_code.state,
            AdminPromoStates.waiting_for_plan.state,
            AdminPromoStates.waiting_for_discount.state,
            AdminPromoStates.waiting_for_expires.state,
            AdminGuideStates.waiting_for_title.state,
            AdminGuideStates.waiting_for_description.state,
            AdminGuideStates.waiting_for_body.state,
            AdminGuideStates.waiting_for_photos.state,
        }:
            await state.clear()

    async def load_panel_context(
        *,
        telegram_id: int,
        username: str | None,
        first_name: str | None,
        last_name: str | None,
        language_code: str | None,
        sync_remote: bool,
    ) -> PanelContext:
        users: list[CachedUserView] = []
        api_error: str | None = None
        is_admin = settings.is_admin(telegram_id)
        config = await get_runtime_config()

        if sync_remote:
            try:
                await payment_service.apply_pending_referral_bonuses(
                    telegram_id=telegram_id,
                    refresh_cache=False,
                )
            except RemnawaveApiError:
                pass

            try:
                users = await sync_service.sync_users_for_telegram(
                    telegram_id=telegram_id,
                    username=username,
                    first_name=first_name,
                    last_name=last_name,
                    language_code=language_code,
                )
            except RemnawaveApiError as exc:
                api_error = exc.message
                users = await sync_service.get_cached_users_for_telegram(telegram_id)
        else:
            users = await sync_service.get_cached_users_for_telegram(telegram_id)

        display_name = " ".join(part for part in [first_name, last_name] if part).strip()
        if not display_name:
            display_name = username or "Пользователь"

        referral_summary = await sync_service.get_referral_summary(telegram_id)
        active_discount = await payment_service.get_active_discount_for_user(
            telegram_id=telegram_id,
        )
        referral_link = ""
        referral_share_url = ""
        if config.referral_enabled and config.bot_public_username_normalized:
            referral_link = (
                f"https://t.me/{config.bot_public_username_normalized}"
                f"?start=ref_{referral_summary.referral_code}"
            )
            share_text = (
                "Вас пригласили по реферальной ссылке. "
                f"Перед оплатой введите код {referral_summary.referral_code} "
                f"и получите скидку {max(config.referral_discount_percent, 0)}%."
            )
            referral_share_url = (
                "https://t.me/share/url"
                f"?url={quote_plus(referral_link)}"
                f"&text={quote_plus(share_text)}"
            )

        plans = await payment_service.get_available_plans(telegram_id=telegram_id)
        primary_plan = next((plan for plan in plans if plan.is_ready), plans[0] if plans else None)
        has_ready_payment_plan = any(plan.is_ready for plan in plans)
        admin_total_telegram_users = 0
        admin_total_cached_users = 0
        admin_active_cached_users = 0
        admin_pending_orders = 0
        admin_review_orders = 0
        admin_open_tickets = 0
        admin_pending_referral_bonuses = 0

        if is_admin:
            admin_stats = await sync_service.get_admin_stats()
            payment_admin_stats = await payment_service.get_admin_stats()
            admin_total_telegram_users = admin_stats.total_telegram_users
            admin_total_cached_users = admin_stats.total_cached_users
            admin_active_cached_users = admin_stats.active_cached_users
            admin_pending_orders = payment_admin_stats.pending_orders
            admin_review_orders = payment_admin_stats.review_orders
            admin_pending_referral_bonuses = payment_admin_stats.pending_referral_bonuses
            admin_open_tickets = await support_ticket_service.get_open_tickets_count()

        return PanelContext(
            telegram_id=telegram_id,
            display_name=display_name,
            username=username,
            language_code=language_code,
            is_admin=is_admin,
            users=users,
            referral_enabled=config.referral_enabled,
            referral_code=referral_summary.referral_code,
            referral_count=referral_summary.total_invited,
            applied_referral_code=(
                active_discount.code if config.referral_enabled else ""
            ),
            referral_link=referral_link,
            referral_share_url=referral_share_url,
            referral_recent_names=referral_summary.recent_names,
            referral_discount_percent=max(
                active_discount.discount_percent or config.referral_discount_percent,
                0,
            ),
            referral_bonus_days=max(config.referral_bonus_days, 0),
            admin_total_telegram_users=admin_total_telegram_users,
            admin_total_cached_users=admin_total_cached_users,
            admin_active_cached_users=admin_active_cached_users,
            admin_pending_orders=admin_pending_orders,
            admin_review_orders=admin_review_orders,
            admin_open_tickets=admin_open_tickets,
            admin_pending_referral_bonuses=admin_pending_referral_bonuses,
            payment_enabled=has_ready_payment_plan,
            payment_plan_title=primary_plan.title if primary_plan else "",
            payment_plan_description=primary_plan.description if primary_plan else "",
            payment_plan_price_stars=primary_plan.amount_rub if primary_plan else 0,
            payment_plan_duration_days=primary_plan.duration_days if primary_plan else 0,
            payment_plan_traffic_limit_bytes=primary_plan.traffic_limit_bytes if primary_plan else 0,
            api_error=api_error,
        )

    def parse_start_referral_code(message: Message) -> str | None:
        text = (message.text or "").strip()
        parts = text.split(maxsplit=1)
        if len(parts) < 2:
            return None

        arg = parts[1].strip()
        if not arg.startswith("ref_"):
            return None

        raw_value = arg.removeprefix("ref_").strip().upper()
        return raw_value if raw_value else None

    async def show_panel(
        message: Message,
        *,
        context: PanelContext,
        section: str,
    ) -> None:
        config = await get_runtime_config()
        caption = render_panel_caption(
            section=section,
            context=context,
            brand_name=config.bot_brand_name,
        )
        keyboard = build_panel_keyboard(
            section=section,
            context=context,
            support_url=config.bot_support_url,
            terms_url=config.bot_terms_url,
            payment_review_url=config.payment_review_link,
            support_ticket_url=config.bot_support_ticket_link,
        )

        start_image_path = get_start_image_path(config)
        if start_image_path is not None:
            await message.answer_photo(
                FSInputFile(str(start_image_path)),
                caption=caption,
                reply_markup=keyboard,
            )
            return

        await message.answer(caption, reply_markup=keyboard)

    async def edit_message_content(
        callback: CallbackQuery,
        *,
        text: str,
        reply_markup,
    ) -> None:
        if callback.message is None:
            await callback.answer()
            return

        try:
            if callback.message.photo or callback.message.document:
                await callback.message.edit_caption(caption=text, reply_markup=reply_markup)
            else:
                await callback.message.edit_text(text, reply_markup=reply_markup)
        except TelegramBadRequest as exc:
            if "message is not modified" not in str(exc).lower():
                raise

    async def show_text_view(
        callback: CallbackQuery,
        *,
        text: str,
        reply_markup,
    ) -> None:
        if callback.message is None:
            await callback.answer()
            return

        try:
            if callback.message.photo or callback.message.document:
                await callback.message.answer(text, reply_markup=reply_markup)
            else:
                await callback.message.edit_text(text, reply_markup=reply_markup)
        except TelegramBadRequest as exc:
            if "message is not modified" not in str(exc).lower():
                raise

    async def update_panel(
        callback: CallbackQuery,
        *,
        context: PanelContext,
        section: str,
    ) -> None:
        config = await get_runtime_config()
        caption = render_panel_caption(
            section=section,
            context=context,
            brand_name=config.bot_brand_name,
        )
        keyboard = build_panel_keyboard(
            section=section,
            context=context,
            support_url=config.bot_support_url,
            terms_url=config.bot_terms_url,
            payment_review_url=config.payment_review_link,
            support_ticket_url=config.bot_support_ticket_link,
        )
        await edit_message_content(callback, text=caption, reply_markup=keyboard)
        await callback.answer()

    async def send_lookup_result(message: Message, *, identifier: str) -> bool:
        try:
            user = await sync_service.sync_user_by_identifier(
                identifier=identifier,
                include_history=True,
            )
        except RemnawaveApiError as exc:
            await message.answer(
                "Пользователь не найден или API вернул ошибку.\n"
                f"Ошибка: <code>{html.escape(exc.message)}</code>"
            )
            return False

        await message.answer(
            format_user_card(
                user=user.record,
                squad_names=user.internal_squads,
                recent_requests=user.recent_requests,
            )
        )
        return True

    async def run_sync_all(message: Message) -> bool:
        await message.answer("Запустил полную синхронизацию Remnawave -> MariaDB.")

        try:
            total = await sync_service.sync_all_users(settings.sync_batch_size)
        except RemnawaveApiError as exc:
            await message.answer(
                "Полная синхронизация завершилась с ошибкой.\n"
                f"Ошибка: <code>{html.escape(exc.message)}</code>"
            )
            return False

        await message.answer(
            f"Полная синхронизация завершена. Обновлено пользователей: <b>{total}</b>"
        )
        return True

    def build_admin_broadcast_preview_keyboard():
        builder = InlineKeyboardBuilder()
        builder.button(
            text="✅ Отправить всем",
            callback_data="admin:broadcast_confirm",
        )
        builder.button(
            text="❌ Отменить",
            callback_data="admin:broadcast_cancel",
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_admin_broadcast_mode_keyboard():
        builder = InlineKeyboardBuilder()
        builder.button(
            text="📣 Уведомление",
            callback_data="admin:broadcast_mode:notice",
        )
        builder.button(
            text="✅ Опрос Да/Нет",
            callback_data="admin:broadcast_mode:poll",
        )
        builder.button(
            text="🛠 Админ-панель",
            callback_data=f"panel:{PANEL_SECTION_ADMIN}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_broadcast_poll_keyboard(*, poll_id: int, selected_answer: str | None = None):
        yes_text = "✅ Да" if selected_answer == "yes" else "Да"
        no_text = "✅ Нет" if selected_answer == "no" else "Нет"
        yes_callback = (
            f"broadcast_poll:yes:{poll_id}" if poll_id > 0 else "admin:broadcast_preview_answer"
        )
        no_callback = (
            f"broadcast_poll:no:{poll_id}" if poll_id > 0 else "admin:broadcast_preview_answer"
        )
        builder = InlineKeyboardBuilder()
        builder.button(text=yes_text, callback_data=yes_callback)
        builder.button(text=no_text, callback_data=no_callback)
        builder.adjust(2)
        return builder.as_markup()

    def get_message_html_text(message: Message) -> tuple[str, str]:
        raw_text = (message.text or message.caption or "").strip()
        formatted_text = (
            (getattr(message, "html_text", None) or getattr(message, "html_caption", None) or "").strip()
        )
        if not formatted_text and raw_text:
            formatted_text = html.escape(raw_text)
        return raw_text, formatted_text

    def render_broadcast_text(
        *,
        brand_name: str,
        body_html: str,
        mode: str,
    ) -> str:
        title = "Опрос" if mode == "poll" else "Уведомление"
        lines = [
            f"<b>📣 {html.escape(brand_name)} · {title}</b>",
            "",
            f"<blockquote>{body_html}</blockquote>",
        ]
        if mode == "poll":
            lines.extend(["", "<i>Выберите один из вариантов ниже.</i>"])
        return "\n".join(lines)

    async def show_admin_broadcast_preview(
        message: Message,
        *,
        body_html: str,
        mode: str,
        state: FSMContext,
    ) -> None:
        config = await get_runtime_config()
        text_html = render_broadcast_text(
            brand_name=config.bot_brand_name,
            body_html=body_html,
            mode=mode,
        )
        recipient_count = len(await sync_service.get_broadcast_telegram_ids())
        await state.update_data(
            broadcast_body_html=body_html,
            broadcast_text_html=text_html,
            broadcast_mode=mode,
        )
        await message.answer(
            "📣 Предпросмотр рассылки.\n\n"
            f"Получателей: <b>{recipient_count}</b>\n"
            "Если всё верно, нажмите кнопку подтверждения ниже."
        )
        if mode == "poll":
            await message.answer(text_html, reply_markup=build_broadcast_poll_keyboard(poll_id=0))
            await message.answer(
                "У пользователей под этим сообщением будут кнопки <b>Да</b> и <b>Нет</b>.",
                reply_markup=build_admin_broadcast_preview_keyboard(),
            )
            return

        await message.answer(text_html, reply_markup=build_admin_broadcast_preview_keyboard())

    async def send_admin_broadcast(
        bot_instance: Bot,
        *,
        text_html: str,
        poll_id: int | None = None,
    ) -> tuple[int, int, int]:
        telegram_ids = await sync_service.get_broadcast_telegram_ids()
        sent_count = 0
        blocked_count = 0
        failed_count = 0
        reply_markup = (
            build_broadcast_poll_keyboard(poll_id=poll_id)
            if poll_id is not None
            else None
        )

        for telegram_id in telegram_ids:
            try:
                await send_branded(
                    bot_instance,
                    chat_id=telegram_id,
                    text=text_html,
                    reply_markup=reply_markup,
                )
                sent_count += 1
            except TelegramRetryAfter as exc:
                await asyncio.sleep(exc.retry_after + 1)
                try:
                    await send_branded(
                        bot_instance,
                        chat_id=telegram_id,
                        text=text_html,
                        reply_markup=reply_markup,
                    )
                    sent_count += 1
                except TelegramForbiddenError:
                    blocked_count += 1
                except TelegramBadRequest:
                    failed_count += 1
                except Exception:
                    failed_count += 1
            except TelegramForbiddenError:
                blocked_count += 1
            except TelegramBadRequest:
                failed_count += 1
            except Exception:
                failed_count += 1

            await asyncio.sleep(0.035)

        return sent_count, blocked_count, failed_count

    def broadcast_answer_label(answer: str) -> str:
        return "Да" if answer == "yes" else "Нет"

    async def notify_broadcast_poll_answer(
        bot_instance: Bot,
        *,
        poll_id: int,
        question_html: str,
        answer: str,
        previous_answer: str | None,
        telegram_id: int,
        username: str | None,
        full_name: str,
    ) -> None:
        config = await get_runtime_config()
        if config.broadcast_results_chat_id is None:
            return

        username_text = f"@{username}" if username else "—"
        changed_text = (
            f"\nПредыдущий ответ: <b>{broadcast_answer_label(previous_answer)}</b>"
            if previous_answer and previous_answer != answer
            else ""
        )
        await bot_instance.send_message(
            chat_id=config.broadcast_results_chat_id,
            message_thread_id=config.broadcast_results_message_thread_id,
            text=(
                "<b>📬 Ответ на опрос рассылки</b>\n\n"
                f"Опрос: <code>{poll_id}</code>\n"
                f"Ответ: <b>{broadcast_answer_label(answer)}</b>{changed_text}\n\n"
                "<b>Пользователь</b>\n"
                "<blockquote>"
                f"ID: <code>{telegram_id}</code>\n"
                f"Имя: <b>{html.escape(full_name or str(telegram_id))}</b>\n"
                f"Username: <b>{html.escape(username_text)}</b>"
                "</blockquote>\n\n"
                "<b>Вопрос</b>\n"
                f"<blockquote>{question_html}</blockquote>"
            ),
        )

    def admin_settings_callback(action: str, value: str | None = None) -> str:
        return f"admin:{action}:{value}" if value else f"admin:{action}"

    def admin_users_page_callback(page: int) -> str:
        return admin_settings_callback("user_list", str(max(page, 1)))

    def admin_user_profile_callback(*, telegram_id: int, page: int) -> str:
        return f"admin:user_view:{telegram_id}:{max(page, 1)}"

    def admin_tickets_page_callback(page: int) -> str:
        return admin_settings_callback("ticket_list", str(max(page, 1)))

    def admin_ticket_view_callback(*, ticket_id: int, page: int) -> str:
        return f"admin:ticket_view:{ticket_id}:{max(page, 1)}"

    def admin_promo_plan_callback(plan_code: str) -> str:
        return f"admin:promo_plan:{plan_code}"

    def compact_inline_label(value: str, *, limit: int = 30) -> str:
        normalized = " ".join(value.split())
        if len(normalized) <= limit:
            return normalized
        return f"{normalized[: max(0, limit - 3)].rstrip()}..."

    def get_payment_plan_label_map(config: BotConfigSnapshot) -> dict[str, str]:
        return {
            plan.code: f"{plan.title} · {plan.days} дней" if plan.title else f"{plan.days} дней"
            for plan in config.payment_plans
        }

    def format_promo_scope(*, plan_code: str | None, config: BotConfigSnapshot) -> str:
        if not plan_code:
            return "все тарифы"
        # "all_30" → "Все тарифы · 30 дней"
        if plan_code.startswith("all_") and plan_code.removeprefix("all_").isdigit():
            days = plan_code.removeprefix("all_")
            return f"Все тарифы · {days} дней"
        label_map = get_payment_plan_label_map(config)
        if plan_code in label_map:
            return label_map[plan_code]
        # Try product-level lookup (e.g. plan_code="vpn_white" → title from spec)
        from app.config import PAYMENT_PRODUCT_PLAN_SPECS
        spec = PAYMENT_PRODUCT_PLAN_SPECS.get(plan_code)
        if spec is not None:
            return spec[0]  # title
        return plan_code

    def format_promo_expires_at(expires_at) -> str:
        return format_datetime(expires_at) if expires_at is not None else "без срока"

    def parse_promo_expires_at(raw_value: str):
        value = (raw_value or "").strip().lower()
        if value in {"0", "-", "none", "no", "без срока", "бессрочно"}:
            return None

        now = datetime.now(timezone.utc).replace(tzinfo=None)
        compact_value = "".join(value.split())
        suffix_map = {
            "h": "hours",
            "ч": "hours",
            "hour": "hours",
            "hours": "hours",
            "d": "days",
            "д": "days",
            "day": "days",
            "days": "days",
        }
        for suffix, unit in suffix_map.items():
            if not compact_value.endswith(suffix):
                continue
            raw_number = compact_value[: -len(suffix)]
            if raw_number.isdigit() and int(raw_number) > 0:
                return now + timedelta(**{unit: int(raw_number)})

        if compact_value.isdigit() and int(compact_value) > 0:
            return now + timedelta(days=int(compact_value))

        for fmt in ("%Y-%m-%d %H:%M", "%Y-%m-%d", "%d.%m.%Y %H:%M", "%d.%m.%Y"):
            try:
                parsed = datetime.strptime(raw_value.strip(), fmt)
            except ValueError:
                continue
            if "%H" not in fmt:
                parsed = parsed.replace(hour=23, minute=59, second=59)
            return parsed

        raise ValueError(
            "Введите срок в днях, например 7, или дату 2026-05-31. Для бессрочного промокода отправьте 0."
        )

    def render_admin_promos_text(
        *,
        promos: list[PromoCodeView],
        config: BotConfigSnapshot,
    ) -> str:
        lines = [
            "<b>🎟 Промокоды</b>",
            "",
            "<blockquote>Промокод вводится пользователем в разделе реферальной системы и даёт скидку перед оплатой.</blockquote>",
        ]
        if not promos:
            lines.extend(["", "<i>Промокоды ещё не созданы.</i>"])
            return "\n".join(lines)

        lines.extend(["", "<b>Последние промокоды</b>"])
        for promo in promos[:10]:
            status_text = "активен" if promo.is_valid else "неактивен"
            lines.extend(
                [
                    "",
                    (
                        "<blockquote>"
                        f"<code>{html.escape(promo.code)}</code> · <b>-{promo.discount_percent}%</b>\n"
                        f"Тариф: <b>{html.escape(format_promo_scope(plan_code=promo.plan_code, config=config))}</b>\n"
                        f"Срок: <b>{html.escape(format_promo_expires_at(promo.expires_at))}</b>\n"
                        f"Статус: <b>{status_text}</b>"
                        "</blockquote>"
                    ),
                ]
            )
        return "\n".join(lines)

    def build_admin_promos_keyboard():
        builder = InlineKeyboardBuilder()
        builder.button(
            text="➕ Создать промокод",
            callback_data=admin_settings_callback("promo_create"),
        )
        builder.button(
            text="↻ Обновить",
            callback_data=admin_settings_callback("promos"),
        )
        builder.button(
            text="🛠 Админ-панель",
            callback_data=f"panel:{PANEL_SECTION_ADMIN}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_admin_promo_plan_keyboard(*, config: BotConfigSnapshot):
        builder = InlineKeyboardBuilder()
        builder.button(
            text="Все тарифы",
            callback_data=admin_promo_plan_callback("all"),
        )
        # Show unique product types, not individual durations
        seen_products: set[str] = set()
        for plan in config.payment_plans:
            product_code = _extract_product_code(plan.code)
            if product_code in seen_products:
                continue
            seen_products.add(product_code)
            builder.button(
                text=plan.title or product_code,
                callback_data=admin_promo_plan_callback(product_code),
            )
        builder.button(
            text="⬅️ К промокодам",
            callback_data=admin_settings_callback("promos"),
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_admin_promo_duration_keyboard(
        *,
        config: BotConfigSnapshot,
        product_code: str,
    ):
        """Build keyboard with duration options for a specific product (or 'all')."""
        builder = InlineKeyboardBuilder()
        if product_code == "all":
            # Show unique durations across all products
            seen_days: set[int] = set()
            for plan in config.payment_plans:
                if plan.days not in seen_days:
                    seen_days.add(plan.days)
                    builder.button(
                        text=f"{plan.days} дней",
                        callback_data=f"admin:promo_duration:all_{plan.days}",
                    )
        else:
            for plan in config.payment_plans:
                if _extract_product_code(plan.code) == product_code:
                    builder.button(
                        text=f"{plan.days} дней · {plan.amount_rub} ₽",
                        callback_data=f"admin:promo_duration:{plan.code}",
                    )
        builder.button(
            text="⬅️ Назад к тарифам",
            callback_data="admin:promo_back_to_plans",
        )
        builder.adjust(1)
        return builder.as_markup()

    def render_guides_menu_text(*, guides: list[GuideView]) -> str:
        lines = [
            "<b>📚 Гайды</b>",
            "",
        ]
        if not guides:
            lines.append("<blockquote>Гайды пока не добавлены.</blockquote>")
            return "\n".join(lines)

        lines.append("<blockquote>Выберите инструкцию ниже.</blockquote>")
        for index, guide in enumerate(guides[:10], start=1):
            lines.extend(
                [
                    "",
                    f"<b>{index}. {html.escape(guide.title)}</b>",
                    html.escape(guide.short_description or "Без описания"),
                ]
            )
        return "\n".join(lines)

    def build_guides_menu_keyboard(*, guides: list[GuideView]):
        builder = InlineKeyboardBuilder()
        for guide in guides[:10]:
            builder.button(
                text=guide.title,
                callback_data=guides_callback("view", str(guide.id)),
            )
        builder.button(
            text="🏠 Главное меню",
            callback_data=f"panel:{PANEL_SECTION_HOME}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def render_guide_text(*, guide: GuideView) -> str:
        lines = [
            f"<b>{html.escape(guide.title)}</b>",
        ]
        if guide.short_description:
            lines.extend(["", html.escape(guide.short_description)])
        lines.extend(
            [
                "",
                f"<blockquote>{html.escape(guide.body_text)}</blockquote>",
            ]
        )
        return "\n".join(lines)

    def build_guide_view_keyboard():
        builder = InlineKeyboardBuilder()
        builder.button(text="⬅️ К гайдам", callback_data=guides_callback("menu"))
        builder.button(text="🏠 Главное меню", callback_data=f"panel:{PANEL_SECTION_HOME}")
        builder.adjust(1)
        return builder.as_markup()

    async def user_can_open_guides(telegram_id: int) -> bool:
        if settings.is_admin(telegram_id):
            return True
        accesses = await sync_service.get_cached_users_for_telegram(telegram_id)
        return any(access.record.status.upper() == "ACTIVE" for access in accesses)

    async def send_guide_view(bot: Bot, *, chat_id: int, guide: GuideView) -> None:
        text = render_guide_text(guide=guide)
        reply_markup = build_guide_view_keyboard()
        photo_file_ids = guide.photo_file_ids[:10]

        if not photo_file_ids:
            await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
            return

        if len(photo_file_ids) == 1:
            if len(text) <= 1024:
                await bot.send_photo(
                    chat_id=chat_id,
                    photo=photo_file_ids[0],
                    caption=text,
                    reply_markup=reply_markup,
                )
                return

            await bot.send_photo(chat_id=chat_id, photo=photo_file_ids[0])
            await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
            return

        media = [InputMediaPhoto(media=file_id) for file_id in photo_file_ids]
        if len(text) <= 1024:
            media[0].caption = text
            await bot.send_media_group(chat_id=chat_id, media=media)
            await bot.send_message(
                chat_id=chat_id,
                text="Навигация",
                reply_markup=reply_markup,
            )
            return

        await bot.send_media_group(chat_id=chat_id, media=media)
        await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)

    def render_admin_guides_text(*, guides: list[GuideView]) -> str:
        lines = [
            "<b>📚 Гайды</b>",
            "",
            "<blockquote>Здесь создаются инструкции, которые увидят пользователи с активной подпиской.</blockquote>",
        ]
        if not guides:
            lines.extend(["", "<i>Гайды ещё не созданы.</i>"])
            return "\n".join(lines)

        lines.extend(["", "<b>Последние гайды</b>"])
        for guide in guides[:10]:
            photo_text = f"{len(guide.photo_file_ids)} фото" if guide.photo_file_ids else "без фото"
            status_text = "активен" if guide.is_active else "скрыт"
            lines.extend(
                [
                    "",
                    (
                        "<blockquote>"
                        f"<b>{html.escape(guide.title)}</b>\n"
                        f"{html.escape(guide.short_description or 'Без описания')}\n"
                        f"Медиа: <b>{photo_text}</b>\n"
                        f"Статус: <b>{status_text}</b>"
                        "</blockquote>"
                    ),
                ]
            )
        return "\n".join(lines)

    def render_admin_guide_delete_confirm_text(*, guide: GuideView) -> str:
        photo_text = f"{len(guide.photo_file_ids)} фото" if guide.photo_file_ids else "без фото"
        return "\n".join(
            [
                "<b>🗑 Удалить гайд?</b>",
                "",
                (
                    "<blockquote>"
                    f"<b>{html.escape(guide.title)}</b>\n"
                    f"{html.escape(guide.short_description or 'Без описания')}\n"
                    f"Медиа: <b>{photo_text}</b>"
                    "</blockquote>"
                ),
                "",
                "<i>После удаления гайд пропадёт у пользователей, а его фото-ссылки будут удалены из базы.</i>",
            ]
        )

    def build_admin_guides_keyboard(*, guides: list[GuideView]):
        builder = InlineKeyboardBuilder()
        builder.button(
            text="➕ Создать гайд",
            callback_data=admin_settings_callback("guide_create"),
        )
        for guide in guides[:10]:
            builder.button(
                text=f"🗑 {compact_inline_label(guide.title, limit=24)}",
                callback_data=admin_settings_callback("guide_delete", str(guide.id)),
            )
        builder.button(
            text="↻ Обновить",
            callback_data=admin_settings_callback("guides"),
        )
        builder.button(
            text="🛠 Админ-панель",
            callback_data=f"panel:{PANEL_SECTION_ADMIN}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_admin_guide_delete_confirm_keyboard(*, guide_id: int):
        builder = InlineKeyboardBuilder()
        builder.button(
            text="🗑 Да, удалить",
            callback_data=admin_settings_callback("guide_delete_confirm", str(guide_id)),
        )
        builder.button(
            text="⬅️ К гайдам",
            callback_data=admin_settings_callback("guides"),
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_admin_guide_photos_keyboard(*, photo_count: int):
        builder = InlineKeyboardBuilder()
        builder.button(
            text="✅ Сохранить гайд",
            callback_data=admin_settings_callback("guide_done"),
        )
        builder.button(
            text=f"Фото добавлено: {photo_count}",
            callback_data=admin_settings_callback("guide_photo_count"),
        )
        builder.button(
            text="❌ Отмена",
            callback_data=admin_settings_callback("guides"),
        )
        builder.adjust(1)
        return builder.as_markup()

    def render_admin_settings_home_text(*, config: BotConfigSnapshot) -> str:
        lines = [
            "<b>⚙️ Настройки бота</b>",
            "",
            "<blockquote>Здесь меняются бизнес-настройки бота без правки .env и перезапуска.</blockquote>",
            "",
            "<b>Разделы</b>",
        ]
        for section, label in BotConfigService.get_sections():
            spec_count = len(BotConfigService.get_specs_for_section(section))
            lines.append(f"• {html.escape(label)}: <b>{spec_count}</b>")
        lines.extend(
            [
                "",
                "<i>Секреты и инфраструктурные параметры по-прежнему остаются в .env.</i>",
            ]
        )
        return "\n".join(lines)

    def build_admin_settings_home_keyboard():
        builder = InlineKeyboardBuilder()
        for section, label in BotConfigService.get_sections():
            builder.button(
                text=f"⚙️ {label}",
                callback_data=admin_settings_callback("settings_section", section),
            )
        builder.button(
            text="↻ Обновить",
            callback_data=admin_settings_callback("settings"),
        )
        builder.button(
            text="🛠 Админ-панель",
            callback_data=f"panel:{PANEL_SECTION_ADMIN}",
        )
        builder.adjust(2, 2, 2, 1, 1)
        return builder.as_markup()

    def render_admin_settings_section_text(
        *,
        config: BotConfigSnapshot,
        section: str,
    ) -> str:
        section_label = BotConfigService.get_section_label(section)
        specs = BotConfigService.get_specs_for_section(section)
        lines = [
            f"<b>⚙️ {html.escape(section_label)}</b>",
            "",
        ]

        for spec in specs:
            current_value = BotConfigService.format_value(snapshot=config, key=spec.key)
            lines.extend(
                [
                    f"<b>{html.escape(spec.label)}</b>",
                    f"<code>{html.escape(current_value)}</code>",
                    html.escape(spec.description),
                    "",
                ]
            )

        lines.append("<i>Нажмите на параметр ниже, чтобы изменить его.</i>")
        return "\n".join(lines)

    def build_admin_settings_section_keyboard(*, section: str):
        builder = InlineKeyboardBuilder()
        specs = BotConfigService.get_specs_for_section(section)
        for spec in specs:
            builder.button(
                text=f"✏️ {spec.label}",
                callback_data=admin_settings_callback("edit", spec.key),
            )
        builder.button(
            text="↻ Обновить",
            callback_data=admin_settings_callback("settings_section", section),
        )
        builder.button(
            text="⬅️ Разделы",
            callback_data=admin_settings_callback("settings"),
        )
        builder.button(
            text="🛠 Админ-панель",
            callback_data=f"panel:{PANEL_SECTION_ADMIN}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def render_admin_setting_prompt(
        *,
        config: BotConfigSnapshot,
        key: str,
    ) -> str:
        spec = BotConfigService.get_spec(key)
        if spec is None:
            return "Неизвестная настройка."

        current_value = BotConfigService.format_value(snapshot=config, key=key)
        return "\n".join(
            [
                f"<b>{html.escape(spec.label)}</b>",
                "",
                html.escape(spec.prompt),
                "",
                f"Текущее значение: <code>{html.escape(current_value)}</code>",
                "",
                f"<i>{html.escape(spec.description)}</i>",
            ]
        )

    def render_admin_users_page_text(*, page_data: AdminTelegramUsersPage) -> str:
        lines = [
            "<b>👤 Пользователи</b>",
            "",
            (
                "<blockquote>"
                f"Всего пользователей: <b>{page_data.total_items}</b>\n"
                f"Страница: <b>{page_data.page}/{page_data.total_pages}</b>"
                "</blockquote>"
            ),
        ]

        if not page_data.items:
            lines.extend(["", "<i>Пользователи ещё не появлялись в боте.</i>"])
            return "\n".join(lines)

        for index, item in enumerate(
            page_data.items,
            start=((page_data.page - 1) * page_data.page_size) + 1,
        ):
            username_text = f"@{item.username}" if item.username else "—"
            badges: list[str] = []
            if item.is_admin:
                badges.append("админ")
            if item.is_blocked:
                badges.append("заблокирован")
            if not badges:
                badges.append("пользователь")

            lines.extend(
                [
                    "",
                    (
                        f"{index}. <b>{html.escape(item.display_name)}</b>\n"
                        "<blockquote>"
                        f"ID: <code>{item.telegram_id}</code>\n"
                        f"Username: <b>{html.escape(username_text)}</b>\n"
                        f"Статус: <b>{html.escape(', '.join(badges))}</b>\n"
                        f"Последняя активность: <b>{format_datetime(item.last_seen_at)}</b>"
                        "</blockquote>"
                    ),
                ]
            )

        return "\n".join(lines)

    def build_admin_users_page_keyboard(*, page_data: AdminTelegramUsersPage):
        builder = InlineKeyboardBuilder()

        for item in page_data.items:
            button_label = item.display_name
            if item.username:
                button_label = f"{button_label} (@{item.username})"
            else:
                button_label = f"{button_label} • {item.telegram_id}"
            builder.row(
                InlineKeyboardButton(
                    text=f"👤 {compact_inline_label(button_label)}",
                    callback_data=admin_user_profile_callback(
                        telegram_id=item.telegram_id,
                        page=page_data.page,
                    ),
                )
            )

        pagination_buttons: list[InlineKeyboardButton] = []
        if page_data.page > 1:
            pagination_buttons.append(
                InlineKeyboardButton(
                    text="⬅️ Назад",
                    callback_data=admin_users_page_callback(page_data.page - 1),
                )
            )
        pagination_buttons.append(
            InlineKeyboardButton(
                text=f"{page_data.page}/{page_data.total_pages}",
                callback_data=admin_users_page_callback(page_data.page),
            )
        )
        if page_data.page < page_data.total_pages:
            pagination_buttons.append(
                InlineKeyboardButton(
                    text="Вперёд ➡️",
                    callback_data=admin_users_page_callback(page_data.page + 1),
                )
            )
        builder.row(*pagination_buttons)
        builder.row(
            InlineKeyboardButton(
                text="↻ Обновить",
                callback_data=admin_users_page_callback(page_data.page),
            ),
            InlineKeyboardButton(
                text="🛠 Админ-панель",
                callback_data=f"panel:{PANEL_SECTION_ADMIN}",
            ),
        )
        return builder.as_markup()

    def render_admin_tickets_page_text(*, page_data: AdminSupportTicketsPage) -> str:
        lines = [
            "<b>🎫 История тикетов</b>",
            "",
            (
                "<blockquote>"
                f"Всего тикетов: <b>{page_data.total_items}</b>\n"
                f"Страница: <b>{page_data.page}/{page_data.total_pages}</b>"
                "</blockquote>"
            ),
        ]

        if not page_data.items:
            lines.extend(["", "<i>Тикетов пока нет.</i>"])
            return "\n".join(lines)

        for index, item in enumerate(
            page_data.items,
            start=((page_data.page - 1) * page_data.page_size) + 1,
        ):
            username_text = f"@{item.username}" if item.username else "—"
            lines.extend(
                [
                    "",
                    (
                        f"{index}. <b>#{html.escape(item.public_id)}</b> — "
                        f"{html.escape(item.display_name)}\n"
                        "<blockquote>"
                        f"Статус: <b>{format_support_ticket_status(item.status)}</b>\n"
                        f"Telegram ID: <code>{item.telegram_id}</code>\n"
                        f"Username: <b>{html.escape(username_text)}</b>\n"
                        f"Создан: <b>{format_datetime(item.created_at)}</b>"
                        "</blockquote>"
                    ),
                ]
            )

        return "\n".join(lines)

    def build_admin_tickets_page_keyboard(*, page_data: AdminSupportTicketsPage):
        builder = InlineKeyboardBuilder()

        for item in page_data.items:
            username_suffix = f" (@{item.username})" if item.username else ""
            builder.row(
                InlineKeyboardButton(
                    text=(
                        f"🎫 #{item.public_id} • "
                        f"{compact_inline_label(item.display_name + username_suffix, limit=20)}"
                    ),
                    callback_data=admin_ticket_view_callback(
                        ticket_id=item.id,
                        page=page_data.page,
                    ),
                )
            )

        pagination_buttons: list[InlineKeyboardButton] = []
        if page_data.page > 1:
            pagination_buttons.append(
                InlineKeyboardButton(
                    text="⬅️ Назад",
                    callback_data=admin_tickets_page_callback(page_data.page - 1),
                )
            )
        pagination_buttons.append(
            InlineKeyboardButton(
                text=f"{page_data.page}/{page_data.total_pages}",
                callback_data=admin_tickets_page_callback(page_data.page),
            )
        )
        if page_data.page < page_data.total_pages:
            pagination_buttons.append(
                InlineKeyboardButton(
                    text="Вперёд ➡️",
                    callback_data=admin_tickets_page_callback(page_data.page + 1),
                )
            )
        builder.row(*pagination_buttons)
        builder.row(
            InlineKeyboardButton(
                text="↻ Обновить",
                callback_data=admin_tickets_page_callback(page_data.page),
            ),
            InlineKeyboardButton(
                text="🛠 Админ-панель",
                callback_data=f"panel:{PANEL_SECTION_ADMIN}",
            ),
        )
        return builder.as_markup()

    def render_admin_ticket_history_text(*, ticket) -> str:
        username_text = f"@{html.escape(ticket.username)}" if ticket.username else "не указан"
        lines = [
            f"<b>🎫 Тикет #{html.escape(ticket.public_id)}</b>",
            "",
            (
                "<blockquote>"
                f"Статус: <b>{format_support_ticket_status(ticket.status)}</b>\n"
                f"Пользователь: <b>{html.escape(ticket.display_name)}</b>\n"
                f"Telegram ID: <code>{ticket.telegram_id}</code>\n"
                f"Username: <b>{username_text}</b>\n"
                f"Создан: <b>{format_datetime(ticket.created_at)}</b>\n"
                f"Обновлён: <b>{format_datetime(ticket.updated_at)}</b>"
                "</blockquote>"
            ),
        ]

        if ticket.closed_at is not None:
            lines.extend(
                [
                    "",
                    (
                        "<blockquote>"
                        f"Закрыт: <b>{format_datetime(ticket.closed_at)}</b>\n"
                        f"Закрыл Telegram ID: <code>{ticket.closed_by_telegram_id or '—'}</code>"
                        "</blockquote>"
                    ),
                ]
            )

        lines.extend(
            [
                "",
                "<b>Сообщение</b>",
                f"<blockquote>{html.escape(ticket.user_message)}</blockquote>",
            ]
        )

        if ticket.support_note:
            lines.extend(
                [
                    "",
                    "<b>История текста</b>",
                    f"<blockquote>{html.escape(ticket.support_note)}</blockquote>",
                ]
            )

        return "\n".join(lines)

    def build_admin_ticket_history_keyboard(*, ticket_id: int, page: int):
        builder = InlineKeyboardBuilder()
        builder.row(
            InlineKeyboardButton(
                text="↻ Обновить",
                callback_data=admin_ticket_view_callback(
                    ticket_id=ticket_id,
                    page=page,
                ),
            ),
            InlineKeyboardButton(
                text="⬅️ К списку",
                callback_data=admin_tickets_page_callback(page),
            ),
        )
        builder.row(
            InlineKeyboardButton(
                text="🛠 Админ-панель",
                callback_data=f"panel:{PANEL_SECTION_ADMIN}",
            )
        )
        return builder.as_markup()

    def render_admin_user_profile_text(*, profile: AdminTelegramUserProfile) -> str:
        user = profile.user
        display_name = " ".join(
            part for part in [user.first_name, user.last_name] if part
        ).strip() or user.username or str(user.telegram_id)
        username_text = f"@{user.username}" if user.username else "—"
        referral_recent_names = (
            ", ".join(profile.recent_invited_names)
            if profile.recent_invited_names
            else "—"
        )

        lines = [
            f"<b>👤 {html.escape(display_name)}</b>",
            "",
            "<b>Telegram-профиль</b>",
            (
                "<blockquote>"
                f"ID: <code>{user.telegram_id}</code>\n"
                f"Username: <b>{html.escape(username_text)}</b>\n"
                f"Имя: <b>{html.escape(user.first_name or '—')}</b>\n"
                f"Фамилия: <b>{html.escape(user.last_name or '—')}</b>\n"
                f"Язык: <b>{html.escape(user.language_code or '—')}</b>\n"
                f"Админ: <b>{'да' if user.is_admin else 'нет'}</b>\n"
                f"Заблокирован: <b>{'да' if user.is_blocked else 'нет'}</b>\n"
                f"Создан: <b>{format_datetime(user.created_at)}</b>\n"
                f"Последняя активность: <b>{format_datetime(user.last_seen_at)}</b>\n"
                f"Обновлён: <b>{format_datetime(user.updated_at)}</b>"
                "</blockquote>"
            ),
            "",
            "<b>Реферальная система</b>",
            (
                "<blockquote>"
                f"Свой код: <code>{html.escape(profile.referral_code or '—')}</code>\n"
                f"Применённый код: <code>{html.escape(profile.applied_referral_code or '—')}</code>\n"
                f"Приглашено пользователей: <b>{profile.invited_count}</b>\n"
                f"Последние приглашённые: <b>{html.escape(referral_recent_names)}</b>"
                "</blockquote>"
            ),
            "",
            f"<b>Доступы Remnawave: {len(profile.accesses)}</b>",
        ]

        if not profile.accesses:
            lines.append("<blockquote>У пользователя пока нет сохранённых доступов.</blockquote>")
            return "\n".join(lines)

        for index, access in enumerate(profile.accesses, start=1):
            squads = ", ".join(sorted(access.internal_squads)) if access.internal_squads else "—"
            lines.extend(
                [
                    "",
                    f"{index}. <b>{html.escape(access.record.username)}</b> <code>{html.escape(access.record.status)}</code>",
                    (
                        "<blockquote>"
                        f"UUID: <code>{html.escape(access.record.rw_uuid)}</code>\n"
                        f"Short UUID: <code>{html.escape(access.record.short_uuid)}</code>\n"
                        f"Истекает: <b>{format_datetime_with_days_left(access.record.expire_at)}</b>\n"
                        f"Трафик: <b>{format_bytes(access.record.used_traffic_bytes)}</b> / "
                        f"<b>{format_traffic_limit(access.record.traffic_limit_bytes)}</b>\n"
                        f"Группы: <b>{html.escape(squads)}</b>\n"
                        f"Email: <b>{html.escape(access.record.email or '—')}</b>\n"
                        f"Tag: <b>{html.escape(access.record.tag or '—')}</b>\n"
                        f"Последнее открытие: <b>{format_datetime(access.record.sub_last_opened_at)}</b>\n"
                        f"Синхронизирован: <b>{format_datetime(access.record.synced_at)}</b>\n"
                        f"Подписка: {html.escape(access.record.subscription_url)}"
                        "</blockquote>"
                    ),
                ]
            )

        return "\n".join(lines)

    def build_admin_user_profile_keyboard(*, profile: AdminTelegramUserProfile, telegram_id: int, page: int):
        builder = InlineKeyboardBuilder()

        is_blocked = profile.user.is_blocked if profile else False
        if is_blocked:
            builder.row(
                InlineKeyboardButton(
                    text="\U0001f513 Разблокировать",
                    callback_data=f"admin:unblock:{telegram_id}:{page}",
                )
            )
        else:
            builder.row(
                InlineKeyboardButton(
                    text="\U0001f512 Заблокировать",
                    callback_data=f"admin:block:{telegram_id}:{page}",
                )
            )

        for access in profile.accesses:
            short = access.record.short_uuid
            status_upper = access.record.status.upper()
            if status_upper == "DISABLED":
                builder.row(
                    InlineKeyboardButton(
                        text=f"\u2705 \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {compact_inline_label(access.record.username, limit=16)}",
                        callback_data=f"admin:en_acc:{short}:{telegram_id}:{page}",
                    )
                )
            else:
                builder.row(
                    InlineKeyboardButton(
                        text=f"\u26d4 \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c {compact_inline_label(access.record.username, limit=16)}",
                        callback_data=f"admin:dis_acc:{short}:{telegram_id}:{page}",
                    )
                )
            builder.row(
                InlineKeyboardButton(
                    text=f"\u23f1 \u0412\u0440\u0435\u043c\u044f {compact_inline_label(access.record.username, limit=16)}",
                    callback_data=f"admin:time_menu:{short}:{telegram_id}:{page}",
                )
            )

        builder.row(
            InlineKeyboardButton(
                text="\u21bb \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c",
                callback_data=admin_user_profile_callback(
                    telegram_id=telegram_id,
                    page=page,
                ),
            ),
            InlineKeyboardButton(
                text="\u2b05\ufe0f \u041a \u0441\u043f\u0438\u0441\u043a\u0443",
                callback_data=admin_users_page_callback(page),
            ),
        )
        builder.row(
            InlineKeyboardButton(
                text="\U0001f6e0 \u0410\u0434\u043c\u0438\u043d-\u043f\u0430\u043d\u0435\u043b\u044c",
                callback_data=f"panel:{PANEL_SECTION_ADMIN}",
            ),
        )
        return builder.as_markup()

    def build_time_adjust_keyboard(*, short_uuid: str, telegram_id: int, page: int):
        builder = InlineKeyboardBuilder()
        for days in [1, 5, 10, 15, 30]:
            builder.button(
                text=f"+{days} \u0434\u043d.",
                callback_data=f"admin:at:+{days}:{short_uuid}:{telegram_id}:{page}",
            )
        builder.adjust(5)
        minus_row: list[InlineKeyboardButton] = []
        for days in [1, 5, 10, 15, 30]:
            minus_row.append(
                InlineKeyboardButton(
                    text=f"-{days} \u0434\u043d.",
                    callback_data=f"admin:at:-{days}:{short_uuid}:{telegram_id}:{page}",
                )
            )
        builder.row(*minus_row)
        builder.row(
            InlineKeyboardButton(
                text="\u2b05\ufe0f \u041d\u0430\u0437\u0430\u0434",
                callback_data=admin_user_profile_callback(
                    telegram_id=telegram_id,
                    page=page,
                ),
            )
        )
        return builder.as_markup()

    def _extract_product_code(plan_code: str) -> str:
        """Extract product type from plan code, e.g. 'vpn_white_30' → 'vpn_white'."""
        parts = plan_code.rsplit("_", 1)
        if len(parts) == 2 and parts[1].isdigit():
            return parts[0]
        return plan_code

    def _get_product_groups(
        plans: list[PaymentPlan],
    ) -> dict[str, list[PaymentPlan]]:
        """Group plans by product code, preserving order."""
        groups: dict[str, list[PaymentPlan]] = {}
        for plan in plans:
            product_code = _extract_product_code(plan.code)
            groups.setdefault(product_code, []).append(plan)
        return groups

    def render_payment_menu_text(
        *,
        plans: list[PaymentPlan],
        referral_code: str,
        referral_enabled: bool,
        referral_discount_percent: int,
        has_existing_access: bool,
    ) -> str:
        lines = [
            "<b>💳 Оплата подписки</b>",
            "",
        ]

        if has_existing_access:
            lines.append(
                "<blockquote>У вас уже есть доступ. После подтверждения оплаты бот автоматически продлит текущую подписку на выбранный срок.</blockquote>"
            )
            lines.append("")

        if referral_enabled:
            lines.append(
                "<blockquote>Если у вас есть реферальный код или промокод, укажите его до оплаты.</blockquote>"
            )
        else:
            lines.append(
                "<blockquote>Выберите тип подписки.</blockquote>"
            )

        if referral_code:
            lines.extend(
                [
                    "",
                    f"🎟 Активный код: <code>{html.escape(referral_code)}</code>",
                    f"Скидка: <b>{max(referral_discount_percent, 0)}%</b>",
                ]
            )

        lines.extend(["", "<b>Выберите тип подписки</b>"])

        product_groups = _get_product_groups(plans)
        for product_code, group_plans in product_groups.items():
            if group_plans:
                label = group_plans[0].label
                # Show price range
                prices = [p.amount_rub for p in group_plans]
                if len(prices) == 1:
                    price_text = f"от <b>{min(prices)} ₽</b>"
                else:
                    price_text = f"от <b>{min(prices)} ₽</b>"
                lines.append(f"• {html.escape(label)} — {price_text}")

        return "\n".join(lines)

    def build_payment_menu_keyboard(
        *,
        plans: list[PaymentPlan],
        referral_code: str,
        referral_enabled: bool,
    ):
        builder = InlineKeyboardBuilder()
        if referral_enabled:
            builder.button(
                text="🎟 Код скидки" if not referral_code else "🎟 Изменить код",
                callback_data="referral:apply",
            )

        product_groups = _get_product_groups(plans)
        for product_code, group_plans in product_groups.items():
            if not group_plans:
                continue
            label = group_plans[0].label
            if len(group_plans) == 1:
                # Single duration — go directly to payment:select
                plan = group_plans[0]
                builder.button(
                    text=f"{label} · {plan.duration_days} дней · {plan.amount_rub} ₽",
                    callback_data=payment_callback("select", plan.code),
                )
            else:
                # Multiple durations — open product submenu
                builder.button(
                    text=label,
                    callback_data=payment_callback("product", product_code),
                )

        builder.button(
            text="🏠 Главное меню",
            callback_data=f"panel:{PANEL_SECTION_HOME}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def render_product_duration_text(
        *,
        product_label: str,
        plans: list[PaymentPlan],
        referral_code: str,
        referral_discount_percent: int,
    ) -> str:
        lines = [
            f"<b>💳 {html.escape(product_label)}</b>",
            "",
            "<blockquote>Выберите срок подписки. После выбора бот покажет реквизиты и попросит отправить чек.</blockquote>",
        ]

        if referral_code:
            lines.extend(
                [
                    "",
                    f"🎟 Код: <code>{html.escape(referral_code)}</code> · скидка <b>{max(referral_discount_percent, 0)}%</b>",
                ]
            )

        lines.extend(["", "<b>Выберите срок</b>"])
        for plan in plans:
            if plan.discount_percent > 0 and plan.original_amount_rub != plan.amount_rub:
                price_text = (
                    f"<b>{plan.amount_rub} ₽</b> "
                    f"<s>{plan.original_amount_rub} ₽</s>"
                )
            else:
                price_text = f"<b>{plan.amount_rub} ₽</b>"
            lines.append(f"• {plan.duration_days} дней — {price_text}")

        return "\n".join(lines)

    def build_product_duration_keyboard(
        *,
        plans: list[PaymentPlan],
    ):
        builder = InlineKeyboardBuilder()
        for plan in plans:
            builder.button(
                text=f"{plan.duration_days} дней · {plan.amount_rub} ₽",
                callback_data=payment_callback("select", plan.code),
            )
        builder.button(
            text="⬅️ Назад",
            callback_data=payment_callback("menu"),
        )
        builder.button(
            text="🏠 Главное меню",
            callback_data=f"panel:{PANEL_SECTION_HOME}",
        )
        builder.adjust(1)
        return builder.as_markup()

    def build_home_keyboard():
        builder = InlineKeyboardBuilder()
        builder.button(text="🏠 Главное меню", callback_data=f"panel:{PANEL_SECTION_HOME}")
        builder.adjust(1)
        return builder.as_markup()

    def support_ticket_callback(action: str, ticket_id: int) -> str:
        return f"ticket_admin:{action}:{ticket_id}"

    def support_ticket_user_callback(action: str, ticket_id: int) -> str:
        return f"ticket_user:{action}:{ticket_id}"

    def format_support_ticket_status(status: str) -> str:
        mapping = {
            OPEN_SUPPORT_TICKET_STATUS: "🟡 Открыт",
            ANSWERED_SUPPORT_TICKET_STATUS: "🔵 Есть ответ",
            CLOSED_SUPPORT_TICKET_STATUS: "⚫ Закрыт",
        }
        return mapping.get(status.upper(), f"⚪ {html.escape(status)}")

    def build_support_ticket_keyboard(*, ticket):
        builder = InlineKeyboardBuilder()
        if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
            builder.button(
                text="🔒 Тикет закрыт",
                callback_data=support_ticket_callback("closed", ticket.id),
            )
            builder.adjust(1)
            return builder.as_markup()

        builder.button(
            text="📝 Добавить текст",
            callback_data=support_ticket_callback("note", ticket.id),
        )
        builder.button(
            text="✅ Закрыть тикет",
            callback_data=support_ticket_callback("close", ticket.id),
        )
        builder.adjust(1, 1)
        return builder.as_markup()

    def build_user_support_ticket_keyboard(*, ticket):
        builder = InlineKeyboardBuilder()
        if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
            builder.button(
                text="🔒 Тикет закрыт",
                callback_data=support_ticket_user_callback("closed", ticket.id),
            )
            builder.button(text="🏠 Главное меню", callback_data=f"panel:{PANEL_SECTION_HOME}")
            builder.adjust(1, 1)
            return builder.as_markup()

        builder.button(
            text="📝 Добавить текст",
            callback_data=support_ticket_user_callback("note", ticket.id),
        )
        builder.button(
            text="✅ Закрыть тикет",
            callback_data=support_ticket_user_callback("close", ticket.id),
        )
        builder.button(text="🏠 Главное меню", callback_data=f"panel:{PANEL_SECTION_HOME}")
        builder.adjust(1, 1, 1)
        return builder.as_markup()

    def render_support_ticket_text(*, ticket) -> str:
        username_text = f"@{html.escape(ticket.username)}" if ticket.username else "не указан"
        lines = [
            f"<b>🎫 ТИКЕТ #{html.escape(ticket.public_id)}</b>",
            "",
            f"<b>Статус</b>: {format_support_ticket_status(ticket.status)}",
            f"<b>ID</b>: <code>{html.escape(ticket.public_id)}</code>",
            f"<b>Пользователь</b>: {html.escape(ticket.display_name)}",
            f"<b>Telegram ID</b>: <code>{ticket.telegram_id}</code>",
            f"<b>Username</b>: {username_text}",
            f"<b>Создан</b>: {format_datetime(ticket.created_at)}",
        ]
        if ticket.closed_at is not None:
            lines.append(f"<b>Закрыт</b>: {format_datetime(ticket.closed_at)}")
        lines.extend(
            [
                "",
                "<b>Сообщение</b>",
                f"<blockquote>{html.escape(ticket.user_message)}</blockquote>",
            ]
        )
        if ticket.support_note:
            lines.extend(
                [
                    "",
                    "<b>Текст к тикету</b>",
                    f"<blockquote>{html.escape(ticket.support_note)}</blockquote>",
                ]
            )
        if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
            lines.extend(
                [
                    "",
                    "<i>Тикет закрыт. Если вопрос снова станет актуален, пользователь может создать новый.</i>",
                ]
            )
        else:
            lines.extend(
                [
                    "",
                    "<i>Ответьте реплаем на это сообщение, чтобы бот доставил ответ пользователю. Кнопками ниже можно добавить текст к тикету или закрыть его.</i>",
                ]
            )
        return "\n".join(lines)

    async def refresh_support_ticket_message(bot: Bot, *, ticket) -> None:
        if ticket.support_message_id is None:
            return

        try:
            await bot.edit_message_text(
                chat_id=ticket.support_chat_id,
                message_id=ticket.support_message_id,
                text=render_support_ticket_text(ticket=ticket),
                reply_markup=build_support_ticket_keyboard(ticket=ticket),
            )
        except TelegramBadRequest as exc:
            if "message is not modified" not in str(exc).lower():
                raise

    def render_user_ticket_created_text(*, ticket) -> str:
        return "\n".join(
            [
                f"🎫 Тикет <b>#{html.escape(ticket.public_id)}</b> отправлен в поддержку.",
                f"Статус: <b>{format_support_ticket_status(ticket.status)}</b>",
                "Ответ придёт сюда, как только его обработают.",
                "",
                "Если хотите уточнить вопрос, добавьте текст или закройте тикет кнопками ниже.",
            ]
        )

    def render_user_ticket_note_added_text(*, ticket) -> str:
        return "\n".join(
            [
                f"📝 Текст добавлен к тикету <b>#{html.escape(ticket.public_id)}</b>.",
                f"Статус: <b>{format_support_ticket_status(ticket.status)}</b>",
                "Мы передали уточнение в поддержку.",
            ]
        )

    def render_support_ticket_note_added_notice(*, ticket, author_name: str, note_text: str) -> str:
        return "\n".join(
            [
                f"<b>📨 Обновление тикета #{html.escape(ticket.public_id)}</b>",
                f"Пользователь <b>{html.escape(author_name)}</b> добавил новый текст:",
                "",
                f"<blockquote>{html.escape(note_text)}</blockquote>",
            ]
        )

    def render_user_ticket_support_note_text(*, ticket, note_text: str) -> str:
        return "\n".join(
            [
                f"<b>📨 Обновление по тикету #{html.escape(ticket.public_id)}</b>",
                "Поддержка добавила к тикету новую информацию:",
                "",
                f"<blockquote>{html.escape(note_text)}</blockquote>",
                "",
                "<i>Если нужно, вы можете дописать тикет кнопкой ниже или закрыть его.</i>",
            ]
        )

    def render_user_ticket_closed_text(*, ticket, closed_by_support: bool) -> str:
        reason_text = (
            "Если вопрос ещё актуален, создайте новый тикет через панель бота."
            if closed_by_support
            else "Если понадобится помощь снова, создайте новый тикет через панель бота."
        )
        return "\n".join(
            [
                f"🎫 Тикет <b>#{html.escape(ticket.public_id)}</b> закрыт.",
                f"Статус: <b>{format_support_ticket_status(ticket.status)}</b>",
                reason_text,
            ]
        )

    def build_payment_receipt_keyboard(*, order_uuid: str):
        builder = InlineKeyboardBuilder()
        builder.button(
            text="📎 Отправить чек",
            callback_data=payment_callback("receipt", order_uuid),
        )
        builder.button(text="⬅️ К тарифам", callback_data=payment_callback("menu"))
        builder.button(text="🏠 Главное меню", callback_data=f"panel:{PANEL_SECTION_HOME}")
        builder.adjust(1)
        return builder.as_markup()

    def build_issued_access_keyboard(*, subscription_url: str):
        builder = InlineKeyboardBuilder()
        if subscription_url.startswith(("http://", "https://")):
            builder.button(text="🔗 Открыть подписку", url=subscription_url)
        builder.button(text="🏠 Главное меню", callback_data=f"panel:{PANEL_SECTION_HOME}")
        builder.adjust(1)
        return builder.as_markup()

    async def send_issued_access_message(
        bot: Bot,
        *,
        chat_id: int,
        subscription_url: str,
        expire_at,
        traffic_limit_bytes: int,
        is_renewal: bool = False,
    ) -> None:
        config = await get_runtime_config()
        caption = format_issued_access(
            subscription_url=subscription_url,
            expire_at=expire_at,
            traffic_limit_bytes=traffic_limit_bytes,
            brand_name=config.bot_brand_name,
            success_text=config.payment_success_text,
            is_renewal=is_renewal,
        )
        reply_markup = build_issued_access_keyboard(subscription_url=subscription_url)

        start_image_path = get_start_image_path(config)
        if start_image_path is not None:
            await bot.send_photo(
                chat_id=chat_id,
                photo=FSInputFile(str(start_image_path)),
                caption=caption,
                reply_markup=reply_markup,
            )
            return

        await bot.send_message(
            chat_id=chat_id,
            text=caption,
            reply_markup=reply_markup,
        )

    async def notify_referral_bonus(
        bot: Bot,
        *,
        inviter_telegram_id: int,
        bonus_days: int,
        status: str,
        expire_at=None,
        total_applied_days: int = 0,
    ) -> None:
        lines = [
            "<b>🎁 Реферальный бонус начислен</b>",
            "",
            f"По вашему коду прошла подтверждённая оплата.",
            f"Новый бонус: <b>+{bonus_days} дней</b>",
        ]
        if status == "APPLIED" and expire_at is not None:
            if total_applied_days > bonus_days:
                lines.append(f"Всего применено накопленных бонусов: <b>+{total_applied_days} дней</b>")
            lines.extend(
                [
                    "",
                    f"Ваш доступ продлён до: <b>{format_datetime(expire_at)}</b>",
                ]
            )
        else:
            lines.extend(
                [
                    "",
                    "Бонус сохранён и применится автоматически, когда у вас появится активный доступ.",
                ]
            )

        await send_branded(
            bot,
            chat_id=inviter_telegram_id,
            text="\n".join(lines),
        )

    def render_payment_order_text(*, order: CreatedPaymentOrder) -> str:
        return payment_service.build_transfer_text(order=order)

    def build_review_keyboard(*, order_uuid: str):
        builder = InlineKeyboardBuilder()
        builder.button(
            text="✅ Подтвердить",
            callback_data=payment_callback("approve", order_uuid),
        )
        builder.button(
            text="❌ Отклонить",
            callback_data=payment_callback("reject", order_uuid),
        )
        builder.adjust(2)
        return builder.as_markup()

    def render_review_text(
        *,
        order: StoredPaymentOrder,
        user_message: Message,
        referral_code: str,
    ) -> str:
        username_text = (
            f"@{html.escape(user_message.from_user.username)}"
            if user_message.from_user and user_message.from_user.username
            else "не указан"
        )
        display_name = " ".join(
            part
            for part in [
                user_message.from_user.first_name if user_message.from_user else None,
                user_message.from_user.last_name if user_message.from_user else None,
            ]
            if part
        ).strip() or (user_message.from_user.full_name if user_message.from_user else "Пользователь")
        receipt_note = (user_message.caption or user_message.text or "").strip()

        lines = [
            f"<b>💸 НОВАЯ ОПЛАТА #{order.order_uuid[:8].upper()}</b>",
            "",
            f"<b>Тариф</b>: {html.escape(order.plan_title)}",
            f"<b>Сумма</b>: <b>{order.amount_rub} ₽</b>",
            f"<b>Пользователь</b>: {html.escape(display_name)}",
            f"<b>Telegram ID</b>: <code>{order.telegram_id}</code>",
            f"<b>Username</b>: {username_text}",
            f"<b>Логин Remnawave</b>: <code>{html.escape(order.provision_username)}</code>",
        ]
        if referral_code:
            lines.append(f"<b>Код скидки</b>: <code>{html.escape(referral_code)}</code>")
        if receipt_note:
            lines.extend(
                [
                    "",
                    "<b>Комментарий</b>",
                    f"<blockquote>{html.escape(receipt_note)}</blockquote>",
                ]
            )
        lines.extend(
            [
                "",
                "<i>Проверьте чек и выберите действие кнопками ниже.</i>",
            ]
        )
        return "\n".join(lines)

    async def show_payment_menu_message(
        message: Message,
        *,
        telegram_id: int,
    ) -> None:
        config = await get_runtime_config()
        plans = [
            plan
            for plan in await payment_service.get_available_plans(telegram_id=telegram_id)
            if plan.is_ready
        ]
        has_existing_access = bool(await sync_service.get_cached_users_for_telegram(telegram_id))
        active_discount = await payment_service.get_active_discount_for_user(
            telegram_id=telegram_id,
        )
        await answer_branded(
            message,
            render_payment_menu_text(
                plans=plans,
                referral_code=active_discount.code if config.referral_enabled else "",
                referral_enabled=config.referral_enabled,
                referral_discount_percent=active_discount.discount_percent
                or config.referral_discount_percent,
                has_existing_access=has_existing_access,
            ),
            reply_markup=build_payment_menu_keyboard(
                plans=plans,
                referral_code=active_discount.code if config.referral_enabled else "",
                referral_enabled=config.referral_enabled,
            ),
        )

    async def show_payment_menu_callback(callback: CallbackQuery) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        config = await get_runtime_config()
        plans = [
            plan
            for plan in await payment_service.get_available_plans(
                telegram_id=callback.from_user.id
            )
            if plan.is_ready
        ]
        has_existing_access = bool(
            await sync_service.get_cached_users_for_telegram(callback.from_user.id)
        )
        active_discount = await payment_service.get_active_discount_for_user(
            telegram_id=callback.from_user.id,
        )
        await edit_message_content(
            callback,
            text=render_payment_menu_text(
                plans=plans,
                referral_code=active_discount.code if config.referral_enabled else "",
                referral_enabled=config.referral_enabled,
                referral_discount_percent=active_discount.discount_percent
                or config.referral_discount_percent,
                has_existing_access=has_existing_access,
            ),
            reply_markup=build_payment_menu_keyboard(
                plans=plans,
                referral_code=active_discount.code if config.referral_enabled else "",
                referral_enabled=config.referral_enabled,
            ),
        )
        await callback.answer()

    async def send_ticket_to_support(message: Message, *, state: FSMContext) -> None:
        if message.from_user is None:
            await state.clear()
            return

        config = await get_runtime_config()
        support_chat_id = config.support_ticket_chat_id
        if support_chat_id is None:
            await message.answer(
                "Канал для тикетов пока не настроен. Проверьте параметры "
                "<code>BOT_SUPPORT_TICKET_LINK</code> или "
                "<code>BOT_SUPPORT_TICKET_CHAT_ID</code>."
            )
            await state.clear()
            return

        raw_message_text = (message.text or message.caption or "").strip()
        if not raw_message_text:
            await message.answer(
                "Для тикета нужен текст вопроса или проблемы.\n"
                "Отправьте одно текстовое сообщение или подпись к файлу."
            )
            return

        ticket_id = secrets.token_hex(4).upper()
        username_text = (
            f"@{html.escape(message.from_user.username)}"
            if message.from_user.username
            else "не указан"
        )
        display_name = " ".join(
            part
            for part in [message.from_user.first_name, message.from_user.last_name]
            if part
        ).strip() or message.from_user.full_name

        ticket_text = "\n".join(
            [
                f"<b>🎫 НОВЫЙ ТИКЕТ #{ticket_id}</b>",
                "",
                f"<b>Пользователь</b>: {html.escape(display_name)}",
                f"<b>Telegram ID</b>: <code>{message.from_user.id}</code>",
                f"<b>Username</b>: {username_text}",
                "",
                f"<blockquote>{html.escape(raw_message_text)}</blockquote>",
            ]
        )

        support_message = await message.bot.send_message(
            chat_id=support_chat_id,
            message_thread_id=config.support_ticket_message_thread_id,
            text=ticket_text,
        )

        ticket = await support_ticket_service.create_ticket(
            public_id=ticket_id,
            telegram_id=message.from_user.id,
            username=message.from_user.username,
            display_name=display_name,
            user_message=raw_message_text,
            support_chat_id=support_chat_id,
            support_thread_id=config.support_ticket_message_thread_id,
            support_message_id=support_message.message_id,
        )
        await refresh_support_ticket_message(message.bot, ticket=ticket)

        await state.clear()
        await answer_branded(
            message,
            render_user_ticket_created_text(ticket=ticket),
            reply_markup=build_user_support_ticket_keyboard(ticket=ticket),
        )

    async def send_receipt_for_review(
        message: Message,
        *,
        order: StoredPaymentOrder,
        referral_code: str,
    ) -> None:
        config = await get_runtime_config()
        review_chat_id = config.payment_review_chat_id
        if review_chat_id is None:
            raise ValueError(
                "Канал проверки оплат не настроен. Проверьте PAYMENT_REVIEW_LINK или PAYMENT_REVIEW_CHAT_ID."
            )

        review_text = render_review_text(
            order=order,
            user_message=message,
            referral_code=referral_code,
        )
        reply_markup = build_review_keyboard(order_uuid=order.order_uuid)

        if message.photo:
            await message.bot.send_photo(
                chat_id=review_chat_id,
                message_thread_id=config.payment_review_message_thread_id,
                photo=message.photo[-1].file_id,
                caption=review_text,
                reply_markup=reply_markup,
            )
            return

        if message.document:
            await message.bot.send_document(
                chat_id=review_chat_id,
                message_thread_id=config.payment_review_message_thread_id,
                document=message.document.file_id,
                caption=review_text,
                reply_markup=reply_markup,
            )
            return

        receipt_note = (message.text or message.caption or "").strip()
        if not receipt_note:
            raise ValueError("Отправьте фото, документ или текст с подтверждением оплаты.")

        await message.bot.send_message(
            chat_id=review_chat_id,
            message_thread_id=config.payment_review_message_thread_id,
            text=review_text,
            reply_markup=reply_markup,
        )

    async def append_review_status(callback: CallbackQuery, *, status_text: str) -> None:
        if callback.message is None:
            return

        source_text = callback.message.caption or callback.message.text or ""
        status_suffix = f"\n\n<b>Статус</b>: {status_text}"
        is_caption = bool(callback.message.photo or callback.message.document)
        max_length = 1024 if is_caption else 4096

        updated_text = f"{source_text}{status_suffix}"
        if len(updated_text) > max_length:
            # Truncate the original text so the status fits within the limit
            available = max_length - len(status_suffix) - 3  # 3 for "..."
            updated_text = f"{source_text[:max(0, available)]}...{status_suffix}"

        try:
            if is_caption:
                await callback.message.edit_caption(caption=updated_text, reply_markup=None)
            else:
                await callback.message.edit_text(updated_text, reply_markup=None)
        except TelegramBadRequest:
            # Fallback: just remove the keyboard without appending status
            try:
                if is_caption:
                    await callback.message.edit_reply_markup(reply_markup=None)
                else:
                    await callback.message.edit_reply_markup(reply_markup=None)
            except TelegramBadRequest:
                pass

    @router.message(CommandStart())
    @router.message(Command("menu"))
    async def cmd_start(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            return

        await clear_interactive_states(state)
        incoming_referral_code = parse_start_referral_code(message)

        await sync_service.register_telegram_user(
            telegram_id=message.from_user.id,
            username=message.from_user.username,
            first_name=message.from_user.first_name,
            last_name=message.from_user.last_name,
            language_code=message.from_user.language_code,
        )

        context = await load_panel_context(
            telegram_id=message.from_user.id,
            username=message.from_user.username,
            first_name=message.from_user.first_name,
            last_name=message.from_user.last_name,
            language_code=message.from_user.language_code,
            sync_remote=True,
        )
        config = await get_runtime_config()
        if incoming_referral_code and config.referral_enabled:
            await message.answer(
                "👥 Вас пригласили по реферальной ссылке.\n"
                f"Чтобы получить скидку <b>{max(config.referral_discount_percent, 0)}%</b>, перед оплатой введите код "
                f"<code>{incoming_referral_code}</code> в разделе "
                "<b>Реферальная система</b>."
            )
        await show_panel(message, context=context, section=PANEL_SECTION_HOME)

    @router.message(Command("buy"))
    async def cmd_buy(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            return

        await clear_interactive_states(state)
        await show_payment_menu_message(message, telegram_id=message.from_user.id)

    @router.message(Command("paysupport"))
    async def cmd_paysupport(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            return

        await clear_interactive_states(state)
        config = await get_runtime_config()
        support_text = config.payment_support_text.strip() or "По вопросам оплаты напишите в поддержку."
        await message.answer(html.escape(support_text))

    @router.message(Command("broadcast"))
    async def cmd_broadcast(message: Message, command: CommandObject, state: FSMContext) -> None:
        if not await ensure_sender(message):
            return
        if not settings.is_admin(message.from_user.id):
            await message.answer("Эта команда доступна только администраторам.")
            return

        await state.clear()
        text = (command.args or "").strip()
        if not text:
            await state.set_state(AdminBroadcastStates.waiting_for_text)
            await state.update_data(broadcast_mode="notice")
            await message.answer(
                "📣 Отправьте текст рассылки следующим сообщением.\n\n"
                "Чтобы отменить действие, отправьте <code>/cancel</code>."
            )
            return

        text_html = html.escape(text)
        if len(text_html) > 4000:
            await message.answer("Текст слишком длинный. Максимум — около 4000 символов.")
            return

        await state.set_state(AdminBroadcastStates.waiting_for_text)
        await show_admin_broadcast_preview(
            message,
            body_html=text_html,
            mode="notice",
            state=state,
        )

    @router.message(Command("poll"))
    async def cmd_poll(message: Message, command: CommandObject, state: FSMContext) -> None:
        if not await ensure_sender(message):
            return
        if not settings.is_admin(message.from_user.id):
            await message.answer("Эта команда доступна только администраторам.")
            return

        await state.clear()
        text = (command.args or "").strip()
        if not text:
            await state.set_state(AdminBroadcastStates.waiting_for_text)
            await state.update_data(broadcast_mode="poll")
            await message.answer(
                "✅ Отправьте текст вопроса для опроса Да/Нет следующим сообщением.\n\n"
                "Чтобы отменить действие, отправьте <code>/cancel</code>."
            )
            return

        text_html = html.escape(text)
        if len(text_html) > 3800:
            await message.answer("Текст слишком длинный. Максимум — около 3800 символов.")
            return

        await state.set_state(AdminBroadcastStates.waiting_for_text)
        await show_admin_broadcast_preview(
            message,
            body_html=text_html,
            mode="poll",
            state=state,
        )

    @router.callback_query(F.data == "guides:menu")
    async def on_guides_menu(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        await clear_interactive_states(state)
        if not await user_can_open_guides(callback.from_user.id):
            await callback.answer("Гайды доступны после оплаты подписки.", show_alert=True)
            return

        guides = await sync_service.get_guides(active_only=True)
        await show_branded_view(
            callback,
            text=render_guides_menu_text(guides=guides),
            reply_markup=build_guides_menu_keyboard(guides=guides),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("guides:view:"))
    async def on_guide_view(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        await clear_interactive_states(state)
        if not await user_can_open_guides(callback.from_user.id):
            await callback.answer("Гайды доступны после оплаты подписки.", show_alert=True)
            return

        raw_guide_id = (callback.data or "").split(":", maxsplit=2)[2]
        try:
            guide_id = int(raw_guide_id)
        except ValueError:
            await callback.answer("Гайд не найден", show_alert=True)
            return

        guide = await sync_service.get_guide(guide_id, active_only=True)
        if guide is None:
            await callback.answer("Гайд не найден", show_alert=True)
            return

        try:
            await send_guide_view(
                callback.bot,
                chat_id=callback.from_user.id,
                guide=guide,
            )
        except TelegramBadRequest:
            await callback.bot.send_message(
                chat_id=callback.from_user.id,
                text=render_guide_text(guide=guide),
                reply_markup=build_guide_view_keyboard(),
            )
        await callback.answer()

    @router.callback_query(F.data == "payment:buy")
    @router.callback_query(F.data == "payment:menu")
    async def on_payment_menu(callback: CallbackQuery, state: FSMContext) -> None:
        await clear_interactive_states(state)
        await show_payment_menu_callback(callback)

    @router.callback_query(F.data.startswith("payment:product:"))
    async def on_payment_product(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        await clear_interactive_states(state)
        product_code = (callback.data or "").split(":", maxsplit=2)[2]

        config = await get_runtime_config()
        all_plans = [
            plan
            for plan in await payment_service.get_available_plans(
                telegram_id=callback.from_user.id
            )
            if plan.is_ready
        ]

        product_plans = [
            p for p in all_plans
            if _extract_product_code(p.code) == product_code
        ]

        if not product_plans:
            await callback.answer("Тариф не найден", show_alert=True)
            return

        active_discount = await payment_service.get_active_discount_for_user(
            telegram_id=callback.from_user.id,
        )

        await edit_message_content(
            callback,
            text=render_product_duration_text(
                product_label=product_plans[0].label,
                plans=product_plans,
                referral_code=active_discount.code if config.referral_enabled else "",
                referral_discount_percent=active_discount.discount_percent
                or config.referral_discount_percent,
            ),
            reply_markup=build_product_duration_keyboard(
                plans=product_plans,
            ),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("payment:select:"))
    async def on_payment_select(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        await clear_interactive_states(state)
        plan_code = (callback.data or "").split(":", maxsplit=2)[2]

        try:
            order = await payment_service.create_order(
                telegram_id=callback.from_user.id,
                username=callback.from_user.username,
                first_name=callback.from_user.first_name,
                last_name=callback.from_user.last_name,
                language_code=callback.from_user.language_code,
                plan_code=plan_code,
            )
        except ValueError as exc:
            await callback.answer(str(exc), show_alert=True)
            return

        await edit_message_content(
            callback,
            text=render_payment_order_text(order=order),
            reply_markup=build_payment_receipt_keyboard(order_uuid=order.order_uuid),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("payment:receipt:"))
    async def on_payment_receipt_start(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return

        order_uuid = (callback.data or "").split(":", maxsplit=2)[2]
        try:
            order = await payment_service.get_order_for_user(
                order_uuid=order_uuid,
                telegram_id=callback.from_user.id,
            )
        except ValueError as exc:
            await callback.answer(str(exc), show_alert=True)
            return

        if order.status == REVIEW_PAYMENT_STATUS:
            await callback.answer("Чек уже отправлен на проверку.", show_alert=True)
            return
        if order.status == FULFILLED_PAYMENT_STATUS:
            await callback.answer("Этот заказ уже подтверждён.", show_alert=True)
            return
        if order.status == REJECTED_PAYMENT_STATUS:
            await callback.answer("Этот заказ уже отклонён. Создайте новый.", show_alert=True)
            return

        await state.set_state(ManualPaymentStates.waiting_for_receipt)
        await state.update_data(order_uuid=order_uuid)
        await callback.message.answer(
            "📎 Отправьте фото, скриншот, документ или текст с подтверждением перевода одним следующим сообщением."
        )
        await callback.answer()

    @router.message(ManualPaymentStates.waiting_for_receipt, Command("cancel"))
    @router.message(SupportTicketStates.waiting_for_message, Command("cancel"))
    @router.message(SupportTicketStates.waiting_for_note, Command("cancel"))
    @router.message(SupportTicketAdminStates.waiting_for_note, Command("cancel"))
    @router.message(ReferralCodeStates.waiting_for_code, Command("cancel"))
    @router.message(AdminBroadcastStates.waiting_for_text, Command("cancel"))
    @router.message(AdminConfigStates.waiting_for_value, Command("cancel"))
    @router.message(AdminPromoStates.waiting_for_code, Command("cancel"))
    @router.message(AdminPromoStates.waiting_for_plan, Command("cancel"))
    @router.message(AdminPromoStates.waiting_for_discount, Command("cancel"))
    @router.message(AdminPromoStates.waiting_for_expires, Command("cancel"))
    @router.message(AdminGuideStates.waiting_for_title, Command("cancel"))
    @router.message(AdminGuideStates.waiting_for_description, Command("cancel"))
    @router.message(AdminGuideStates.waiting_for_body, Command("cancel"))
    @router.message(AdminGuideStates.waiting_for_photos, Command("cancel"))
    async def cancel_interactive_flow(message: Message, state: FSMContext) -> None:
        await state.clear()
        await message.answer("Действие отменено.")

    @router.message(ManualPaymentStates.waiting_for_receipt)
    async def on_payment_receipt_message(message: Message, state: FSMContext) -> None:
        if message.from_user is None:
            await state.clear()
            return

        state_data = await state.get_data()
        order_uuid = str(state_data.get("order_uuid") or "").strip()
        if not order_uuid:
            await state.clear()
            await message.answer("Заказ не найден. Начните оплату заново.")
            return

        try:
            order = await payment_service.get_order_for_user(
                order_uuid=order_uuid,
                telegram_id=message.from_user.id,
            )
        except ValueError as exc:
            await state.clear()
            await message.answer(html.escape(str(exc)))
            return

        if order.status == REVIEW_PAYMENT_STATUS:
            await state.clear()
            await message.answer(
                "Чек уже отправлен на проверку.",
                reply_markup=build_home_keyboard(),
            )
            return
        if order.status == FULFILLED_PAYMENT_STATUS:
            await state.clear()
            await message.answer(
                "Этот заказ уже подтверждён.",
                reply_markup=build_home_keyboard(),
            )
            return
        if order.status == REJECTED_PAYMENT_STATUS:
            await state.clear()
            await message.answer(
                "Этот заказ уже отклонён. Создайте новый платёж.",
                reply_markup=build_home_keyboard(),
            )
            return

        try:
            order = await payment_service.mark_order_under_review(
                order_uuid=order_uuid,
                telegram_id=message.from_user.id,
            )
        except ValueError as exc:
            await message.answer(html.escape(str(exc)))
            return

        try:
            referral_code = await payment_service.get_referral_code_for_order(order_uuid=order_uuid)
            await send_receipt_for_review(
                message,
                order=order,
                referral_code=referral_code,
            )
        except (TelegramBadRequest, TelegramForbiddenError, ValueError) as exc:
            await payment_service.revert_order_to_pending(
                order_uuid=order_uuid,
                telegram_id=message.from_user.id,
            )
            await message.answer(html.escape(str(exc)))
            return

        await state.clear()
        await message.answer(
            "✅ Чек отправлен на проверку.\n"
            "После подтверждения администратором доступ придёт сюда.",
            reply_markup=build_home_keyboard(),
        )

    @router.callback_query(F.data == "referral:apply")
    async def on_referral_apply(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return

        config = await get_runtime_config()
        if not config.referral_enabled:
            await callback.answer("Реферальная система сейчас отключена", show_alert=True)
            return

        await state.set_state(ReferralCodeStates.waiting_for_code)
        await callback.message.answer(
            "🎟 Отправьте реферальный код или промокод одним следующим сообщением.\n\n"
            "Код будет сохранён и применён перед оплатой, если он подходит к выбранному тарифу.\n"
            "Чтобы отменить ввод, отправьте <code>/cancel</code>."
        )
        await callback.answer()

    @router.callback_query(F.data == "ticket:create")
    async def on_ticket_create(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return

        config = await get_runtime_config()
        if config.support_ticket_chat_id is None:
            await callback.answer("Канал для тикетов не настроен", show_alert=True)
            return

        await state.set_state(SupportTicketStates.waiting_for_message)
        await callback.message.answer(
            "🎫 Опишите ваш вопрос или проблему одним следующим сообщением.\n\n"
            "Лучше отправить одно текстовое сообщение, чтобы тикет ушёл в поддержку в чистом виде.\n"
            "Чтобы отменить создание тикета, отправьте <code>/cancel</code>."
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("ticket_user:note:"))
    async def on_user_ticket_note_start(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return

        ticket_id_text = (callback.data or "").split(":")[-1]
        if not ticket_id_text.isdigit():
            await callback.answer("Тикет не найден", show_alert=True)
            return

        ticket = await support_ticket_service.get_ticket(ticket_id=int(ticket_id_text))
        if ticket is None or ticket.telegram_id != callback.from_user.id:
            await callback.answer("Тикет не найден", show_alert=True)
            return
        if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
            await callback.answer("Тикет уже закрыт", show_alert=True)
            return

        await clear_interactive_states(state)
        await state.set_state(SupportTicketStates.waiting_for_note)
        await state.update_data(user_ticket_id=ticket.id)
        await callback.message.answer(
            f"📝 Отправьте текст для тикета <b>#{ticket.public_id}</b> одним следующим сообщением.\n"
            "Он будет добавлен к текущему тикету и виден поддержке.\n"
            "Чтобы отменить действие, отправьте <code>/cancel</code>."
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("ticket_user:close:"))
    async def on_user_ticket_close(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return

        ticket_id_text = (callback.data or "").split(":")[-1]
        if not ticket_id_text.isdigit():
            await callback.answer("Тикет не найден", show_alert=True)
            return

        ticket = await support_ticket_service.get_ticket(ticket_id=int(ticket_id_text))
        if ticket is None or ticket.telegram_id != callback.from_user.id:
            await callback.answer("Тикет не найден", show_alert=True)
            return
        if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
            await callback.answer("Тикет уже закрыт", show_alert=True)
            return

        closed_ticket = await support_ticket_service.close_ticket(
            ticket_id=ticket.id,
            closed_by_telegram_id=callback.from_user.id,
        )
        if closed_ticket is None:
            await callback.answer("Не удалось закрыть тикет", show_alert=True)
            return

        await clear_interactive_states(state)
        await refresh_support_ticket_message(callback.bot, ticket=closed_ticket)
        await send_branded(
            callback.bot,
            chat_id=closed_ticket.telegram_id,
            text=render_user_ticket_closed_text(ticket=closed_ticket, closed_by_support=False),
            reply_markup=build_user_support_ticket_keyboard(ticket=closed_ticket),
        )
        await callback.answer("Тикет закрыт")

    @router.callback_query(F.data.startswith("ticket_user:closed:"))
    async def on_user_ticket_closed(callback: CallbackQuery) -> None:
        await callback.answer("Тикет уже закрыт", show_alert=True)

    @router.callback_query(F.data.startswith("panel:"))
    async def on_panel_callback(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        if await sync_service.is_user_blocked(callback.from_user.id):
            await callback.answer("⛔ Ваш аккаунт заблокирован", show_alert=True)
            return

        await clear_interactive_states(state)

        parts = (callback.data or "").split(":")
        action = parts[1] if len(parts) > 1 else PANEL_SECTION_HOME
        refresh = len(parts) > 2 and parts[2] == "refresh"

        valid_sections = {
            PANEL_SECTION_HOME,
            PANEL_SECTION_ADMIN,
            PANEL_SECTION_SUBSCRIPTION,
            PANEL_SECTION_REFERRAL,
            PANEL_SECTION_SUPPORT,
            PANEL_SECTION_SUPPORT_TICKET,
            PANEL_SECTION_TERMS,
        }
        section = action if action in valid_sections else PANEL_SECTION_HOME
        if section == PANEL_SECTION_ADMIN and not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        config = await get_runtime_config()
        if section == PANEL_SECTION_REFERRAL and not config.referral_enabled:
            await callback.answer("Реферальная система сейчас отключена", show_alert=True)
            section = PANEL_SECTION_HOME

        context = await load_panel_context(
            telegram_id=callback.from_user.id,
            username=callback.from_user.username,
            first_name=callback.from_user.first_name,
            last_name=callback.from_user.last_name,
            language_code=callback.from_user.language_code,
            sync_remote=refresh,
        )
        await update_panel(callback, context=context, section=section)

    @router.message(SupportTicketStates.waiting_for_message)
    async def on_ticket_message(message: Message, state: FSMContext) -> None:
        await send_ticket_to_support(message, state=state)

    @router.message(SupportTicketStates.waiting_for_note)
    async def on_user_ticket_note_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return

        note_text = (message.text or message.caption or "").strip()
        if not note_text:
            await message.answer("Отправьте текст уточнения одним сообщением.")
            return

        state_data = await state.get_data()
        ticket_id = int(state_data.get("user_ticket_id") or 0)
        if ticket_id <= 0:
            await state.clear()
            await message.answer("Тикет не найден. Нажмите кнопку ещё раз.")
            return

        ticket = await support_ticket_service.get_ticket(ticket_id=ticket_id)
        if ticket is None or ticket.telegram_id != message.from_user.id:
            await state.clear()
            await message.answer("Тикет не найден.")
            return
        if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
            await state.clear()
            await message.answer(
                "Тикет уже закрыт.",
                reply_markup=build_user_support_ticket_keyboard(ticket=ticket),
            )
            return

        display_name = " ".join(
            part for part in [message.from_user.first_name, message.from_user.last_name] if part
        ).strip() or message.from_user.full_name
        note_entry = "\n".join(
            [
                f"[{format_datetime(message.date.replace(tzinfo=None))}] {display_name}",
                note_text,
            ]
        )
        updated_ticket = await support_ticket_service.append_note(
            ticket_id=ticket.id,
            note_text=note_entry,
            status=OPEN_SUPPORT_TICKET_STATUS,
        )
        if updated_ticket is None:
            await state.clear()
            await message.answer("Не удалось добавить текст к тикету.")
            return

        await refresh_support_ticket_message(message.bot, ticket=updated_ticket)
        await message.bot.send_message(
            chat_id=updated_ticket.support_chat_id,
            text=render_support_ticket_note_added_notice(
                ticket=updated_ticket,
                author_name=display_name,
                note_text=note_text,
            ),
            message_thread_id=updated_ticket.support_thread_id,
        )
        await state.clear()
        await answer_branded(
            message,
            render_user_ticket_note_added_text(ticket=updated_ticket),
            reply_markup=build_user_support_ticket_keyboard(ticket=updated_ticket),
        )

    @router.message(ReferralCodeStates.waiting_for_code)
    async def on_referral_code_message(message: Message, state: FSMContext) -> None:
        if message.from_user is None:
            await state.clear()
            return

        config = await get_runtime_config()
        if not config.referral_enabled:
            await state.clear()
            await message.answer("Реферальная система сейчас отключена.")
            return

        raw_code = (message.text or "").strip().upper()
        if not raw_code:
            await message.answer("Отправьте реферальный код или промокод текстом.")
            return

        try:
            applied_code = await sync_service.apply_referral_code(
                telegram_id=message.from_user.id,
                username=message.from_user.username,
                first_name=message.from_user.first_name,
                last_name=message.from_user.last_name,
                language_code=message.from_user.language_code,
                referral_code=raw_code,
            )
        except ValueError as exc:
            await message.answer(html.escape(str(exc)))
            return

        await state.clear()
        active_discount = await payment_service.get_active_discount_for_user(
            telegram_id=message.from_user.id,
        )
        discount_percent = active_discount.discount_percent or config.referral_discount_percent
        code_kind_text = "Промокод" if active_discount.source == "promo" else "Реферальный код"
        await message.answer(
            f"✅ {code_kind_text} <code>{applied_code}</code> сохранён.\n"
            f"Скидка <b>{max(discount_percent, 0)}%</b> будет применена перед оплатой."
        )
        await show_payment_menu_message(message, telegram_id=message.from_user.id)

    @router.callback_query(F.data == "admin:lookup")
    async def on_admin_lookup(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await state.set_state(AdminLookupStates.waiting_for_identifier)
        await callback.message.answer(
            "🔍 Отправьте идентификатор пользователя следующим сообщением.\n\n"
            "Поддерживаются: uuid, id, username, short UUID.\n"
            "Чтобы отменить действие, отправьте <code>/cancel</code>."
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:broadcast")
    async def on_admin_broadcast(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await state.clear()
        await callback.message.answer(
            "📣 Выберите тип рассылки.",
            reply_markup=build_admin_broadcast_mode_keyboard(),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:broadcast_mode:"))
    async def on_admin_broadcast_mode(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        mode = (callback.data or "").split(":")[-1]
        if mode not in {"notice", "poll"}:
            await callback.answer("Тип рассылки не найден", show_alert=True)
            return

        await state.clear()
        await state.set_state(AdminBroadcastStates.waiting_for_text)
        await state.update_data(broadcast_mode=mode)
        if mode == "poll":
            prompt = (
                "✅ Отправьте текст вопроса для опроса Да/Нет следующим сообщением.\n\n"
                "Бот покажет предпросмотр и попросит подтвердить отправку.\n"
                "Чтобы отменить действие, отправьте <code>/cancel</code>."
            )
        else:
            prompt = (
                "📣 Отправьте текст уведомления следующим сообщением.\n\n"
                "Бот красиво оформит текст, покажет предпросмотр и попросит подтвердить отправку.\n"
                "Чтобы отменить действие, отправьте <code>/cancel</code>."
            )

        await callback.message.answer(prompt)
        await callback.answer()

    @router.callback_query(F.data == "admin:broadcast_preview_answer")
    async def on_admin_broadcast_preview_answer(callback: CallbackQuery) -> None:
        await callback.answer("Это предпросмотр. Пользователи смогут выбрать Да или Нет.", show_alert=True)

    @router.callback_query(F.data.startswith("broadcast_poll:"))
    async def on_broadcast_poll_answer(callback: CallbackQuery) -> None:
        if callback.from_user is None:
            await callback.answer()
            return

        parts = (callback.data or "").split(":")
        if len(parts) != 3:
            await callback.answer("Опрос не найден", show_alert=True)
            return

        answer = parts[1]
        try:
            poll_id = int(parts[2])
        except ValueError:
            await callback.answer("Опрос не найден", show_alert=True)
            return

        result = await sync_service.record_broadcast_poll_answer(
            poll_id=poll_id,
            telegram_id=callback.from_user.id,
            username=callback.from_user.username,
            first_name=callback.from_user.first_name,
            last_name=callback.from_user.last_name,
            answer=answer,
        )
        if result is None:
            await callback.answer("Опрос уже недоступен", show_alert=True)
            return

        if callback.message is not None:
            try:
                await callback.message.edit_reply_markup(
                    reply_markup=build_broadcast_poll_keyboard(
                        poll_id=poll_id,
                        selected_answer=result.answer.answer,
                    )
                )
            except TelegramBadRequest as exc:
                if "message is not modified" not in str(exc).lower():
                    raise

        full_name = " ".join(
            part for part in [callback.from_user.first_name, callback.from_user.last_name] if part
        ).strip()
        try:
            await notify_broadcast_poll_answer(
                callback.bot,
                poll_id=poll_id,
                question_html=result.poll.question_text,
                answer=result.answer.answer,
                previous_answer=result.previous_answer,
                telegram_id=callback.from_user.id,
                username=callback.from_user.username,
                full_name=full_name,
            )
        except Exception:
            pass

        await callback.answer(
            f"Спасибо, ответ «{broadcast_answer_label(result.answer.answer)}» принят.",
            show_alert=True,
        )

    @router.callback_query(F.data == "admin:broadcast_cancel")
    async def on_admin_broadcast_cancel(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await state.clear()
        await callback.answer("Рассылка отменена")
        if callback.message is not None:
            await callback.message.answer("Действие отменено.")

    @router.callback_query(F.data == "admin:broadcast_confirm")
    async def on_admin_broadcast_confirm(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        state_data = await state.get_data()
        text_html = str(state_data.get("broadcast_text_html") or "").strip()
        body_html = str(state_data.get("broadcast_body_html") or "").strip()
        mode = str(state_data.get("broadcast_mode") or "notice").strip()
        if not text_html:
            await state.clear()
            await callback.answer("Текст рассылки не найден", show_alert=True)
            return

        await state.clear()
        await callback.answer("Рассылка запущена")
        if callback.message is not None:
            await callback.message.answer("📣 Рассылка запущена. Пришлю итог после отправки.")

        poll_id: int | None = None
        if mode == "poll":
            poll = await sync_service.create_broadcast_poll(
                question_text=body_html or text_html,
                created_by_telegram_id=callback.from_user.id,
            )
            poll_id = poll.id

        sent_count, blocked_count, failed_count = await send_admin_broadcast(
            callback.bot,
            text_html=text_html,
            poll_id=poll_id,
        )
        if callback.message is not None:
            await callback.message.answer(
                "✅ Рассылка завершена.\n\n"
                f"Отправлено: <b>{sent_count}</b>\n"
                f"Недоступны/заблокировали бота: <b>{blocked_count}</b>\n"
                f"Ошибок отправки: <b>{failed_count}</b>"
                + (f"\nID опроса: <code>{poll_id}</code>" if poll_id is not None else "")
            )

    @router.callback_query(F.data == "admin:sync_all")
    async def on_admin_sync_all(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        await callback.answer("Синхронизация запущена")
        await run_sync_all(callback.message)

        context = await load_panel_context(
            telegram_id=callback.from_user.id,
            username=callback.from_user.username,
            first_name=callback.from_user.first_name,
            last_name=callback.from_user.last_name,
            language_code=callback.from_user.language_code,
            sync_remote=False,
        )
        await update_panel(callback, context=context, section=PANEL_SECTION_ADMIN)

    @router.callback_query(F.data == "admin:guides")
    async def on_admin_guides(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        guides = await sync_service.get_guides(active_only=False)
        await show_branded_view(
            callback,
            text=render_admin_guides_text(guides=guides),
            reply_markup=build_admin_guides_keyboard(guides=guides),
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:guide_create")
    async def on_admin_guide_create(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await state.clear()
        await state.set_state(AdminGuideStates.waiting_for_title)
        await callback.message.answer(
            "📚 Отправьте название гайда одним сообщением.\n\n"
            "Чтобы отменить создание, отправьте <code>/cancel</code>."
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:guide_photo_count")
    async def on_admin_guide_photo_count(callback: CallbackQuery) -> None:
        await callback.answer("Отправьте ещё фото или сохраните гайд.", show_alert=True)

    @router.callback_query(F.data == "admin:guide_done")
    async def on_admin_guide_done(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        state_data = await state.get_data()
        try:
            guide = await sync_service.create_guide(
                title=str(state_data.get("guide_title") or ""),
                short_description=str(state_data.get("guide_description") or ""),
                body_text=str(state_data.get("guide_body") or ""),
                photo_file_ids=list(state_data.get("guide_photos") or []),
                created_by_telegram_id=callback.from_user.id,
            )
        except ValueError as exc:
            await callback.answer(str(exc), show_alert=True)
            return

        await state.clear()
        guides = await sync_service.get_guides(active_only=False)
        await show_branded_view(
            callback,
            text=(
                f"✅ Гайд <b>{html.escape(guide.title)}</b> создан.\n\n"
                + render_admin_guides_text(guides=guides)
            ),
            reply_markup=build_admin_guides_keyboard(guides=guides),
        )
        await callback.answer("Гайд сохранён")

    @router.callback_query(F.data.startswith("admin:guide_delete:"))
    async def on_admin_guide_delete(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        raw_guide_id = (callback.data or "").split(":", maxsplit=2)[2]
        try:
            guide_id = int(raw_guide_id)
        except ValueError:
            await callback.answer("Гайд не найден", show_alert=True)
            return

        guide = await sync_service.get_guide(guide_id, active_only=False)
        if guide is None:
            await callback.answer("Гайд не найден", show_alert=True)
            return

        await show_branded_view(
            callback,
            text=render_admin_guide_delete_confirm_text(guide=guide),
            reply_markup=build_admin_guide_delete_confirm_keyboard(guide_id=guide.id),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:guide_delete_confirm:"))
    async def on_admin_guide_delete_confirm(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        raw_guide_id = (callback.data or "").split(":", maxsplit=2)[2]
        try:
            guide_id = int(raw_guide_id)
        except ValueError:
            await callback.answer("Гайд не найден", show_alert=True)
            return

        deleted_guide = await sync_service.delete_guide(guide_id)
        if deleted_guide is None:
            await callback.answer("Гайд уже удалён", show_alert=True)
            return

        guides = await sync_service.get_guides(active_only=False)
        await show_branded_view(
            callback,
            text=(
                f"✅ Гайд <b>{html.escape(deleted_guide.title)}</b> удалён.\n\n"
                + render_admin_guides_text(guides=guides)
            ),
            reply_markup=build_admin_guides_keyboard(guides=guides),
        )
        await callback.answer("Гайд удалён")

    @router.callback_query(F.data == "admin:promos")
    async def on_admin_promos(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        config = await get_runtime_config()
        promos = await sync_service.get_admin_promo_codes()
        await show_branded_view(
            callback,
            text=render_admin_promos_text(promos=promos, config=config),
            reply_markup=build_admin_promos_keyboard(),
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:promo_create")
    async def on_admin_promo_create(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await state.clear()
        await state.set_state(AdminPromoStates.waiting_for_code)
        await callback.message.answer(
            "🎟 Отправьте слово для промокода.\n\n"
            "Можно использовать буквы, цифры, <code>_</code> и <code>-</code>. "
            "Чтобы отменить создание, отправьте <code>/cancel</code>."
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:promo_plan:"))
    async def on_admin_promo_plan(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        state_data = await state.get_data()
        promo_code = str(state_data.get("promo_code") or "").strip()
        if not promo_code:
            await callback.answer("Сначала введите слово промокода", show_alert=True)
            return

        raw_plan_code = (callback.data or "").split(":", maxsplit=2)[2].removeprefix("promo_plan:")

        config = await get_runtime_config()

        if raw_plan_code == "all":
            # "all" → show duration submenu with unique durations
            product_label = "Все тарифы"
        else:
            # Specific product selected
            product_plans = [
                p for p in config.payment_plans
                if _extract_product_code(p.code) == raw_plan_code
            ]
            if not product_plans:
                await callback.answer("Тариф не найден", show_alert=True)
                return
            product_label = product_plans[0].title or raw_plan_code

        await edit_message_content(
            callback,
            text=(
                f"<b>Промокод:</b> <code>{html.escape(promo_code)}</code>\n\n"
                f"Тип: <b>{html.escape(product_label)}</b>\n\n"
                "Выберите период, на который будет действовать скидка."
            ),
            reply_markup=build_admin_promo_duration_keyboard(
                config=config,
                product_code=raw_plan_code,
            ),
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:promo_back_to_plans")
    async def on_admin_promo_back_to_plans(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        state_data = await state.get_data()
        promo_code = str(state_data.get("promo_code") or "").strip()
        config = await get_runtime_config()
        await edit_message_content(
            callback,
            text=(
                f"<b>Промокод:</b> <code>{html.escape(promo_code)}</code>\n\n"
                "Выберите тариф, на который будет действовать скидка."
            ),
            reply_markup=build_admin_promo_plan_keyboard(config=config),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:promo_duration:"))
    async def on_admin_promo_duration(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        state_data = await state.get_data()
        promo_code = str(state_data.get("promo_code") or "").strip()
        if not promo_code:
            await callback.answer("Сначала введите слово промокода", show_alert=True)
            return

        plan_code = (callback.data or "").removeprefix("admin:promo_duration:")
        config = await get_runtime_config()

        # Validate the plan code
        available_plan_codes = {plan.code for plan in config.payment_plans}
        is_all_duration = plan_code.startswith("all_") and plan_code.removeprefix("all_").isdigit()

        if not is_all_duration and plan_code not in available_plan_codes:
            await callback.answer("Тариф не найден", show_alert=True)
            return

        # Build display label
        if is_all_duration:
            days = plan_code.removeprefix("all_")
            plan_label = f"Все тарифы · {days} дней"
        else:
            plan_info = next((p for p in config.payment_plans if p.code == plan_code), None)
            plan_label = f"{plan_info.title} · {plan_info.days} дней" if plan_info and plan_info.title else plan_code

        await state.set_state(AdminPromoStates.waiting_for_discount)
        await state.update_data(promo_plan_code=plan_code)
        await callback.message.answer(
            f"Тариф: <b>{html.escape(plan_label)}</b>\n\n"
            "Введите скидку по промокоду в процентах.\n"
            "Например: <code>15</code>. Допустимо от 1 до 100."
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:settings")
    async def on_admin_settings(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        config = await get_runtime_config()
        await show_branded_view(
            callback,
            text=render_admin_settings_home_text(config=config),
            reply_markup=build_admin_settings_home_keyboard(),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:settings_section:"))
    async def on_admin_settings_section(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        section = (callback.data or "").split(":", maxsplit=2)[2]
        if not BotConfigService.get_specs_for_section(section):
            await callback.answer("Раздел не найден", show_alert=True)
            return

        config = await get_runtime_config()
        await show_branded_view(
            callback,
            text=render_admin_settings_section_text(config=config, section=section),
            reply_markup=build_admin_settings_section_keyboard(section=section),
        )
        await callback.answer()

    @router.callback_query(F.data == "admin:users")
    async def on_admin_users(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        page_data = await sync_service.get_admin_telegram_users_page(page=1)
        await show_branded_view(
            callback,
            text=render_admin_users_page_text(page_data=page_data),
            reply_markup=build_admin_users_page_keyboard(page_data=page_data),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:user_list:"))
    async def on_admin_users_page(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await callback.answer()
        await clear_interactive_states(state)
        raw_page = (callback.data or "").split(":", maxsplit=2)[2]
        try:
            page = int(raw_page)
        except ValueError:
            page = 1

        page_data = await sync_service.get_admin_telegram_users_page(page=page)
        await show_branded_view(
            callback,
            text=render_admin_users_page_text(page_data=page_data),
            reply_markup=build_admin_users_page_keyboard(page_data=page_data),
        )

    @router.callback_query(F.data.startswith("admin:user_view:"))
    async def on_admin_user_profile(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        parts = (callback.data or "").split(":")
        if len(parts) < 4:
            await callback.answer("Некорректный пользователь", show_alert=True)
            return

        try:
            telegram_id = int(parts[2])
            page = int(parts[3])
        except ValueError:
            await callback.answer("Некорректный пользователь", show_alert=True)
            return

        await callback.answer()
        profile = await sync_service.get_admin_telegram_user_profile(telegram_id=telegram_id)
        if profile is None:
            await callback.message.answer("Пользователь не найден.")
            return

        await show_branded_view(
            callback,
            text=render_admin_user_profile_text(profile=profile),
            reply_markup=build_admin_user_profile_keyboard(
                profile=profile,
                telegram_id=telegram_id,
                page=page,
            ),
        )

    # ── Admin: Block / Unblock user ──────────────────────────────────

    @router.callback_query(F.data == "admin:tickets")
    async def on_admin_tickets(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        page_data = await support_ticket_service.get_admin_tickets_page(page=1)
        await show_branded_view(
            callback,
            text=render_admin_tickets_page_text(page_data=page_data),
            reply_markup=build_admin_tickets_page_keyboard(page_data=page_data),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:ticket_list:"))
    async def on_admin_tickets_page(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        raw_page = (callback.data or "").split(":", maxsplit=2)[2]
        try:
            page = int(raw_page)
        except ValueError:
            page = 1

        page_data = await support_ticket_service.get_admin_tickets_page(page=page)
        await show_branded_view(
            callback,
            text=render_admin_tickets_page_text(page_data=page_data),
            reply_markup=build_admin_tickets_page_keyboard(page_data=page_data),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:ticket_view:"))
    async def on_admin_ticket_view(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        await clear_interactive_states(state)
        parts = (callback.data or "").split(":")
        if len(parts) < 4:
            await callback.answer("Некорректный тикет", show_alert=True)
            return

        try:
            ticket_id = int(parts[2])
            page = int(parts[3])
        except ValueError:
            await callback.answer("Некорректный тикет", show_alert=True)
            return

        ticket = await support_ticket_service.get_ticket(ticket_id=ticket_id)
        if ticket is None:
            await callback.answer("Тикет не найден", show_alert=True)
            return

        await show_branded_view(
            callback,
            text=render_admin_ticket_history_text(ticket=ticket),
            reply_markup=build_admin_ticket_history_keyboard(
                ticket_id=ticket.id,
                page=page,
            ),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:block:"))
    async def on_admin_block_user(callback: CallbackQuery) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        parts = callback.data.split(":")
        if len(parts) < 4:
            await callback.answer()
            return

        try:
            target_telegram_id = int(parts[2])
            page = int(parts[3])
        except ValueError:
            await callback.answer("Некорректные данные", show_alert=True)
            return

        await sync_service.block_telegram_user(target_telegram_id)

        # Auto-disable all Remnawave accesses
        try:
            remote_users = await sync_service._remnawave_client.get_users_by_telegram_id(
                target_telegram_id
            )
            for ru in remote_users:
                if ru.status.upper() != "DISABLED":
                    await sync_service._remnawave_client.update_user(
                        {"uuid": str(ru.uuid), "status": "DISABLED"}
                    )
        except RemnawaveApiError:
            pass

        try:
            await sync_service.refresh_cached_users_for_telegram(
                telegram_id=target_telegram_id,
            )
        except RemnawaveApiError:
            pass

        profile = await sync_service.get_admin_telegram_user_profile(telegram_id=target_telegram_id)
        if profile is None:
            await callback.answer("Пользователь не найден", show_alert=True)
            return

        await show_branded_view(
            callback,
            text=render_admin_user_profile_text(profile=profile),
            reply_markup=build_admin_user_profile_keyboard(
                profile=profile,
                telegram_id=target_telegram_id,
                page=page,
            ),
        )
        await callback.answer("🔒 Пользователь заблокирован", show_alert=True)

    @router.callback_query(F.data.startswith("admin:unblock:"))
    async def on_admin_unblock_user(callback: CallbackQuery) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        parts = callback.data.split(":")
        if len(parts) < 4:
            await callback.answer()
            return

        try:
            target_telegram_id = int(parts[2])
            page = int(parts[3])
        except ValueError:
            await callback.answer("Некорректные данные", show_alert=True)
            return

        await sync_service.unblock_telegram_user(target_telegram_id)

        # Auto-enable all Remnawave accesses
        try:
            remote_users = await sync_service._remnawave_client.get_users_by_telegram_id(
                target_telegram_id
            )
            for ru in remote_users:
                if ru.status.upper() == "DISABLED":
                    await sync_service._remnawave_client.update_user(
                        {"uuid": str(ru.uuid), "status": "ACTIVE"}
                    )
        except RemnawaveApiError:
            pass

        try:
            await sync_service.refresh_cached_users_for_telegram(
                telegram_id=target_telegram_id,
            )
        except RemnawaveApiError:
            pass

        profile = await sync_service.get_admin_telegram_user_profile(telegram_id=target_telegram_id)
        if profile is None:
            await callback.answer("Пользователь не найден", show_alert=True)
            return

        await show_branded_view(
            callback,
            text=render_admin_user_profile_text(profile=profile),
            reply_markup=build_admin_user_profile_keyboard(
                profile=profile,
                telegram_id=target_telegram_id,
                page=page,
            ),
        )
        await callback.answer("🔓 Пользователь разблокирован", show_alert=True)

    # ── Admin: Disable / Enable individual Remnawave access ──────────

    @router.callback_query(F.data.startswith("admin:dis_acc:"))
    async def on_admin_disable_access(callback: CallbackQuery) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        parts = callback.data.split(":")
        if len(parts) < 5:
            await callback.answer()
            return

        short_uuid = parts[2]
        try:
            target_telegram_id = int(parts[3])
            page = int(parts[4])
        except ValueError:
            await callback.answer("Некорректные данные", show_alert=True)
            return

        try:
            rw_user = await sync_service._remnawave_client.get_user_by_short_uuid(short_uuid)
            await sync_service._remnawave_client.update_user(
                {"uuid": str(rw_user.uuid), "status": "DISABLED"}
            )
        except RemnawaveApiError as exc:
            await callback.answer(f"Ошибка: {exc.message[:100]}", show_alert=True)
            return

        try:
            await sync_service.refresh_cached_users_for_telegram(telegram_id=target_telegram_id)
        except RemnawaveApiError:
            pass

        profile = await sync_service.get_admin_telegram_user_profile(telegram_id=target_telegram_id)
        if profile is None:
            await callback.answer("Пользователь не найден", show_alert=True)
            return

        await show_branded_view(
            callback,
            text=render_admin_user_profile_text(profile=profile),
            reply_markup=build_admin_user_profile_keyboard(
                profile=profile, telegram_id=target_telegram_id, page=page,
            ),
        )
        await callback.answer("⛔ Доступ отключён", show_alert=True)

    @router.callback_query(F.data.startswith("admin:en_acc:"))
    async def on_admin_enable_access(callback: CallbackQuery) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        parts = callback.data.split(":")
        if len(parts) < 5:
            await callback.answer()
            return

        short_uuid = parts[2]
        try:
            target_telegram_id = int(parts[3])
            page = int(parts[4])
        except ValueError:
            await callback.answer("Некорректные данные", show_alert=True)
            return

        try:
            rw_user = await sync_service._remnawave_client.get_user_by_short_uuid(short_uuid)
            await sync_service._remnawave_client.update_user(
                {"uuid": str(rw_user.uuid), "status": "ACTIVE"}
            )
        except RemnawaveApiError as exc:
            await callback.answer(f"Ошибка: {exc.message[:100]}", show_alert=True)
            return

        try:
            await sync_service.refresh_cached_users_for_telegram(telegram_id=target_telegram_id)
        except RemnawaveApiError:
            pass

        profile = await sync_service.get_admin_telegram_user_profile(telegram_id=target_telegram_id)
        if profile is None:
            await callback.answer("Пользователь не найден", show_alert=True)
            return

        await show_branded_view(
            callback,
            text=render_admin_user_profile_text(profile=profile),
            reply_markup=build_admin_user_profile_keyboard(
                profile=profile, telegram_id=target_telegram_id, page=page,
            ),
        )
        await callback.answer("✅ Доступ включён", show_alert=True)

    # ── Admin: Time adjustment menu & actions ────────────────────────

    @router.callback_query(F.data.startswith("admin:time_menu:"))
    async def on_admin_time_menu(callback: CallbackQuery) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        parts = callback.data.split(":")
        if len(parts) < 5:
            await callback.answer()
            return

        short_uuid = parts[2]
        try:
            target_telegram_id = int(parts[3])
            page = int(parts[4])
        except ValueError:
            await callback.answer("Некорректные данные", show_alert=True)
            return

        try:
            rw_user = await sync_service._remnawave_client.get_user_by_short_uuid(short_uuid)
        except RemnawaveApiError as exc:
            await callback.answer(f"Ошибка: {exc.message[:100]}", show_alert=True)
            return

        text = (
            f"<b>⏱ Изменение срока подписки</b>\n\n"
            f"Пользователь: <b>{html.escape(rw_user.username)}</b>\n"
            f"Текущий срок: <b>{format_datetime_with_days_left(rw_user.expire_at)}</b>\n\n"
            f"Выберите количество дней для добавления или снятия:"
        )

        await show_branded_view(
            callback,
            text=text,
            reply_markup=build_time_adjust_keyboard(
                short_uuid=short_uuid,
                telegram_id=target_telegram_id,
                page=page,
            ),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("admin:at:"))
    async def on_admin_adjust_time(callback: CallbackQuery) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return

        parts = callback.data.split(":")
        # admin:at:+5:shortUuid:telegramId:page
        if len(parts) < 6:
            await callback.answer()
            return

        days_str = parts[2]
        short_uuid = parts[3]
        try:
            target_telegram_id = int(parts[4])
            page = int(parts[5])
            days = int(days_str)
        except ValueError:
            await callback.answer("Некорректные данные", show_alert=True)
            return

        from datetime import timedelta, timezone

        try:
            rw_user = await sync_service._remnawave_client.get_user_by_short_uuid(short_uuid)
        except RemnawaveApiError as exc:
            await callback.answer(f"Ошибка: {exc.message[:100]}", show_alert=True)
            return

        current_expire = rw_user.expire_at
        if current_expire.tzinfo is None:
            from datetime import timezone as tz
            current_expire = current_expire.replace(tzinfo=tz.utc)

        from datetime import datetime as dt

        now = dt.now(timezone.utc)

        if days > 0:
            new_expire = max(current_expire, now) + timedelta(days=days)
        else:
            new_expire = current_expire + timedelta(days=days)
            if new_expire < now:
                new_expire = now

        try:
            await sync_service._remnawave_client.update_user(
                {
                    "uuid": str(rw_user.uuid),
                    "expireAt": new_expire.isoformat().replace("+00:00", "Z"),
                }
            )
        except RemnawaveApiError as exc:
            await callback.answer(f"Ошибка: {exc.message[:100]}", show_alert=True)
            return

        try:
            await sync_service.refresh_cached_users_for_telegram(telegram_id=target_telegram_id)
        except RemnawaveApiError:
            pass

        sign = "+" if days > 0 else ""
        await callback.answer(f"✅ {sign}{days} дн. применено", show_alert=True)

        # Re-fetch and show updated profile
        profile = await sync_service.get_admin_telegram_user_profile(telegram_id=target_telegram_id)
        if profile is None:
            return

        await show_branded_view(
            callback,
            text=render_admin_user_profile_text(profile=profile),
            reply_markup=build_admin_user_profile_keyboard(
                profile=profile, telegram_id=target_telegram_id, page=page,
            ),
        )

    @router.callback_query(F.data.startswith("admin:edit:"))
    async def on_admin_edit_setting(callback: CallbackQuery, state: FSMContext) -> None:
        if callback.from_user is None or callback.message is None:
            await callback.answer()
            return
        if not settings.is_admin(callback.from_user.id):
            await callback.answer("Недостаточно прав", show_alert=True)
            return
        if not isinstance(bot_config_service, BotConfigService):
            await callback.answer("Редактирование сейчас недоступно", show_alert=True)
            return

        key = (callback.data or "").split(":", maxsplit=2)[2]
        spec = BotConfigService.get_spec(key)
        if spec is None:
            await callback.answer("Настройка не найдена", show_alert=True)
            return

        config = await get_runtime_config()
        await state.set_state(AdminConfigStates.waiting_for_value)
        await state.update_data(config_key=key, config_section=spec.section)
        await callback.message.answer(
            render_admin_setting_prompt(config=config, key=key),
            reply_markup=build_admin_settings_section_keyboard(section=spec.section),
        )
        await callback.answer()

    @router.message(AdminGuideStates.waiting_for_title)
    async def on_admin_guide_title_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        title = (message.text or message.caption or "").strip()
        if not title:
            await message.answer("Отправьте название гайда текстом.")
            return
        if len(title) > 255:
            await message.answer("Название должно быть не длиннее 255 символов.")
            return

        await state.set_state(AdminGuideStates.waiting_for_description)
        await state.update_data(guide_title=title)
        await message.answer(
            "Теперь отправьте короткое описание гайда.\n\n"
            "Оно будет видно в списке гайдов."
        )

    @router.message(AdminGuideStates.waiting_for_description)
    async def on_admin_guide_description_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        description = (message.text or message.caption or "").strip()
        if not description:
            await message.answer("Отправьте короткое описание текстом.")
            return
        if len(description) > 512:
            await message.answer("Описание должно быть не длиннее 512 символов.")
            return

        await state.set_state(AdminGuideStates.waiting_for_body)
        await state.update_data(guide_description=description)
        await message.answer(
            "Теперь отправьте сам гайд одним сообщением.\n\n"
            "Бот покажет этот текст пользователю как цитату."
        )

    @router.message(AdminGuideStates.waiting_for_body)
    async def on_admin_guide_body_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        body_text = (message.text or message.caption or "").strip()
        if not body_text:
            await message.answer("Отправьте текст гайда.")
            return

        await state.set_state(AdminGuideStates.waiting_for_photos)
        await state.update_data(guide_body=body_text, guide_photos=[])
        await message.answer(
            "Теперь отправьте фото для гайда.\n\n"
            "Можно отправить несколько фото одним альбомом или по одному. "
            "Когда закончите, нажмите кнопку сохранения или отправьте <code>/done</code>.",
            reply_markup=build_admin_guide_photos_keyboard(photo_count=0),
        )

    @router.message(AdminGuideStates.waiting_for_photos, Command("done"))
    async def on_admin_guide_done_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        state_data = await state.get_data()
        try:
            guide = await sync_service.create_guide(
                title=str(state_data.get("guide_title") or ""),
                short_description=str(state_data.get("guide_description") or ""),
                body_text=str(state_data.get("guide_body") or ""),
                photo_file_ids=list(state_data.get("guide_photos") or []),
                created_by_telegram_id=message.from_user.id,
            )
        except ValueError as exc:
            await message.answer(html.escape(str(exc)))
            return

        await state.clear()
        guides = await sync_service.get_guides(active_only=False)
        await message.answer(f"✅ Гайд <b>{html.escape(guide.title)}</b> создан.")
        await message.answer(
            render_admin_guides_text(guides=guides),
            reply_markup=build_admin_guides_keyboard(guides=guides),
        )

    @router.message(AdminGuideStates.waiting_for_photos)
    async def on_admin_guide_photo_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        if not message.photo:
            await message.answer(
                "Отправьте фото или завершите создание командой <code>/done</code>.",
                reply_markup=build_admin_guide_photos_keyboard(
                    photo_count=len((await state.get_data()).get("guide_photos") or []),
                ),
            )
            return

        state_data = await state.get_data()
        photo_file_ids = list(state_data.get("guide_photos") or [])
        file_id = message.photo[-1].file_id
        if file_id not in photo_file_ids:
            photo_file_ids.append(file_id)
        await state.update_data(guide_photos=photo_file_ids)
        await message.answer(
            f"Фото добавлено: <b>{len(photo_file_ids)}</b>.",
            reply_markup=build_admin_guide_photos_keyboard(photo_count=len(photo_file_ids)),
        )

    @router.message(AdminPromoStates.waiting_for_code)
    async def on_admin_promo_code_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        promo_code = sync_service.normalize_discount_code(message.text or message.caption or "")
        if not promo_code:
            await message.answer("Отправьте слово для промокода текстом.")
            return
        if len(promo_code) > 32:
            await message.answer("Промокод должен быть не длиннее 32 символов.")
            return
        if not all(ch.isalnum() or ch in "_-" for ch in promo_code):
            await message.answer("Промокод может содержать только буквы, цифры, _ и -.")
            return

        config = await get_runtime_config()
        await state.set_state(AdminPromoStates.waiting_for_plan)
        await state.update_data(promo_code=promo_code)
        await message.answer(
            f"Промокод: <code>{html.escape(promo_code)}</code>\n\n"
            "Выберите тариф, на который будет действовать скидка.",
            reply_markup=build_admin_promo_plan_keyboard(config=config),
        )

    @router.message(AdminPromoStates.waiting_for_plan)
    async def on_admin_promo_plan_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        config = await get_runtime_config()
        await message.answer(
            "Выберите тариф кнопкой ниже.",
            reply_markup=build_admin_promo_plan_keyboard(config=config),
        )

    @router.message(AdminPromoStates.waiting_for_discount)
    async def on_admin_promo_discount_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        raw_discount = (message.text or message.caption or "").strip().removesuffix("%")
        try:
            discount_percent = int(raw_discount)
        except ValueError:
            await message.answer("Введите скидку числом, например <code>15</code>.")
            return
        if discount_percent <= 0 or discount_percent > 100:
            await message.answer("Скидка должна быть от 1 до 100%.")
            return

        await state.set_state(AdminPromoStates.waiting_for_expires)
        await state.update_data(promo_discount_percent=discount_percent)
        await message.answer(
            "Введите срок действия промокода.\n\n"
            "Примеры: <code>7</code> = 7 дней, <code>24h</code> = 24 часа, "
            "<code>2026-05-31</code> = до даты. Для бессрочного промокода отправьте <code>0</code>."
        )

    @router.message(AdminPromoStates.waiting_for_expires)
    async def on_admin_promo_expires_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        state_data = await state.get_data()
        promo_code = str(state_data.get("promo_code") or "").strip()
        plan_code = state_data.get("promo_plan_code")
        discount_percent = int(state_data.get("promo_discount_percent") or 0)
        if not promo_code or discount_percent <= 0:
            await state.clear()
            await message.answer("Данные промокода потерялись. Начните создание заново.")
            return

        try:
            expires_at = parse_promo_expires_at(message.text or message.caption or "")
            promo = await sync_service.create_promo_code(
                code=promo_code,
                plan_code=str(plan_code) if plan_code else None,
                discount_percent=discount_percent,
                expires_at=expires_at,
                created_by_telegram_id=message.from_user.id,
            )
        except ValueError as exc:
            await message.answer(html.escape(str(exc)))
            return

        await state.clear()
        config = await get_runtime_config()
        promos = await sync_service.get_admin_promo_codes()
        await message.answer(
            "\n".join(
                [
                    f"✅ Промокод <code>{html.escape(promo.code)}</code> создан.",
                    f"Скидка: <b>{promo.discount_percent}%</b>",
                    f"Тариф: <b>{html.escape(format_promo_scope(plan_code=promo.plan_code, config=config))}</b>",
                    f"Срок: <b>{html.escape(format_promo_expires_at(promo.expires_at))}</b>",
                ]
            )
        )
        await message.answer(
            render_admin_promos_text(promos=promos, config=config),
            reply_markup=build_admin_promos_keyboard(),
        )

    @router.message(AdminLookupStates.waiting_for_identifier)
    async def on_admin_lookup_message(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        identifier = (message.text or "").strip()
        if not identifier:
            await message.answer("Отправьте идентификатор пользователя текстом.")
            return

        found = await send_lookup_result(message, identifier=identifier)
        if found:
            await state.clear()

    @router.message(AdminBroadcastStates.waiting_for_text)
    async def on_admin_broadcast_text(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return

        raw_text, text_html = get_message_html_text(message)
        state_data = await state.get_data()
        mode = str(state_data.get("broadcast_mode") or "notice")
        if not raw_text:
            await message.answer("Отправьте текст рассылки одним сообщением.")
            return
        if len(text_html) > 3800:
            await message.answer("Текст слишком длинный. Максимум — около 3800 символов.")
            return

        await show_admin_broadcast_preview(
            message,
            body_html=text_html,
            mode=mode if mode in {"notice", "poll"} else "notice",
            state=state,
        )

    @router.message(AdminConfigStates.waiting_for_value)
    async def on_admin_setting_value(message: Message, state: FSMContext) -> None:
        if not await ensure_sender(message):
            await state.clear()
            return
        if not settings.is_admin(message.from_user.id):
            await state.clear()
            await message.answer("Недостаточно прав.")
            return
        if not isinstance(bot_config_service, BotConfigService):
            await state.clear()
            await message.answer("Редактирование настроек сейчас недоступно.")
            return

        state_data = await state.get_data()
        key = str(state_data.get("config_key") or "").strip()
        section = str(state_data.get("config_section") or "").strip()
        spec = BotConfigService.get_spec(key)
        if not key or spec is None:
            await state.clear()
            await message.answer("Настройка не найдена. Откройте админ-панель заново.")
            return

        raw_value = (message.text or message.caption or "").strip()
        if not raw_value:
            await message.answer("Отправьте новое значение текстом.")
            return

        try:
            await bot_config_service.update_setting(
                key=key,
                raw_value=raw_value,
                updated_by_telegram_id=message.from_user.id,
            )
        except ValueError as exc:
            await message.answer(html.escape(str(exc)))
            return

        await state.clear()
        config = await get_runtime_config()
        current_value = BotConfigService.format_value(snapshot=config, key=key)
        await message.answer(
            "\n".join(
                [
                    f"✅ Настройка <b>{html.escape(spec.label)}</b> сохранена.",
                    f"Новое значение: <code>{html.escape(current_value)}</code>",
                ]
            ),
            reply_markup=build_admin_settings_section_keyboard(section=section or spec.section),
        )
        await message.answer(
            render_admin_settings_section_text(config=config, section=section or spec.section),
            reply_markup=build_admin_settings_section_keyboard(section=section or spec.section),
        )

    if True:

        @router.callback_query(F.data.startswith("payment:approve:"))
        async def on_payment_approve(callback: CallbackQuery) -> None:
            if callback.from_user is None:
                await callback.answer()
                return

            if not (
                settings.is_admin(callback.from_user.id)
                or settings.is_moderator(callback.from_user.id)
            ):
                await callback.answer("Недостаточно прав", show_alert=True)
                return

            order_uuid = (callback.data or "").split(":", maxsplit=2)[2]
            try:
                access = await payment_service.approve_order(order_uuid=order_uuid)
            except (ValueError, RemnawaveApiError) as exc:
                await callback.answer(str(exc), show_alert=True)
                return

            try:
                await send_issued_access_message(
                    callback.bot,
                    chat_id=access.telegram_id,
                    subscription_url=access.subscription_url,
                    expire_at=access.expire_at,
                    traffic_limit_bytes=access.traffic_limit_bytes,
                    is_renewal=access.is_renewal,
                )
            except TelegramForbiddenError:
                if access.referral_bonus is not None:
                    try:
                        await notify_referral_bonus(
                            callback.bot,
                            inviter_telegram_id=access.referral_bonus.inviter_telegram_id,
                            bonus_days=access.referral_bonus.bonus_days,
                            status=access.referral_bonus.status,
                            expire_at=access.referral_bonus.expire_at,
                            total_applied_days=access.referral_bonus.total_applied_days,
                        )
                    except TelegramForbiddenError:
                        pass
                await append_review_status(callback, status_text="✅ подтверждено, бот у пользователя недоступен")
                await callback.answer(
                    "Доступ создан, но пользователь заблокировал бота.",
                    show_alert=True,
                )
                return

            if access.referral_bonus is not None:
                try:
                    await notify_referral_bonus(
                        callback.bot,
                        inviter_telegram_id=access.referral_bonus.inviter_telegram_id,
                        bonus_days=access.referral_bonus.bonus_days,
                        status=access.referral_bonus.status,
                        expire_at=access.referral_bonus.expire_at,
                        total_applied_days=access.referral_bonus.total_applied_days,
                    )
                except TelegramForbiddenError:
                    pass

            await append_review_status(callback, status_text="✅ подтверждено")
            await callback.answer("Оплата подтверждена")

        @router.callback_query(F.data.startswith("payment:reject:"))
        async def on_payment_reject(callback: CallbackQuery) -> None:
            if callback.from_user is None:
                await callback.answer()
                return

            if not (
                settings.is_admin(callback.from_user.id)
                or settings.is_moderator(callback.from_user.id)
            ):
                await callback.answer("Недостаточно прав", show_alert=True)
                return

            order_uuid = (callback.data or "").split(":", maxsplit=2)[2]
            config = await get_runtime_config()
            try:
                order = await payment_service.reject_order(
                    order_uuid=order_uuid,
                    reason=config.payment_support_text.strip() or "Платёж отклонён",
                )
            except ValueError as exc:
                await callback.answer(str(exc), show_alert=True)
                return

            try:
                await send_branded(
                    callback.bot,
                    chat_id=order.telegram_id,
                    text=(
                        "<b>Платёж не подтверждён</b>\n\n"
                        "Проверьте реквизиты и чек. "
                        f"{html.escape(config.payment_support_text.strip() or 'Если это ошибка, обратитесь в поддержку.')}"
                    ),
                    reply_markup=build_home_keyboard(),
                )
            except TelegramForbiddenError:
                await append_review_status(callback, status_text="❌ отклонено, бот у пользователя недоступен")
                await callback.answer(
                    "Не удалось уведомить пользователя: бот недоступен.",
                    show_alert=True,
                )
                return

            await append_review_status(callback, status_text="❌ отклонено")
            await callback.answer("Оплата отклонена")

    if True:

        def _format_device_label(device) -> str:
            parts = []
            if device.device_model:
                parts.append(device.device_model)
            if device.platform:
                parts.append(device.platform)
            if device.os_version:
                parts.append(device.os_version)
            if parts:
                return " · ".join(parts)
            if device.user_agent:
                ua = device.user_agent
                return ua[:40] + "..." if len(ua) > 40 else ua
            return device.hwid[:16] + "..."

        def _build_devices_list_text(
            devices,
            *,
            user_uuid_short: str,
            hwid_device_limit: int | None,
        ) -> str:
            limit_text = str(hwid_device_limit) if hwid_device_limit else "∞"
            lines = [
                "<b>📱 Подключённые устройства</b>",
                f"<blockquote>Подключено: <b>{len(devices)}</b> / <b>{limit_text}</b></blockquote>",
            ]
            if not devices:
                lines.extend([
                    "",
                    "<i>Нет подключённых устройств.</i>",
                ])
            else:
                for idx, device in enumerate(devices, start=1):
                    label = html.escape(_format_device_label(device))
                    connected = format_datetime(device.created_at)
                    lines.append(
                        f"\n{idx}. <b>{label}</b>"
                        f"\n<blockquote>Подключено: {connected}</blockquote>"
                    )

            lines.extend([
                "",
                "<i>Отключите устройство, чтобы освободить слот для нового подключения.</i>",
            ])
            return "\n".join(lines)

        def _build_devices_keyboard(devices, *, user_uuid: str):
            builder = InlineKeyboardBuilder()
            for device in devices:
                label = _format_device_label(device)
                short_hwid = device.hwid[:12]
                builder.button(
                    text=f"❌ {label[:30]}",
                    callback_data=f"devices:remove:{user_uuid}:{short_hwid}",
                )
            if len(devices) > 1:
                builder.button(
                    text="🗑 Отключить все устройства",
                    callback_data=f"devices:remove_all:{user_uuid}",
                )
            builder.button(
                text="↻ Обновить",
                callback_data="devices:list",
            )
            builder.button(
                text="🏠 Главное меню",
                callback_data=f"panel:{PANEL_SECTION_HOME}",
            )
            builder.adjust(1)
            return builder.as_markup()

        @router.callback_query(F.data == "devices:list")
        async def on_devices_list(callback: CallbackQuery) -> None:
            if callback.from_user is None:
                await callback.answer()
                return

            telegram_id = callback.from_user.id
            cached_users = await sync_service.get_cached_users_for_telegram(telegram_id)
            if not cached_users:
                await callback.answer("У вас нет активной подписки.", show_alert=True)
                return

            primary = None
            for user in cached_users:
                if user.record.status.upper() == "ACTIVE":
                    primary = user
                    break
            if primary is None:
                primary = cached_users[0]

            user_uuid = str(primary.record.rw_uuid)
            try:
                hwid_response = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
            except RemnawaveApiError as exc:
                await callback.answer(f"Ошибка API: {exc.message}", show_alert=True)
                return

            text = _build_devices_list_text(
                hwid_response.devices,
                user_uuid_short=primary.record.short_uuid,
                hwid_device_limit=primary.record.hwid_device_limit,
            )
            keyboard = _build_devices_keyboard(
                hwid_response.devices,
                user_uuid=user_uuid,
            )

            await show_text_view(callback, text=text, reply_markup=keyboard)
            await callback.answer()

        @router.callback_query(F.data.startswith("devices:remove_all:"))
        async def on_devices_remove_all(callback: CallbackQuery) -> None:
            if callback.from_user is None:
                await callback.answer()
                return

            telegram_id = callback.from_user.id
            parts = (callback.data or "").split(":", maxsplit=2)
            if len(parts) < 3:
                await callback.answer("Некорректные данные.", show_alert=True)
                return

            user_uuid = parts[2]

            # Verify the user_uuid belongs to the caller
            cached_users = await sync_service.get_cached_users_for_telegram(telegram_id)
            matching = any(str(u.record.rw_uuid) == user_uuid for u in cached_users)
            if not matching:
                await callback.answer("Доступ запрещён.", show_alert=True)
                return

            try:
                await payment_service._remnawave_client.delete_all_user_hwid_devices(user_uuid)
            except RemnawaveApiError as exc:
                await callback.answer(f"Ошибка: {exc.message}", show_alert=True)
                return

            primary = next((u for u in cached_users if str(u.record.rw_uuid) == user_uuid), cached_users[0])

            try:
                hwid_response = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
            except RemnawaveApiError:
                hwid_response = type("R", (), {"devices": [], "total": 0})()

            text = _build_devices_list_text(
                hwid_response.devices,
                user_uuid_short=primary.record.short_uuid,
                hwid_device_limit=primary.record.hwid_device_limit,
            )
            keyboard = _build_devices_keyboard(
                hwid_response.devices,
                user_uuid=user_uuid,
            )

            await show_text_view(callback, text=text, reply_markup=keyboard)
            await callback.answer("Все устройства отключены")

        @router.callback_query(F.data.startswith("devices:remove:"))
        async def on_devices_remove(callback: CallbackQuery) -> None:
            if callback.from_user is None:
                await callback.answer()
                return

            telegram_id = callback.from_user.id
            parts = (callback.data or "").split(":", maxsplit=3)
            if len(parts) < 4:
                await callback.answer("Некорректные данные.", show_alert=True)
                return

            user_uuid = parts[2]
            short_hwid = parts[3]

            # Verify the user_uuid belongs to the caller
            cached_users = await sync_service.get_cached_users_for_telegram(telegram_id)
            matching = any(str(u.record.rw_uuid) == user_uuid for u in cached_users)
            if not matching:
                await callback.answer("Доступ запрещён.", show_alert=True)
                return

            # Find the full HWID matching the short prefix
            try:
                current_devices = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
            except RemnawaveApiError as exc:
                await callback.answer(f"Ошибка: {exc.message}", show_alert=True)
                return

            target_device = None
            for device in current_devices.devices:
                if device.hwid.startswith(short_hwid):
                    target_device = device
                    break

            if target_device is None:
                await callback.answer("Устройство не найдено или уже отключено.", show_alert=True)
                return

            try:
                await payment_service._remnawave_client.delete_user_hwid_device(
                    user_uuid=user_uuid,
                    hwid=target_device.hwid,
                )
            except RemnawaveApiError as exc:
                await callback.answer(f"Ошибка: {exc.message}", show_alert=True)
                return

            primary = next((u for u in cached_users if str(u.record.rw_uuid) == user_uuid), cached_users[0])

            try:
                hwid_response = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
            except RemnawaveApiError:
                hwid_response = type("R", (), {"devices": [], "total": 0})()

            text = _build_devices_list_text(
                hwid_response.devices,
                user_uuid_short=primary.record.short_uuid,
                hwid_device_limit=primary.record.hwid_device_limit,
            )
            keyboard = _build_devices_keyboard(
                hwid_response.devices,
                user_uuid=user_uuid,
            )

            device_label = _format_device_label(target_device)
            await show_text_view(callback, text=text, reply_markup=keyboard)
            await callback.answer(f"Устройство «{device_label[:30]}» отключено")

    if True:

        @router.callback_query(F.data.startswith("ticket_admin:note:"))
        async def on_support_ticket_note_start(callback: CallbackQuery, state: FSMContext) -> None:
            if callback.from_user is None or callback.message is None:
                await callback.answer()
                return

            if not (
                settings.is_admin(callback.from_user.id)
                or settings.is_moderator(callback.from_user.id)
            ):
                await callback.answer("Недостаточно прав", show_alert=True)
                return

            config = await get_runtime_config()
            if config.support_ticket_chat_id is None or callback.message.chat.id != config.support_ticket_chat_id:
                await callback.answer()
                return

            ticket_id_text = (callback.data or "").split(":")[-1]
            if not ticket_id_text.isdigit():
                await callback.answer("Тикет не найден", show_alert=True)
                return

            ticket = await support_ticket_service.get_ticket(ticket_id=int(ticket_id_text))
            if ticket is None:
                await callback.answer("Тикет не найден", show_alert=True)
                return
            if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
                await callback.answer("Тикет уже закрыт", show_alert=True)
                return

            await state.set_state(SupportTicketAdminStates.waiting_for_note)
            await state.update_data(support_ticket_id=ticket.id)
            await callback.message.reply(
                f"📝 Отправьте текст для тикета #{ticket.public_id} одним следующим сообщением.\n"
                "Он будет добавлен к карточке тикета в чате поддержки.\n"
                "Чтобы отменить действие, отправьте /cancel."
            )
            await callback.answer()

        @router.callback_query(F.data.startswith("ticket_admin:close:"))
        async def on_support_ticket_close(callback: CallbackQuery) -> None:
            if callback.from_user is None or callback.message is None:
                await callback.answer()
                return

            if not (
                settings.is_admin(callback.from_user.id)
                or settings.is_moderator(callback.from_user.id)
            ):
                await callback.answer("Недостаточно прав", show_alert=True)
                return

            config = await get_runtime_config()
            if config.support_ticket_chat_id is None or callback.message.chat.id != config.support_ticket_chat_id:
                await callback.answer()
                return

            ticket_id_text = (callback.data or "").split(":")[-1]
            if not ticket_id_text.isdigit():
                await callback.answer("Тикет не найден", show_alert=True)
                return

            ticket = await support_ticket_service.get_ticket(ticket_id=int(ticket_id_text))
            if ticket is None:
                await callback.answer("Тикет не найден", show_alert=True)
                return
            if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
                await callback.answer("Тикет уже закрыт", show_alert=True)
                return

            closed_ticket = await support_ticket_service.close_ticket(
                ticket_id=ticket.id,
                closed_by_telegram_id=callback.from_user.id,
            )
            if closed_ticket is None:
                await callback.answer("Не удалось закрыть тикет", show_alert=True)
                return

            await refresh_support_ticket_message(callback.bot, ticket=closed_ticket)

            try:
                await send_branded(
                    callback.bot,
                    chat_id=closed_ticket.telegram_id,
                    text=render_user_ticket_closed_text(
                        ticket=closed_ticket,
                        closed_by_support=True,
                    ),
                    reply_markup=build_user_support_ticket_keyboard(ticket=closed_ticket),
                )
            except TelegramForbiddenError:
                await callback.answer(
                    "Тикет закрыт, но пользователь недоступен для уведомления.",
                    show_alert=True,
                )
                return

            await callback.answer("Тикет закрыт")

        @router.callback_query(F.data.startswith("ticket_admin:closed:"))
        async def on_support_ticket_closed(callback: CallbackQuery) -> None:
            await callback.answer("Тикет уже закрыт", show_alert=True)

        @router.message(SupportTicketAdminStates.waiting_for_note)
        async def on_support_ticket_note_message(message: Message, state: FSMContext) -> None:
            if message.from_user is None:
                await state.clear()
                return

            if not (
                settings.is_admin(message.from_user.id)
                or settings.is_moderator(message.from_user.id)
            ):
                await state.clear()
                await message.answer("Недостаточно прав.")
                return

            config = await get_runtime_config()
            if config.support_ticket_chat_id is None or message.chat.id != config.support_ticket_chat_id:
                await message.answer("Добавьте текст к тикету прямо в чате поддержки.")
                return

            note_text = (message.text or message.caption or "").strip()
            if not note_text:
                await message.answer("Отправьте текст заметки одним сообщением.")
                return

            state_data = await state.get_data()
            ticket_id = int(state_data.get("support_ticket_id") or 0)
            if ticket_id <= 0:
                await state.clear()
                await message.answer("Тикет не найден. Нажмите кнопку ещё раз.")
                return

            ticket = await support_ticket_service.get_ticket(ticket_id=ticket_id)
            if ticket is None:
                await state.clear()
                await message.answer("Тикет не найден.")
                return
            if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
                await state.clear()
                await message.answer("Тикет уже закрыт.")
                return

            note_entry = "\n".join(
                [
                    f"[{format_datetime(message.date.replace(tzinfo=None))}] {message.from_user.full_name}",
                    note_text,
                ]
            )
            updated_ticket = await support_ticket_service.append_note(
                ticket_id=ticket.id,
                note_text=note_entry,
            )
            if updated_ticket is None:
                await state.clear()
                await message.answer("Не удалось добавить текст к тикету.")
                return

            await refresh_support_ticket_message(message.bot, ticket=updated_ticket)
            await state.clear()
            await message.reply(f"Текст добавлен к тикету #{updated_ticket.public_id}.")

            try:
                await send_branded(
                    message.bot,
                    chat_id=updated_ticket.telegram_id,
                    text=render_user_ticket_support_note_text(
                        ticket=updated_ticket,
                        note_text=note_text,
                    ),
                    reply_markup=build_user_support_ticket_keyboard(ticket=updated_ticket),
                )
            except TelegramForbiddenError:
                await message.reply("Текст добавлен, но пользователь недоступен для уведомления.")
                return

        @router.message(F.reply_to_message)
        async def on_support_reply(message: Message) -> None:
            if message.reply_to_message is None or message.from_user is None:
                return

            if not (
                settings.is_admin(message.from_user.id)
                or settings.is_moderator(message.from_user.id)
            ):
                await message.reply("Недостаточно прав для ответа на тикеты.")
                return

            config = await get_runtime_config()
            if config.support_ticket_chat_id is None or message.chat.id != config.support_ticket_chat_id:
                return

            reply_text = (message.text or message.caption or "").strip()
            if not reply_text:
                return

            ticket = await support_ticket_service.get_ticket_by_support_message(
                support_chat_id=message.chat.id,
                support_message_id=message.reply_to_message.message_id,
            )
            if ticket is None:
                return
            if ticket.status == CLOSED_SUPPORT_TICKET_STATUS:
                await message.reply(f"Тикет #{ticket.public_id} уже закрыт.")
                return

            try:
                await send_branded(
                    message.bot,
                    chat_id=ticket.telegram_id,
                    text=(
                        f"<b>🆘 Ответ по тикету #{ticket.public_id}</b>\n\n"
                        f"{html.escape(reply_text)}\n\n"
                        "<i>Если вопрос решён, закройте тикет. Если нужно что-то уточнить, добавьте текст кнопкой ниже.</i>"
                    ),
                    reply_markup=build_user_support_ticket_keyboard(ticket=ticket),
                )
            except TelegramForbiddenError:
                await message.reply(
                    "Не удалось доставить ответ: пользователь заблокировал бота или недоступен."
                )
                return

            updated_ticket = await support_ticket_service.mark_answered(ticket_id=ticket.id)
            if updated_ticket is not None:
                await refresh_support_ticket_message(message.bot, ticket=updated_ticket)
            await message.reply(f"Ответ по тикету #{ticket.public_id} отправлен пользователю.")

    @router.message(Command("help"))
    async def cmd_help(message: Message) -> None:
        if not await ensure_sender(message):
            return

        await answer_branded(
            message,
            build_help_text(is_admin=settings.is_admin(message.from_user.id))
        )

    @router.message(Command("sync"))
    async def cmd_sync(message: Message) -> None:
        if not await ensure_sender(message):
            return

        try:
            users = await sync_service.sync_users_for_telegram(
                telegram_id=message.from_user.id,
                username=message.from_user.username,
                first_name=message.from_user.first_name,
                last_name=message.from_user.last_name,
                language_code=message.from_user.language_code,
            )
        except RemnawaveApiError as exc:
            await message.answer(
                "Не удалось обновить данные из Remnawave.\n"
                f"Ошибка: <code>{html.escape(exc.message)}</code>"
            )
            return

        if not users:
            await message.answer(
                "Синхронизация завершена, но связанных аккаунтов не найдено."
            )
            return

        context = await load_panel_context(
            telegram_id=message.from_user.id,
            username=message.from_user.username,
            first_name=message.from_user.first_name,
            last_name=message.from_user.last_name,
            language_code=message.from_user.language_code,
            sync_remote=False,
        )
        await show_panel(message, context=context, section=PANEL_SECTION_HOME)

    @router.message(Command("link"))
    async def cmd_link(message: Message, command: CommandObject) -> None:
        if not await ensure_sender(message):
            return

        short_uuid = (command.args or "").strip()
        if not short_uuid:
            await message.answer(
                "Укажите <code>short_uuid</code> из вашей ссылки подписки.\n"
                "Пример значения: <code>AbCdEF12</code>"
            )
            return

        try:
            await sync_service.link_user_by_short_uuid(
                short_uuid=short_uuid,
                telegram_id=message.from_user.id,
                username=message.from_user.username,
                first_name=message.from_user.first_name,
                last_name=message.from_user.last_name,
                language_code=message.from_user.language_code,
            )
        except ValueError as exc:
            await message.answer(html.escape(str(exc)))
            return
        except RemnawaveApiError as exc:
            await message.answer(
                "Не удалось привязать аккаунт.\n"
                f"Ошибка API: <code>{html.escape(exc.message)}</code>"
            )
            return

        context = await load_panel_context(
            telegram_id=message.from_user.id,
            username=message.from_user.username,
            first_name=message.from_user.first_name,
            last_name=message.from_user.last_name,
            language_code=message.from_user.language_code,
            sync_remote=False,
        )
        await message.answer("Аккаунт успешно привязан к вашему Telegram.")
        await show_panel(message, context=context, section=PANEL_SECTION_SUBSCRIPTION)

    @router.message(Command("lookup"))
    async def cmd_lookup(message: Message, command: CommandObject) -> None:
        if not await ensure_sender(message):
            return

        if not settings.is_admin(message.from_user.id):
            await message.answer("Эта команда доступна только администраторам.")
            return

        identifier = (command.args or "").strip()
        if not identifier:
            await message.answer(
                "Укажите идентификатор пользователя.\n"
                "Поддерживаются: uuid, id, username, short_uuid.\n"
                "Пример: <code>/lookup 9b488e20-2a1f-4d69-b04d-c7b7f435f1a5</code>"
            )
            return

        await send_lookup_result(message, identifier=identifier)

    @router.message(Command("sync_all"))
    async def cmd_sync_all(message: Message) -> None:
        if not await ensure_sender(message):
            return

        if not settings.is_admin(message.from_user.id):
            await message.answer("Эта команда доступна только администраторам.")
            return

        await run_sync_all(message)

    return router
