"""Конфигурация приложения, загруженная из переменных окружения / файла .env."""

from functools import lru_cache

from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from core.plugins.config import AiLevel


class Settings(BaseSettings):
    """Центральные параметры приложения.

    Значения читаются из переменных окружения (или файла `.env`).
    """

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

    database_url: str = "postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf"
    redis_url: str = "redis://localhost:6379/0"
    plugins_config_path: str = "../config/plugins.yaml"

    # --- Пул соединений с БД ---
    # Дефолт SQLAlchemy (5 + 10) на нагрузочном тесте 28.07.2026 выгребался
    # за секунды: 226 ошибок `QueuePool limit of size 5 overflow 10 reached`
    # и 37 ответов 500 на путях входа в конференцию.
    #
    # ⚠️ Бюджет соединений считается на ВЕСЬ инстанс, а не на процесс: каждый
    # воркер uvicorn (`UVICORN_WORKERS`) держит собственный пул, плюс
    # соединения нужны Celery-воркерам и alembic при миграциях. При
    # `max_connections=100` у Postgres и двух воркерах 2 × (10 + 10) = 40
    # оставляет запас. Поднимая значения на более крупном сервере, поднимайте
    # и `max_connections` — иначе вместо понятной ошибки приложения получите
    # отказ Postgres, который диагностируется куда хуже.
    db_pool_size: int = 10
    db_max_overflow: int = 10
    # 10 секунд вместо дефолтных 30 — сознательно: пусть запрос падает быстро
    # и показывает проблему, а не висит полминуты, делая вид, что всё живо.
    db_pool_timeout: int = 10

    # --- Проверка доступности БД вне основного пула (`core/db.py::check_db_up`) ---
    # Таймаут TCP/auth отдельного соединения-пробы (не путать с
    # `db_pool_timeout` выше — тот про очередь на основной пул). Дефолт
    # asyncpg — 60с, для сигнала мониторинга это неприемлемо долго: пусть
    # `vidconf_db_up` станет 0 за секунды, а не через минуту.
    db_probe_timeout_s: float = 3.0

    # --- Пул соединений с Redis ---
    # Считается по УЧАСТНИКАМ, а не по запросам: каждое WS-подключение комнаты
    # (`api/chat.py`) держит собственное pub/sub-соединение всё время, пока
    # человек сидит в конференции, — и берёт его из этого же пула, что и
    # обычные команды. redis-py 8 поставил дефолт `max_connections=100`
    # (раньше предел был условно бесконечным), поэтому сотый участник на
    # воркер выгребал пул досуха и WS падал уже на `hgetall` очереди рук —
    # воспроизведено локально при 99 одновременных подключениях.
    # 500 — с запасом на инстанс, рассчитанный на пару сотен участников
    # на воркер; соединения создаются по мере надобности, само по себе
    # значение ничего не стоит. Потолок сверху — `maxclients` у Redis
    # (дефолт 10000) на ВСЕ процессы вместе, включая Celery-воркеры.
    redis_max_connections: int = 500

    # --- Версия инстанса (релиз v0.0.1) ---
    # install.sh копирует значение из файла `VERSION` (корень репозитория) в
    # `.env` при каждой установке/обновлении — здесь только чтение готового
    # значения. Отдаётся в `GET /api/health` (футер админки, Блок 4).
    vidconf_version: str = "0.0.0"

    # Домен не должен попадать в список special-use/reserved (RFC 6761,
    # напр. `.local`/`.test`): email-validator (`EmailStr`) их отклоняет, а
    # раньше это ловилось и на выходе — старый дефолт `admin@vidconf.local`
    # ронял `GET /users/me` 500 `ResponseValidationError`, пока `UserOut.email`
    # был `EmailStr`. `.example` (RFC 2606) email-validator пропускает.
    seed_admin_email: str = "admin@vidconf.example"
    seed_admin_password: str = "change-me"

    # --- Auth (JWT + email-подтверждение) ---
    jwt_secret: str = "dev-only-insecure-secret-change-me"
    access_token_ttl_minutes: int = 15
    refresh_token_ttl_days: int = 14
    email_verification_ttl_hours: int = 24
    frontend_url: str = "http://localhost:5173"
    # Флаг Secure для refresh-cookie. false нужен только для dev по
    # http://localhost (Safari, в отличие от Chrome, не сохраняет
    # Secure-cookie без HTTPS); в проде обязательно true.
    auth_cookie_secure: bool = True

    # --- LiveKit ---
    livekit_api_key: str = "devkey"
    livekit_api_secret: str = "change-me-livekit-secret"
    livekit_public_url: str = "ws://localhost:7880"
    # Внутренний server-to-server URL для вызовов LiveKit RoomService (Celery
    # maintenance-задача); в отличие от `livekit_public_url` не проксируется
    # через nginx/TLS для браузера. LiveKit SDK сам нормализует ws:// в http://.
    livekit_url: str = "ws://localhost:7880"

    # --- Пайплайн транскрибации ---
    # Общий volume между LiveKit Egress и celery-воркером `transcription`
    # (см. `deploy/docker-compose.yml`); в тестах переопределяется на `tmp_path`.
    recordings_dir: str = "/recordings"

    # Таймаут запуска Track Egress. Когда egress-сервиса в деплое нет (профиль
    # `transcribe` не поднят), LiveKit ждёт ответа воркера через Redis до
    # собственного таймаута psrpc — на тесте 28.07.2026 это давало по 20–25
    # секунд на каждый вызов. Ждать столько бессмысленно: если egress жив, он
    # отвечает за доли секунды.
    egress_start_timeout_s: float = 3.0

    # --- Email (SMTP-бэкенд) ---
    # `console` — дефолт для dev (письмо только логируется); `smtp` — реальная
    # отправка через aiosmtplib. Секреты SMTP — только в `.env` (инвариант №6),
    # переключатель бэкенда — тоже переменная окружения, а не настройка в БД
    # (`instance_settings`).
    email_backend: str = "console"
    smtp_host: str = "localhost"
    smtp_port: int = 587
    smtp_username: str | None = None
    smtp_password: str | None = None
    smtp_start_tls: bool = True
    smtp_use_tls: bool = False
    smtp_from: str = "VidConf <no-reply@vidconf.example>"
    smtp_timeout_s: int = 30

    # --- Медиа (аватары пользователей) ---
    # Каталог, куда сохраняются загруженные файлы (аватары — `avatars/{user_id}.{ext}`);
    # раздаётся статикой по `/media` (`main.py`, dev) либо через nginx `location /media/`
    # в проде (`deploy/nginx/nginx.conf`, volume `media`). Относительный путь по
    # умолчанию — рабочая директория backend (аналог `recordings_dir`, но без
    # требования root для локального запуска вне Docker).
    media_root: str = "media"

    # --- Автодетект железа: install.sh определяет `nproc`/`free -m`/
    # `nvidia-smi` и пишет в `.env`; читает `services/ai_levels.py` для детекта
    # доступности уровней AI (ADR-004) без torch/nvidia-smi внутри процесса
    # backend/воркеров. `None` — install.sh не запускался (dev-окружение) либо
    # GPU не обнаружен (`hw_gpu_name`/`hw_vram_mb`).
    hw_cpus: int | None = None
    hw_ram_mb: int | None = None
    hw_gpu_name: str | None = None
    hw_vram_mb: int | None = None

    # --- Матрица «пресет → настройки» инсталлятора: install.sh пишет эти три
    # переменные в `.env` по выбранному пресету (1–5), lifespan backend
    # передаёт их бутстрапу `instance_settings` (`services/instance_settings.py`,
    # `bootstrap_overrides_from_settings`) как overrides дефолтов
    # `plugins.yaml` — БЕЗ этого механизма бутстрап всегда включал чат и
    # AI-модули независимо от пресета. `None` — install.sh не запускался
    # (dev-окружение) либо переменная не установлена для этого пресета:
    # бутстрап тогда использует дефолты `plugins.yaml` как раньше.
    bootstrap_chat_enabled: bool | None = None
    bootstrap_transcription_enabled: bool | None = None
    bootstrap_ai_level: AiLevel | None = None

    @field_validator(
        "hw_cpus",
        "hw_ram_mb",
        "hw_gpu_name",
        "hw_vram_mb",
        "bootstrap_chat_enabled",
        "bootstrap_transcription_enabled",
        "bootstrap_ai_level",
        mode="before",
    )
    @classmethod
    def _empty_hw_string_to_none(cls, value: object) -> object:
        """Пустая строка env (`KEY=`, а не отсутствие переменной) → `None`.

        `docker-compose` подставляет `env_file` дословно: `HW_VRAM_MB=` в `.env`
        (пишет `install.sh` на любой машине без NVIDIA GPU, пресеты 1–4;
        `.env.example` — все четыре `HW_*` пустыми по умолчанию) превращается в
        переменную окружения со значением `""`, а не в отсутствующую переменную —
        без этой нормализации pydantic не парсит `""` как `int` и роняет
        `Settings()` уже на импорте модуля (`main.py`, `workers/celery_app.py`),
        не давая контейнеру стартовать. Та же проблема для `BOOTSTRAP_*`
        (`.env.example` — пустыми по умолчанию, install.sh заполняет по пресету).
        """
        if value == "":
            return None
        return value


@lru_cache
def get_settings() -> Settings:
    """Вернуть кэшированный экземпляр `Settings`."""
    return Settings()
