"""
services/geo_service.py
========================
Données géographiques depuis SQLite :
  - Zones de bruit (noise_zones) → GeoJSON
  - Espaces naturels (nature_zones) → GeoJSON avec fusion Shapely optionnelle
  - Points d'intérêt (poi) → GeoJSON
"""

from __future__ import annotations

import json
import logging

import pymysql

from domain.db import get_db

logger = logging.getLogger(__name__)


def get_noise_polygons(
    min_lng: float,
    min_lat: float,
    max_lng: float,
    max_lat: float,
    sources: str = "routier,ferroviaire",
    min_lden: int = 55,
) -> dict:
    """
    Retourne un GeoJSON FeatureCollection des axes bruyants (OSM) pour la bbox.

    Args:
        sources: chaîne de types séparés par virgule (routier, ferroviaire).
        min_lden: niveau LDEN minimum (dB), filtre inclusif.

    Returns:
        GeoJSON FeatureCollection.

    Raises:
        RuntimeError: si la table noise_zones est absente.
    """
    source_list = [s.strip() for s in sources.split(",") if s.strip()]
    if not source_list:
        return {"type": "FeatureCollection", "features": []}

    placeholders = ",".join("?" * len(source_list))
    params: list = [max_lng, min_lng, max_lat, min_lat, min_lden] + source_list

    try:
        with get_db() as conn:
            rows = conn.execute(
                f"""SELECT source_type, lden_min, geom_type, coordinates
                    FROM noise_zones
                    WHERE bbox_min_lng < ? AND bbox_max_lng > ?
                      AND bbox_min_lat < ? AND bbox_max_lat > ?
                      AND lden_min >= ?
                      AND source_type IN ({placeholders})
                    ORDER BY lden_min DESC
                    LIMIT 2000""",
                params,
            ).fetchall()
    except pymysql.Error as exc:
        logger.warning("noise_polygons DB error: %s", exc)
        raise RuntimeError(
            "Table 'noise_zones' introuvable — exécutez build_noise_zones.py"
        ) from exc

    features = [
        {
            "type": "Feature",
            "geometry": {"type": r["geom_type"], "coordinates": json.loads(r["coordinates"])},
            "properties": {"source_type": r["source_type"], "lden_min": r["lden_min"]},
        }
        for r in rows
    ]
    logger.debug(
        "noise_polygons | sources=%s min_lden=%d → %d features", sources, min_lden, len(features)
    )
    return {"type": "FeatureCollection", "features": features}


# Distance threshold for "merge small into nearby large" rule (≈ 500m at lat 50°).
_NEIGHBOUR_DIST_DEG = 0.0045


def _zoom_params(zoom: int) -> dict:
    """
    Return Shapely processing parameters calibrated for a Leaflet zoom level.

    min_area     – minimum individual polygon area (deg²) to be "large"
    merge_dist   – buffer distance for gap-closing union (deg)
    shrink_dist  – shrink after union to restore approximate original edge (deg)
    simplify_tol – Douglas-Peucker tolerance (deg)
    min_area_out – minimum output polygon area after simplification (deg²)
    use_neighbour– apply the 0.5 km neighbour-merge rule for small polygons
    db_prefilter – conservative bbox_area floor for the SQL query (deg²)

    Conversion at lat 50°: 1 m² ≈ 1.26e-10 deg²
      0.5 ha ≈ 6.3e-7 deg²   1 ha ≈ 1.26e-6 deg²   3 ha ≈ 3.8e-6 deg²
    """
    if zoom <= 10:
        return dict(min_area=3e-6,  merge_dist=0.003,  shrink_dist=0.0015,  simplify_tol=0.0004,  min_area_out=4e-7,  use_neighbour=True,  db_prefilter=3e-8)
    if zoom == 11:
        return dict(min_area=1e-6,  merge_dist=0.002,  shrink_dist=0.001,   simplify_tol=0.0003,  min_area_out=1e-7,  use_neighbour=True,  db_prefilter=1e-8)
    if zoom == 12:
        return dict(min_area=3e-7,  merge_dist=0.001,  shrink_dist=0.0005,  simplify_tol=0.0002,  min_area_out=3e-8,  use_neighbour=True,  db_prefilter=3e-9)
    if zoom == 13:
        return dict(min_area=1e-7,  merge_dist=0.0007, shrink_dist=0.00035, simplify_tol=0.00015, min_area_out=1e-8,  use_neighbour=True,  db_prefilter=0)
    if zoom == 14:
        return dict(min_area=3e-8,  merge_dist=0.0006, shrink_dist=0.0003,  simplify_tol=0.0001,  min_area_out=5e-9,  use_neighbour=False, db_prefilter=0)
    # zoom >= 15
    return         dict(min_area=5e-9,  merge_dist=0.0003, shrink_dist=0.0001,  simplify_tol=0.00005, min_area_out=5e-10, use_neighbour=False, db_prefilter=0)


def get_nature_polygons(
    min_lng: float,
    min_lat: float,
    max_lng: float,
    max_lat: float,
    category: str = "",
    zoom: int = 14,
) -> dict:
    """
    GeoJSON des espaces naturels, adapté au niveau de zoom courant.

    Behaviour by zoom level
    -----------------------
    zoom ≥ 14, no category  → fast path: pre-merged table (full detail, no Shapely).
    zoom ≥ 14, with category→ Shapely path with small area threshold.
    zoom < 14               → Shapely path with zoom-calibrated parameters.

    Returns:
        GeoJSON FeatureCollection.

    Raises:
        RuntimeError: si la table nature_zones est absente.
    """
    params = _zoom_params(zoom)

    try:
        with get_db() as conn:
            # ── Fast path: pre-merged table, full detail, no category filter ─────
            if not category and zoom >= 14:
                has_merged = conn.execute(
                    "SELECT 1 FROM information_schema.tables"
                    " WHERE table_schema=DATABASE() AND table_name='nature_zones_merged'"
                ).fetchone()
                if has_merged:
                    rows = conn.execute(
                        """SELECT geom_type, coordinates
                           FROM nature_zones_merged
                           WHERE bbox_min_lng < ? AND bbox_max_lng > ?
                             AND bbox_min_lat < ? AND bbox_max_lat > ?""",
                        [max_lng, min_lng, max_lat, min_lat],
                    ).fetchall()
                    features = [
                        {
                            "type": "Feature",
                            "geometry": {
                                "type": r["geom_type"],
                                "coordinates": json.loads(r["coordinates"]),
                            },
                            "properties": {},
                        }
                        for r in rows
                    ]
                    logger.debug("nature_polygons fast path | zoom=%d | %d features", zoom, len(features))
                    return {"type": "FeatureCollection", "features": features}

            # ── Slow path: Shapely computation with zoom-calibrated parameters ───
            from shapely.geometry import shape as _shape
            from shapely.ops import unary_union as _union
            from shapely.strtree import STRtree

            query_params: list = [max_lng, min_lng, max_lat, min_lat]
            cat_clause = ""
            if category:
                cat_list = [c.strip() for c in category.split(",") if c.strip()]
                if cat_list:
                    cat_clause = f" AND category IN ({','.join('?' * len(cat_list))})"
                    query_params.extend(cat_list)

            area_clause = ""
            if params["db_prefilter"] > 0:
                area_clause = " AND (bbox_max_lng - bbox_min_lng) * (bbox_max_lat - bbox_min_lat) >= ?"
                query_params.append(params["db_prefilter"])

            rows = conn.execute(
                f"""SELECT geom_type, coordinates,
                           (bbox_max_lng - bbox_min_lng) * (bbox_max_lat - bbox_min_lat) AS bbox_area
                    FROM nature_zones
                    WHERE bbox_min_lng < ? AND bbox_max_lng > ?
                      AND bbox_min_lat < ? AND bbox_max_lat > ?
                      {cat_clause}{area_clause}
                    ORDER BY bbox_area DESC
                    LIMIT 4000""",
                query_params,
            ).fetchall()

    except pymysql.Error as exc:
        logger.warning("nature_polygons DB error: %s", exc)
        raise RuntimeError(
            "Table 'nature_zones' introuvable — exécutez build_nature_zones.py"
        ) from exc

    if not rows:
        return {"type": "FeatureCollection", "features": []}

    # ── Build Shapely objects; classify as large / small by exact area ───────
    from shapely.geometry import shape as _shape
    from shapely.ops import unary_union as _union
    from shapely.strtree import STRtree

    min_area = params["min_area"]
    large_polys: list = []
    small_polys: list = []

    for r in rows:
        coords = json.loads(r["coordinates"])
        if coords and not isinstance(coords[0][0], list):
            coords = [coords]
        try:
            poly = _shape({"type": r["geom_type"], "coordinates": coords})
            if not poly.is_valid:
                poly = poly.buffer(0)
            if not poly.is_valid or poly.is_empty:
                continue
            (large_polys if poly.area >= min_area else small_polys).append(poly)
        except Exception:
            continue

    if not large_polys:
        if not small_polys:
            return {"type": "FeatureCollection", "features": []}
        all_polys = large_polys + small_polys
        all_polys.sort(key=lambda g: g.area, reverse=True)
        large_polys = all_polys[:max(1, int(len(all_polys) * 0.70))]
        logger.debug("nature_polygons fallback to top-70%% | zoom=%d | %d polys", zoom, len(large_polys))

    # ── Neighbour-merge rule: include small polys within 500 m of a large one ─
    if params["use_neighbour"] and small_polys:
        large_buffered = [g.buffer(_NEIGHBOUR_DIST_DEG) for g in large_polys]
        tree = STRtree(large_buffered)
        for s_geom in small_polys:
            candidates = list(tree.query(s_geom))
            if any(large_buffered[i].intersects(s_geom) for i in candidates):
                large_polys.append(s_geom)

    # ── Buffer-union-simplify with zoom-calibrated distances ─────────────────
    merge_dist   = params["merge_dist"]
    shrink_dist  = params["shrink_dist"]
    simplify_tol = params["simplify_tol"]
    min_area_out = params["min_area_out"]

    try:
        merged = _union([p.buffer(merge_dist) for p in large_polys]).buffer(-shrink_dist)
        final  = merged.simplify(simplify_tol, preserve_topology=True)
    except Exception:
        try:
            final = _union(large_polys)
        except Exception:
            return {"type": "FeatureCollection", "features": []}

    def _collect(geom):
        t = geom.geom_type
        if t in ("Polygon", "MultiPolygon"):
            if geom.area >= min_area_out:
                yield geom
        elif t == "GeometryCollection":
            for g in geom.geoms:
                yield from _collect(g)

    def _to_coords(geom):
        if geom.geom_type == "Polygon":
            rings = [list(geom.exterior.coords)] + [list(r.coords) for r in geom.interiors]
            return "Polygon", [[[round(x, 6), round(y, 6)] for x, y in ring] for ring in rings]
        polys_out = []
        for p in geom.geoms:
            rings = [list(p.exterior.coords)] + [list(r.coords) for r in p.interiors]
            polys_out.append([[[round(x, 6), round(y, 6)] for x, y in ring] for ring in rings])
        return "MultiPolygon", polys_out

    features = []
    for geom in _collect(final):
        gtype, coords = _to_coords(geom)
        features.append({
            "type": "Feature",
            "geometry": {"type": gtype, "coordinates": coords},
            "properties": {},
        })

    logger.debug(
        "nature_polygons slow path | zoom=%d | large=%d → %d features",
        zoom, len(large_polys), len(features),
    )
    return {"type": "FeatureCollection", "features": features}


def get_poi_geojson(types: str = "mairie,gare,metro,tram") -> dict:
    """
    Retourne les POIs demandés comme GeoJSON FeatureCollection.

    Args:
        types: chaîne de types séparés par virgule (mairie, gare, metro, tram).

    Returns:
        GeoJSON FeatureCollection.

    Raises:
        RuntimeError: si la table poi est absente.
    """
    type_list = [t.strip() for t in types.split(",") if t.strip()]
    if not type_list:
        return {"type": "FeatureCollection", "features": []}

    placeholders = ",".join("?" * len(type_list))
    try:
        with get_db() as conn:
            rows = conn.execute(
                f"SELECT type, name, lat, lon FROM poi"
                f" WHERE type IN ({placeholders})"
                f" ORDER BY type, name",
                type_list,
            ).fetchall()
    except pymysql.Error as exc:
        logger.warning("poi_geojson DB error: %s", exc)
        raise RuntimeError("Table 'poi' introuvable — exécutez build_poi.py") from exc

    features = [
        {
            "type": "Feature",
            "geometry": {"type": "Point", "coordinates": [r["lon"], r["lat"]]},
            "properties": {"type": r["type"], "name": r["name"]},
        }
        for r in rows
    ]
    logger.debug("poi_geojson | types=%s → %d features", types, len(features))
    return {"type": "FeatureCollection", "features": features}
