"""Celery task: periodic catalog-object scrape для DOM.РФ.

Дополняет ~25 NULL колонок в domrf_kn_objects из SSR-страниц каталога.
kn-API эти поля не возвращает — они только на публичных страницах объектов.

Selector logic:
  - catalog_scraped_at IS NULL — "новый" объект, всегда грузим
  - DATE(catalog_scraped_at) < CURRENT_DATE — сегодня ещё не обновлялся, грузим
  - DATE(catalog_scraped_at) = CURRENT_DATE — уже сегодня обновлён, пропускаем
  - force=True — игнорирует фильтр, загружает все объекты последнего snapshot

Beat schedule: вторник 04:00 UTC (в beat_schedule.py).
"""

from __future__ import annotations

import asyncio
import logging
from datetime import date
from typing import Any

from sqlalchemy import text

from app.core.db import SessionLocal
from app.workers.celery_app import celery_app

logger = logging.getLogger(__name__)

# Запрос для выбора объектов на скрап.
# Возвращает obj_id + snapshot_date одним запросом, чтобы избежать race condition:
# если между двумя запросами kn-scraper запишет новый snapshot — UPDATE по старой
# snapshot_date не затронет ни одной строки.  MAX subquery ограничена тем же
# region_cd чтобы не захватить snapshot другого региона.
#
# Фильтр (:force = false): берём только те, что ещё не обновлялись сегодня
# (catalog_scraped_at IS NULL — никогда не скрапились, либо DATE(...) < today).
# При :force = true фильтр снимается — грузим все объекты последнего snapshot.
#
# LIMIT :max_objects: в PostgreSQL `LIMIT NULL` == без лимита, поэтому при
# max_objects=None (force "Загрузить все" без явного потолка) грузим ВСЕ строки
# последнего snapshot, а не молча режем до _DEFAULT_MAX_OBJECTS.
_SELECT_TARGETS_SQL = text(
    """
    SELECT obj_id, snapshot_date
      FROM domrf_kn_objects
     WHERE region_cd = :region_code
       AND (
           CAST(:force AS boolean)
           OR catalog_scraped_at IS NULL
           OR DATE(catalog_scraped_at) < CURRENT_DATE
       )
       AND snapshot_date = (
           SELECT MAX(snapshot_date)
             FROM domrf_kn_objects
            WHERE region_cd = :region_code
       )
     ORDER BY catalog_scraped_at NULLS FIRST
     LIMIT :max_objects
    """
)

# Лимит по умолчанию если max_objects не задан явно.
_DEFAULT_MAX_OBJECTS = 300


@celery_app.task(
    bind=True,
    name="tasks.scrape_kn_catalog_objects.scrape_kn_catalog_objects",
    time_limit=3600,
)
def scrape_kn_catalog_objects(
    self: Any,
    region_code: int = 66,
    max_objects: int | None = None,
    force: bool = False,
) -> dict[str, Any]:
    """Periodic catalog-object scrape.

    Args:
        region_code: Код региона (ОКАТО prefix). Default 66 = Свердловская обл.
        max_objects: Максимум объектов за один run. Если не задан: при force=True
            лимита нет (грузим все), при force=False — _DEFAULT_MAX_OBJECTS (300).
        force: Если True — игнорирует фильтр "уже сегодня обновлён" и (при не
            заданном max_objects) снимает лимит, грузя ВСЕ объекты последнего
            snapshot (admin "Загрузить все"). По умолчанию False — пропускает
            то, что уже скраплено сегодня, и режет batch до 300.

    Returns:
        dict с ключами: region_code, snapshot_date, obj_ids_count,
        processed, succeeded, failed, skipped.

    Concurrency:
        No Redis lock — consistent with sibling tasks (scrape_kn_region etc.).
        Beat is configured for non-overlapping fire (Tuesday 04:00 UTC, ~5min run),
        so concurrent execution is extremely rare. If it occurs:
        - UPDATE is idempotent (COALESCE, catalog_scraped_at = NOW())
        - Max risk: 2x WAF load on DOM.РФ for the same batch
        - Both tasks complete; second update is no-op (catalog_scraped_at расхождение)

        Add Redis lock if WAF blocks observed or beat schedule changes to overlap.
    """
    from app.services.scrapers.domrf_catalog_object import scrape_catalog_objects

    # Явный max_objects всегда уважается. Без него:
    #   force=True ("Загрузить все") → лимита нет (limit=None → SQL LIMIT NULL = все строки);
    #   force=False (beat / ad-hoc pass) → дефолтный batch _DEFAULT_MAX_OBJECTS.
    if max_objects is not None:
        limit: int | None = max_objects
    elif force:
        limit = None
    else:
        limit = _DEFAULT_MAX_OBJECTS

    db = SessionLocal()
    try:
        rows = (
            db.execute(
                _SELECT_TARGETS_SQL,
                {"region_code": region_code, "max_objects": limit, "force": force},
            )
            .mappings()
            .all()
        )
        obj_ids: list[int] = [int(r["obj_id"]) for r in rows]
    except Exception as exc:
        logger.error("scrape_kn_catalog_objects: failed to fetch obj_ids: %s", exc)
        db.close()
        raise

    if not obj_ids:
        logger.info(
            "scrape_kn_catalog_objects: nothing to do for region=%d (force=%s)",
            region_code,
            force,
        )
        db.close()
        return {
            "region_code": region_code,
            "force": force,
            "obj_ids_count": 0,
            "processed": 0,
            "succeeded": 0,
            "failed": 0,
            "skipped": 0,
        }

    # snapshot_date берётся из первой строки результата — все строки одинаковые
    # (WHERE snapshot_date = MAX(snapshot_date)).  Это атомарно: один SELECT вместо двух,
    # что устраняет race condition с kn-scraper.
    snapshot_date_val: date = rows[0]["snapshot_date"]

    logger.info(
        "scrape_kn_catalog_objects: region=%d snapshot_date=%s obj_ids=%d limit=%s force=%s",
        region_code,
        snapshot_date_val,
        len(obj_ids),
        "ALL" if limit is None else limit,
        force,
    )

    try:
        stats = asyncio.run(
            scrape_catalog_objects(
                db=db,
                obj_ids=obj_ids,
                snapshot_date=snapshot_date_val,
                region_code=region_code,
            )
        )
    except Exception as exc:
        logger.error("scrape_kn_catalog_objects: scrape failed: %s", exc)
        raise
    finally:
        db.close()

    result: dict[str, Any] = {
        "region_code": region_code,
        "force": force,
        "snapshot_date": str(snapshot_date_val),
        "obj_ids_count": len(obj_ids),
        **stats,
    }
    logger.info("scrape_kn_catalog_objects done: %s", result)
    return result
