"""Минимальный settings для standalone trade-in MVP."""

from typing import Literal

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


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

    # required — задаётся через env DATABASE_URL. Нет дефолта: fail-fast при старте
    # если переменная не задана (C-3 security audit).
    database_url: str

    # In-app asyncio scheduler enable flag (#581).
    # Set SCHEDULER_ENABLE=false when using the systemd timer trigger instead.
    # Default true preserves existing behaviour.
    scheduler_enable: bool = Field(default=True, validation_alias="SCHEDULER_ENABLE")

    cors_origins: list[str] = ["http://localhost", "http://localhost:3000", "http://localhost:8080"]
    environment: str = "dev"

    # Geocoder. Env var name `YANDEX_GEOCODER_API_KEY` — consistent с scripts/
    # backfill_house_coords.py + audit_address_mismatch.py + main backend
    # OpenRouteService_API_KEY pattern. Renamed from YANDEX_GEOCODER_KEY (PR F).
    yandex_geocoder_api_key: str | None = None  # 25K req/day free после регистрации
    yandex_suggest_key: str | None = None  # для frontend autocomplete (proxy через backend)
    # для User-Agent в Nominatim (Nominatim Usage Policy)
    contact_email: str = "erginrajpopxbe@outlook.com"

    # Public URL — для QR-кода в PDF, shareable links, etc.
    public_url: str = "http://127.0.0.1:8080"

    # GlitchTip DSN — мониторинг ошибок (Sentry-совместимый). #396.
    # Пусто = мониторинг выключен (dev). В prod — env GLITCHTIP_DSN из .env.runtime.
    glitchtip_dsn: str | None = None

    # Ключ шифрования для pgp_sym_encrypt (Cian session cookies).
    # Задаётся через env COOKIE_ENCRYPTION_KEY. Пусто = шифрование не работает.
    cookie_encryption_key: str = ""

    # Redis URL для hot-cache (Phase 3.2). Задаётся через env REDIS_URL.
    redis_url: str = "redis://localhost:6379/0"

    # Rate-limit публичного /api/* (per-IP sliding window). ENV: RATE_LIMIT,
    # RATE_LIMIT_WINDOW_S. Не более rate_limit запросов за rate_limit_window_s
    # секунд с одного IP. Аутентифицированный трафик (X-Authenticated-User от
    # Caddy basic_auth) не лимитируется — см. ratelimit.py (#655).
    rate_limit: int = 300
    rate_limit_window_s: float = 60.0

    # Password for tradein_fdw_reader role — used by backend startup to create/refresh
    # USER MAPPING for postgres_fdw → gendesign DB (gendesign_remote server).
    # Пусто = USER MAPPING не создаётся, gendesign_cad_buildings не работает (dev).
    gendesign_fdw_password: str | None = None

    # DaData /clean/address — обогащение target-адреса канонической формой,
    # kadastr_num, ФИАС, координатами, метро. Используется в estimator для
    # on-demand enrichment (PR Q1). Demo tier: 100 req/день. Если хотя бы один
    # не задан — service возвращает None gracefully, estimator продолжает.
    # ENV: DADATA_API_TOKEN, DADATA_API_SECRET.
    dadata_api_token: str | None = None
    dadata_api_secret: str | None = None

    # ── #651: IMV / Yandex blend (killer accuracy fix) ──────────────────────
    # Радиусная медиана ₽/м² системно недооценивает премиум/видовые квартиры
    # (нет class/segment/IMV-коррекции → premium ~2x underestimate, case 50М vs
    # факт ~100М). Если внешний якорь (Avito IMV recommended_price из
    # house_imv_evaluations, либо Yandex sale) выше нашей медианы более чем в
    # `threshold` раз — подмешиваем якорь к медиане с весом `weight` и
    # расширяем верх диапазона. ОДНОНАПРАВЛЕННО: только повышаем (баг — занижение).
    # Полностью за флагами — безопасно выкатить до демо; при отсутствии IMV/Yandex
    # no-op (медиана не меняется).
    estimate_imv_blend_enabled: bool = True
    estimate_imv_blend_weight: float = 0.5  # вес якоря в blend: median*(1-w)+A*w
    estimate_imv_blend_threshold: float = 1.15  # якорь должен быть > медианы ×1.15

    # ── #651/#652 v2: same-building anchor (validated, 55 golden cases) ──────────
    # Радиусная медиана размывает премию дома/ЖК → премиум ~2.5x недооценка,
    # комфорт −15-25%. v2 берёт PRIMARY якорь из комплов ТОГО ЖЕ ДОМА (Tier A),
    # similarity-weighted по площади/комнатам, premium-uplift к ~p70 для топ-юнита
    # дома, asking→sold haircut (banded по ppm²), hard guardrail (est ≥ min-comp×0.95)
    # и tighter FSD-диапазон. Полностью за флагом — OFF ⇒ точно текущее поведение.
    # Спек+KPI: vault inbox 2026-05-30 tradein-valuation-algorithm-v2.
    estimate_same_building_anchor_enabled: bool = True
    estimate_sb_min_comps: int = 4  # стоп на первом тире с ≥ N активных комплов
    estimate_sb_area_sigma: float = 0.18  # σ log-нормального area-веса (Gaussian)
    estimate_sb_rooms_match_boost: float = 1.6  # ×вес если rooms компла == target
    # #680-WB within-building heterogeneity refine: floor-similarity Gaussian по
    # ОТНОСИТЕЛЬНОЙ вертикальной позиции (floor/total_floors). Прижимает якорь к
    # комплам с похожим этажом — мелкокомнатный/нижний юнит во флагман-доме больше
    # не наследует цену видового топ-этажа. 0.0 → выключено (точно старое поведение).
    # Откалибровано на 55 golden (offline): σ_f=0.25 даёт лучший medAPE без потери
    # покрытия; Хохрякова 3к/153 overshoot 64%→1.5%, флагман 4к 17.5%→5.4%.
    estimate_sb_floor_sigma: float = 0.25
    estimate_sb_guardrail_tol: float = 0.05  # hard floor: est ≥ min(comp ppm²)×(1−tol)
    estimate_sb_mad_k: float = 3.5  # MAD-clip: drop comps с |ppm2−median| > k×MAD
    asking_to_sold_haircut: float = 0.05  # дефолтная asking→sold скидка (banded по ppm²)
    estimate_fsd_k: float = 1.65  # множитель FSD → полуширина диапазона

    # ── #764: per-cadastral-quarter price index correction ───────────────────
    # Gap-correction: квартальный индекс применяется ТОЛЬКО в pure-radius пути
    # (когда same-building anchor и IMV-blend не сработали). Корректирует РАЗРЫВ
    # между квартальным уровнем целевого объекта и усреднённым квартальным уровнем
    # аналогов — не дублирует location, уже заложенный в медиану аналогов.
    # Формула: adjusted_ppm2 = base_ppm2 × target_index / avg_analog_index.
    # За флагом: False → точно текущее поведение (backward-compatible).
    estimate_quarter_index_enabled: bool = True
    # Минимальное число сделок в квартале (sparse fallback: меньше — no-op).
    estimate_quarter_index_min_n_deals: int = 10
    # Guard-2 (no double-count): если доля аналогов ИЗ ТОГО ЖЕ квартала > порога —
    # аналоги уже несут локацию квартала → skip (location in median).
    estimate_quarter_match_skip_ratio: float = 0.6
    # Bimodal/nominal guard (backtest 2026-05-31): структурно неоднородные кварталы
    # дают индекс > 2.0 при малой выборке → no-op чтобы избежать регрессию.
    estimate_quarter_index_max_for_small_n: float = 2.0
    estimate_quarter_index_small_n_threshold: int = 50
    # Sanity-clamp на factor = target_index / avg_analog_index (#859).
    # Belt-and-suspenders против патологичных FDW-данных. Нормальные квартальные
    # индексы РФ лежат в [0.6, 1.8]; за этими порогами — артефакт, а не сигнал.
    estimate_quarter_index_factor_min: float = 0.6
    estimate_quarter_index_factor_max: float = 1.8

    # ── Estimate enrichment time-budgets (#654) ──────────────────────────────
    # POST /estimate делает несколько ПОСЛЕДОВАТЕЛЬНЫХ блокирующих сетевых
    # вызовов (geocode → Overpass → Yandex valuation → IMV → Cian). Yandex
    # valuation (внутренний httpx timeout 30s) НЕ gated на наличие floor и
    # выполняется на каждой оценке — главный подозреваемый на gateway-таймаут
    # (Caddy 502/504). Эти budget'ы оборачивают самые медленные ungated-вызовы
    # в asyncio.wait_for(): при превышении источник деградирует в None (тот же
    # graceful-путь что и сетевая ошибка), а НЕ роняет весь /estimate в 5xx.
    # Держать суммарный budget ниже Caddy read/write timeout (см.
    # deploy/Caddyfile.tradein-fragment). ENV: ESTIMATE_YANDEX_VALUATION_TIMEOUT_S,
    # ESTIMATE_GEOCODE_BUDGET_S, ESTIMATE_HOUSE_META_TIMEOUT_S.
    estimate_yandex_valuation_timeout_s: float = 8.0
    estimate_geocode_budget_s: float = 12.0
    estimate_house_meta_timeout_s: float = 8.0

    # Лимит успешных оценок trade-in за календарный месяц на аккаунт (#658).
    # Конфигурируется через env ESTIMATE_QUOTA_LIMIT. Default 15.
    estimate_quota_limit: int = 15

    # Фильтр junk-/премиум-порога для asking→sold derivation (#767).
    # Нижняя граница 30 000 ₽/м² отсекает нежилые/технические сделки; менять не стоит.
    # Верхняя граница — поднята с 600 000 до 1 200 000 ₽/м², чтобы покрыть ЕКБ-premium
    # (>600k). Точное значение стоит сверить с `SELECT max(price_per_m2) FROM deals
    # WHERE source='rosreestr'` на проде — см. QA-note в asking_to_sold_ratio.py.
    # ENV: ASKING_RATIO_PPM2_MAX.
    asking_ratio_ppm2_max: int = 1_200_000

    # ppm2-tier asking->sold lookup (#928) -- gate + empirical-Bayes shrink parameter.
    # tier_aware_ratio_enabled=False (default) -> byte-identical flag-OFF path via
    # asking_to_sold_ratios (old table). True -> tiered lookup from
    # asking_to_sold_ratios_tiered (requires migration 098 applied).
    # ENV: TIER_AWARE_RATIO_ENABLED.
    tier_aware_ratio_enabled: bool = False
    # Empirical-Bayes shrink weight denominator: w = n_deals / (n_deals + K).
    # K=150 -> cell with 150 deals gets w=0.5 (50% own tier ratio, 50% rooms-'all').
    # Higher K -> more shrinkage toward 'all' ratio (conservative); lower -> more tier.
    # ENV: TIER_RATIO_SHRINK_K.
    tier_ratio_shrink_k: int = 150

    # SSRF-защита для admin scrape endpoints (#756).
    # Список хостов которым разрешено передавать абсолютные URL в параметрах *_url.
    # Относительные пути (без netloc) проходят без проверки — хост подставляется
    # фиксированным в самом scraper'е. Задаётся через env SCRAPE_ALLOWED_HOSTS
    # (comma-separated). По умолчанию — все хосты используемые scrapers/*.py.
    scrape_allowed_hosts: set[str] = {
        "www.avito.ru",
        "avito.ru",
        "www.cian.ru",
        "cian.ru",
        "ekb.cian.ru",
        "realty.yandex.ru",
        "ekaterinburg.n1.ru",
        "n1.ru",
        "domclick.ru",
        "www.domclick.ru",
    }

    # ── Scraper mobile proxy (#806) ──────────────────────────────────────────
    # Мобильный прокси (RU, mobileproxy.space) используется ВСЕМИ scraper-сессиями:
    # Avito (#623) + Cian (#806). Datacenter-IP блокируется обоими сайтами.
    # Пусто = прямое подключение (dev/staging без прокси).
    #
    # Приоритет ENV-переменных (precedence):
    #   1. SCRAPER_PROXY_URL — новый общий ENV; когда задан — используется первым.
    #   2. AVITO_PROXY_URL   — legacy ENV; fallback, чтобы prod-серверы с уже
    #      настроенным AVITO_PROXY_URL работали без изменений .env.runtime (#806).
    # property `scraper_proxy_url` реализует эту логику; используй его везде.
    # validation_alias привязывает поле к env SCRAPER_PROXY_URL (без него
    # pydantic-settings читал бы SCRAPER_PROXY_URL_ENV по имени поля — #806 fixup).
    scraper_proxy_url_env: str | None = Field(default=None, validation_alias="SCRAPER_PROXY_URL")
    avito_proxy_url: str | None = None  # ENV: AVITO_PROXY_URL (legacy fallback)

    @property
    def scraper_proxy_url(self) -> str | None:
        """Единый прокси URL для всех scraper-сессий (Avito + Cian).

        Приоритет: SCRAPER_PROXY_URL > AVITO_PROXY_URL > None (прямое подключение).
        Prod-серверы с существующим AVITO_PROXY_URL работают без изменений env.
        """
        return self.scraper_proxy_url_env or self.avito_proxy_url

    # changeip-ссылка mobileproxy: GET меняет мобильный IP за ~9с. Дёргается при
    # детекте бана Avito перед повтором. Пусто = ротация выключена (raise сразу).
    # ENV: AVITO_PROXY_ROTATE_URL.
    avito_proxy_rotate_url: str | None = None
    # Сколько раз сменить IP при блоке прежде чем сдаться (на одну страницу).
    avito_proxy_max_rotations: int = 2

    # ── Cian dedicated mobile proxy (separate egress from Avito) ──────────────
    # Cian и Avito делят один мобильный IP при общем scraper_proxy_url → конкуренция
    # за единственный egress → взаимные таймауты/баны при параллельных прогонах.
    # Отдельный прокси для Cian устраняет contention. Если не задан — fallback на
    # общий scraper_proxy_url (backward-compat). ENV: CIAN_PROXY_URL.
    cian_proxy_url_env: str | None = Field(default=None, validation_alias="CIAN_PROXY_URL")
    # changeip-ссылка для Cian-прокси (ротация IP при бане/таймауте). Если не задан —
    # fallback на avito_proxy_rotate_url. ENV: CIAN_PROXY_ROTATE_URL.
    cian_proxy_rotate_url: str | None = None

    @property
    def cian_proxy_url(self) -> str | None:
        """Прокси для Cian-скраперов. CIAN_PROXY_URL > scraper_proxy_url (fallback)."""
        return self.cian_proxy_url_env or self.scraper_proxy_url

    # ── Yandex dedicated mobile proxy (separate egress from Avito/Cian) ────────
    # Отдельный прокси для Yandex устраняет contention при параллельных прогонах.
    # Если не задан — fallback на общий scraper_proxy_url (backward-compat).
    # ENV: YANDEX_PROXY_URL.
    yandex_proxy_url_env: str | None = Field(default=None, validation_alias="YANDEX_PROXY_URL")
    # changeip-ссылка для Yandex-прокси (ротация IP при капче/таймауте). Если не задан —
    # fallback на avito_proxy_rotate_url. ENV: YANDEX_PROXY_ROTATE_URL.
    yandex_proxy_rotate_url: str | None = None

    @property
    def yandex_proxy_url(self) -> str | None:
        """Прокси для Yandex-скраперов. YANDEX_PROXY_URL > scraper_proxy_url (fallback)."""
        return self.yandex_proxy_url_env or self.scraper_proxy_url

    # ── #759: deactivate stale avito listings ───────────────────────────────
    # Avito-объявления, не виденные scraper'ом более avito_stale_ttl_days дней,
    # помечаются is_active=false ночной задачей deactivate_stale_avito_listings.
    # TTL=10 — баланс между "снятое объявление" (обычно 3-7 дней молчания) и
    # допуском на перерыв в работе scraper'а. ENV: AVITO_STALE_TTL_DAYS.
    avito_stale_ttl_days: int = 10

    # ── Yandex SERP cookies (#801/T4) ───────────────────────────────────────
    # Путь к JSON-файлу с cookies браузера (формат: [{name, value, ...}, ...]).
    # Если задан и файл существует — cookies передаются в curl_cffi-сессию при
    # Yandex SERP-запросах; снижает вероятность captcha на datacenter IP.
    # Пусто / файл не найден = запросы без cookies (не падаем, только warning).
    # ENV: YANDEX_COOKIES_FILE.
    yandex_cookies_file: str | None = None

    # ── #639: Cian browser auto-login (Variant B) ────────────────────────────
    # Провалидировано вживую 2026-05-31: email+пароль, без SMS/капчи. Флоу 2-шаговый
    # (после 1-го сабмита экран «Введите пароль» → повтор). Селекторы env-overridable.
    cian_login_email: str | None = None
    cian_login_password: str | None = None
    cian_login_url: str = "https://ekb.cian.ru/"
    # Последовательность кликов до формы: открыть модалку → (опц.) другой аккаунт →
    # переключить на email-вход. AnotherAccountBtn на fresh headless отсутствует (скипнется).
    cian_login_pre_click_selectors: list[str] = [
        "[data-name='LoginButton']",
        "[data-name='AnotherAccountBtn']",
        "[data-name='SwitchToEmailAuthBtn']",
    ]
    cian_login_email_selector: str = "input[name='username']"
    cian_login_password_selector: str = "input[name='password']"
    cian_login_submit_selector: str = "button[data-name='ContinueAuthBtn']"
    cian_login_success_cookie: str = "DMIR_AUTH"
    cian_login_wait_ms: int = 4000

    # ── #884/#905: BrowserFetcher — HTTP-клиент к tradein-browser контейнеру ────
    # scraper_fetch_mode: "curl_cffi" (дефолт, текущее поведение) или "browser"
    # (HTTP POST к tradein-browser /fetch эндпоинту). Phase 1+.
    # Пока дефолт "curl_cffi" — никаких изменений в рантайме.
    # ENV: SCRAPER_FETCH_MODE.
    scraper_fetch_mode: Literal["curl_cffi", "browser"] = "curl_cffi"
    # HTTP-эндпоинт tradein-browser сервиса. В Docker-сети — имя сервиса из compose.
    # ENV: BROWSER_HTTP_ENDPOINT.
    browser_http_endpoint: str = "http://tradein-browser:3000"
    # Сколько страниц обработать в одном browser-сеансе перед перезапуском браузера
    # (ограничение утечек памяти). Читается сервером из env BROWSER_RECYCLE_PAGES.
    # Оставлено для справки / compat. ENV: BROWSER_RECYCLE_PAGES.
    browser_recycle_pages: int = 15
    # Таймаут навигации (page.goto) в мс. Читается сервером из env BROWSER_NAV_TIMEOUT_MS.
    # ENV: BROWSER_NAV_TIMEOUT_MS.
    browser_nav_timeout_ms: int = 60000
    # Ожидание после DOMContentLoaded для JS-гидрации в мс. Читается сервером.
    # ENV: BROWSER_WAIT_MS.
    browser_wait_ms: int = 6000


settings = Settings()
