"""Daily per-source snapshot writer (#570).

Берёт текущее состояние listing_sources (последний снимок на canonical listing × source)
и пишет ежедневный ряд в listing_source_snapshots + change-log price_change в
listing_source_events. Так история per-source цены копится СРАЗУ, независимо от
(сейчас DORMANT) скраперов — см. шапку data/sql/079_listing_source_history.sql.

Задача синхронная (DB-only, никаких внешних HTTP-вызовов) — запускается kit-scheduler'ом
через product_handlers._job_listing_source_snapshot,
по образцу import_rosreestr_dkp (sync task в run_in_executor).

Вся работа — два set-based SQL statement'а (snapshot upsert + event-diff CTE),
никакого row-by-row Python: 18 355 строк обслуживаются одним INSERT … SELECT каждый.
"""

from __future__ import annotations

import logging

from sqlalchemy import text
from sqlalchemy.orm import Session

from app.services import scrape_runs as runs_mod

logger = logging.getLogger(__name__)

# Окно свежести: источник считается активным, если last_seen_at не старше N дней.
FRESHNESS_WINDOW_DAYS = 7

# ── Daily snapshot upsert ─────────────────────────────────────────────────────
# Снимок на (listing_source_id, CURRENT_DATE). ON CONFLICT → last-write-wins за день
# (повторный прогон в те же сутки перезаписывает снимок свежими значениями).
# is_active derived: last_seen_at в пределах окна свежести на момент снимка.
# payload_hash = md5(raw_payload::text) — ::text на колонке допустим (это не bind-param).
# run_id через CAST(:run_id AS bigint) — psycopg v3 (никогда :run_id::bigint).
_SNAPSHOT_SQL = text(
    """
    INSERT INTO listing_source_snapshots (
        listing_source_id, snapshot_date, price_rub, is_active,
        last_seen_at, payload_hash, observed_at, run_id
    )
    SELECT
        id,
        CURRENT_DATE,
        price_rub,
        (last_seen_at > now() - make_interval(days => :freshness_days)) AS is_active,
        last_seen_at,
        md5(raw_payload::text),
        now(),
        CAST(:run_id AS bigint)
    FROM listing_sources
    ON CONFLICT (listing_source_id, snapshot_date) DO UPDATE SET
        price_rub    = EXCLUDED.price_rub,
        is_active    = EXCLUDED.is_active,
        last_seen_at = EXCLUDED.last_seen_at,
        payload_hash = EXCLUDED.payload_hash,
        observed_at  = EXCLUDED.observed_at,
        run_id       = EXCLUDED.run_id
    """
)

# ── Event diff: price_change ──────────────────────────────────────────────────
# Для каждого источника сравниваем сегодняшнюю цену (snapshot_date = CURRENT_DATE) с
# самым свежим ПРЕДЫДУЩИМ снимком (snapshot_date < CURRENT_DATE). Если цена изменилась
# (обе NOT NULL, old <> 0) — пишем price_change.
#   today  — снимок за сегодня (только что записан _SNAPSHOT_SQL).
#   prior  — последний снимок строго ДО сегодня (DISTINCT ON … ORDER BY date DESC).
# Полностью set-based: один INSERT … SELECT по всем источникам, без Python-цикла.
# change_time = now() детерминирует UNIQUE(listing_source_id, change_time, event_type)
# в пределах прогона → ON CONFLICT DO NOTHING делает писатель идемпотентным.
_EVENT_DIFF_SQL = text(
    """
    WITH today AS (
        SELECT listing_source_id, price_rub
        FROM listing_source_snapshots
        WHERE snapshot_date = CURRENT_DATE
    ),
    prior AS (
        SELECT DISTINCT ON (listing_source_id)
            listing_source_id, price_rub
        FROM listing_source_snapshots
        WHERE snapshot_date < CURRENT_DATE
        ORDER BY listing_source_id, snapshot_date DESC
    )
    INSERT INTO listing_source_events (
        listing_source_id, change_time, event_type, price_rub, diff_percent
    )
    SELECT
        t.listing_source_id,
        now(),
        'price_change',
        t.price_rub,
        round((t.price_rub - p.price_rub)::numeric / p.price_rub * 100, 4)
    FROM today t
    JOIN prior p ON p.listing_source_id = t.listing_source_id
    WHERE t.price_rub IS NOT NULL
      AND p.price_rub IS NOT NULL
      AND p.price_rub <> 0
      AND t.price_rub <> p.price_rub
    ON CONFLICT (listing_source_id, change_time, event_type) DO NOTHING
    """
)


def snapshot_listing_sources(db: Session, run_id: int) -> dict[str, int]:
    """Записать дневной снимок listing_sources + price_change-события.

    Sync (вызывается scheduler-триггером в executor, как import_rosreestr_dkp).
    Два set-based statement'а в одной транзакции:
      1. upsert снимка на (listing_source_id, CURRENT_DATE) — last-write-wins.
      2. diff сегодняшней цены против последнего предыдущего снимка → price_change-события.

    Финализирует scrape_runs (mark_done / mark_failed) и пишет counters.

    Returns {"snapshotted": N, "price_change_events": M}.
    """
    counters: dict[str, int] = {"snapshotted": 0, "price_change_events": 0}
    try:
        snap_result = db.execute(
            _SNAPSHOT_SQL,
            {"freshness_days": FRESHNESS_WINDOW_DAYS, "run_id": run_id},
        )
        counters["snapshotted"] = snap_result.rowcount or 0

        event_result = db.execute(_EVENT_DIFF_SQL)
        counters["price_change_events"] = event_result.rowcount or 0

        db.commit()
        runs_mod.mark_done(db, run_id, counters)
        logger.info(
            "snapshot_listing_sources run_id=%d done: snapshotted=%d price_change_events=%d",
            run_id,
            counters["snapshotted"],
            counters["price_change_events"],
        )
        return counters
    except Exception as exc:
        logger.exception("snapshot_listing_sources run_id=%d failed", run_id)
        db.rollback()
        runs_mod.mark_failed(db, run_id, str(exc)[:1000], counters)
        raise
