"""Scheduled backfill: detail-enrichment for legacy yandex listings (#1553).

Nightly window 12:00-15:00 UTC (migration 113, source=yandex_detail_backfill).
Offset from avito_detail_backfill (09-12 UTC) to avoid parallel egress on shared IP.

Problem: ~3952 yandex listings have detail_enriched_at IS NULL.
Yandex city sweep does not call YandexDetailScraper — SERP data only.
area_m2 coverage 23%, living/kitchen 0%, repair_state 1% on prod.

Solution: single snapshot SELECT at start (guarantees termination), fetch each
offer detail page via curl_cffi AsyncSession (chrome120 + proxy) — mirrors
yandex_address_backfill.py which already gets full HTML from Yandex on prod.
Parse HTML via YandexDetailScraper.parse (pure, no network). Persist via
save_detail_enrichment. Track consecutive parse→None results; abort after
max_consecutive_blocks (mark_done, not mark_failed — retry next night via
NULL detail_enriched_at).

Why curl_cffi and not YandexDetailScraper.fetch_detail:
  fetch_detail uses BaseScraper._http_get (plain httpx, no proxy, no TLS
  fingerprinting). On datacenter IPs Yandex returns captcha / shell-HTML
  → parse always returns None → backfill would be 0% effective. The
  curl_cffi path (chrome120 impersonation + mobile proxy) is already proven
  by yandex_address_backfill, which fetches identical offer detail pages.
"""

from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass, field

from curl_cffi.requests import AsyncSession
from scraper_kit.providers.yandex.detail import YandexDetailScraper, save_detail_enrichment
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.config import settings
from app.services import scrape_runs as runs_mod

logger = logging.getLogger(__name__)

__all__ = [
    "YandexDetailBackfillResult",
    "run_yandex_detail_backfill",
]


@dataclass
class YandexDetailBackfillResult:
    """Counters for one yandex detail backfill run."""

    attempted: int = 0
    enriched: int = 0
    failed: int = 0
    duration_sec: float = field(default=0.0)

    def to_dict(self) -> dict[str, int]:
        return {
            "attempted": self.attempted,
            "enriched": self.enriched,
            "failed": self.failed,
            "duration_sec": int(self.duration_sec),
        }


async def run_yandex_detail_backfill(
    db: Session,
    *,
    run_id: int,
    params: dict,
) -> YandexDetailBackfillResult:
    """Backfill detail_enriched_at for legacy yandex listings via curl_cffi + parse.

    Params (from default_params jsonb in scrape_schedules):
        batch_size: int -- snapshot size (SELECT LIMIT), default 800.
        budget_sec: float -- wall-clock budget per run, default 3600s.
        request_delay_sec: float -- delay between listings, default 5s.
        max_consecutive_blocks: int -- consecutive parse→None before abort, default 5.

    Fetch mechanism:
        curl_cffi AsyncSession(impersonate="chrome120") + scraper_proxy_url — mirrors
        yandex_address_backfill. On HTTP 200: pass resp.text to
        YandexDetailScraper().parse(html, offer_url). parse→None counts as a fail
        (possible captcha wall); consecutive None → abort after max_consecutive_blocks.

    Lifecycle: update_heartbeat -> snapshot -> loop with budget guard ->
    mark_done (incl. partial / consecutive-None abort) / mark_failed (exception only).
    """
    batch_size = int(params.get("batch_size", 800))
    budget_sec = float(params.get("budget_sec", 3600))
    request_delay_sec = float(params.get("request_delay_sec", 5.0))
    max_consecutive_blocks = int(params.get("max_consecutive_blocks", 5))

    counters = YandexDetailBackfillResult()
    current_counters: dict[str, int] = counters.to_dict()

    start = time.monotonic()

    try:
        runs_mod.update_heartbeat(db, run_id, current_counters)

        # SNAPSHOT: single SELECT at start -- NOT re-selected in loop.
        # Priority: is_active DESC (active first), scraped_at DESC (newest first).
        snapshot = (
            db.execute(
                text(
                    """
                    SELECT id, source_url
                    FROM listings
                    WHERE source = 'yandex'
                      AND detail_enriched_at IS NULL
                      AND source_url IS NOT NULL
                    ORDER BY is_active DESC NULLS LAST, scraped_at DESC NULLS LAST
                    LIMIT CAST(:batch_size AS int)
                    """
                ),
                {"batch_size": batch_size},
            )
            .mappings()
            .all()
        )

        if not snapshot:
            logger.info(
                "yandex_detail_backfill: run_id=%d -- no pending listings "
                "(detail_enriched_at IS NULL = 0), done",
                run_id,
            )
            runs_mod.mark_done(db, run_id, current_counters)
            return counters

        logger.info(
            "yandex_detail_backfill: run_id=%d snapshot=%d (budget=%.0fs "
            "delay=%.1fs max_consecutive_none=%d)",
            run_id,
            len(snapshot),
            budget_sec,
            request_delay_sec,
            max_consecutive_blocks,
        )

        # Build proxies dict once — mirrors yandex_address_backfill.py
        _proxy = settings.scraper_proxy_url
        _proxies = {"http": _proxy, "https": _proxy} if _proxy else None

        consecutive_none = 0
        do_sleep = False
        scraper = YandexDetailScraper()

        async with AsyncSession(
            impersonate="chrome120",
            timeout=30.0,
            proxies=_proxies,
            headers={
                "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
            },
        ) as session:
            for idx, row in enumerate(snapshot):
                # Budget guard
                elapsed = time.monotonic() - start
                if elapsed > budget_sec:
                    logger.info(
                        "yandex_detail_backfill: run_id=%d -- budget %.0fs exhausted "
                        "(elapsed=%.1fs), stopping at #%d/%d",
                        run_id,
                        budget_sec,
                        elapsed,
                        idx,
                        len(snapshot),
                    )
                    break

                # Delay before each request except the first
                if do_sleep:
                    await asyncio.sleep(request_delay_sec)
                do_sleep = True

                source_url: str = row["source_url"]
                listing_id: int = row["id"]
                counters.attempted += 1

                try:
                    try:
                        resp = await session.get(source_url, allow_redirects=True)
                    except Exception as fetch_exc:
                        consecutive_none += 1
                        counters.failed += 1
                        logger.warning(
                            "yandex_detail_backfill: run_id=%d listing_id=%d "
                            "fetch error (consecutive=%d): %s",
                            run_id,
                            listing_id,
                            consecutive_none,
                            fetch_exc,
                        )
                        if consecutive_none >= max_consecutive_blocks:
                            logger.error(
                                "yandex_detail_backfill: run_id=%d ABORT -- %d consecutive "
                                "errors. enriched=%d attempted=%d",
                                run_id,
                                consecutive_none,
                                counters.enriched,
                                counters.attempted,
                            )
                            break
                        continue

                    if resp.status_code != 200:
                        consecutive_none += 1
                        counters.failed += 1
                        logger.warning(
                            "yandex_detail_backfill: run_id=%d listing_id=%d "
                            "HTTP %d (consecutive=%d)",
                            run_id,
                            listing_id,
                            resp.status_code,
                            consecutive_none,
                        )
                        if consecutive_none >= max_consecutive_blocks:
                            logger.error(
                                "yandex_detail_backfill: run_id=%d ABORT -- %d consecutive "
                                "non-200 responses. enriched=%d attempted=%d",
                                run_id,
                                consecutive_none,
                                counters.enriched,
                                counters.attempted,
                            )
                            break
                        continue

                    enrichment = scraper.parse(resp.text, offer_url=source_url)

                    if enrichment is None:
                        # parse→None: captcha wall / shell-HTML / no JSON-LD.
                        # Do not mark listing as done — retry next night.
                        consecutive_none += 1
                        counters.failed += 1
                        logger.warning(
                            "yandex_detail_backfill: run_id=%d listing_id=%d source_url=%s "
                            "-> parse None (consecutive=%d)",
                            run_id,
                            listing_id,
                            source_url,
                            consecutive_none,
                        )
                        if consecutive_none >= max_consecutive_blocks:
                            logger.error(
                                "yandex_detail_backfill: run_id=%d ABORT -- %d consecutive "
                                "parse-None results (captcha wall?). enriched=%d attempted=%d",
                                run_id,
                                consecutive_none,
                                counters.enriched,
                                counters.attempted,
                            )
                            break
                        continue

                    consecutive_none = 0
                    if save_detail_enrichment(db, listing_id, enrichment):
                        counters.enriched += 1

                except Exception as exc:
                    counters.failed += 1
                    logger.warning(
                        "yandex_detail_backfill: save/iteration error for listing_id=%d: %s",
                        listing_id,
                        exc,
                    )
                    try:
                        db.rollback()
                    except Exception:
                        pass

                if counters.attempted % 25 == 0:
                    current_counters = counters.to_dict()
                    runs_mod.update_heartbeat(db, run_id, current_counters)

        counters.duration_sec = time.monotonic() - start
        current_counters = counters.to_dict()
        runs_mod.mark_done(db, run_id, current_counters)
        logger.info(
            "yandex_detail_backfill: run_id=%d DONE -- attempted=%d enriched=%d "
            "failed=%d duration=%.1fs",
            run_id,
            counters.attempted,
            counters.enriched,
            counters.failed,
            counters.duration_sec,
        )
        return counters

    except Exception as exc:
        counters.duration_sec = time.monotonic() - start
        logger.exception(
            "yandex_detail_backfill: run_id=%d FAILED after %.1fs",
            run_id,
            counters.duration_sec,
        )
        runs_mod.mark_failed(db, run_id, str(exc)[:1000], counters.to_dict())
        raise
