from __future__ import annotations

from dataclasses import dataclass
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from app.config import PAYMENT_PRODUCT_PLAN_SPECS, PaymentPlanSettings, Settings
from app.db.base import utcnow
from app.db.models import BotSetting


@dataclass(frozen=True, slots=True)
class BotConfigFieldSpec:
    key: str
    label: str
    section: str
    prompt: str
    description: str
    placeholder: str = ""
    is_secret_like: bool = False


@dataclass(slots=True)
class BotConfigSnapshot:
    bot_brand_name: str
    bot_public_username: str
    bot_support_url: str
    bot_terms_url: str
    bot_support_ticket_link: str
    payment_review_link: str
    broadcast_results_link: str
    bot_start_image_enabled: bool
    bot_start_image_path: str
    referral_enabled: bool
    referral_discount_percent: int
    referral_bonus_days: int
    payment_plans_raw: str
    payment_product_plans_raw: str
    payment_transfer_text: str
    payment_success_text: str
    payment_support_text: str
    payment_plan_traffic_limit_gb: int
    payment_plan_traffic_reset_period: str
    payment_internal_squad_uuids_raw: str
    payment_vpn_squad_uuids_raw: str
    payment_white_squad_uuids_raw: str
    payment_external_squad_uuid: str
    payment_username_prefix: str
    payment_user_tag: str
    notification_enabled: bool = True
    notification_days_before: str = "1,3"
    notification_check_interval_hours: int = 6
    fallback_support_ticket_chat_id: int | None = None
    fallback_support_ticket_thread_id: int | None = None
    fallback_payment_review_chat_id: int | None = None
    fallback_payment_review_thread_id: int | None = None
    fallback_broadcast_results_chat_id: int | None = None
    fallback_broadcast_results_thread_id: int | None = None

    @property
    def bot_public_username_normalized(self) -> str:
        return self.bot_public_username.strip().removeprefix("@")

    @property
    def payment_external_squad_uuid_normalized(self) -> str | None:
        value = self.payment_external_squad_uuid.strip()
        return value or None

    @property
    def payment_username_prefix_normalized(self) -> str:
        normalized = "".join(
            ch for ch in self.payment_username_prefix.strip() if ch.isalnum() or ch in "_-"
        )
        return normalized[:12] or "Oreol"

    @property
    def payment_user_tag_normalized(self) -> str | None:
        normalized = "".join(
            ch for ch in self.payment_user_tag.strip().upper() if ch.isalnum() or ch == "_"
        )
        return normalized[:16] or None

    @property
    def payment_internal_squad_uuids(self) -> list[str]:
        return self._parse_csv_list(self.payment_internal_squad_uuids_raw)

    @property
    def payment_vpn_squad_uuids(self) -> list[str]:
        return self._parse_csv_list(self.payment_vpn_squad_uuids_raw)

    @property
    def payment_white_squad_uuids(self) -> list[str]:
        return self._parse_csv_list(self.payment_white_squad_uuids_raw)

    @property
    def notification_days_list(self) -> list[int]:
        values: list[int] = []
        for raw_part in self.notification_days_before.split(","):
            part = raw_part.strip()
            if part.isdigit():
                day = int(part)
                if day > 0:
                    values.append(day)
        return sorted(set(values), reverse=True) if values else [1, 3]

    @property
    def payment_plans(self) -> list[PaymentPlanSettings]:
        product_plans = self._parse_product_plans(
            self.payment_product_plans_raw,
            default_days=30,
        )
        if product_plans:
            return product_plans

        values: list[PaymentPlanSettings] = []
        for raw_part in self.payment_plans_raw.split(","):
            part = raw_part.strip()
            if not part:
                continue

            fragments = [fragment.strip() for fragment in part.split(":")]
            if len(fragments) != 2 or not fragments[0].isdigit() or not fragments[1].isdigit():
                continue

            days = int(fragments[0])
            amount_rub = int(fragments[1])
            if days <= 0 or amount_rub <= 0:
                continue

            values.append(
                PaymentPlanSettings(
                    code=f"{days}d",
                    days=days,
                    amount_rub=amount_rub,
                )
            )

        return sorted(values, key=lambda item: item.days)

    @staticmethod
    def _parse_csv_list(raw_value: str) -> list[str]:
        values: list[str] = []
        for raw_part in raw_value.split(","):
            part = raw_part.strip()
            if part:
                values.append(part)
        return values

    @staticmethod
    def _parse_product_plans(
        raw_value: str,
        *,
        default_days: int,
    ) -> list[PaymentPlanSettings]:
        values: list[PaymentPlanSettings] = []
        seen_codes: set[str] = set()

        for raw_part in raw_value.split(","):
            part = raw_part.strip()
            if not part:
                continue

            fragments = [fragment.strip() for fragment in part.split(":")]

            # Legacy format: code:amount (2 fields)
            if len(fragments) == 2:
                code, raw_amount = fragments
                pairs = [(str(default_days), raw_amount)]
            # Standard format: code:days:amount (3 fields)
            elif len(fragments) == 3:
                code = fragments[0]
                pairs = [(fragments[1], fragments[2])]
            # Extended format: code:days1:amount1:days2:amount2:... (odd ≥ 5)
            elif len(fragments) >= 5 and len(fragments) % 2 == 1:
                code = fragments[0]
                pairs = [
                    (fragments[i], fragments[i + 1])
                    for i in range(1, len(fragments), 2)
                ]
            else:
                continue

            normalized_code = code.lower()
            spec = PAYMENT_PRODUCT_PLAN_SPECS.get(normalized_code)
            if spec is None or normalized_code in seen_codes:
                continue

            title, description, squad_groups = spec
            use_duration_suffix = len(pairs) > 1
            for raw_days, raw_amount in pairs:
                if not raw_days.isdigit() or not raw_amount.isdigit():
                    continue

                days = int(raw_days)
                amount_rub = int(raw_amount)
                if days <= 0 or amount_rub <= 0:
                    continue

                plan_code = f"{normalized_code}_{days}" if use_duration_suffix else normalized_code
                values.append(
                    PaymentPlanSettings(
                        code=plan_code,
                        days=days,
                        amount_rub=amount_rub,
                        title=title,
                        description=description,
                        squad_groups=squad_groups,
                    )
                )

            seen_codes.add(normalized_code)

        return values

    @staticmethod
    def _compact_text(raw_value: str, *, max_length: int = 80) -> str:
        normalized = " ".join(raw_value.split()).strip()
        if not normalized:
            return "-"
        if len(normalized) <= max_length:
            return normalized
        return f"{normalized[: max_length - 3]}..."

    @property
    def support_ticket_chat_id(self) -> int | None:
        parsed_chat_id, _ = Settings._parse_private_topic_link(self.bot_support_ticket_link)
        return parsed_chat_id if parsed_chat_id is not None else self.fallback_support_ticket_chat_id

    @property
    def support_ticket_message_thread_id(self) -> int | None:
        _, parsed_thread_id = Settings._parse_private_topic_link(self.bot_support_ticket_link)
        return (
            parsed_thread_id
            if parsed_thread_id is not None
            else self.fallback_support_ticket_thread_id
        )

    @property
    def payment_review_chat_id(self) -> int | None:
        parsed_chat_id, _ = Settings._parse_private_topic_link(self.payment_review_link)
        return parsed_chat_id if parsed_chat_id is not None else self.fallback_payment_review_chat_id

    @property
    def payment_review_message_thread_id(self) -> int | None:
        _, parsed_thread_id = Settings._parse_private_topic_link(self.payment_review_link)
        return (
            parsed_thread_id
            if parsed_thread_id is not None
            else self.fallback_payment_review_thread_id
        )

    @property
    def broadcast_results_chat_id(self) -> int | None:
        parsed_chat_id, _ = Settings._parse_private_topic_link(self.broadcast_results_link)
        return (
            parsed_chat_id
            if parsed_chat_id is not None
            else self.fallback_broadcast_results_chat_id
        )

    @property
    def broadcast_results_message_thread_id(self) -> int | None:
        _, parsed_thread_id = Settings._parse_private_topic_link(self.broadcast_results_link)
        return (
            parsed_thread_id
            if parsed_thread_id is not None
            else self.fallback_broadcast_results_thread_id
        )


class StaticBotConfigService:
    def __init__(self, settings: Settings) -> None:
        self._settings = settings

    async def get_snapshot(self) -> BotConfigSnapshot:
        return BotConfigSnapshot(
            bot_brand_name=self._settings.bot_brand_name,
            bot_public_username=self._settings.bot_public_username,
            bot_support_url=self._settings.bot_support_url,
            bot_terms_url=self._settings.bot_terms_url,
            bot_support_ticket_link=self._settings.bot_support_ticket_link,
            payment_review_link=self._settings.payment_review_link,
            broadcast_results_link=self._settings.broadcast_results_link,
            bot_start_image_enabled=self._settings.bot_start_image_enabled,
            bot_start_image_path=self._settings.bot_start_image_path,
            referral_enabled=True,
            referral_discount_percent=self._settings.referral_discount_percent,
            referral_bonus_days=self._settings.referral_bonus_days,
            payment_plans_raw=self._settings.payment_plans_raw,
            payment_product_plans_raw=self._settings.payment_product_plans_raw,
            payment_transfer_text=self._settings.payment_transfer_text,
            payment_success_text=getattr(
                self._settings,
                "payment_success_text",
                Settings.model_fields["payment_success_text"].default,
            ),
            payment_support_text=self._settings.payment_support_text,
            payment_plan_traffic_limit_gb=self._settings.payment_plan_traffic_limit_gb,
            payment_plan_traffic_reset_period=self._settings.payment_plan_traffic_reset_period,
            notification_enabled=True,
            notification_days_before="1,3",
            notification_check_interval_hours=6,
            payment_internal_squad_uuids_raw=self._settings.payment_internal_squad_uuids_raw,
            payment_vpn_squad_uuids_raw=self._settings.payment_vpn_squad_uuids_raw,
            payment_white_squad_uuids_raw=self._settings.payment_white_squad_uuids_raw,
            payment_external_squad_uuid=self._settings.payment_external_squad_uuid,
            payment_username_prefix=self._settings.payment_username_prefix,
            payment_user_tag=self._settings.payment_user_tag,
            fallback_support_ticket_chat_id=self._settings.support_ticket_chat_id,
            fallback_support_ticket_thread_id=self._settings.support_ticket_message_thread_id,
            fallback_payment_review_chat_id=self._settings.payment_review_chat_id,
            fallback_payment_review_thread_id=self._settings.payment_review_message_thread_id,
            fallback_broadcast_results_chat_id=self._settings.broadcast_results_chat_id,
            fallback_broadcast_results_thread_id=self._settings.broadcast_results_message_thread_id,
        )


class BotConfigService(StaticBotConfigService):
    FIELD_SPECS: dict[str, BotConfigFieldSpec] = {
        "bot_brand_name": BotConfigFieldSpec(
            key="bot_brand_name",
            label="Название бренда",
            section="brand",
            prompt="Введите новое название бренда.",
            description="Используется в главной панели и пользовательских сообщениях.",
            placeholder="OREOL VPN",
        ),
        "bot_public_username": BotConfigFieldSpec(
            key="bot_public_username",
            label="Username бота",
            section="brand",
            prompt="Введите username бота без @. Для очистки отправьте `-`.",
            description="Нужен для генерации реферальных ссылок.",
            placeholder="oreol_vpn_bot",
        ),
        "bot_start_image_enabled": BotConfigFieldSpec(
            key="bot_start_image_enabled",
            label="Стартовая картинка",
            section="brand",
            prompt="Введите `on` или `off`.",
            description="Включает или выключает бренд-картинку в панели и выдаче доступа.",
            placeholder="on",
        ),
        "bot_start_image_path": BotConfigFieldSpec(
            key="bot_start_image_path",
            label="Путь к картинке",
            section="brand",
            prompt="Введите путь к изображению. Для очистки отправьте `-`.",
            description="Можно указать относительный путь от корня проекта.",
            placeholder="assets/main.png",
        ),
        "payment_plans_raw": BotConfigFieldSpec(
            key="payment_plans_raw",
            label="Тарифы и цены",
            section="pricing",
            prompt="Введите тарифы в формате `30:250,180:600,365:1000`.",
            description="Каждая пара - это `дни:цена_в_рублях`.",
            placeholder="30:250,180:600,365:1000",
        ),
        "payment_product_plans_raw": BotConfigFieldSpec(
            key="payment_product_plans_raw",
            label="Типы подписок и цены",
            section="pricing",
            prompt=(
                "Введите типы подписок в формате "
                "`vpn_white:30:450:180:2200:365:4000,white:30:250,vpn:30:200`.\n"
                "Каждый продукт: `код:дни1:цена1:дни2:цена2:...`"
            ),
            description=(
                "Если заполнено, заменяет обычные PAYMENT_PLANS. "
                "Коды: vpn_white, white, vpn. Формат: `код:дни1:цена1[:дни2:цена2:...]`."
            ),
            placeholder="vpn_white:30:450:180:2200:365:4000,white:30:250:180:1200,vpn:30:200:180:1000",
        ),
        "payment_plan_traffic_limit_gb": BotConfigFieldSpec(
            key="payment_plan_traffic_limit_gb",
            label="Лимит трафика, GB",
            section="pricing",
            prompt="Введите лимит трафика в гигабайтах. `0` = безлимит.",
            description="Применяется к тарифам, создаваемым ботом.",
            placeholder="0",
        ),
        "payment_plan_traffic_reset_period": BotConfigFieldSpec(
            key="payment_plan_traffic_reset_period",
            label="Стратегия сброса трафика",
            section="pricing",
            prompt="Введите стратегию, например `NO_RESET`.",
            description="Значение передаётся в Remnawave.",
            placeholder="NO_RESET",
        ),
        "referral_enabled": BotConfigFieldSpec(
            key="referral_enabled",
            label="Реферальная система",
            section="referral",
            prompt="Введите `on` или `off`.",
            description="Включает или выключает ввод рефкода и начисление реферальных бонусов.",
            placeholder="on",
        ),
        "referral_discount_percent": BotConfigFieldSpec(
            key="referral_discount_percent",
            label="Скидка по рефкоду, %",
            section="referral",
            prompt="Введите размер скидки в процентах от 0 до 100.",
            description="Применяется к покупателю перед оплатой.",
            placeholder="5",
        ),
        "referral_bonus_days": BotConfigFieldSpec(
            key="referral_bonus_days",
            label="Бонус рефереру, дней",
            section="referral",
            prompt="Введите количество бонусных дней. `0` отключает бонус.",
            description="Начисляется после подтверждённой оплаты по рефкоду.",
            placeholder="7",
        ),
        "bot_support_url": BotConfigFieldSpec(
            key="bot_support_url",
            label="Ссылка на поддержку",
            section="links",
            prompt="Введите URL поддержки. Для очистки отправьте `-`.",
            description="Кнопка быстрой связи с поддержкой в пользовательской панели.",
            placeholder="https://t.me/your_support_account",
        ),
        "bot_terms_url": BotConfigFieldSpec(
            key="bot_terms_url",
            label="Ссылка на Terms",
            section="links",
            prompt="Введите URL правил использования. Для очистки отправьте `-`.",
            description="Открывается из раздела условий использования.",
            placeholder="https://example.com/terms",
        ),
        "bot_support_ticket_link": BotConfigFieldSpec(
            key="bot_support_ticket_link",
            label="Ссылка на чат тикетов",
            section="links",
            prompt="Введите приватную ссылку на чат/топик тикетов. Для очистки отправьте `-`.",
            description="Формат: `https://t.me/c/<chat>/<topic>/<message>`.",
            placeholder="https://t.me/c/1234567890/2/3",
        ),
        "payment_review_link": BotConfigFieldSpec(
            key="payment_review_link",
            label="Ссылка на очередь оплат",
            section="links",
            prompt="Введите приватную ссылку на чат/топик проверки оплат. Для очистки отправьте `-`.",
            description="Формат: `https://t.me/c/<chat>/<topic>/<message>`.",
            placeholder="https://t.me/c/1234567890/56/57",
        ),
        "broadcast_results_link": BotConfigFieldSpec(
            key="broadcast_results_link",
            label="Ссылка на ответы рассылок",
            section="links",
            prompt="Введите приватную ссылку на чат/топик, куда будут приходить ответы опросов. Для очистки отправьте `-`.",
            description="Формат: `https://t.me/c/<chat>/<topic>/<message>`.",
            placeholder="https://t.me/c/3646494169/67/68",
        ),
        "payment_transfer_text": BotConfigFieldSpec(
            key="payment_transfer_text",
            label="Инструкция по оплате",
            section="texts",
            prompt="Введите текст с реквизитами или инструкцией перевода.",
            description="Показывается пользователю после выбора тарифа.",
            placeholder="Карта 0000 0000 0000 0000; банк OREOL; получатель OREOL VPN",
        ),
        "payment_support_text": BotConfigFieldSpec(
            key="payment_support_text",
            label="Текст поддержки по оплате",
            section="texts",
            prompt="Введите текст, который бот покажет при проблемах с оплатой.",
            description="Используется при отклонении оплаты и ошибках выдачи доступа.",
            placeholder="Если оплата прошла, но доступ не выдался, напишите в поддержку.",
        ),
        "payment_success_text": BotConfigFieldSpec(
            key="payment_success_text",
            label="Текст после покупки",
            section="texts",
            prompt="Введите текст, который бот добавит к сообщению со ссылкой после подтверждения оплаты.",
            description="Показывается покупателю вместе со ссылкой на подписку после выдачи или продления доступа.",
            placeholder="Доступ выдан. Сохраните ссылку и используйте её для подключения.",
        ),
        "payment_internal_squad_uuids_raw": BotConfigFieldSpec(
            key="payment_internal_squad_uuids_raw",
            label="UUID внутренних групп",
            section="provisioning",
            prompt="Введите UUID внутренних групп через запятую.",
            description="Эти группы назначаются пользователю в Remnawave.",
            placeholder="11111111-1111-1111-1111-111111111111",
        ),
        "payment_vpn_squad_uuids_raw": BotConfigFieldSpec(
            key="payment_vpn_squad_uuids_raw",
            label="UUID VPN-групп",
            section="provisioning",
            prompt="Введите UUID squad для тарифа VPN через запятую.",
            description="Назначается тарифам `vpn` и `vpn_white`.",
            placeholder="11111111-1111-1111-1111-111111111111",
        ),
        "payment_white_squad_uuids_raw": BotConfigFieldSpec(
            key="payment_white_squad_uuids_raw",
            label="UUID групп белых списков",
            section="provisioning",
            prompt="Введите UUID squad для белых списков через запятую.",
            description="Назначается тарифам `white` и `vpn_white`.",
            placeholder="22222222-2222-2222-2222-222222222222",
        ),
        "payment_external_squad_uuid": BotConfigFieldSpec(
            key="payment_external_squad_uuid",
            label="UUID внешней группы",
            section="provisioning",
            prompt="Введите UUID внешней группы. Для очистки отправьте `-`.",
            description="Необязательный внешний squad для новых пользователей.",
            placeholder="11111111-1111-1111-1111-111111111111",
        ),
        "payment_username_prefix": BotConfigFieldSpec(
            key="payment_username_prefix",
            label="Префикс логина",
            section="provisioning",
            prompt="Введите префикс логина для новых пользователей Remnawave.",
            description="Итоговый логин строится как `<prefix>-<telegram_id>-<name>`.",
            placeholder="Oreol",
        ),
        "payment_user_tag": BotConfigFieldSpec(
            key="payment_user_tag",
            label="Tag пользователей",
            section="provisioning",
            prompt="Введите tag для создаваемых пользователей. Для очистки отправьте `-`.",
            description="Опциональный tag в Remnawave.",
            placeholder="BOT",
        ),
        "notification_enabled": BotConfigFieldSpec(
            key="notification_enabled",
            label="Уведомления об истечении",
            section="notifications",
            prompt="Введите `on` или `off`.",
            description="Включает или выключает уведомления о скором истечении подписки.",
            placeholder="on",
        ),
        "notification_days_before": BotConfigFieldSpec(
            key="notification_days_before",
            label="Дни до истечения",
            section="notifications",
            prompt="Введите дни через запятую, например `1,3`.",
            description="За сколько дней до истечения отправлять уведомления.",
            placeholder="1,3",
        ),
        "notification_check_interval_hours": BotConfigFieldSpec(
            key="notification_check_interval_hours",
            label="Интервал проверки, ч",
            section="notifications",
            prompt="Введите интервал проверки в часах.",
            description="Как часто бот проверяет подписки на истечение.",
            placeholder="6",
        ),
    }

    SECTION_LABELS: dict[str, str] = {
        "brand": "Бренд",
        "pricing": "Тарифы",
        "referral": "Рефералка",
        "links": "Ссылки",
        "texts": "Тексты",
        "provisioning": "Provisioning",
        "notifications": "Уведомления",
    }

    def __init__(
        self,
        *,
        settings: Settings,
        session_factory: async_sessionmaker[AsyncSession],
    ) -> None:
        super().__init__(settings)
        self._session_factory = session_factory

    async def get_snapshot(self) -> BotConfigSnapshot:
        overrides = await self._get_overrides()

        return BotConfigSnapshot(
            bot_brand_name=str(overrides.get("bot_brand_name", self._settings.bot_brand_name)),
            bot_public_username=str(overrides.get("bot_public_username", self._settings.bot_public_username)),
            bot_support_url=str(overrides.get("bot_support_url", self._settings.bot_support_url)),
            bot_terms_url=str(overrides.get("bot_terms_url", self._settings.bot_terms_url)),
            bot_support_ticket_link=str(overrides.get("bot_support_ticket_link", self._settings.bot_support_ticket_link)),
            payment_review_link=str(overrides.get("payment_review_link", self._settings.payment_review_link)),
            broadcast_results_link=str(
                overrides.get(
                    "broadcast_results_link",
                    self._settings.broadcast_results_link,
                )
            ),
            bot_start_image_enabled=self._parse_bool(
                overrides.get("bot_start_image_enabled"),
                default=self._settings.bot_start_image_enabled,
            ),
            bot_start_image_path=str(overrides.get("bot_start_image_path", self._settings.bot_start_image_path)),
            referral_enabled=self._parse_bool(
                overrides.get("referral_enabled"),
                default=True,
            ),
            referral_discount_percent=self._parse_int(
                overrides.get("referral_discount_percent"),
                default=self._settings.referral_discount_percent,
            ),
            referral_bonus_days=self._parse_int(
                overrides.get("referral_bonus_days"),
                default=self._settings.referral_bonus_days,
            ),
            payment_plans_raw=str(overrides.get("payment_plans_raw", self._settings.payment_plans_raw)),
            payment_product_plans_raw=str(
                overrides.get(
                    "payment_product_plans_raw",
                    self._settings.payment_product_plans_raw,
                )
            ),
            payment_transfer_text=str(overrides.get("payment_transfer_text", self._settings.payment_transfer_text)),
            payment_success_text=str(
                overrides.get(
                    "payment_success_text",
                    getattr(
                        self._settings,
                        "payment_success_text",
                        Settings.model_fields["payment_success_text"].default,
                    ),
                )
            ),
            payment_support_text=str(overrides.get("payment_support_text", self._settings.payment_support_text)),
            payment_plan_traffic_limit_gb=self._parse_int(
                overrides.get("payment_plan_traffic_limit_gb"),
                default=self._settings.payment_plan_traffic_limit_gb,
            ),
            payment_plan_traffic_reset_period=str(
                overrides.get(
                    "payment_plan_traffic_reset_period",
                    self._settings.payment_plan_traffic_reset_period,
                )
            ),
            payment_internal_squad_uuids_raw=str(
                overrides.get(
                    "payment_internal_squad_uuids_raw",
                    self._settings.payment_internal_squad_uuids_raw,
                )
            ),
            payment_vpn_squad_uuids_raw=str(
                overrides.get(
                    "payment_vpn_squad_uuids_raw",
                    self._settings.payment_vpn_squad_uuids_raw,
                )
            ),
            payment_white_squad_uuids_raw=str(
                overrides.get(
                    "payment_white_squad_uuids_raw",
                    self._settings.payment_white_squad_uuids_raw,
                )
            ),
            payment_external_squad_uuid=str(
                overrides.get(
                    "payment_external_squad_uuid",
                    self._settings.payment_external_squad_uuid,
                )
            ),
            payment_username_prefix=str(
                overrides.get("payment_username_prefix", self._settings.payment_username_prefix)
            ),
            payment_user_tag=str(overrides.get("payment_user_tag", self._settings.payment_user_tag)),
            notification_enabled=self._parse_bool(
                overrides.get("notification_enabled"),
                default=True,
            ),
            notification_days_before=str(overrides.get("notification_days_before", "1,3")),
            notification_check_interval_hours=self._parse_int(
                overrides.get("notification_check_interval_hours"),
                default=6,
            ),
            fallback_support_ticket_chat_id=self._settings.support_ticket_chat_id,
            fallback_support_ticket_thread_id=self._settings.support_ticket_message_thread_id,
            fallback_payment_review_chat_id=self._settings.payment_review_chat_id,
            fallback_payment_review_thread_id=self._settings.payment_review_message_thread_id,
            fallback_broadcast_results_chat_id=self._settings.broadcast_results_chat_id,
            fallback_broadcast_results_thread_id=self._settings.broadcast_results_message_thread_id,
        )

    async def update_setting(
        self,
        *,
        key: str,
        raw_value: str,
        updated_by_telegram_id: int,
    ) -> str:
        spec = self.FIELD_SPECS.get(key)
        if spec is None:
            raise ValueError("Неизвестная настройка.")

        normalized_value = self._normalize_value(key, raw_value)

        async with self._session_factory() as session:
            record = await session.scalar(
                select(BotSetting).where(BotSetting.key == key)
            )
            if record is None:
                record = BotSetting(key=key)
                session.add(record)

            record.value = normalized_value
            record.updated_by_telegram_id = updated_by_telegram_id
            record.updated_at = utcnow()
            await session.commit()

        return normalized_value

    @classmethod
    def get_spec(cls, key: str) -> BotConfigFieldSpec | None:
        return cls.FIELD_SPECS.get(key)

    @classmethod
    def get_specs_for_section(cls, section: str) -> list[BotConfigFieldSpec]:
        return [spec for spec in cls.FIELD_SPECS.values() if spec.section == section]

    @classmethod
    def get_sections(cls) -> list[tuple[str, str]]:
        return list(cls.SECTION_LABELS.items())

    @classmethod
    def get_section_label(cls, section: str) -> str:
        return cls.SECTION_LABELS.get(section, section)

    @classmethod
    def format_value(cls, *, snapshot: BotConfigSnapshot, key: str) -> str:
        value = getattr(snapshot, key, "")

        if key in {"bot_start_image_enabled", "referral_enabled", "notification_enabled"}:
            return "on" if bool(value) else "off"

        if key == "bot_public_username":
            normalized = snapshot.bot_public_username_normalized
            return f"@{normalized}" if normalized else "-"

        if key in {
            "bot_support_url",
            "bot_terms_url",
            "bot_support_ticket_link",
            "payment_review_link",
            "broadcast_results_link",
            "bot_start_image_path",
            "payment_product_plans_raw",
            "payment_vpn_squad_uuids_raw",
            "payment_white_squad_uuids_raw",
            "payment_external_squad_uuid",
            "payment_user_tag",
            "payment_username_prefix",
            "payment_internal_squad_uuids_raw",
            "payment_vpn_squad_uuids_raw",
            "payment_white_squad_uuids_raw",
            "payment_plans_raw",
            "payment_product_plans_raw",
            "payment_plan_traffic_reset_period",
            "bot_brand_name",
            "notification_days_before",
        }:
            return cls._compact_text(str(value))

        if key == "payment_plan_traffic_limit_gb":
            amount = int(value or 0)
            return "0 GB (unlimited)" if amount <= 0 else f"{amount} GB"

        if key == "referral_discount_percent":
            return f"{max(int(value or 0), 0)}%"

        if key == "referral_bonus_days":
            return f"{max(int(value or 0), 0)} days"

        if key == "notification_check_interval_hours":
            return f"{max(int(value or 6), 1)} ч"

        if key in {"payment_transfer_text", "payment_success_text", "payment_support_text"}:
            return cls._compact_text(str(value), max_length=120)

        return cls._compact_text(str(value))

    @staticmethod
    def _compact_text(raw_value: str, *, max_length: int = 80) -> str:
        normalized = " ".join(raw_value.split()).strip()
        if not normalized:
            return "-"
        if len(normalized) <= max_length:
            return normalized
        return f"{normalized[: max_length - 3]}..."

    async def _get_overrides(self) -> dict[str, str]:
        async with self._session_factory() as session:
            rows = await session.execute(select(BotSetting))
            return {
                row.key: row.value
                for row in rows.scalars().all()
            }

    def _normalize_value(self, key: str, raw_value: str) -> str:
        value = raw_value.strip()
        if key in {
            "bot_support_url",
            "bot_terms_url",
            "bot_support_ticket_link",
            "payment_review_link",
            "broadcast_results_link",
            "bot_start_image_path",
            "payment_external_squad_uuid",
            "payment_user_tag",
            "bot_public_username",
        } and value == "-":
            return ""

        if key == "bot_brand_name":
            if not value:
                raise ValueError("Название бренда не может быть пустым.")
            return value[:80]

        if key == "bot_public_username":
            return value.removeprefix("@")

        if key == "bot_start_image_enabled":
            return "true" if self._parse_bool_token(value) else "false"

        if key == "bot_start_image_path":
            return value

        if key == "payment_plans_raw":
            plans = self._parse_payment_plans(value)
            return ",".join(f"{plan.days}:{plan.amount_rub}" for plan in plans)

        if key == "payment_product_plans_raw":
            plans = self._parse_product_plans(value)
            # Re-serialize: group plans by product code prefix
            product_groups: dict[str, list[PaymentPlanSettings]] = {}
            for plan in plans:
                # Extract product code from plan code (e.g. "vpn_white" from "vpn_white_30")
                product_code = "_".join(plan.code.split("_")[:-1]) if "_" in plan.code and plan.code.split("_")[-1].isdigit() else plan.code
                product_groups.setdefault(product_code, []).append(plan)
            parts = []
            for product_code, group_plans in product_groups.items():
                pairs = ":".join(f"{p.days}:{p.amount_rub}" for p in group_plans)
                parts.append(f"{product_code}:{pairs}")
            return ",".join(parts)

        if key == "payment_plan_traffic_limit_gb":
            amount = self._parse_non_negative_int(value, "Лимит трафика")
            return str(amount)

        if key == "payment_plan_traffic_reset_period":
            if not value:
                raise ValueError("Стратегия сброса трафика не может быть пустой.")
            return value.upper()

        if key == "referral_enabled":
            return "true" if self._parse_bool_token(value) else "false"

        if key == "referral_discount_percent":
            amount = self._parse_non_negative_int(value, "Размер скидки")
            if amount > 100:
                raise ValueError("Размер скидки должен быть в диапазоне от 0 до 100.")
            return str(amount)

        if key == "referral_bonus_days":
            amount = self._parse_non_negative_int(value, "Размер бонуса")
            return str(amount)

        if key in {
            "bot_support_url",
            "bot_terms_url",
            "bot_support_ticket_link",
            "payment_review_link",
            "broadcast_results_link",
        }:
            return value

        if key in {"payment_transfer_text", "payment_success_text", "payment_support_text"}:
            if not value:
                raise ValueError("Текст не может быть пустым.")
            return value[:4000]

        if key == "payment_internal_squad_uuids_raw":
            normalized_parts = [part.strip() for part in value.split(",") if part.strip()]
            if not normalized_parts:
                raise ValueError("Нужно указать хотя бы один UUID внутренней группы.")
            return ",".join(normalized_parts)

        if key in {"payment_vpn_squad_uuids_raw", "payment_white_squad_uuids_raw"}:
            normalized_parts = [part.strip() for part in value.split(",") if part.strip()]
            if not normalized_parts:
                raise ValueError("Нужно указать хотя бы один UUID группы.")
            return ",".join(normalized_parts)

        if key == "payment_external_squad_uuid":
            return value

        if key == "payment_username_prefix":
            if not value:
                raise ValueError("Префикс логина не может быть пустым.")
            return value[:32]

        if key == "payment_user_tag":
            return value

        if key == "notification_enabled":
            return "true" if self._parse_bool_token(value) else "false"

        if key == "notification_days_before":
            parts = [p.strip() for p in value.split(",") if p.strip().isdigit() and int(p.strip()) > 0]
            if not parts:
                raise ValueError("Нужно указать хотя бы один день, например `1,3`.")
            return ",".join(parts)

        if key == "notification_check_interval_hours":
            amount = self._parse_non_negative_int(value, "Интервал проверки")
            if amount < 1:
                raise ValueError("Интервал должен быть не менее 1 часа.")
            return str(amount)

        raise ValueError("Настройка пока не поддерживается.")

    @staticmethod
    def _parse_int(raw_value: object | None, *, default: int) -> int:
        try:
            return int(str(raw_value).strip())
        except (TypeError, ValueError):
            return default

    @staticmethod
    def _parse_bool(raw_value: object | None, *, default: bool) -> bool:
        if raw_value is None:
            return default
        try:
            return BotConfigService._parse_bool_token(str(raw_value))
        except ValueError:
            return default

    @staticmethod
    def _parse_bool_token(raw_value: str) -> bool:
        normalized = raw_value.strip().lower()
        if normalized in {"1", "true", "on", "yes", "y", "да", "вкл"}:
            return True
        if normalized in {"0", "false", "off", "no", "n", "нет", "выкл"}:
            return False
        raise ValueError("Введите `on` или `off`.")

    @staticmethod
    def _parse_non_negative_int(raw_value: str, label: str) -> int:
        if not raw_value.strip().isdigit():
            raise ValueError(f"{label} должен быть целым неотрицательным числом.")
        return int(raw_value.strip())

    @staticmethod
    def _parse_product_plans(raw_value: str) -> list[PaymentPlanSettings]:
        values: list[PaymentPlanSettings] = []
        seen_codes: set[str] = set()

        for raw_part in raw_value.split(","):
            part = raw_part.strip()
            if not part:
                continue

            fragments = [fragment.strip() for fragment in part.split(":")]

            # Legacy: code:amount (2 fields)
            if len(fragments) == 2:
                code, raw_amount = fragments
                pairs = [("30", raw_amount)]
            # Standard: code:days:amount (3 fields)
            elif len(fragments) == 3:
                code = fragments[0]
                pairs = [(fragments[1], fragments[2])]
            # Extended: code:days1:amount1:days2:amount2:... (odd ≥ 5)
            elif len(fragments) >= 5 and len(fragments) % 2 == 1:
                code = fragments[0]
                pairs = [
                    (fragments[i], fragments[i + 1])
                    for i in range(1, len(fragments), 2)
                ]
            else:
                raise ValueError(
                    "Типы подписок: `код:дни1:цена1[:дни2:цена2:...]`, "
                    "например `vpn_white:30:450:180:2200,white:30:250,vpn:30:200`."
                )

            normalized_code = code.lower()
            spec = PAYMENT_PRODUCT_PLAN_SPECS.get(normalized_code)
            if spec is None:
                raise ValueError("Неизвестный код подписки. Доступны: vpn_white, white, vpn.")
            if normalized_code in seen_codes:
                raise ValueError("Коды подписок не должны повторяться.")

            title, description, squad_groups = spec
            use_duration_suffix = len(pairs) > 1
            for raw_days, raw_amount in pairs:
                if not raw_days.isdigit() or not raw_amount.isdigit():
                    raise ValueError("Дни и цена должны быть целыми положительными числами.")

                days = int(raw_days)
                amount_rub = int(raw_amount)
                if days <= 0 or amount_rub <= 0:
                    raise ValueError("У подписок дни и цена должны быть больше нуля.")

                plan_code = f"{normalized_code}_{days}" if use_duration_suffix else normalized_code
                values.append(
                    PaymentPlanSettings(
                        code=plan_code,
                        days=days,
                        amount_rub=amount_rub,
                        title=title,
                        description=description,
                        squad_groups=squad_groups,
                    )
                )

            seen_codes.add(normalized_code)

        if not values:
            raise ValueError("Нужно указать хотя бы один тип подписки.")

        return values

    @staticmethod
    def _parse_payment_plans(raw_value: str) -> list[PaymentPlanSettings]:
        values: list[PaymentPlanSettings] = []
        for raw_part in raw_value.split(","):
            part = raw_part.strip()
            if not part:
                continue

            fragments = [fragment.strip() for fragment in part.split(":")]
            if len(fragments) != 2 or not fragments[0].isdigit() or not fragments[1].isdigit():
                raise ValueError(
                    "Тарифы должны быть в формате `дни:цена`, например `30:250,180:600`."
                )

            days = int(fragments[0])
            amount_rub = int(fragments[1])
            if days <= 0 or amount_rub <= 0:
                raise ValueError("У тарифов дни и цена должны быть больше нуля.")

            values.append(
                PaymentPlanSettings(
                    code=f"{days}d",
                    days=days,
                    amount_rub=amount_rub,
                )
            )

        if not values:
            raise ValueError("Нужно указать хотя бы один тариф.")

        return sorted(values, key=lambda item: item.days)
