"""DomClick.ru scraper — вторичка через headless BrowserFetcher (#796).

Стратегия: HTML scrape через camoufox (AsyncCamoufox) — единственный
рабочий путь: DataDome пропускает headless Playwright, curl_cffi -> 401.

URL шаблон:
    https://domclick.ru/search?deal_type=sale&category=living&offer_type=flat
        &city_id={city_id}&rooms={r}&p={page}

Оффер-карточки: `a[href*="/card/"]` с href-паттерном `/card/sale__flat__<id>`.
Координаты SERP не отдаёт (lat = lon = None).

Важно: selectolax `.text()` объединяет все дочерние текстовые узлы без
разделителей. Числа из адреса (номер дома) могут слипнуться с ценой.
Поэтому цену извлекаем из каждого дочернего элемента отдельно (а не из
суммарного card_text), где элемент содержит символ рубля.
"""

from __future__ import annotations

import logging
import re
from urllib.parse import urljoin

from selectolax.parser import HTMLParser, Node

from app.services.scraper_settings import get_scraper_delay
from app.services.scrapers.base import BaseScraper, ScrapedLot
from app.services.scrapers.domclick_exceptions import DomClickBlockedError

logger = logging.getLogger(__name__)

# ── DataDome block detection ─────────────────────────────────────────────────
_DATADOME_MARKERS = ("datadome", "blocked", "access denied", "bot detected")


def _is_blocked_page(html: str) -> bool:
    head = html[:2048].lower()
    return any(m in head for m in _DATADOME_MARKERS)


# ── Regex helpers ────────────────────────────────────────────────────────────
# _RE_ROOMS расширен относительно avito.py: DomClick пишет «2-комн. квартира»
# (с «комн.») — avito.py-вариант ловит только «N-к. кв.» / «N-к квартира».
_RE_ROOMS = re.compile(
    r"(\d)-(?:комн\.?\s*(?:квартира|кв\.?)?|к\.?\s*(?:квартира|кв\.?))",
    re.IGNORECASE,
)
_RE_STUDIO = re.compile(r"\bстуди[яиюей]\b", re.IGNORECASE)
_RE_AREA = re.compile(r"(\d+[.,]?\d*)\s*м[2²]", re.IGNORECASE)
_RE_FLOOR = re.compile(r"(\d+)\s*/\s*(\d+)\s*эт\.?", re.IGNORECASE)

# Цена: парсим из одного элемента DOM (не из суммарного card_text) —
# иначе число дома может слипнуться с ценой. Цена всегда в элементе с «₽».
_RE_PRICE_EL = re.compile(r"([\d\s  ]+)\s*[₽р](?:уб\.?)?", re.IGNORECASE)

# source_id из href вида /card/sale__flat__2075671636
_RE_SOURCE_ID = re.compile(r"sale__flat__(\d+)")

# Адресные ключевые слова (эвристика)
_RE_ADDR_KW = re.compile(
    r"ул\.|улица|пер\.|переулок|пр-т|проспект"
    r"|бул\.|бульвар|шоссе|наб\.|набережная"
    r"|пл\.|площадь|тракт|д\.\s*\d",
    re.IGNORECASE,
)


# ── Вспомогательные функции ──────────────────────────────────────────────────


def _extract_rooms(text: str) -> int | None:
    if _RE_STUDIO.search(text):
        return 0
    m = _RE_ROOMS.search(text)
    return int(m.group(1)) if m else None


def _extract_area(text: str) -> float | None:
    m = _RE_AREA.search(text)
    return float(m.group(1).replace(",", ".")) if m else None


def _extract_floor(text: str) -> tuple[int | None, int | None]:
    m = _RE_FLOOR.search(text)
    if m:
        return int(m.group(1)), int(m.group(2))
    return None, None


def _extract_price_from_element(text: str) -> int | None:
    """Извлекаем цену из текста ОДНОГО элемента DOM.

    Парсим из одного span/div — не из суммарного card_text. Это исключает
    случай, когда адресный номер дома стоит вплотную перед ценой в
    объединённом тексте карточки («Ленина, 503 100 000 руб.»).
    """
    m = _RE_PRICE_EL.search(text)
    if m:
        # Нормализуем пробелы любого вида (обычный, неразрывный, узкий)
        raw = re.sub(r"[\s  ]", "", m.group(1))
        if raw.isdigit():
            val = int(raw)
            if val > 0:
                return val
    return None


def _extract_source_id(href: str) -> str | None:
    """Числовой ID из href типа /card/sale__flat__2075671636."""
    m = _RE_SOURCE_ID.search(href)
    return m.group(1) if m else None


# ── DomClickScraper ──────────────────────────────────────────────────────────


class DomClickScraper(BaseScraper):
    """DomClick вторичка parser. Источник = 'domklik'.

    Использует BrowserFetcher (camoufox headless Firefox) — DataDome пропускает
    его без дополнительной stealth-настройки. curl_cffi -> 401 (DataDome блок).

    Основной метод: fetch_city(city_id, rooms, pages).
    fetch_around() не реализован: DomClick не поддерживает geo-radius в URL.
    """

    name = "domklik"
    source = "domklik"
    base_url = "https://domclick.ru"
    # DataDome — консервативная задержка между страницами (fallback до init)
    request_delay_sec = 8.0

    def __init__(self) -> None:
        super().__init__()
        self.request_delay_sec = get_scraper_delay(self.name)
        # Счётчик карточек с неожиданной DOM-структурой (для observability)
        self.parse_failures: int = 0

    async def __aenter__(self) -> DomClickScraper:
        await super().__aenter__()
        return self

    # ── fetch_around — stub ───────────────────────────────────────────────────

    async def fetch_around(self, lat: float, lon: float, radius_m: int = 1000) -> list[ScrapedLot]:
        """DomClick не поддерживает geo-radius. Используй fetch_city()."""
        raise NotImplementedError(
            "DomClick не поддерживает geo-radius; используй fetch_city(city_id=...)"
        )

    # ── Основной метод ────────────────────────────────────────────────────────

    async def fetch_city(
        self,
        city_id: int,
        rooms: list[int] | None = None,
        pages: int = 20,
    ) -> list[ScrapedLot]:
        """Citywide sweep: все страницы SERP для city_id.

        Args:
            city_id: числовой ID города в DomClick (например, 4 = Екатеринбург).
            rooms: список значений комнатности (0=студия, 1, 2, 3, 4, ...).
                   None -> без фильтра комнатности (все квартиры сразу).
            pages: максимальное число страниц на один sweep. Break on empty.

        Returns:
            Дедуплицированный по source_id список ScrapedLot.
        """
        from app.services.scrapers.browser_fetcher import BrowserFetcher

        all_lots: list[ScrapedLot] = []
        seen_ids: set[str] = set()

        # Формируем список room-значений для sweep'ов
        room_values: list[int | None] = [None] if not rooms else list(rooms)

        async with BrowserFetcher() as fetcher:
            for room_val in room_values:
                room_label = f"rooms={room_val}" if room_val is not None else "all_rooms"
                logger.info(
                    "domklik: city_id=%d %s sweep (max %d pages)", city_id, room_label, pages
                )

                for page_num in range(1, pages + 1):
                    url = self._build_url(city_id, room_val, page_num)
                    logger.debug("domklik: fetch %s", url)

                    try:
                        html = await fetcher.fetch(url)
                    except Exception as exc:
                        logger.error(
                            "domklik: fetch failed city_id=%d page=%d: %s",
                            city_id,
                            page_num,
                            exc,
                        )
                        break

                    try:
                        lots = self._parse_html(html)
                    except DomClickBlockedError:
                        logger.warning(
                            "domklik: DataDome block at city_id=%d page=%d — stopping sweep",
                            city_id,
                            page_num,
                        )
                        break

                    if not lots:
                        logger.info(
                            "domklik: empty page city_id=%d %s page=%d — stopping sweep",
                            city_id,
                            room_label,
                            page_num,
                        )
                        break

                    # Дедупликация по source_id (кросс-sweep и кросс-страница)
                    new_lots: list[ScrapedLot] = []
                    for lot in lots:
                        key = lot.source_id or lot.source_url
                        if key not in seen_ids:
                            seen_ids.add(key)
                            new_lots.append(lot)

                    all_lots.extend(new_lots)
                    logger.info(
                        "domklik: city_id=%d %s page=%d -> %d new (total %d)",
                        city_id,
                        room_label,
                        page_num,
                        len(new_lots),
                        len(all_lots),
                    )

                    # Пауза между страницами (anti-DataDome)
                    if page_num < pages:
                        await self.sleep_between_requests()

        logger.info(
            "domklik: fetch_city done city_id=%d total=%d parse_failures=%d",
            city_id,
            len(all_lots),
            self.parse_failures,
        )
        return all_lots

    # ── URL builder ───────────────────────────────────────────────────────────

    def _build_url(self, city_id: int, rooms: int | None, page: int) -> str:
        """Строит URL SERP DomClick."""
        url = (
            f"{self.base_url}/search"
            f"?deal_type=sale&category=living&offer_type=flat"
            f"&city_id={city_id}"
        )
        if rooms is not None:
            url += f"&rooms={rooms}"
        url += f"&p={page}"
        return url

    # ── HTML parsing ──────────────────────────────────────────────────────────

    def _parse_html(self, html: str) -> list[ScrapedLot]:
        """Парсим карточки из HTML через selectolax.

        Селектор: `a[href*="/card/"]` с паттерном `/card/sale__flat__<id>`.
        """
        if _is_blocked_page(html):
            logger.warning("domklik: DataDome block page detected, 0 cards returned")
            raise DomClickBlockedError("DomClick returned DataDome block page")
        tree = HTMLParser(html)
        card_links = tree.css('a[href*="/card/"]')

        seen_hrefs: set[str] = set()
        lots: list[ScrapedLot] = []

        for link in card_links:
            href = link.attributes.get("href", "")
            # Только карточки вторичного жилья
            if "sale__flat__" not in href:
                continue
            if href in seen_hrefs:
                continue
            seen_hrefs.add(href)

            lot = self._card_link_to_lot(link, href)
            if lot is not None:
                lots.append(lot)

        return lots

    def _card_link_to_lot(self, link: Node, href: str) -> ScrapedLot | None:
        """Парсинг одной карточки-ссылки -> ScrapedLot.

        DomClick SERP оборачивает карточку в `<a href="/card/...">` — вся
        информация (title, цена, адрес) внутри этого элемента.

        Цену извлекаем из отдельных дочерних элементов (не из объединённого
        card_text) чтобы исключить слипание числа дома с ценой.
        """
        try:
            source_id = _extract_source_id(href)
            source_url = urljoin(self.base_url, href)

            # Суммарный текст — для rooms/area/floor (эти поля не подвержены
            # проблеме слипания, т.к. используют специфичные маркеры: «м²», «эт.»)
            card_text = link.text(strip=True)
            if not card_text:
                return None

            rooms = _extract_rooms(card_text)
            area = _extract_area(card_text)
            floor, total_floors = _extract_floor(card_text)

            # Цена: ищем в каждом дочернем элементе отдельно чтобы не слипались
            # числа дома с ценой при конкатенации card_text
            price = self._extract_price_from_children(link)
            if not price or price <= 0:
                return None

            address = self._extract_address(link)

            return ScrapedLot(
                source="domklik",
                source_url=source_url,
                source_id=source_id,
                address=address,
                lat=None,  # DomClick SERP координаты не отдаёт
                lon=None,
                rooms=rooms,
                area_m2=area,
                floor=floor,
                total_floors=total_floors,
                price_rub=price,
                listing_segment="vtorichka",
                raw_payload={"card_text": card_text[:500]},
            )
        except Exception:
            self.parse_failures += 1
            logger.warning(
                "domclick _card_link_to_lot: parse failed href=%r (parse_failures=%d)",
                href,
                self.parse_failures,
                exc_info=True,
            )
            return None

    def _extract_price_from_children(self, link: Node) -> int | None:
        """Ищем цену в дочерних элементах карточки по символу рубля.

        Итерируем по всем потомкам: первый элемент с рублёвым символом
        в тексте — источник цены.
        """
        for selector in ("span", "div", "p", "strong", "b"):
            for el in link.css(selector):
                text = el.text(strip=True)
                price = _extract_price_from_element(text)
                if price:
                    return price
        # Fallback: суммарный текст карточки (если структура нестандартная)
        return _extract_price_from_element(link.text(strip=True))

    def _extract_address(self, link: Node) -> str | None:
        """Эвристика извлечения адреса из DOM карточки.

        DomClick помещает адрес в отдельный текстовый блок внутри карточки.
        Обходим вложенные элементы и ищем первый текст с адресными ключевыми
        словами, пропуская строки с площадью/этажом/ценой.

        Возвращает None если адрес не найден.
        """
        for selector in ("span", "p", "div"):
            for el in link.css(selector):
                text = el.text(strip=True)
                if not text or len(text) < 5 or len(text) > 200:
                    continue
                # Пропускаем блоки с площадью / этажом / ценой
                if _RE_AREA.search(text) or _RE_FLOOR.search(text):
                    continue
                if _RE_PRICE_EL.search(text):
                    continue
                if _RE_ADDR_KW.search(text):
                    return text
        return None


# ── Convenience runner (для Celery tasks) ────────────────────────────────────


async def scrape_domclick_city(
    city_id: int,
    rooms: list[int] | None = None,
    pages: int = 20,
) -> list[ScrapedLot]:
    """Удобная точка входа для вызова из Celery tasks.

    Пример::

        import asyncio
        lots = asyncio.run(scrape_domclick_city(city_id=4, rooms=[1, 2, 3]))
    """
    async with DomClickScraper() as scraper:
        return await scraper.fetch_city(city_id=city_id, rooms=rooms, pages=pages)
