"""Ингест JSONL от локального DomClick-раннера (`scripts/domclick_local_runner.py`) в БД tradein.

Запускается ВНУТРИ tradein-контейнера (импортит `app`). Идемпотентно.

Каждая ok-запись (status=="ok" и валидный price_rub) →
  1. upsert в `listings` через save_listings() (house-match хук — fault-tolerant, в SAVEPOINT);
  2. deep-detail enrichment через save_detail_enrichment()
     (owners / encumbrances / kitchen / views / wall / floor / price-history).

Listing.id не возвращается из save_listings — достаём отдельным SELECT по (source, source_id).

Usage:
    python -m scripts.ingest_domclick_jsonl --jsonl /tmp/domclick.jsonl [--limit N] [--dry-run]
"""

from __future__ import annotations

import argparse
import json
import logging
from typing import Any

from scraper_kit.base import ScrapedLot, save_listings
from scraper_kit.orchestration.pipeline import DEFAULT_REGION_CODE
from scraper_kit.providers.domclick.detail import (
    DomClickDetailEnrichment,
    save_detail_enrichment,
)
from sqlalchemy import text

from app.core.db import SessionLocal
from app.services.scraper_adapters import RealMatcherAdapter

logger = logging.getLogger(__name__)

SOURCE = "domklik"


def _to_int(v: Any) -> int | None:
    """int() None-safe: возвращает None на None / нечисловом мусоре (не крэшит прогон)."""
    if v is None:
        return None
    try:
        return int(v)
    except (TypeError, ValueError):
        return None


def _to_float(v: Any) -> float | None:
    """float() None-safe: возвращает None на None / нечисловом мусоре."""
    if v is None:
        return None
    try:
        return float(v)
    except (TypeError, ValueError):
        return None


def _build_lot(rec: dict[str, Any]) -> ScrapedLot:
    """Построить ScrapedLot из JSONL-записи. price_rub уже провалидирован caller'ом."""
    raw_extra = rec.get("raw_extra") or {}
    return ScrapedLot(
        source=SOURCE,
        source_url=rec["source_url"],
        source_id=str(rec["id"]),
        address=rec.get("address"),
        lat=_to_float(rec.get("lat")),
        lon=_to_float(rec.get("lon")),
        rooms=_to_int(rec.get("rooms")),
        area_m2=_to_float(rec.get("area_m2")),
        floor=_to_int(rec.get("floor")),
        total_floors=_to_int(rec.get("total_floors")),
        year_built=_to_int(rec.get("year_built")),
        price_rub=int(rec["price_rub"]),
        listing_segment="vtorichka",
        repair_state=rec.get("repair_state"),
        raw_payload={
            "is_sber_collateral": rec.get("is_sber_collateral"),
            "square_price": raw_extra.get("square_price"),
            "avm": rec.get("avm") or {},
        },
    )


def _build_enrichment(rec: dict[str, Any]) -> DomClickDetailEnrichment:
    """Построить deep-detail enrichment из JSONL-записи."""
    raw_extra_in = rec.get("raw_extra") or {}
    raw_extra: dict[str, Any] = {
        "wall_type": rec.get("wall_type"),
        "floor_type": rec.get("floor_type"),
        "calls": rec.get("calls"),
        "is_sber_collateral": rec.get("is_sber_collateral"),
        "avm": rec.get("avm") or {},
        **raw_extra_in,
    }
    return DomClickDetailEnrichment(
        item_id=str(rec["id"]),
        source_url=rec["source_url"],
        repair_state=rec.get("repair_state"),
        repair_type=rec.get("repair_raw"),
        living_area_m2=_to_float(rec.get("living_area_m2")),
        kitchen_area_m2=_to_float(rec.get("kitchen_area_m2")),
        sale_type=rec.get("sale_type"),
        owners_count=_to_int(rec.get("owners_count")),
        encumbrances_clean=rec.get("encumbrances_clean"),
        year_built=_to_int(rec.get("year_built")),
        views_total=_to_int(rec.get("views")),
        price_changes=rec.get("price_changes") or [],
        raw_extra=raw_extra,
    )


def run(jsonl_path: str, limit: int | None, dry_run: bool) -> dict[str, int]:
    """Прочитать JSONL, отфильтровать ok+price, upsert listings + enrichment.

    Returns: dict со счётчиками для итогового лога / тестов.
    """
    lots: list[ScrapedLot] = []
    valid_records: list[dict[str, Any]] = []  # (parallel to lots) для enrichment-шага

    skipped_status = 0
    skipped_no_price = 0
    parse_failed = 0

    with open(jsonl_path, encoding="utf-8") as fh:
        for line in fh:
            line = line.strip()
            if not line:
                continue
            try:
                rec = json.loads(line)
            except json.JSONDecodeError as exc:
                logger.warning("битая JSON-строка пропущена: %s", exc)
                parse_failed += 1
                continue

            if rec.get("status") != "ok":
                skipped_status += 1
                continue
            price = _to_int(rec.get("price_rub"))
            if price is None or price <= 0:
                skipped_no_price += 1
                continue

            try:
                lot = _build_lot(rec)
            except Exception as exc:
                logger.warning("не удалось построить lot для id=%s: %s", rec.get("id"), exc)
                parse_failed += 1
                continue

            lots.append(lot)
            valid_records.append(rec)

            if limit is not None and len(lots) >= limit:
                break

    total_valid = len(lots)
    logger.info(
        "прочитано: valid=%d skipped_status=%d skipped_no_price=%d parse_failed=%d",
        total_valid,
        skipped_status,
        skipped_no_price,
        parse_failed,
    )

    inserted = 0
    updated = 0
    house_matched = 0
    match_failed = 0
    detail_applied = 0
    price_history_attempted = 0
    enrich_failed = 0

    if dry_run:
        logger.info("[dry-run] %d записей распарсено, запись в БД пропущена", total_valid)
        return {
            "inserted": inserted,
            "updated": updated,
            "house_matched": house_matched,
            "match_failed": match_failed,
            "detail_applied": detail_applied,
            "price_history_attempted": price_history_attempted,
            "enrich_failed": enrich_failed,
            "skipped_status": skipped_status,
            "skipped_no_price": skipped_no_price,
            "parse_failed": parse_failed,
        }

    db = SessionLocal()
    try:
        if lots:
            inserted, updated = save_listings(
                db, lots, matcher=RealMatcherAdapter(), region_code=DEFAULT_REGION_CODE
            )
            logger.info("save_listings: inserted=%d updated=%d", inserted, updated)

        # Достаём listing.id + house_id_fk одним SELECT по source_id.
        source_ids = [lot.source_id for lot in lots if lot.source_id is not None]
        id_map: dict[str, tuple[int, Any]] = {}
        if source_ids:
            rows = db.execute(
                text(
                    "SELECT source_id, id, house_id_fk FROM listings "
                    "WHERE source = :source AND source_id = ANY(:ids)"
                ),
                {"source": SOURCE, "ids": source_ids},
            ).fetchall()
            for sid, lid, house_id_fk in rows:
                id_map[str(sid)] = (int(lid), house_id_fk)

        total_saved = len(id_map)
        house_matched = sum(1 for _, hid in id_map.values() if hid is not None)
        match_failed = total_saved - house_matched

        # Deep-detail enrichment по каждой ok-записи.
        for rec in valid_records:
            sid = str(rec["id"])
            entry = id_map.get(sid)
            if entry is None:
                logger.warning("listing id не найден для source_id=%s — enrichment пропущен", sid)
                continue
            listing_id, _ = entry
            # Per-record изоляция: save_detail_enrichment коммитит сама, поэтому
            # падение одной записи иначе оборвало бы остаток valid_records трейсбэком.
            # Уже закоммиченные строки персистят → повторный прогон добьёт хвост.
            try:
                enrichment = _build_enrichment(rec)
                price_history_attempted += len(enrichment.price_changes)
                if save_detail_enrichment(db, listing_id, enrichment):
                    detail_applied += 1
            except Exception as exc:
                db.rollback()
                enrich_failed += 1
                logger.warning("enrichment упал для source_id=%s: %s", sid, exc)
                continue
    finally:
        db.close()

    logger.info(
        "ИТОГО: saved(inserted=%d/updated=%d) house_matched=%d match_failed=%d "
        "detail_applied=%d price_history_attempted=%d enrich_failed=%d "
        "skipped_status=%d skipped_no_price=%d parse_failed=%d",
        inserted,
        updated,
        house_matched,
        match_failed,
        detail_applied,
        price_history_attempted,
        enrich_failed,
        skipped_status,
        skipped_no_price,
        parse_failed,
    )

    return {
        "inserted": inserted,
        "updated": updated,
        "house_matched": house_matched,
        "match_failed": match_failed,
        "detail_applied": detail_applied,
        "price_history_attempted": price_history_attempted,
        "enrich_failed": enrich_failed,
        "skipped_status": skipped_status,
        "skipped_no_price": skipped_no_price,
        "parse_failed": parse_failed,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Ингест DomClick JSONL (от domclick_local_runner) в БД tradein.",
    )
    parser.add_argument("--jsonl", required=True, help="путь к JSONL-файлу раннера")
    parser.add_argument(
        "--limit",
        type=int,
        default=None,
        help="обработать первые N валидных (ok+price) записей",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="парсить + логировать, но НЕ писать в БД",
    )
    args = parser.parse_args()
    run(args.jsonl, args.limit, args.dry_run)


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )
    main()
