"""Generative Design — Stage 1c DXF export via ezdxf.

Renders the parcel context (boundary + buildable area) and one variant's placed
building footprints into a binary DXF for hand-off to architects. Geometry is drawn
in the parcel's *metric* CRS (metres) — architects work in metres, and DXF has no
geographic CRS concept, so emitting WGS84 degrees would be unusable.

Layers:
  * ``PARCEL``    — границы участка (синий).
  * ``BUILDABLE`` — пятно застройки после отступов (зелёный, пунктир-цвет).
  * ``BUILDINGS`` — секции варианта (красный), с текстовой подписью номера секции.

Returns ``bytes`` (binary DXF, R2010) ready for an HTTP response / file write.
Deterministic, no DB / no network. ``ezdxf`` is a light import, so it stays at
module level (unlike WeasyPrint in :mod:`pdf`).
"""

from __future__ import annotations

import io
import logging

# ezdxf.new живёт в ezdxf.filemanagement и не реэкспортируется через ezdxf.__all__;
# импорт из модуля удовлетворяет strict no-implicit-reexport.
from ezdxf.enums import TextEntityAlignment
from ezdxf.filemanagement import new as ezdxf_new

# Modelspace определён в ezdxf.layouts.layout и не в __all__ пакета ezdxf.layouts;
# импорт из модуля-определителя удовлетворяет strict no-implicit-reexport (как ezdxf_new).
from ezdxf.layouts.layout import Modelspace
from shapely.coords import CoordinateSequence
from shapely.geometry import Polygon

from app.schemas.concept import ConceptVariant
from app.services.generative.geometry import Parcel

logger = logging.getLogger(__name__)

# AutoCAD Color Index (ACI) per layer.
_ACI_PARCEL = 5  # blue
_ACI_BUILDABLE = 3  # green
_ACI_BUILDINGS = 1  # red

_LAYER_PARCEL = "PARCEL"
_LAYER_BUILDABLE = "BUILDABLE"
_LAYER_BUILDINGS = "BUILDINGS"

# Высота текста подписи секции (метры в модельном пространстве).
_LABEL_HEIGHT_M = 2.0


def _ring_points(coords: CoordinateSequence) -> list[tuple[float, float]]:
    """Кольцо (exterior/interior) как список (x, y) для LWPolyline (без замыкающей точки)."""
    pts = list(coords)
    # Shapely дублирует первую точку в конце; close=True у ezdxf замкнёт сам.
    if len(pts) > 1 and pts[0] == pts[-1]:
        pts = pts[:-1]
    return [(float(x), float(y)) for x, y in pts]


def _add_polygon(msp: Modelspace, poly: Polygon, layer: str) -> None:
    """Нарисовать полигон на слое: внешнее кольцо + каждое внутреннее (отверстие).

    LWPolyline не умеет дырки, поэтому каждое interior-кольцо эмитируется отдельной
    замкнутой полилинией на том же слое — иначе легальные вырезы (двор, сервитут,
    охранная зона) терялись бы и заливались сплошняком.
    """
    msp.add_lwpolyline(
        _ring_points(poly.exterior.coords),
        close=True,
        dxfattribs={"layer": layer},
    )
    for interior in poly.interiors:
        msp.add_lwpolyline(
            _ring_points(interior.coords),
            close=True,
            dxfattribs={"layer": layer},
        )


def export_concept_dxf(parcel: Parcel, variant: ConceptVariant) -> bytes:
    """Собрать binary DXF: участок + пятно застройки + секции одного варианта.

    Args:
        parcel: Stage 1a участок (метрическая геометрия parcel/buildable).
        variant: вариант, чьи секции рисуем (footprints берём из его geojson —
            но геометрию рисуем из метрического parcel-space через свежий парсинг
            geojson обратно нельзя без CRS, поэтому секции восстанавливаем ниже).

    Returns:
        bytes: бинарный DXF R2010.
    """
    doc = ezdxf_new("R2010")
    doc.layers.add(_LAYER_PARCEL, color=_ACI_PARCEL)
    doc.layers.add(_LAYER_BUILDABLE, color=_ACI_BUILDABLE)
    doc.layers.add(_LAYER_BUILDINGS, color=_ACI_BUILDINGS)
    msp = doc.modelspace()

    # Участок и пятно застройки — из метрической геометрии Parcel.
    _add_polygon(msp, parcel.polygon_m, _LAYER_PARCEL)
    _add_polygon(msp, parcel.buildable_m, _LAYER_BUILDABLE)

    # Секции: восстанавливаем метрические footprints из WGS84-geojson варианта.
    features = variant.buildings_geojson.get("features", [])
    section_count = 0
    if isinstance(features, list):
        for feature in features:
            footprint = _feature_to_metric_polygon(parcel, feature)
            if footprint is None:
                continue
            section_count += 1
            _add_polygon(msp, footprint, _LAYER_BUILDINGS)
            centroid = footprint.centroid
            label = str(_feature_section_id(feature, section_count))
            text = msp.add_text(
                label,
                dxfattribs={"layer": _LAYER_BUILDINGS, "height": _LABEL_HEIGHT_M},
            )
            text.set_placement(
                (float(centroid.x), float(centroid.y)),
                align=TextEntityAlignment.MIDDLE_CENTER,
            )

    stream = io.BytesIO()
    doc.write(stream, fmt="bin")
    data = stream.getvalue()
    logger.info(
        "DXF export: strategy=%s sections=%d bytes=%d",
        variant.strategy,
        section_count,
        len(data),
    )
    return data


def _feature_section_id(feature: object, fallback: int) -> int:
    """Достать section_id из properties Feature, иначе fallback-счётчик."""
    if isinstance(feature, dict):
        props = feature.get("properties")
        if isinstance(props, dict):
            sid = props.get("section_id")
            if isinstance(sid, int):
                return sid
    return fallback


def _feature_to_metric_polygon(parcel: Parcel, feature: object) -> Polygon | None:
    """WGS84 GeoJSON Feature -> метрический Shapely Polygon (через обратный трансформер).

    Возвращает None для невалидных/непригодных фич (graceful — экспорт не падает).
    """
    if not isinstance(feature, dict):
        return None
    geometry = feature.get("geometry")
    if not isinstance(geometry, dict) or geometry.get("type") != "Polygon":
        return None
    coords = geometry.get("coordinates")
    # Shapely mapping() emits nested tuples; accept both tuple and list.
    if not isinstance(coords, list | tuple) or not coords:
        return None
    ring = coords[0]
    if not isinstance(ring, list | tuple) or len(ring) < 4:
        return None

    metric_pts: list[tuple[float, float]] = []
    for pt in ring:
        if not isinstance(pt, list | tuple) or len(pt) < 2:
            return None
        lon, lat = float(pt[0]), float(pt[1])
        x, y = parcel.wgs84_to_metric(lon, lat)
        metric_pts.append((x, y))

    try:
        poly = Polygon(metric_pts)
    except (ValueError, TypeError):
        return None
    return poly if (poly.is_valid and not poly.is_empty) else None


__all__ = ["export_concept_dxf"]
