"""S-CANVAS 구조물 상세도면 DXF 치수 파서.

상세도면(단면도/상세도)에서 TEXT, MTEXT, DIMENSION, ATTRIB 엔티티를 분석하여
구조물의 높이, 폭, 두께, 계획고, 관경, 사면경사 등 설계 치수를 자동 추출한다.

사용법:
    parser = DetailParser()
    result = parser.parse(dxf_path)
    # result.dimensions  → [ParsedDimension(...), ...]
    # result.summary()   → {"height": 5.0, "width": 3.0, ...}
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path

import ezdxf
import numpy as np


# ---------------------------------------------------------------------------
# 데이터 클래스
# ---------------------------------------------------------------------------

@dataclass
class ParsedDimension:
    """파싱된 개별 치수 항목."""
    param: str            # height, width, thickness, elevation, diameter, slope, length, ...
    value: float          # 수치 값 (미터 단위로 정규화)
    raw_text: str         # 원본 텍스트
    source: str           # "text" | "mtext" | "dimension" | "attrib"
    layer: str            # DXF 레이어명
    position: tuple       # (x, y) 좌표
    confidence: float     # 0.0 ~ 1.0 신뢰도
    unit: str = "m"       # 단위 (m, mm)
    secondary: float | None = None  # 보조값 (slope의 경우 비율 등)


@dataclass
class ParseResult:
    """전체 파싱 결과."""
    dxf_path: str
    dimensions: list[ParsedDimension] = field(default_factory=list)
    layer_names: list[str] = field(default_factory=list)
    entity_summary: dict = field(default_factory=dict)  # {entity_type: count}

    def summary(self) -> dict:
        """파라미터별 최고 신뢰도 값을 딕셔너리로 반환.

        Returns:
            {"height": 5.0, "width": 3.0, "elevation": 85.0, ...}
        """
        best: dict[str, ParsedDimension] = {}
        for d in self.dimensions:
            if d.param not in best or d.confidence > best[d.param].confidence:
                best[d.param] = d
        return {k: v.value for k, v in best.items()}

    def by_param(self, param: str) -> list[ParsedDimension]:
        """특정 파라미터의 모든 파싱 결과를 신뢰도 내림차순으로 반환."""
        return sorted(
            [d for d in self.dimensions if d.param == param],
            key=lambda d: -d.confidence,
        )

    def all_params(self) -> list[str]:
        """발견된 모든 파라미터 종류."""
        return sorted(set(d.param for d in self.dimensions))


# ---------------------------------------------------------------------------
# 패턴 정의
# ---------------------------------------------------------------------------

# AutoCAD 특수문자: %%C = Φ (파이), %%D = ° (도), %%P = ± (플마)
_AUTOCAD_PHI = r"%%[cC]"

# 토목 도면 치수 패턴 (한글/영문 혼용)
_PATTERNS: list[tuple[str, str, re.Pattern, float]] = [
    # (param, description, compiled_pattern, base_confidence)

    # --- 계획고 (elevation) ---
    ("elevation", "EL.xxx.xx",
     re.compile(r"EL\.?\s*[=:]?\s*(\d+\.?\d*)", re.IGNORECASE), 0.95),

    ("elevation", "표고 xxx.x",
     re.compile(r"(?:표고|계획고|설계고)\s*[=:]?\s*(\d+\.?\d*)", re.IGNORECASE), 0.90),

    # --- 높이 (height) ---
    ("height", "H=xxx",
     re.compile(r"[Hh]\s*[=:]\s*(\d+\.?\d*)\s*[mM]?"), 0.90),

    ("height", "높이=xxx",
     re.compile(r"(?:높이|전고|벽고|壁高)\s*[=:]?\s*(\d+\.?\d*)\s*[mM]?"), 0.90),

    ("height", "h xxx.x m",
     re.compile(r"(?:^|\s)[Hh]\s+(\d+\.?\d*)\s*[mM](?:\s|$)"), 0.75),

    # --- 폭 (width) ---
    ("width", "W=xxx 또는 B=xxx",
     re.compile(r"[WwBb]\s*[=:]\s*(\d+\.?\d*)\s*[mM]?"), 0.90),

    ("width", "폭=xxx",
     re.compile(r"(?:폭|너비|幅|底幅|천단폭)\s*[=:]?\s*(\d+\.?\d*)\s*[mM]?"), 0.90),

    # --- 두께 (thickness) ---
    ("thickness", "T=xxx 또는 t=xxx",
     re.compile(r"[Tt]\s*[=:]\s*(\d+\.?\d*)\s*[mM]?"), 0.85),

    ("thickness", "두께=xxx",
     re.compile(r"(?:두께|벽두께|슬래브두께)\s*[=:]?\s*(\d+\.?\d*)\s*[mM]?"), 0.90),

    # --- 길이 (length) ---
    ("length", "L=xxx",
     re.compile(r"[Ll]\s*[=:]\s*(\d+\.?\d*)\s*[mM]?"), 0.85),

    ("length", "길이=xxx 또는 연장=xxx",
     re.compile(r"(?:길이|연장|延長|총연장)\s*[=:]?\s*(\d+\.?\d*)\s*[mM]?"), 0.90),

    # --- 관경/직경 (diameter) ---
    ("diameter", "%%Cxxx (AutoCAD Φ기호)",
     re.compile(r"%%[cC]\s*(\d+\.?\d*)"), 0.95),

    ("diameter", "D=xxx 또는 Φxxx",
     re.compile(r"(?:[Dd]|[ΦφΦ])\s*[=:]?\s*(\d+\.?\d*)\s*(?:mm|MM)?"), 0.85),

    ("diameter", "관경=xxx",
     re.compile(r"(?:관경|내경|외경|직경)\s*[=:]?\s*(\d+\.?\d*)\s*(?:mm|MM|[mM])?"), 0.90),

    # --- 사면 경사 (slope) ---
    ("slope", "1:N.N",
     re.compile(r"(\d+)\s*:\s*(\d+\.?\d*)"), 0.90),

    # --- 박스/U형 단면 (composite: width × height) ---
    ("_box", "BOX W×H",
     re.compile(r"BOX\s*(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)", re.IGNORECASE), 0.95),

    ("_uchannel", "U W×H",
     re.compile(r"U\s*(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)", re.IGNORECASE), 0.95),

    # --- 반경 (radius) ---
    ("radius", "R=xxx",
     re.compile(r"[Rr]\s*[=:]\s*(\d+\.?\d*)\s*[mM]?"), 0.80),

    # --- 간격 (spacing) ---
    ("spacing", "@xxx",
     re.compile(r"@\s*(\d+\.?\d*)"), 0.80),

    # --- 근입깊이 (embedment depth) ---
    ("embedment", "근입=xxx",
     re.compile(r"(?:근입|근입깊이|매입깊이)\s*[=:]?\s*(\d+\.?\d*)\s*[mM]?"), 0.90),
]


# ---------------------------------------------------------------------------
# 메인 파서
# ---------------------------------------------------------------------------

class DetailParser:
    """구조물 상세도면 DXF에서 설계 치수를 추출하는 파서."""

    def __init__(self, unit_threshold_mm: float = 50.0):
        """
        Args:
            unit_threshold_mm: 이 값보다 큰 수치는 mm 단위로 간주하여 m로 변환.
                               관경(%%C300 등)은 별도 처리.
        """
        self.unit_threshold = unit_threshold_mm

    def parse(self, dxf_path: str | Path) -> ParseResult:
        """DXF 파일에서 치수를 파싱.

        Args:
            dxf_path: DXF 파일 경로

        Returns:
            ParseResult 객체
        """
        dxf_path = Path(dxf_path)
        doc = ezdxf.readfile(str(dxf_path))
        msp = doc.modelspace()

        result = ParseResult(dxf_path=str(dxf_path))
        result.layer_names = [layer.dxf.name for layer in doc.layers]

        # 엔티티 요약 수집
        for entity in msp:
            et = entity.dxftype()
            result.entity_summary[et] = result.entity_summary.get(et, 0) + 1

        # 1) TEXT / MTEXT 파싱
        self._parse_text_entities(msp, result)

        # 2) DIMENSION 엔티티 파싱
        self._parse_dimension_entities(msp, result)

        # 3) INSERT 블록 내 ATTRIB 파싱
        self._parse_attrib_entities(msp, result)

        return result

    # ----- TEXT / MTEXT -----

    def _parse_text_entities(self, msp, result: ParseResult):
        """TEXT, MTEXT 엔티티에서 패턴 매칭으로 치수 추출."""
        for entity in msp:
            etype = entity.dxftype()
            if etype not in ("TEXT", "MTEXT"):
                continue

            try:
                txt = entity.dxf.text.strip() if etype == "TEXT" else (entity.text or "").strip()
            except Exception:
                continue

            if not txt:
                continue

            layer = entity.dxf.layer
            try:
                pos = entity.dxf.insert
                position = (pos.x, pos.y)
            except Exception:
                position = (0.0, 0.0)

            source = etype.lower()
            self._match_patterns(txt, source, layer, position, result)

    def _match_patterns(self, txt: str, source: str, layer: str,
                        position: tuple, result: ParseResult):
        """텍스트에 대해 모든 패턴을 매칭하고 결과를 추가."""
        for param, _desc, pattern, base_conf in _PATTERNS:
            m = pattern.search(txt)
            if not m:
                continue

            groups = m.groups()

            # --- 복합 치수 (BOX, U형) → width + height 분리 ---
            if param == "_box":
                w, h = float(groups[0]), float(groups[1])
                w, w_unit = self._normalize_unit(w, param="width")
                h, h_unit = self._normalize_unit(h, param="height")
                result.dimensions.append(ParsedDimension(
                    param="width", value=w, raw_text=txt, source=source,
                    layer=layer, position=position, confidence=base_conf,
                    unit=w_unit,
                ))
                result.dimensions.append(ParsedDimension(
                    param="height", value=h, raw_text=txt, source=source,
                    layer=layer, position=position, confidence=base_conf,
                    unit=h_unit,
                ))
                return  # 복합 패턴 매칭 시 개별 패턴 중복 방지

            if param == "_uchannel":
                w, h = float(groups[0]), float(groups[1])
                w, w_unit = self._normalize_unit(w, param="width")
                h, h_unit = self._normalize_unit(h, param="height")
                result.dimensions.append(ParsedDimension(
                    param="width", value=w, raw_text=txt, source=source,
                    layer=layer, position=position, confidence=base_conf,
                    unit=w_unit,
                ))
                result.dimensions.append(ParsedDimension(
                    param="height", value=h, raw_text=txt, source=source,
                    layer=layer, position=position, confidence=base_conf,
                    unit=h_unit,
                ))
                return

            # --- slope (1:N) → 특수 처리 ---
            if param == "slope":
                v1, v2 = float(groups[0]), float(groups[1])
                # 사면 경사비가 아닌 좌표 구분자(218,200)와 혼동 방지
                # 일반적인 경사비: 1:0.3 ~ 1:5.0
                if v1 <= 2 and 0.1 <= v2 <= 10.0:
                    result.dimensions.append(ParsedDimension(
                        param="slope", value=v2, raw_text=txt, source=source,
                        layer=layer, position=position, confidence=base_conf,
                        unit="ratio", secondary=v1,
                    ))
                return  # slope는 항상 여기서 종료 (다른 패턴과 중복 방지)

            # --- 일반 단일값 ---
            value = float(groups[0])

            # 관경은 항상 mm 단위
            if param == "diameter":
                if value > self.unit_threshold:
                    value /= 1000.0
                    unit = "mm→m"
                else:
                    unit = "m"
            else:
                value, unit = self._normalize_unit(value, param)

            result.dimensions.append(ParsedDimension(
                param=param, value=value, raw_text=txt, source=source,
                layer=layer, position=position, confidence=base_conf,
                unit=unit,
            ))

    # ----- DIMENSION 엔티티 -----

    def _parse_dimension_entities(self, msp, result: ParseResult):
        """DIMENSION 엔티티에서 치수 추출.

        DIMENSION 엔티티는 defpoint 좌표와 actual_measurement를 직접 제공하므로
        가장 신뢰도가 높다.
        """
        for entity in msp:
            if entity.dxftype() != "DIMENSION":
                continue

            layer = entity.dxf.layer

            try:
                # 실제 측정값 (ezdxf가 계산)
                measurement = entity.dxf.get("actual_measurement", None)
                if measurement is None:
                    continue

                measurement = float(measurement)
                if measurement <= 0:
                    continue

                # 치수 방향 판별 (수직 → 높이, 수평 → 폭)
                param = self._classify_dimension_direction(entity)

                # 텍스트 오버라이드 확인
                text_override = entity.dxf.get("text", "")
                raw_text = text_override if text_override else f"[DIM]{measurement:.3f}"

                # 위치: defpoint 중간점
                try:
                    dp1 = entity.dxf.defpoint
                    dp2 = entity.dxf.defpoint2 if entity.dxf.hasattr("defpoint2") else dp1
                    position = ((dp1.x + dp2.x) / 2, (dp1.y + dp2.y) / 2)
                except Exception:
                    position = (0.0, 0.0)

                value, unit = self._normalize_unit(measurement, param)

                result.dimensions.append(ParsedDimension(
                    param=param, value=value, raw_text=raw_text,
                    source="dimension", layer=layer, position=position,
                    confidence=0.95, unit=unit,
                ))

            except Exception:
                continue

    def _classify_dimension_direction(self, dim_entity) -> str:
        """DIMENSION 엔티티의 방향을 분석하여 파라미터 종류를 추론.

        수직(Y 차이 > X 차이) → height
        수평(X 차이 > Y 차이) → width
        """
        try:
            dp1 = dim_entity.dxf.defpoint
            dp2 = dim_entity.dxf.defpoint2 if dim_entity.dxf.hasattr("defpoint2") else dp1

            dx = abs(dp2.x - dp1.x)
            dy = abs(dp2.y - dp1.y)

            if dy > dx * 1.5:
                return "height"
            elif dx > dy * 1.5:
                return "width"
            else:
                return "length"  # 대각선이면 길이로 분류
        except Exception:
            return "length"

    # ----- ATTRIB (블록 속성) -----

    def _parse_attrib_entities(self, msp, result: ParseResult):
        """INSERT 블록의 ATTRIB에서 치수 추출."""
        # 속성 태그명 → 파라미터 매핑
        tag_map = {
            "HEIGHT": "height", "H": "height", "높이": "height",
            "WIDTH": "width", "W": "width", "폭": "width", "B": "width",
            "THICKNESS": "thickness", "T": "thickness", "두께": "thickness",
            "ELEVATION": "elevation", "EL": "elevation", "표고": "elevation",
            "DIAMETER": "diameter", "DIA": "diameter", "D": "diameter", "관경": "diameter",
            "LENGTH": "length", "L": "length", "길이": "length", "연장": "length",
        }

        for entity in msp:
            if entity.dxftype() != "INSERT":
                continue

            try:
                attribs = list(entity.attribs) if hasattr(entity, "attribs") else []
            except Exception:
                continue

            layer = entity.dxf.layer
            try:
                pos = entity.dxf.insert
                position = (pos.x, pos.y)
            except Exception:
                position = (0.0, 0.0)

            for attrib in attribs:
                try:
                    tag = attrib.dxf.tag.strip().upper()
                    text = attrib.dxf.text.strip()

                    param = tag_map.get(tag)
                    if not param:
                        continue

                    # 숫자 추출
                    m = re.search(r"(\d+\.?\d*)", text)
                    if not m:
                        continue

                    value = float(m.group(1))
                    value, unit = self._normalize_unit(value, param)

                    result.dimensions.append(ParsedDimension(
                        param=param, value=value, raw_text=f"{tag}={text}",
                        source="attrib", layer=layer, position=position,
                        confidence=0.85, unit=unit,
                    ))
                except Exception:
                    continue

    # ----- 유틸리티 -----

    def _normalize_unit(self, value: float, param: str) -> tuple[float, str]:
        """값의 단위를 판단하여 미터로 정규화.

        토목 도면 관례:
        - 관경: 보통 mm (300, 600, 1000 등)
        - 높이/폭/두께: 보통 m (0.5 ~ 30 정도)
        - 계획고(EL): 항상 m (해발 기준)
        """
        if param == "elevation":
            # 계획고는 항상 m 단위 (음수 가능, 큰 값 가능)
            return value, "m"

        if param == "diameter":
            if value > self.unit_threshold:
                return value / 1000.0, "mm→m"
            return value, "m"

        # spacing(@간격)은 보통 mm
        if param == "spacing":
            if value > self.unit_threshold:
                return value / 1000.0, "mm→m"
            return value, "m"

        # 일반 치수: 50 이상이면 mm로 간주
        if value > self.unit_threshold:
            return value / 1000.0, "mm→m"

        return value, "m"


# ---------------------------------------------------------------------------
# 공간 연결: 텍스트 치수를 인근 지오메트리에 매칭
# ---------------------------------------------------------------------------

def associate_dimensions_to_structures(
    parse_result: ParseResult,
    structure_positions: dict[str, tuple[float, float]],
    max_distance: float = 50.0,
) -> dict[str, list[ParsedDimension]]:
    """파싱된 치수를 인근 구조물에 공간적으로 연결.

    Args:
        parse_result: DetailParser.parse() 결과
        structure_positions: {"구조물명": (x, y), ...} 구조물 중심 좌표
        max_distance: 최대 연결 거리 (m)

    Returns:
        {"구조물명": [ParsedDimension, ...], ...}
    """
    if not parse_result.dimensions or not structure_positions:
        return {}

    # 구조물 좌표 배열
    names = list(structure_positions.keys())
    positions = np.array([structure_positions[n] for n in names])

    associated: dict[str, list[ParsedDimension]] = {n: [] for n in names}

    for dim in parse_result.dimensions:
        dim_pos = np.array(dim.position)
        dists = np.linalg.norm(positions - dim_pos, axis=1)
        min_idx = int(np.argmin(dists))

        if dists[min_idx] <= max_distance:
            associated[names[min_idx]].append(dim)

    return associated


# ---------------------------------------------------------------------------
# 치수 → structure params 매핑
# ---------------------------------------------------------------------------

def dimensions_to_structure_params(
    dimensions: list[ParsedDimension],
) -> dict:
    """파싱된 치수 목록을 structure_v1.yaml 호환 파라미터 딕셔너리로 변환.

    Returns:
        {"height": 5.0, "width": 3.0, "z_offset": -2.0, ...}
    """
    # 파라미터별 최고 신뢰도 값 선택
    best: dict[str, ParsedDimension] = {}
    for d in dimensions:
        if d.param not in best or d.confidence > best[d.param].confidence:
            best[d.param] = d

    params: dict = {}

    if "height" in best:
        params["height"] = best["height"].value

    if "width" in best:
        params["width"] = best["width"].value

    if "thickness" in best:
        params["thickness"] = best["thickness"].value

    if "diameter" in best:
        params["diameter"] = best["diameter"].value

    if "length" in best:
        params["length"] = best["length"].value

    if "elevation" in best:
        params["elevation"] = best["elevation"].value

    if "slope" in best:
        params["slope_ratio"] = best["slope"].value

    if "embedment" in best:
        params["embedment"] = best["embedment"].value

    if "radius" in best:
        params["radius"] = best["radius"].value

    return params
