"""House metadata service — обогащение данных дома по координатам (#392).

Когда пользователь не указал год постройки / тип дома, подтягиваем данные
из OpenStreetMap через Overpass API: building:levels, start_date, материал.

Стратегия:
- Cache lookup в `house_metadata` (Postgres) по близости координат — TTL 30 дней
- Cache miss → Overpass API → парсинг тегов → запись в кэш
- Best-effort: ЛЮБАЯ ошибка → None, оценка работает без обогащения (не 500-ит)

Используется в estimator.estimate_quality перед house-match аналогов (#6).
"""

from __future__ import annotations

import json
import logging
import re
from dataclasses import dataclass

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

from app.core.config import settings

logger = logging.getLogger(__name__)

OVERPASS_URL = "https://overpass-api.de/api/interpreter"
_CACHE_RADIUS_M = 40       # дом в пределах 40 м от точки — считаем тем же
_OVERPASS_RADIUS_M = 25    # ищем здание в 25 м от геокодированной точки
_CAD_RADIUS_M = 60         # радиус поиска здания в cad_buildings (#393)


@dataclass(frozen=True, slots=True)
class HouseMetadata:
    lat: float
    lon: float
    year_built: int | None
    total_floors: int | None
    house_type: str | None      # panel / brick / monolith / monolith_brick / other
    total_units: int | None
    source: str                 # 'osm' / 'cache'


# ── Парсинг тегов OSM ────────────────────────────────────────────────────────
_YEAR_KEYS = (
    "start_date", "construction_date", "building:year",
    "year_of_construction", "building:start_date",
)


def _parse_year(tags: dict[str, str]) -> int | None:
    """Год постройки из OSM-тегов. Значение бывает '1975', '1975-06-01',
    '1970s', '1960..1965', 'C20' — берём первое 4-значное число 1800–2100."""
    for key in _YEAR_KEYS:
        raw = tags.get(key)
        if not raw:
            continue
        m = re.search(r"\b(1[89]\d\d|20\d\d)\b", raw)
        if m:
            year = int(m.group(1))
            if 1800 <= year <= 2100:
                return year
    return None


def _parse_int(tags: dict[str, str], *keys: str) -> int | None:
    """Первое валидное целое (0 < n < 1000) из перечисленных тегов."""
    for key in keys:
        raw = (tags.get(key) or "").strip()
        if raw.isdigit():
            value = int(raw)
            if 0 < value < 1000:
                return value
    return None


# OSM building:material → наш house_type. Покрытие в OSM неполное — best-effort.
_MATERIAL_TO_TYPE = {
    "concrete_panels": "panel",
    "panels": "panel",
    "panel": "panel",
    "brick": "brick",
    "concrete": "monolith",
    "reinforced_concrete": "monolith",
}


def _parse_house_type(tags: dict[str, str]) -> str | None:
    material = (tags.get("building:material") or "").lower().strip()
    return _MATERIAL_TO_TYPE.get(material)


# ── Cache (house_metadata, TTL 30 дней) ──────────────────────────────────────
def _cache_get(db: Session, lat: float, lon: float) -> HouseMetadata | None:
    row = db.execute(
        text(
            """
            SELECT lat, lon, year_built, total_floors, house_type, total_units
            FROM house_metadata
            WHERE expires_at > NOW()
              AND ST_DWithin(geom::geography, ST_MakePoint(:lon, :lat)::geography, :radius)
            ORDER BY ST_Distance(geom::geography, ST_MakePoint(:lon, :lat)::geography)
            LIMIT 1
            """
        ),
        {"lat": lat, "lon": lon, "radius": _CACHE_RADIUS_M},
    ).fetchone()
    if row is None:
        return None
    return HouseMetadata(
        lat=row.lat, lon=row.lon,
        year_built=row.year_built, total_floors=row.total_floors,
        house_type=row.house_type, total_units=row.total_units,
        source="cache",
    )


def _cache_put(db: Session, meta: HouseMetadata, raw: dict) -> None:
    db.execute(
        text(
            """
            INSERT INTO house_metadata
                (lat, lon, year_built, total_floors, house_type,
                 total_units, source, raw_payload)
            VALUES (:lat, :lon, :year, :floors, :htype,
                    :units, 'osm', CAST(:raw AS jsonb))
            """
        ),
        {
            "lat": meta.lat, "lon": meta.lon,
            "year": meta.year_built, "floors": meta.total_floors,
            "htype": meta.house_type, "units": meta.total_units,
            "raw": json.dumps(raw, ensure_ascii=False),
        },
    )
    db.commit()


# ── Источник #393: кадастр зданий (gendesign_cad_buildings FDW) ──────────────
def _cad_buildings_get(db: Session, lat: float, lon: float) -> HouseMetadata | None:
    """Ближайшее здание из gendesign_cad_buildings FDW (данные Росреестра) по координатам.

    Приоритетный источник house_metadata — год / этажность полнее, чем OSM
    (~36к домов ЕКБ с годом постройки). Использует bbox+haversine вместо ST_DWithin —
    FDW-таблица не имеет PostGIS-колонки geom (только lat/lon скалярные).
    """
    try:
        row = db.execute(
            text(
                """
                WITH candidates AS (
                  SELECT year_built, floors, lat, lon,
                         111320.0 * sqrt(
                             pow(CAST(:lat AS double precision) - lat, 2) +
                             pow(
                                 cos(radians(CAST(:lat AS double precision)))
                                 * (CAST(:lon AS double precision) - lon), 2
                             )
                         ) AS dist_m
                  FROM gendesign_cad_buildings
                  WHERE (year_built IS NOT NULL OR floors IS NOT NULL)
                    AND lat BETWEEN CAST(:lat AS double precision) - 0.0010
                                AND CAST(:lat AS double precision) + 0.0010
                    AND lon BETWEEN CAST(:lon AS double precision) - 0.0015
                                AND CAST(:lon AS double precision) + 0.0015
                )
                SELECT year_built, floors
                FROM candidates
                WHERE dist_m < CAST(:radius AS double precision)
                ORDER BY dist_m ASC
                LIMIT 1
                """
            ),
            {"lat": lat, "lon": lon, "radius": float(_CAD_RADIUS_M)},
        ).fetchone()
    except Exception:
        logger.warning(
            "gendesign_cad_buildings lookup failed at (%.5f, %.5f)", lat, lon, exc_info=True
        )
        return None

    if row is None:
        return None
    return HouseMetadata(
        lat=lat, lon=lon,
        year_built=row.year_built,
        total_floors=row.floors,
        house_type=None,      # purpose не маппится в panel/brick
        total_units=None,
        source="cadastre",
    )


# ── Provider: OSM Overpass ───────────────────────────────────────────────────
async def _overpass_lookup(lat: float, lon: float) -> tuple[HouseMetadata, dict] | None:
    """Запрос здания у точки через Overpass API. (meta, raw_payload) или None."""
    query = (
        "[out:json][timeout:20];"
        f'way(around:{_OVERPASS_RADIUS_M},{lat},{lon})["building"];'
        "out tags center;"
    )
    headers = {"User-Agent": f"TradeInMVP/0.1 (contact: {settings.contact_email})"}
    async with httpx.AsyncClient(timeout=15.0, headers=headers) as client:
        response = await client.post(OVERPASS_URL, data={"data": query})
        response.raise_for_status()
        data = response.json()

    elements = [e for e in data.get("elements", []) if e.get("tags")]
    if not elements:
        return None

    # Ближайшее здание (по center) к геокодированной точке.
    def _dist2(el: dict) -> float:
        center = el.get("center") or {}
        return (center.get("lat", 0.0) - lat) ** 2 + (center.get("lon", 0.0) - lon) ** 2

    best = min(elements, key=_dist2)
    tags = best.get("tags", {})
    meta = HouseMetadata(
        lat=lat, lon=lon,
        year_built=_parse_year(tags),
        total_floors=_parse_int(tags, "building:levels"),
        house_type=_parse_house_type(tags),
        total_units=_parse_int(tags, "building:flats", "flats"),
        source="osm",
    )
    return meta, {"osm_way_id": best.get("id"), "tags": tags}


# ── Public API ───────────────────────────────────────────────────────────────
async def get_house_metadata(lat: float, lon: float, db: Session) -> HouseMetadata | None:
    """Метаданные дома по координатам: cache (TTL 30д) → Overpass.

    Best-effort: при ЛЮБОЙ ошибке возвращает None и откатывает сессию —
    оценка обязана работать даже если картография недоступна.
    """
    try:
        # #393: кадастр зданий (Росреестр, cad_buildings) — приоритетный источник.
        cad = _cad_buildings_get(db, lat, lon)
        if cad is not None:
            logger.info(
                "house_metadata: cad_buildings (%.5f, %.5f) → year=%s floors=%s",
                lat, lon, cad.year_built, cad.total_floors,
            )
            return cad

        cached = _cache_get(db, lat, lon)
        if cached is not None:
            logger.info("house_metadata cache hit: (%.5f, %.5f)", lat, lon)
            return cached

        result = await _overpass_lookup(lat, lon)
        if result is None:
            logger.info("house_metadata: OSM не нашёл здание у (%.5f, %.5f)", lat, lon)
            return None

        meta, raw = result
        # Пустой результат (ни года, ни этажности) кэшировать смысла нет.
        if meta.year_built is None and meta.total_floors is None:
            return None

        _cache_put(db, meta, raw)
        logger.info(
            "house_metadata OSM: (%.5f, %.5f) → year=%s floors=%s type=%s",
            lat, lon, meta.year_built, meta.total_floors, meta.house_type,
        )
        return meta
    except Exception:
        logger.warning(
            "house_metadata enrichment failed at (%.5f, %.5f)", lat, lon, exc_info=True
        )
        try:
            db.rollback()
        except Exception:
            pass
        return None
