"""Scheduled poll: detect when a new Rosreestr quarter becomes available (#888).

Граница ответственности модуля:
  - DETECT: определить, появился ли новый квартал (next after latest loaded) в
    открытых данных Росреестра.
  - ALERT: залогировать actionable-сообщение, если квартал доступен.
  - НЕ скачивает много-гигабайтный ZIP и НЕ вызывает shell-loaders — это ручной
    ops-шаг. После алерта оператор запускает:
      data/sql/02_load_all_quarters.sh
      tradein-mvp/deploy/import-rosreestr.sh

Архив открытых данных Росреестра (сделки ДКП/ДДУ) публикуется на портале:
  https://rosreestr.gov.ru/opendata/

Файлы именуются по шаблону:
  dataset_СДЕЛКИ_r-r_01-92_y_{YYYY}_q_{N}.csv.zip

Индексный JSON живёт по адресу:
  https://rosreestr.gov.ru/opendata/f.json (список всех датасетов)

Проверяем HEAD-запросом (без скачивания) прямой ссылки на следующий квартал.
Формат ссылки:
  https://rosreestr.gov.ru/opendata/dataset_СДЕЛКИ_r-r_01-92_y_{YYYY}_q_{N}.csv.zip

При сетевой ошибке / HTTP 5xx — логируем warning, возвращаем available=False.
Статус 404 → квартал не опубликован → available=False (штатный случай до начала августа).
"""

from __future__ import annotations

import logging
from typing import Any

import httpx
from sqlalchemy import text
from sqlalchemy.orm import Session

logger = logging.getLogger(__name__)

# Базовый URL открытых данных Росреестра (прямая ссылка на ZIP-файл квартала).
# Шаблон подтверждён по документации портала opendata.rosreestr.gov.ru.
_ROSREESTR_DATASET_URL_TEMPLATE = (
    "https://rosreestr.gov.ru/opendata/"
    "dataset_%D0%A1%D0%94%D0%95%D0%9B%D0%9A%D0%98_r-r_01-92_y_{year}_q_{quarter}.csv.zip"
)

# Таймаут HEAD-запроса. Росреестр может быть медленным — 15s достаточно.
_HTTP_TIMEOUT = 15.0


def _next_quarter(year: int, quarter: int) -> tuple[int, int]:
    """Вернуть (year, quarter) для следующего квартала.

    >>> _next_quarter(2026, 1)
    (2026, 2)
    >>> _next_quarter(2026, 4)
    (2027, 1)
    """
    if quarter < 4:
        return year, quarter + 1
    return year + 1, 1


def _period_start_to_quarter(period_start_date: Any) -> tuple[int, int]:
    """Конвертировать дату начала периода в (year, quarter).

    period_start_date — дата первого числа квартала (e.g. 2026-01-01 → Q1 2026).
    Принимает datetime.date, datetime.datetime или ISO-строку.
    """
    if hasattr(period_start_date, "year"):
        year = period_start_date.year
        month = period_start_date.month
    else:
        # ISO string fallback
        import datetime

        d = datetime.date.fromisoformat(str(period_start_date)[:10])
        year = d.year
        month = d.month
    quarter = (month - 1) // 3 + 1
    return year, quarter


def latest_loaded_quarter(db: Session) -> tuple[int, int] | None:
    """Вернуть (year, quarter) последнего загруженного квартала Росреестра.

    Запрашивает MAX(period_start_date) FROM deals WHERE source='rosreestr'.
    Возвращает None если таблица пуста или данных от rosreestr нет.

    Использует синхронный Session (SQLAlchemy, psycopg v3 через SQLAlchemy dialect).
    """
    row = db.execute(
        text(
            """
            SELECT MAX(deal_date) AS max_date
            FROM deals
            WHERE source = 'rosreestr'
              AND deal_date IS NOT NULL
            """
        )
    ).fetchone()

    if row is None or row[0] is None:
        logger.info(
            "rosreestr_poll: no rosreestr deals found in DB — cannot determine latest quarter"
        )
        return None

    year, quarter = _period_start_to_quarter(row[0])
    logger.info(
        "rosreestr_poll: latest loaded quarter = Q%d %d (derived from max deal_date=%s)",
        quarter,
        year,
        row[0],
    )
    return year, quarter


def rosreestr_dataset_url(year: int, quarter: int) -> str:
    """Сформировать URL датасета Росреестра для заданного квартала.

    Возвращает прямую ссылку на ZIP-архив (percent-encoded шаблон).
    """
    return _ROSREESTR_DATASET_URL_TEMPLATE.format(year=year, quarter=quarter)


async def check_new_quarter_available(
    client: httpx.AsyncClient,
    year: int,
    quarter: int,
) -> bool:
    """Проверить, опубликован ли датасет Росреестра для (year, quarter).

    HEAD-запрос к прямой ссылке на ZIP — без скачивания файла.

    Возвращает True если сервер ответил 200 (или 2xx/3xx с redirect).
    Возвращает False при 404 (квартал ещё не опубликован) или сетевой ошибке.
    Никогда не поднимает исключения в вызывающий код (scheduler-safe).
    """
    url = rosreestr_dataset_url(year, quarter)
    try:
        resp = await client.head(url, follow_redirects=True)
        if resp.status_code == 200:
            logger.info(
                "rosreestr_poll: Q%d %d available at %s (HTTP %d)",
                quarter,
                year,
                url,
                resp.status_code,
            )
            return True
        if resp.status_code == 404:
            logger.info(
                "rosreestr_poll: Q%d %d not yet published (HTTP 404)",
                quarter,
                year,
            )
            return False
        # Другие коды (403, 5xx, etc.) — логируем как warning, не кидаем
        logger.warning(
            "rosreestr_poll: unexpected HTTP %d for Q%d %d url=%s — treating as unavailable",
            resp.status_code,
            quarter,
            year,
            url,
        )
        return False
    except httpx.TimeoutException:
        logger.warning(
            "rosreestr_poll: timeout checking Q%d %d url=%s — treating as unavailable",
            quarter,
            year,
            url,
        )
        return False
    except httpx.RequestError as exc:
        logger.warning(
            "rosreestr_poll: network error checking Q%d %d: %s — treating as unavailable",
            quarter,
            year,
            exc,
        )
        return False
    except Exception as exc:
        logger.warning(
            "rosreestr_poll: unexpected error checking Q%d %d: %s — treating as unavailable",
            quarter,
            year,
            exc,
        )
        return False


async def poll_rosreestr_new_quarter(db: Session) -> dict[str, Any]:
    """Orchestrator: определить следующий квартал после загруженного, проверить наличие.

    Шаги:
      1. Найти MAX(deal_date) WHERE source='rosreestr' → (loaded_year, loaded_quarter).
      2. Вычислить next = _next_quarter(loaded_year, loaded_quarter).
      3. HEAD-запросом проверить наличие датасета на rosreestr.gov.ru.
      4. Если доступен — логировать actionable INFO с инструкцией по запуску ingest.
      5. Вернуть dict с результатом.

    Если в БД нет данных rosreestr → предполагаем 2025Q4, проверяем 2026Q1.
    Это fallback — в штатном режиме данные всегда есть после initial load.

    Возвращает:
        {"available": bool, "year": int, "quarter": int,
         "latest_loaded_year": int | None, "latest_loaded_quarter": int | None}
    """
    # 1. Текущий максимальный загруженный квартал
    latest = latest_loaded_quarter(db)
    if latest is None:
        # Fallback: нет данных — проверяем 2026Q1 как первую точку
        loaded_year, loaded_quarter = 2025, 4
        logger.warning(
            "rosreestr_poll: no deals in DB — using fallback baseline 2025Q4, "
            "checking 2026Q1 availability"
        )
    else:
        loaded_year, loaded_quarter = latest

    # 2. Следующий квартал
    next_year, next_quarter = _next_quarter(loaded_year, loaded_quarter)
    logger.info(
        "rosreestr_poll: checking Q%d %d (next after loaded Q%d %d)",
        next_quarter,
        next_year,
        loaded_quarter,
        loaded_year,
    )

    # 3. Проверка наличия
    async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as client:
        available = await check_new_quarter_available(client, next_year, next_quarter)

    # 4. Алерт если доступен
    if available:
        logger.info(
            "rosreestr_poll: NEW QUARTER AVAILABLE — Q%d %d. "
            "Run ingest to load: "
            "data/sql/02_load_all_quarters.sh + tradein-mvp/deploy/import-rosreestr.sh",
            next_quarter,
            next_year,
        )

    return {
        "available": available,
        "year": next_year,
        "quarter": next_quarter,
        "latest_loaded_year": loaded_year if latest is not None else None,
        "latest_loaded_quarter": loaded_quarter if latest is not None else None,
    }
