"""Общий фундамент для `providers/{avito,cian,yandex,domclick}/*` (#2358, Foundation).

Извлечено по факту чтения ~13k строк providers/* (issue #2358, эпик #2277 F4).
Три РЕАЛЬНО повторяющихся паттерна:

1. **`BrowserFetcher(...)` конструировался независимо в 6+ местах**
   (`avito/serp.py:389`, `cian/serp.py:142`, `cian/newbuilding.py:147`,
   `yandex/serp.py:510`, `yandex/newbuilding.py:189,354`, `domclick/serp.py:319`),
   каждый раз вручную собирая `endpoint=config.browser_http_endpoint` +
   `proxy_provider=`/`use_pool=config.use_proxy_pool_browser`. Это ИМЕННО источник
   class-бага #2322/#2330: `endpoint=`/`config=` — либо optional и молча теряет
   поведение (proxy pool не подключается), либо забыт целиком (`TypeError` на
   вызове, как было в `yandex/cian.newbuilding` до #2322). `build_browser_fetcher()`
   принимает `config: ScraperConfig` **mandatory** (без default) — пропустить
   endpoint/use_pool технически невозможно, баг-класс закрыт структурно, а не
   точечным патчем.

2. **curl_cffi `AsyncSession(impersonate="chrome120", proxies=..., headers=...)`
   дублировался почти дословно** в `avito/serp.py::_build_cffi_session` (404-428),
   `avito/detail.py::_build_detail_session` (256-283, ИДЕНТИЧНЫЙ headers-dict),
   `avito/imv.py` (headers=_DOC_HEADERS, тот же dict третий раз), `cian/detail.py`
   (115-127), `cian/newbuilding.py` (дважды: 651-663 и 757-769), `cian/session.py`
   (117-128). Общий кусок вынесен в `build_curl_cffi_session` /
   `build_document_session`; идиома `{"http": url, "https": url} if url else None`
   (тот же 6+ раз) — в `http_proxies()`.

3. **"Document" browser-headers** (`Accept`/`Accept-Language`/`Cache-Control`/
   `Sec-Fetch-*`/`Upgrade-Insecure-Requests`) — идентичный dict в `avito/serp.py`,
   `avito/detail.py`, `avito/imv.py` (там уже был назван `_DOC_HEADERS`). Вынесен
   в `DOCUMENT_HEADERS` — единая точка правды.

**Осознанно НЕ извлечено (см. issue #2358 отчёт):** retry/backoff-циклы
(`yandex/serp.py::fetch_around` tarpit-retry — rotate_ip+sleep(2) на status==0/
JSON-error; `avito/serp.py::_fetch_serp_html_with_retry` — page-level backoff
на generic Exception, кроме `AvitoBlockedError`/`AvitoRateLimitedError`;
`BaseScraper._http_get` — tenacity-декоратор). Все три семантически РАЗНЫЕ:
разные множества исключений, разное действие между попытками (IP-ротация vs
голый sleep vs tenacity), разная политика "что не ретраить". Общий helper
проглотил бы эти различия (leaky abstraction) — риск больше выгоды при текущей
похожести. Кандидат на пересмотр, если появится 4-й похожий цикл.

**Scope:** этот модуль — НОВЫЙ, аддитивный. Existing `providers/{avito,cian,
yandex,domclick}/*.py` НЕ переведены на него (миграция call-site'ов — отдельные
future issues F4a-d). Здесь только новый shared-слой + тесты, 0 изменений в
провайдерах.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from curl_cffi.requests import AsyncSession

from scraper_kit.browser_fetcher import BrowserFetcher

if TYPE_CHECKING:
    from scraper_kit.contracts import ProxyProvider, ScraperConfig

logger = logging.getLogger(__name__)

# curl_cffi impersonate-профиль, используемый ВСЕМИ providers без исключения
# (TLS ClientHello настоящего Chrome — обход fingerprint-детекта).
DEFAULT_IMPERSONATE = "chrome120"

# "Document"-style browser headers для curl_cffi GET верхнеуровневых HTML-страниц
# (SERP/detail — НЕ XHR/JSON API-запросы, для них headers не нужны/другие).
# Идентичен ранее продублированному `_DOC_HEADERS` (avito/imv.py) и inline-dict
# в avito/serp.py::_build_cffi_session / avito/detail.py::_build_detail_session.
DOCUMENT_HEADERS: dict[str, str] = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
    "Cache-Control": "max-age=0",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
}


def http_proxies(proxy_url: str | None) -> dict[str, str] | None:
    """curl_cffi-совместимый `proxies=` dict из одного url.

    `None`/пустая строка → `None` (прямое подключение — без прокси).

    Извлечено из идентичной идиомы `{"http": url, "https": url} if url else None`,
    продублированной в avito/serp.py, avito/detail.py, avito/imv.py, cian/detail.py,
    cian/newbuilding.py (дважды), cian/session.py.

    ВАЖНО: не для httpx — начиная с httpx 0.28 `Client`/`AsyncClient` больше не
    принимают dict в `proxies=` (только одиночный `proxy=` строкой). Не передавай
    этот dict в httpx-клиент — `TypeError`.
    """
    return {"http": proxy_url, "https": proxy_url} if proxy_url else None


def build_curl_cffi_session(
    *,
    proxy_url: str | None,
    timeout: float = 25.0,
    headers: dict[str, str] | None = None,
    cookies: dict[str, str] | None = None,
    impersonate: str = DEFAULT_IMPERSONATE,
) -> AsyncSession:
    """Собрать curl_cffi `AsyncSession` с TLS-fingerprint impersonation.

    `proxy_url` — уже РЕЗОЛВЛЕННЫЙ url, не `ScraperConfig`. Разные providers
    достают proxy-url по-разному (`config.scraper_proxy_url` для avito,
    `config.cian_proxy_url` для cian, pool-aware `providers._proxy.curl_proxy_url()`
    контекст-менеджер для yandex/#2163) — эта функция намеренно НЕ решает, откуда
    взять url, только собирает сессию из уже готового значения. Заставлять её
    принимать `ScraperConfig` было бы overengineering: пришлось бы либо гадать имя
    атрибута по provider-строке (магия), либо всё равно требовать явный proxy_url
    параметр — тогда мандатори-`config` ничего не даёт (в отличие от
    `build_browser_fetcher`, где `ScraperConfig` содержит ОДНОЗНАЧНЫЙ единый
    `browser_http_endpoint`/`use_proxy_pool_browser` независимо от provider).

    `headers=None` → curl_cffi дефолтные заголовки (caller решает нужны ли
    document-style headers; см. `build_document_session` для явного варианта).
    """
    return AsyncSession(
        impersonate=impersonate,
        timeout=timeout,
        proxies=http_proxies(proxy_url),
        headers=headers,
        cookies=cookies,
    )


def build_document_session(
    *,
    proxy_url: str | None,
    timeout: float = 25.0,
    impersonate: str = DEFAULT_IMPERSONATE,
) -> AsyncSession:
    """`build_curl_cffi_session` с `DOCUMENT_HEADERS` — document-like GET (SERP/detail HTML).

    Это ровно та форма, что дублирована в `avito/serp.py::_build_cffi_session` и
    `avito/detail.py::_build_detail_session` (identical headers, timeout=25,
    impersonate=chrome120) — единственная разница между сайтами вызова была
    источник `proxy_url` (`config.scraper_proxy_url` в обоих случаях, но
    прокидывалось разными путями).
    """
    return build_curl_cffi_session(
        proxy_url=proxy_url,
        timeout=timeout,
        headers=DOCUMENT_HEADERS,
        impersonate=impersonate,
    )


def build_browser_fetcher(
    config: ScraperConfig,
    source: str,
    *,
    proxy_provider: ProxyProvider | None = None,
    fetch_timeout_s: float | None = None,
) -> BrowserFetcher:
    """Собрать `BrowserFetcher` с `config: ScraperConfig` **mandatory**.

    Устраняет footgun-класс #2322/#2330: до этой фабрики каждый provider
    конструировал `BrowserFetcher(...)` вручную и был обязан не забыть
    `endpoint=config.browser_http_endpoint` — если забывал (или `config` был
    optional где-то выше по стеку), браузерный fetch либо падал `TypeError`
    (endpoint — required kwarg без default в `BrowserFetcher.__init__`), либо
    молча не подключал proxy-pool (`use_pool` не проставлен → всегда False).

    Здесь `config` не имеет default — вызвать эту функцию без реального
    `ScraperConfig` невозможно на уровне сигнатуры (не полагаемся на
    code-review чтобы поймать пропущенный параметр).

    `proxy_provider=None` (дефолт) — валидно: часть providers (avito legacy
    curl-only путь, domclick, cian/yandex newbuilding) пока не подключены к
    browser-proxy-пулу (#2160 P4 wiring gap, отдельный issue) — `use_pool` в
    этом случае эффективно игнорируется `BrowserFetcher` (`proxy_provider is
    None` → env-fallback, см. `browser_fetcher.py::_pool_proxy`).

    `fetch_timeout_s=None` (дефолт) → используется дефолт `BrowserFetcher`
    (120s). Yandex-провайдер передаёt здесь 30s явно (см. вызовы в
    `yandex/serp.py`/`yandex/newbuilding.py`) — единственный provider с
    отличным от дефолта таймаутом.
    """
    if fetch_timeout_s is None:
        return BrowserFetcher(
            source=source,
            endpoint=config.browser_http_endpoint,
            proxy_provider=proxy_provider,
            use_pool=config.use_proxy_pool_browser,
        )
    return BrowserFetcher(
        source=source,
        fetch_timeout_s=fetch_timeout_s,
        endpoint=config.browser_http_endpoint,
        proxy_provider=proxy_provider,
        use_pool=config.use_proxy_pool_browser,
    )


__all__ = [
    "DEFAULT_IMPERSONATE",
    "DOCUMENT_HEADERS",
    "build_browser_fetcher",
    "build_curl_cffi_session",
    "build_document_session",
    "http_proxies",
]
