"""Celery-beat data-freshness monitor: алёрт в GlitchTip при устаревших/упавших источниках.

GET /api/v1/admin/scrape/freshness считает свежесть по 5 run-таблицам, но только ЧИТАЕТ
(чтобы UI-поллинг не спамил issue). Эта beat-таска периодически запускает ту же агрегацию
(compute_freshness — единый источник истины) и пушит Sentry/GlitchTip-сообщение, когда есть
проблемные источники.

Какие источники считаем алёртабельными:
  - status == "failed" (нет успехов вообще / возраст >= stale_days) — серьёзно;
  - status == "stale"  (возраст между fresh_days и stale_days) — предупреждение.
Severity сообщения: "error", если хотя бы один КРИТИЧНЫЙ источник (registry.critical=True)
полностью упал (failed); иначе "warning". Если sentry в worker-процессе не инициализирован
(нет glitchtip_dsn) — capture_message молча no-op, поэтому дополнительно логируем WARNING.

Mirror conventions (zone_regulation_refresh.py / backend.md): SessionLocal()+try/finally;
broad except не валит beat-loop (любая ошибка → log + возврат summary-dict).
"""

from __future__ import annotations

import logging
from typing import Any, Literal

import sentry_sdk

from app.api.v1.admin_scrape import compute_freshness
from app.core.db import SessionLocal
from app.workers.celery_app import celery_app

logger = logging.getLogger(__name__)

# Статусы из compute_freshness, которые считаем поводом для алёрта.
_ALERTABLE = {"stale", "failed"}


def _format_age(source: dict[str, Any]) -> str:
    """Человекочитаемый возраст источника для тела сообщения."""
    age = source.get("age_days")
    if age is None:
        return "нет успешных прогонов"
    return f"{age}d"


@celery_app.task(name="tasks.scrape_freshness_check.scrape_freshness_check")
def scrape_freshness_check() -> dict[str, Any]:
    """Проверяет свежесть источников и шлёт GlitchTip-алёрт при stale/failed.

    Returns:
        Summary-dict для observability/тестов:
        {'checked': N, 'alertable': M, 'stale': S, 'failed': F,
         'overall_status': str, 'alerted': bool, 'level': str|None}
        При внутренней ошибке: {'error': '<msg>'} — исключение НЕ пробрасывается,
        чтобы не сорвать beat-loop.
    """
    db = SessionLocal()
    try:
        payload = compute_freshness(db)
    except Exception as exc:  # любая ошибка агрегации не должна валить beat
        logger.exception("scrape_freshness_check: compute_freshness упал: %s", exc)
        return {"error": str(exc)}
    finally:
        db.close()

    sources: list[dict[str, Any]] = payload.get("sources", [])
    overall = payload.get("overall_status", "unknown")

    alertable = [s for s in sources if s.get("status") in _ALERTABLE]
    stale = [s for s in alertable if s.get("status") == "stale"]
    failed = [s for s in alertable if s.get("status") == "failed"]

    summary: dict[str, Any] = {
        "checked": len(sources),
        "alertable": len(alertable),
        "stale": len(stale),
        "failed": len(failed),
        "overall_status": overall,
        "alerted": False,
        "level": None,
    }

    if not alertable:
        logger.info(
            "scrape_freshness_check: все источники свежие (checked=%d, overall=%s)",
            len(sources),
            overall,
        )
        return summary

    # Severity: критичный источник упал полностью → error, иначе warning.
    critical_failed = [s for s in failed if s.get("critical")]
    level: Literal["error", "warning"] = "error" if critical_failed else "warning"

    parts = [f"{s['source']} [{s['status']}, {_format_age(s)}]" for s in alertable]
    message = (
        f"Data freshness alert (overall={overall}): "
        f"{len(failed)} failed, {len(stale)} stale — " + ", ".join(parts)
    )

    # capture_message — no-op, если SDK не инициализирован (нет glitchtip_dsn);
    # поэтому всегда дублируем в лог (WARNING/ERROR по severity).
    sentry_sdk.capture_message(message, level=level)
    log = logger.error if level == "error" else logger.warning
    log("scrape_freshness_check: %s", message)

    summary["alerted"] = True
    summary["level"] = level
    return summary
