from __future__ import annotations

from dataclasses import dataclass
from functools import cached_property
from urllib.parse import quote_plus, urlparse

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


@dataclass(frozen=True, slots=True)
class PaymentPlanSettings:
    code: str
    days: int
    amount_rub: int
    title: str = ""
    description: str = ""
    squad_groups: tuple[str, ...] = ()


PAYMENT_PRODUCT_PLAN_SPECS: dict[str, tuple[str, str, tuple[str, ...]]] = {
    "vpn_white": (
        "VPN + Белые списки",
        "VPN и белые списки в одном тарифе",
        ("vpn", "white"),
    ),
    "white": (
        "Белые списки",
        "Доступ только к белым спискам",
        ("white",),
    ),
    "vpn": (
        "VPN",
        "Доступ только к VPN без белых списков",
        ("vpn",),
    ),
}


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    bot_token: str = Field(validation_alias="BOT_TOKEN")
    bot_admin_ids_raw: str = Field(default="", validation_alias="BOT_ADMIN_IDS")
    bot_moderator_ids_raw: str = Field(default="", validation_alias="BOT_MODERATOR_IDS")
    telegram_proxy_url: str = Field(default="", validation_alias="TELEGRAM_PROXY_URL")
    bot_brand_name: str = Field(default="OREOL VPN", validation_alias="BOT_BRAND_NAME")
    bot_public_username: str = Field(default="", validation_alias="BOT_PUBLIC_USERNAME")
    bot_support_url: str = Field(default="", validation_alias="BOT_SUPPORT_URL")
    bot_terms_url: str = Field(default="", validation_alias="BOT_TERMS_URL")
    bot_support_ticket_link: str = Field(default="", validation_alias="BOT_SUPPORT_TICKET_LINK")
    bot_support_ticket_chat_id_raw: str = Field(
        default="",
        validation_alias="BOT_SUPPORT_TICKET_CHAT_ID",
    )
    bot_support_ticket_thread_id: int = Field(
        default=0,
        validation_alias="BOT_SUPPORT_TICKET_THREAD_ID",
    )
    bot_start_image_enabled: bool = Field(
        default=True,
        validation_alias="BOT_START_IMAGE_ENABLED",
    )
    bot_start_image_path: str = Field(default="", validation_alias="BOT_START_IMAGE_PATH")

    remnawave_base_url: str = Field(validation_alias="REMNAWAVE_BASE_URL")
    remnawave_api_token: str = Field(default="", validation_alias="REMNAWAVE_API_TOKEN")
    remnawave_caddy_api_key: str = Field(default="", validation_alias="REMNAWAVE_CADDY_API_KEY")
    remnawave_timeout_seconds: float = Field(
        default=20.0,
        validation_alias="REMNAWAVE_TIMEOUT_SECONDS",
    )

    db_host: str = Field(default="127.0.0.1", validation_alias="DB_HOST")
    db_port: int = Field(default=3306, validation_alias="DB_PORT")
    db_name: str = Field(default="telegabot", validation_alias="DB_NAME")
    db_user: str = Field(default="telegabot", validation_alias="DB_USER")
    db_password: str = Field(default="", validation_alias="DB_PASSWORD")
    db_echo: bool = Field(default=False, validation_alias="DB_ECHO")
    create_database_on_start: bool = Field(
        default=True,
        validation_alias="CREATE_DATABASE_ON_START",
    )
    create_tables_on_start: bool = Field(
        default=True,
        validation_alias="CREATE_TABLES_ON_START",
    )
    sync_subscription_history: bool = Field(
        default=True,
        validation_alias="SYNC_SUBSCRIPTION_HISTORY",
    )
    sync_batch_size: int = Field(default=100, validation_alias="SYNC_BATCH_SIZE")
    log_level: str = Field(default="INFO", validation_alias="LOG_LEVEL")

    payment_plans_raw: str = Field(
        default="30:250,180:600,365:1000",
        validation_alias="PAYMENT_PLANS",
    )
    payment_product_plans_raw: str = Field(
        default="",
        validation_alias="PAYMENT_PRODUCT_PLANS",
    )
    payment_transfer_text: str = Field(
        default="Переведите оплату по указанным реквизитам и отправьте чек в бот.",
        validation_alias="PAYMENT_TRANSFER_TEXT",
    )
    payment_success_text: str = Field(
        default="Доступ выдан. Сохраните ссылку и используйте её для подключения.",
        validation_alias="PAYMENT_SUCCESS_TEXT",
    )
    payment_review_link: str = Field(default="", validation_alias="PAYMENT_REVIEW_LINK")
    payment_review_chat_id_raw: str = Field(
        default="",
        validation_alias="PAYMENT_REVIEW_CHAT_ID",
    )
    payment_review_thread_id: int = Field(
        default=0,
        validation_alias="PAYMENT_REVIEW_THREAD_ID",
    )
    broadcast_results_link: str = Field(
        default="https://t.me/c/3646494169/67/68",
        validation_alias="BROADCAST_RESULTS_LINK",
    )
    broadcast_results_chat_id_raw: str = Field(
        default="",
        validation_alias="BROADCAST_RESULTS_CHAT_ID",
    )
    broadcast_results_thread_id: int = Field(
        default=0,
        validation_alias="BROADCAST_RESULTS_THREAD_ID",
    )
    referral_discount_percent: int = Field(
        default=5,
        validation_alias="REFERRAL_DISCOUNT_PERCENT",
    )
    referral_bonus_days: int = Field(
        default=7,
        validation_alias="REFERRAL_BONUS_DAYS",
    )
    payment_plan_code: str = Field(default="vpn_30d", validation_alias="PAYMENT_PLAN_CODE")
    payment_plan_title: str = Field(
        default="OREOL VPN на 30 дней",
        validation_alias="PAYMENT_PLAN_TITLE",
    )
    payment_plan_description: str = Field(
        default="Доступ к VPN на 30 дней",
        validation_alias="PAYMENT_PLAN_DESCRIPTION",
    )
    payment_plan_price_stars: int = Field(default=500, validation_alias="PAYMENT_PLAN_PRICE_STARS")
    payment_plan_duration_days: int = Field(
        default=30,
        validation_alias="PAYMENT_PLAN_DURATION_DAYS",
    )
    payment_plan_traffic_limit_gb: int = Field(
        default=0,
        validation_alias="PAYMENT_PLAN_TRAFFIC_LIMIT_GB",
    )
    payment_plan_traffic_reset_period: str = Field(
        default="NO_RESET",
        validation_alias="PAYMENT_PLAN_TRAFFIC_RESET_PERIOD",
    )
    payment_internal_squad_uuids_raw: str = Field(
        default="",
        validation_alias="PAYMENT_INTERNAL_SQUAD_UUIDS",
    )
    payment_vpn_squad_uuids_raw: str = Field(
        default="",
        validation_alias="PAYMENT_VPN_SQUAD_UUIDS",
    )
    payment_white_squad_uuids_raw: str = Field(
        default="",
        validation_alias="PAYMENT_WHITE_SQUAD_UUIDS",
    )
    payment_external_squad_uuid: str = Field(
        default="",
        validation_alias="PAYMENT_EXTERNAL_SQUAD_UUID",
    )
    payment_support_text: str = Field(
        default="Если оплата прошла, но доступ не выдался, напишите в поддержку.",
        validation_alias="PAYMENT_SUPPORT_TEXT",
    )
    payment_username_prefix: str = Field(
        default="Oreol",
        validation_alias="PAYMENT_USERNAME_PREFIX",
    )
    payment_user_tag: str = Field(default="BOT", validation_alias="PAYMENT_USER_TAG")

    @cached_property
    def bot_admin_ids(self) -> set[int]:
        values: set[int] = set()
        for raw_part in self.bot_admin_ids_raw.split(","):
            part = raw_part.strip()
            if part:
                values.add(int(part))
        return values

    @cached_property
    def bot_moderator_ids(self) -> set[int]:
        values: set[int] = set()
        for raw_part in self.bot_moderator_ids_raw.split(","):
            part = raw_part.strip()
            if part:
                values.add(int(part))
        return values

    @property
    def database_url(self) -> str:
        user = quote_plus(self.db_user)
        password = quote_plus(self.db_password)
        return (
            f"mysql+aiomysql://{user}:{password}"
            f"@{self.db_host}:{self.db_port}/{self.db_name}?charset=utf8mb4"
        )

    @property
    def database_server_url(self) -> str:
        user = quote_plus(self.db_user)
        password = quote_plus(self.db_password)
        return (
            f"mysql+aiomysql://{user}:{password}"
            f"@{self.db_host}:{self.db_port}/?charset=utf8mb4"
        )

    @cached_property
    def support_ticket_chat_id(self) -> int | None:
        if self.bot_support_ticket_chat_id_raw.strip():
            return int(self.bot_support_ticket_chat_id_raw.strip())

        parsed_chat_id, _ = self._parse_private_topic_link(self.bot_support_ticket_link)
        return parsed_chat_id

    @cached_property
    def support_ticket_message_thread_id(self) -> int | None:
        if self.bot_support_ticket_thread_id > 0:
            return self.bot_support_ticket_thread_id

        _, parsed_thread_id = self._parse_private_topic_link(self.bot_support_ticket_link)
        return parsed_thread_id

    @cached_property
    def payment_review_chat_id(self) -> int | None:
        if self.payment_review_chat_id_raw.strip():
            return int(self.payment_review_chat_id_raw.strip())

        parsed_chat_id, _ = self._parse_private_topic_link(self.payment_review_link)
        return parsed_chat_id

    @cached_property
    def payment_review_message_thread_id(self) -> int | None:
        if self.payment_review_thread_id > 0:
            return self.payment_review_thread_id

        _, parsed_thread_id = self._parse_private_topic_link(self.payment_review_link)
        return parsed_thread_id

    @cached_property
    def broadcast_results_chat_id(self) -> int | None:
        if self.broadcast_results_chat_id_raw.strip():
            return int(self.broadcast_results_chat_id_raw.strip())

        parsed_chat_id, _ = self._parse_private_topic_link(self.broadcast_results_link)
        return parsed_chat_id

    @cached_property
    def broadcast_results_message_thread_id(self) -> int | None:
        if self.broadcast_results_thread_id > 0:
            return self.broadcast_results_thread_id

        _, parsed_thread_id = self._parse_private_topic_link(self.broadcast_results_link)
        return parsed_thread_id

    @staticmethod
    def _parse_private_topic_link(link: str) -> tuple[int | None, int | None]:
        cleaned = link.strip()
        if not cleaned:
            return None, None

        parsed = urlparse(cleaned)
        if parsed.netloc not in {"t.me", "telegram.me", "www.t.me", "www.telegram.me"}:
            return None, None

        parts = [part for part in parsed.path.split("/") if part]
        if len(parts) < 3 or parts[0] != "c" or not parts[1].isdigit():
            return None, None

        chat_id = int(f"-100{parts[1]}")
        thread_id = int(parts[2]) if len(parts) >= 4 and parts[2].isdigit() else None
        return chat_id, thread_id

    def is_admin(self, telegram_id: int | None) -> bool:
        return telegram_id is not None and telegram_id in self.bot_admin_ids

    def is_moderator(self, telegram_id: int | None) -> bool:
        return telegram_id is not None and telegram_id in self.bot_moderator_ids

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

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

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

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

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

    @cached_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"

    @cached_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

    @cached_property
    def payment_plans(self) -> list[PaymentPlanSettings]:
        product_plans = self._parse_product_plans(
            self.payment_product_plans_raw,
            default_days=max(self.payment_plan_duration_days, 1),
        )
        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,
                )
            )

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

        fallback_amount = max(self.payment_plan_price_stars, 0)
        fallback_days = max(self.payment_plan_duration_days, 0)
        if fallback_amount > 0 and fallback_days > 0:
            return [
                PaymentPlanSettings(
                    code=self.payment_plan_code.strip() or f"{fallback_days}d",
                    days=fallback_days,
                    amount_rub=fallback_amount,
                )
            ]

        return []

    @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) → single duration using default_days
            if len(fragments) == 2:
                code, raw_amount = fragments
                pairs = [(str(default_days), raw_amount)]
            # Standard format: code:days:amount (3 fields) → single duration
            elif len(fragments) == 3:
                code = fragments[0]
                pairs = [(fragments[1], fragments[2])]
            # Extended format: code:days1:amount1:days2:amount2:... (odd number ≥ 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
