"""구조물 3D 렌더링 템플릿 레지스트리.

모든 구조물 유형은 StructureTemplate을 상속하여 3개 메서드를 구현한다:
1. parse(dxf_paths) → StructureParams (DXF에서 파라미터 추출)
2. build_meshes(params) → list[(mesh, color, opacity)]
3. get_parameter_schema() → 파라미터 편집 UI용 메타데이터

레지스트리에 등록된 템플릿은 UI에서 드롭다운으로 선택 가능.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, ClassVar

import numpy as np
import pyvista as pv


# ---------------------------------------------------------------------------
# 공통 데이터 구조
# ---------------------------------------------------------------------------

@dataclass
class StructureParams:
    """범용 구조물 파라미터.

    template_id로 어떤 템플릿에서 사용되는지 식별.
    params dict에 모든 수치/설정이 들어가며, 템플릿마다 의미가 다름.
    """
    template_id: str = "generic_box"
    name: str = "Structure"
    params: dict = field(default_factory=dict)

    # 공통 메타데이터
    source_files: list = field(default_factory=list)
    raw_annotations: list = field(default_factory=list)  # [(text, x, y), ...]

    def get(self, key: str, default: Any = None) -> Any:
        return self.params.get(key, default)

    def set(self, key: str, value: Any):
        self.params[key] = value

    def update(self, other: dict):
        self.params.update(other)


@dataclass
class ParamField:
    """파라미터 편집 UI 필드 정의."""
    name: str                # internal key (e.g., "height")
    label: str               # UI 표시 이름 (e.g., "높이")
    unit: str = "m"          # 단위
    default: float = 0.0
    min_val: float = 0.0
    max_val: float = 1000.0
    param_type: str = "float"  # "float" | "int" | "text" | "choice"
    choices: list = None     # param_type="choice"일 때
    description: str = ""


# ---------------------------------------------------------------------------
# 베이스 클래스
# ---------------------------------------------------------------------------

class StructureTemplate(ABC):
    """모든 구조물 템플릿의 베이스 클래스."""

    template_id: str = ""
    name_ko: str = ""
    description: str = ""
    icon_hint: str = ""  # UI용 이모지/텍스트 힌트 (non-functional)

    # 이 템플릿이 요구하는 입력 파일 개수 (min, max, typical)
    required_files: tuple[int, int, int] = (1, 2, 1)

    # 뷰 기반 재구성 지원 여부 (기본 True)
    supports_view_based: bool = True

    @abstractmethod
    def get_parameter_schema(self) -> list[ParamField]:
        """파라미터 편집 UI용 필드 정의 목록."""
        ...

    @abstractmethod
    def parse(self, dxf_paths: list[str]) -> StructureParams:
        """DXF 파일에서 파라미터 추출."""
        ...

    @abstractmethod
    def build_meshes(self, params: StructureParams) -> list[tuple[pv.PolyData, str, float]]:
        """파라미터로 3D 메쉬 생성. (mesh, color, opacity) 튜플 리스트 반환."""
        ...

    def default_params(self) -> dict:
        """스키마의 기본값 딕셔너리."""
        return {f.name: f.default for f in self.get_parameter_schema()}

    def try_view_based_parse(self, dxf_paths: list[str]) -> dict:
        """뷰 검출을 시도하고 결과 메타를 반환.

        각 템플릿의 parse()에서 호출하여 params에 저장.
        Returns: {"views": [...], "detected": bool, "views_count": int}
        """
        if not self.supports_view_based:
            return {"views": [], "detected": False, "views_count": 0}
        try:
            from view_detector import detect_views_multi
            views = detect_views_multi(dxf_paths)
            return {
                "views": views,
                "detected": len(views) > 0,
                "views_count": len(views),
            }
        except Exception:
            return {"views": [], "detected": False, "views_count": 0}

    def try_view_based_meshes(self, params: StructureParams):
        """뷰 기반 3D 재구성 시도.

        Returns:
            list of (mesh, color, opacity) or None if failed.
        """
        if not self.supports_view_based:
            return None
        views = params.params.get("_views")
        if not views:
            return None
        try:
            from view_reconstructor import reconstruct_from_views
            meshes = reconstruct_from_views(views, self.template_id)
            return meshes if meshes else None
        except Exception:
            return None


# ---------------------------------------------------------------------------
# 공통 유틸리티: 기본 형상
# ---------------------------------------------------------------------------

def make_box(x0, x1, y0, y1, z0, z1) -> pv.PolyData:
    """축정렬 박스 메쉬."""
    pts = np.array([
        [x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0],
        [x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1],
    ])
    faces = np.hstack([
        [4, 0, 3, 2, 1], [4, 4, 5, 6, 7],
        [4, 0, 1, 5, 4], [4, 2, 3, 7, 6],
        [4, 1, 2, 6, 5], [4, 0, 4, 7, 3],
    ])
    return pv.PolyData(pts, faces)


def make_flat_rect(x0, x1, y0, y1, z) -> pv.PolyData:
    """수평 사각 평판."""
    pts = np.array([
        [x0, y0, z], [x1, y0, z], [x1, y1, z], [x0, y1, z],
    ])
    return pv.PolyData(pts, np.array([4, 0, 1, 2, 3]))


def make_cylinder(cx, cy, z0, z1, radius, n_sides=24) -> pv.PolyData:
    """세로 원기둥."""
    cyl = pv.Cylinder(
        center=(cx, cy, (z0 + z1) / 2),
        direction=(0, 0, 1),
        radius=radius,
        height=(z1 - z0),
        resolution=n_sides,
    )
    return cyl.extract_surface()


# ---------------------------------------------------------------------------
# 템플릿 구현: 여수로 수문 (기존 gate_3d_builder 래핑)
# ---------------------------------------------------------------------------

class SpillwayGateTemplate(StructureTemplate):
    template_id = "spillway_gate"
    name_ko = "여수로 수문 (래디얼)"
    description = "ogee 여수로 + 래디얼(Tainter) 수문 + 공도교 + 개폐장치"
    required_files = (1, 2, 2)  # plan + section

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("n_gates", "수문 개수", "문", 3, 1, 20, "int"),
            ParamField("gate_width", "수문 폭", "m", 15.0, 1.0, 50.0),
            ParamField("gate_height", "수문 높이", "m", 7.0, 1.0, 30.0),
            ParamField("pier_width", "교각 폭", "m", 4.85, 0.5, 10.0),
            ParamField("pier_length", "교각 길이", "m", 25.0, 2.0, 60.0),
            ParamField("el_gate_sill", "Gate Sill EL.", "m", 46.700, -100, 500),
            ParamField("el_weir_crest", "Weir Crest EL.", "m", 47.000, -100, 500),
            ParamField("el_gate_top", "Gate Top EL.", "m", 53.700, -100, 500),
            ParamField("el_trunnion_pin", "Trunnion EL.", "m", 50.200, -100, 500),
            ParamField("el_bridge_top", "Bridge Top EL.", "m", 56.000, -100, 500),
            ParamField("el_nhwl", "N.H.W.L", "m", 52.500, -100, 500),
            ParamField("el_mwl", "M.W.L", "m", 53.830, -100, 500),
            ParamField("el_lwl", "L.W.L", "m", 45.000, -100, 500),
            ParamField("el_upstream_bed", "상류 바닥 EL.", "m", 41.500, -100, 500),
            ParamField("el_downstream", "하류 바닥 EL.", "m", 44.000, -100, 500),
            # 부속 on/off 토글 (1=켜기, 0=끄기) — 사용자가 파서 자동검출을 오버라이드 가능
            ParamField("has_service_bridge", "공도교 (1=켜기/0=끄기)", "", 0, 0, 1, "int"),
            ParamField("has_hoist_housings", "여수로 개폐장치 (1/0)", "", 1, 0, 1, "int"),
            ParamField("has_downstream_apron", "하류 에이프런 (1/0)", "", 1, 0, 1, "int"),
            ParamField("has_water_surface", "상류 수면 (1/0)", "", 1, 0, 1, "int"),
            # 공도교 위치 수동지정 — 4개 모두 x1>x0·y1>y0 유효하면 사용자 override로 사용
            # 자동 검출값이 있으면 여기에 채워져 표시됨. 0 또는 역순이면 자동·parametric 사용
            ParamField("bridge_x_start", "공도교 X 시작", "m", 0.0, -50, 500),
            ParamField("bridge_x_end", "공도교 X 끝", "m", 0.0, -50, 500),
            ParamField("bridge_y_start", "공도교 Y 시작(상류)", "m", 0.0, -50, 500),
            ParamField("bridge_y_end", "공도교 Y 끝(하류)", "m", 0.0, -50, 500),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from gate_parser import parse_gate_dxf, GateParams

        plan = dxf_paths[0]
        section = dxf_paths[1] if len(dxf_paths) > 1 else None

        sp: GateParams = parse_gate_dxf(plan, section)

        # GateParams → StructureParams 변환
        params = StructureParams(template_id=self.template_id, name="여수로 수문")
        params.source_files = sp.source_files
        params.raw_annotations = sp.raw_text_annotations

        params.params = {
            "n_gates": sp.n_gates,
            "gate_width": sp.gate_width,
            "gate_height": sp.gate_height,
            "pier_width": sp.pier_width,
            "pier_length": sp.pier_length,
            "el_gate_sill": sp.el_gate_sill,
            "el_weir_crest": sp.el_weir_crest,
            "el_gate_top": sp.el_gate_top,
            "el_trunnion_pin": sp.el_trunnion_pin,
            "el_bridge_top": sp.el_bridge_top,
            "el_mwl": sp.el_mwl,
            "el_nhwl": sp.el_nhwl,
            "el_lwl": sp.el_lwl,
            "el_upstream_bed": sp.el_upstream_bed,
            "el_downstream": sp.el_downstream,
            "total_span": sp.total_span,
            "gate_centers_x": sp.gate_centers_x,
            "ogee_profile": sp.ogee_profile,
            # Phase A: 부속 존재성 플래그 (schema에 int 토글로 노출, 1/0으로 저장)
            "has_service_bridge": int(bool(sp.has_service_bridge)),
            "has_hoist_housings": int(bool(sp.has_hoist_housings)),
            "has_downstream_apron": int(bool(sp.has_downstream_apron)),
            "has_water_surface": int(bool(sp.has_water_surface)),
            # Phase B': 실제 도면 기하 (pass-through, UI 편집 없음)
            "plan_outline_polygon": sp.plan_outline_polygon,
            "pier_plan_polygons": sp.pier_plan_polygons,
            "bridge_plan_bbox": sp.bridge_plan_bbox,
            "bridge_deck_thickness_m": sp.bridge_deck_thickness_m,
            # 방향 결정용 (FLOW 화살표 → plan_frame_angle)
            "plan_frame_angle_deg": sp.plan_frame_angle_deg,
            "flow_direction_2d": sp.flow_direction_2d,
            # 사용자 편집 가능한 bridge 위치 (파서가 추출한 bbox 값으로 초기화됨)
            "bridge_x_start": float(sp.bridge_x_start) if sp.bridge_x_start is not None else 0.0,
            "bridge_y_start": float(sp.bridge_y_start) if sp.bridge_y_start is not None else 0.0,
            "bridge_x_end": float(sp.bridge_x_end) if sp.bridge_x_end is not None else 0.0,
            "bridge_y_end": float(sp.bridge_y_end) if sp.bridge_y_end is not None else 0.0,
        }
        return params

    def build_meshes(self, params: StructureParams):
        from gate_parser import GateParams
        from gate_3d_builder import GateBuilder

        # StructureParams → GateParams 변환
        # 사용자가 UI에서 수정한 has_* 플래그(int 1/0)를 bool로 변환 반영
        sp = GateParams()
        for k, v in params.params.items():
            if hasattr(sp, k):
                # 부속 존재 플래그는 int(1/0) ↔ bool 상호 변환
                if k.startswith("has_") and isinstance(v, (int, float)):
                    setattr(sp, k, bool(v))
                else:
                    setattr(sp, k, v)

        # 편집된 값이 있으면 반영, gate_centers_x와 total_span 재계산
        pw = sp.pier_width
        gw = sp.gate_width
        if not sp.gate_centers_x or len(sp.gate_centers_x) != sp.n_gates:
            sp.gate_centers_x = [
                pw + gw * 0.5 + i * (gw + pw) for i in range(sp.n_gates)
            ]
            sp.total_span = sp.n_gates * gw + (sp.n_gates + 1) * pw

        builder = GateBuilder(sp)
        return builder.build_all()


# ---------------------------------------------------------------------------
# 공통 헬퍼: 지오메트리 기반 추출 & 빌드
# ---------------------------------------------------------------------------

def _center_points(pts_list: list[list[tuple]], bounds: tuple) -> list[list[tuple]]:
    """좌표들을 bbox 중심 기준으로 원점 보정."""
    cx = (bounds[0] + bounds[2]) / 2
    cy = (bounds[1] + bounds[3]) / 2
    return [[(p[0] - cx, p[1] - cy) for p in poly] for poly in pts_list]


def _extrude_polygon(pts_2d: list, base_z: float, height: float,
                     color: str = "#BDC3C7", opacity: float = 1.0):
    """2D 폴리곤을 높이만큼 extrude하여 (mesh, color, opacity) 튜플 반환."""
    if len(pts_2d) < 3:
        return None

    arr = np.array(pts_2d)
    # 중복 끝점 제거
    if np.allclose(arr[0], arr[-1]):
        arr = arr[:-1]
    n = len(arr)
    if n < 3:
        return None

    pts_3d = np.zeros((2 * n, 3))
    for i, (x, y) in enumerate(arr):
        pts_3d[i] = [x, y, base_z]
        pts_3d[i + n] = [x, y, base_z + height]

    faces = []
    # 측면
    for i in range(n):
        ni = (i + 1) % n
        faces.append([3, i, ni, ni + n])
        faces.append([3, i, ni + n, i + n])
    # 바닥/상부 (fan triangulation)
    for i in range(1, n - 1):
        faces.append([3, 0, i + 1, i])
        faces.append([3, n, n + i, n + i + 1])

    try:
        mesh = pv.PolyData(pts_3d, np.concatenate(faces))
        return (mesh, color, opacity)
    except Exception:
        return None


def _extrude_polyline_wall(pts_2d: list, base_z: float, height: float,
                           thickness: float = 0.5,
                           color: str = "#7F8C8D", opacity: float = 1.0):
    """열린 폴리라인을 두께 있는 수직 벽체로 extrude."""
    if len(pts_2d) < 2:
        return None

    arr = np.array(pts_2d)

    # 각 세그먼트의 법선 방향으로 양쪽 오프셋
    left_pts = []
    right_pts = []
    n_segs = len(arr)
    half_t = thickness / 2

    for i in range(n_segs):
        # 세그먼트 방향 (중간 노드는 평균)
        if i == 0:
            d = arr[1] - arr[0]
        elif i == n_segs - 1:
            d = arr[-1] - arr[-2]
        else:
            d = arr[i + 1] - arr[i - 1]
        length = np.linalg.norm(d)
        if length < 1e-6:
            continue
        normal = np.array([-d[1] / length, d[0] / length])
        left_pts.append(arr[i] + normal * half_t)
        right_pts.append(arr[i] - normal * half_t)

    if len(left_pts) < 2:
        return None

    # 폐합 폴리곤 (left + right 뒤집기)
    outline = left_pts + right_pts[::-1]
    return _extrude_polygon(outline, base_z, height, color, opacity)


def _build_ground_plane(bounds: tuple, base_el: float = 0.0,
                        margin: float = 0.3) -> tuple:
    """지반 평면 생성."""
    minx, miny, maxx, maxy = bounds
    w = max(maxx - minx, 1.0)
    h = max(maxy - miny, 1.0)
    mx = w * margin
    my = h * margin
    g = make_flat_rect(
        -(w / 2 + mx), (w / 2 + mx),
        -(h / 2 + my), (h / 2 + my),
        base_el - 0.05,
    )
    return (g, "#8B7D6B", 1.0)


# ---------------------------------------------------------------------------
# 템플릿 구현: 건축물 (Building)
# ---------------------------------------------------------------------------

class BuildingTemplate(StructureTemplate):
    template_id = "building"
    name_ko = "건축물 / 가설건물"
    description = "평면(폐합 폴리라인)에서 높이만큼 박스 extrude"
    required_files = (1, 1, 1)

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("height", "건물 높이", "m", 5.0, 0.5, 200.0),
            ParamField("n_floors", "층수", "층", 1, 1, 100, "int"),
            ParamField("base_el", "지반 EL.", "m", 0.0, -100, 500),
            ParamField("min_area", "무시할 최소 면적 (m²)", "", 0.5, 0.0, 100.0),
            ParamField("max_buildings", "최대 건물 개수", "", 20, 1, 200, "int"),
            ParamField("use_view_based", "뷰 기반 재구성 사용", "", 1, 0, 1, "choice",
                       choices=["끄기", "켜기"]),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from detail_parser import DetailParser
        from dxf_geometry import extract_all

        params = StructureParams(template_id=self.template_id, name="건축물")
        params.source_files = dxf_paths
        params.params = self.default_params()

        # 뷰 검출 시도
        view_info = self.try_view_based_parse(dxf_paths)
        params.set("_views", view_info["views"])
        params.set("_views_detected", view_info["detected"])

        # 1) 치수 텍스트 파싱 (높이 추출용)
        parser = DetailParser()
        all_dims = []
        for p in dxf_paths:
            try:
                result = parser.parse(p)
                all_dims.extend(result.dimensions)
                params.raw_annotations.extend(
                    [(d.raw_text, d.position[0], d.position[1]) for d in result.dimensions]
                )
            except Exception:
                pass
        # DIMENSION에서 높이 값 후보 수집 (vertical만, sanity 범위)
        # 너무 작은/큰 값은 제외 (2m~100m만 건물 높이로 간주)
        h_candidates = [d.value for d in all_dims
                        if d.param == "height" and 2.0 <= d.value <= 100.0]
        if h_candidates:
            # 중앙값 사용 (단일 튀는 값 영향 최소화)
            params.set("height", float(np.median(h_candidates)))

        # 2) 구조 지오메트리 추출 (단위 자동 감지)
        geom = extract_all(dxf_paths)
        params.set("_geom_unit", geom.detected_unit)
        params.set("_geom_bounds", geom.total_bounds)

        # 3) 건물 외곽 후보: closed shape들
        min_area = params.get("min_area", 0.5)
        max_buildings = int(params.get("max_buildings", 20))
        candidates = [s for s in geom.closed_shapes if s.area >= min_area]

        if not candidates:
            # 폐합이 없으면 가장 긴 열린 폴리라인 사용
            longest = geom.longest_polyline()
            if longest and longest.length > 3.0:
                candidates = [longest]

        # 면적 내림차순, 상위 N개
        candidates.sort(key=lambda s: -s.area)
        candidates = candidates[:max_buildings]

        outlines = [{
            "points": s.points,
            "closed": s.closed,
            "layer": s.layer,
            "area": s.area,
        } for s in candidates]
        params.set("outlines", outlines)
        return params

    def build_meshes(self, params: StructureParams):
        # 뷰 기반 재구성 우선 시도
        use_view = int(params.get("use_view_based", 1)) == 1
        if use_view and params.get("_views_detected", False):
            view_meshes = self.try_view_based_meshes(params)
            if view_meshes and len(view_meshes) >= 2:  # 지반 포함 2개 이상
                return view_meshes

        # Fallback: 기존 geometry-based
        height = params.get("height", 5.0)
        base_el = params.get("base_el", 0.0)
        outlines = params.get("outlines") or []
        bounds = params.get("_geom_bounds", (0, 0, 20, 20))

        meshes = []

        # 원점 보정 (bbox 중심을 원점으로)
        cx = (bounds[0] + bounds[2]) / 2
        cy = (bounds[1] + bounds[3]) / 2

        if outlines:
            for outline in outlines:
                pts = outline["points"]
                centered = [(p[0] - cx, p[1] - cy) for p in pts]
                if outline["closed"]:
                    result = _extrude_polygon(centered, base_el, height, "#BDC3C7")
                    if result:
                        meshes.append(result)
                else:
                    result = _extrude_polyline_wall(centered, base_el, height,
                                                     thickness=0.3, color="#BDC3C7")
                    if result:
                        meshes.append(result)
        else:
            box = make_box(-5, 5, -5, 5, base_el, base_el + height)
            meshes.append((box, "#BDC3C7", 1.0))

        bounds_centered = (bounds[0] - cx, bounds[1] - cy, bounds[2] - cx, bounds[3] - cy)
        meshes.append(_build_ground_plane(bounds_centered, base_el))

        return meshes


# ---------------------------------------------------------------------------
# 템플릿 구현: 옹벽 (Retaining Wall)
# ---------------------------------------------------------------------------

class RetainingWallTemplate(StructureTemplate):
    template_id = "retaining_wall"
    name_ko = "옹벽 / 방벽"
    description = "DXF 폴리라인 경로를 따라 수직 벽체 생성"
    required_files = (1, 1, 1)

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("height", "벽체 높이", "m", 4.0, 0.5, 30.0),
            ParamField("thickness", "벽체 두께", "m", 0.5, 0.1, 3.0),
            ParamField("base_el", "바닥 EL.", "m", 0.0, -100, 500),
            ParamField("batter", "경사 (상단수축 비율)", "", 0.0, 0.0, 0.3),
            ParamField("min_length", "무시할 최소 길이 (m)", "", 5.0, 0.0, 100.0),
            ParamField("max_walls", "최대 벽체 개수", "", 10, 1, 100, "int"),
            ParamField("use_view_based", "뷰 기반 재구성 사용", "", 1, 0, 1, "choice",
                       choices=["끄기", "켜기"]),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from detail_parser import DetailParser
        from dxf_geometry import extract_all

        params = StructureParams(template_id=self.template_id, name="옹벽")
        params.source_files = dxf_paths
        params.params = self.default_params()

        # 뷰 검출 시도
        view_info = self.try_view_based_parse(dxf_paths)
        params.set("_views", view_info["views"])
        params.set("_views_detected", view_info["detected"])

        # 치수 파싱
        parser = DetailParser()
        all_dims = []
        for p in dxf_paths:
            try:
                result = parser.parse(p)
                all_dims.extend(result.dimensions)
                params.raw_annotations.extend(
                    [(d.raw_text, d.position[0], d.position[1]) for d in result.dimensions]
                )
            except Exception:
                pass
        # 옹벽 높이: 1~30m 범위만 유효
        h_candidates = [d.value for d in all_dims
                        if d.param == "height" and 1.0 <= d.value <= 30.0]
        if h_candidates:
            params.set("height", float(np.median(h_candidates)))
        # 두께: 0.2~3m 범위
        t_candidates = [d.value for d in all_dims
                        if d.param == "thickness" and 0.2 <= d.value <= 3.0]
        if t_candidates:
            params.set("thickness", float(np.median(t_candidates)))

        # 지오메트리 추출: 옹벽은 보통 열린 폴리라인으로 표현됨
        geom = extract_all(dxf_paths)
        params.set("_geom_bounds", geom.total_bounds)

        # 최소 길이 이상의 폴리라인/선을 모두 벽체 경로로 사용
        min_len = params.get("min_length", 5.0)
        max_walls = int(params.get("max_walls", 10))
        paths = [{
            "points": s.points,
            "closed": s.closed,
            "layer": s.layer,
            "length": s.length,
        } for s in geom.shapes
          if s.kind in ("polyline", "line") and s.length >= min_len]

        # 길이 내림차순 정렬 → 상위 N개만
        paths.sort(key=lambda x: -x["length"])
        paths = paths[:max_walls]
        params.set("wall_paths", paths)
        return params

    def build_meshes(self, params: StructureParams):
        # 뷰 기반 재구성 우선 시도
        use_view = int(params.get("use_view_based", 1)) == 1
        if use_view and params.get("_views_detected", False):
            view_meshes = self.try_view_based_meshes(params)
            if view_meshes and len(view_meshes) >= 2:
                return view_meshes

        h = params.get("height", 4.0)
        t = params.get("thickness", 0.5)
        base = params.get("base_el", 0.0)
        batter = params.get("batter", 0.0)
        paths = params.get("wall_paths") or []
        bounds = params.get("_geom_bounds", (0, 0, 20, 20))

        meshes = []
        cx = (bounds[0] + bounds[2]) / 2
        cy = (bounds[1] + bounds[3]) / 2

        if paths:
            for path in paths:
                pts = path["points"]
                centered = [(p[0] - cx, p[1] - cy) for p in pts]

                if path["closed"]:
                    result = _extrude_polygon(centered, base, h, "#7F8C8D")
                else:
                    # 배터(경사)는 현재 버전에서 무시. 두께 있는 벽체로.
                    result = _extrude_polyline_wall(centered, base, h,
                                                     thickness=t, color="#7F8C8D")
                if result:
                    meshes.append(result)
        else:
            # 폴백: 기본 사다리꼴 벽체
            L = 20.0
            t_top = t * (1 - batter)
            pts = np.array([
                [0, -t / 2, base], [0, t / 2, base],
                [0, t_top / 2, base + h], [0, -t_top / 2, base + h],
                [L, -t / 2, base], [L, t / 2, base],
                [L, t_top / 2, base + h], [L, -t_top / 2, base + h],
            ])
            faces = np.hstack([
                [4, 0, 3, 2, 1], [4, 4, 5, 6, 7],
                [4, 0, 1, 5, 4], [4, 3, 7, 6, 2],
                [4, 0, 4, 7, 3], [4, 1, 2, 6, 5],
            ])
            meshes.append((pv.PolyData(pts, faces), "#7F8C8D", 1.0))

        # 지반
        bounds_centered = (bounds[0] - cx, bounds[1] - cy, bounds[2] - cx, bounds[3] - cy)
        meshes.append(_build_ground_plane(bounds_centered, base))

        return meshes


# ---------------------------------------------------------------------------
# 템플릿 구현: 교량 (Bridge)
# ---------------------------------------------------------------------------

class BridgeTemplate(StructureTemplate):
    template_id = "bridge"
    name_ko = "교량"
    description = "DXF에서 상판 외곽+교각 위치 추출하여 3D 교량 생성"
    required_files = (1, 1, 1)

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("deck_width", "상판 폭", "m", 10.0, 3.0, 40.0),
            ParamField("deck_thickness", "상판 두께", "m", 1.2, 0.3, 3.0),
            ParamField("pier_height", "교각 높이", "m", 8.0, 2.0, 80.0),
            ParamField("pier_width", "교각 폭", "m", 1.5, 0.5, 6.0),
            ParamField("base_el", "지반 EL.", "m", 0.0, -100, 500),
            ParamField("n_fallback_spans", "외곽 없을 시 경간 수", "개", 3, 1, 20, "int"),
            ParamField("use_view_based", "뷰 기반 재구성 사용", "", 1, 0, 1, "choice",
                       choices=["끄기", "켜기"]),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        import math as _math
        from detail_parser import DetailParser
        from dxf_geometry import extract_all

        params = StructureParams(template_id=self.template_id, name="교량")
        params.source_files = dxf_paths
        params.params = self.default_params()

        # 뷰 검출
        view_info = self.try_view_based_parse(dxf_paths)
        params.set("_views", view_info["views"])
        params.set("_views_detected", view_info["detected"])

        # 치수 파싱
        parser = DetailParser()
        all_dims = []
        for p in dxf_paths:
            try:
                result = parser.parse(p)
                all_dims.extend(result.dimensions)
                params.raw_annotations.extend(
                    [(d.raw_text, d.position[0], d.position[1]) for d in result.dimensions]
                )
            except Exception:
                pass
        for d in all_dims:
            if d.param == "height" and d.value > 2:
                params.set("pier_height", d.value)
            elif d.param == "width" and d.value > 3:
                params.set("deck_width", d.value)

        # 지오메트리
        geom = extract_all(dxf_paths)
        params.set("_geom_bounds", geom.total_bounds)

        # 상판 외곽 = 가장 긴 폴리라인 or 가장 큰 closed shape (둘 중 선택)
        longest = geom.longest_polyline()
        largest = geom.largest_closed()

        deck_outline = None
        if largest and largest.area > 5.0:
            deck_outline = {
                "points": largest.points,
                "closed": True,
                "length": largest.length,
            }
        elif longest and longest.length > 5.0:
            deck_outline = {
                "points": longest.points,
                "closed": longest.closed,
                "length": longest.length,
            }
        params.set("deck_outline", deck_outline)

        # 교각 위치: 작은 closed 폴리곤들 (교각 단면)
        piers = [{"centroid": s.centroid, "area": s.area}
                 for s in geom.closed_shapes
                 if 0.2 <= s.area <= 30.0]  # 교각 단면 크기 범위
        # 중심점이 너무 가까운 것은 제거 (1m 이내)
        unique_piers = []
        for p in piers:
            pcx, pcy = p["centroid"]
            if not any(
                _math.sqrt((pcx - u["centroid"][0]) ** 2 + (pcy - u["centroid"][1]) ** 2) < 1.0
                for u in unique_piers
            ):
                unique_piers.append(p)
        params.set("pier_positions", unique_piers)

        return params

    def build_meshes(self, params: StructureParams):
        # 뷰 기반 재구성 우선
        use_view = int(params.get("use_view_based", 1)) == 1
        if use_view and params.get("_views_detected", False):
            view_meshes = self.try_view_based_meshes(params)
            if view_meshes and len(view_meshes) >= 2:
                return view_meshes

        deck_w = params.get("deck_width", 10.0)
        deck_t = params.get("deck_thickness", 1.2)
        pier_h = params.get("pier_height", 8.0)
        pier_w = params.get("pier_width", 1.5)
        base = params.get("base_el", 0.0)
        deck_outline = params.get("deck_outline")
        pier_positions = params.get("pier_positions") or []
        bounds = params.get("_geom_bounds", (0, 0, 30, 10))

        meshes = []
        cx = (bounds[0] + bounds[2]) / 2
        cy = (bounds[1] + bounds[3]) / 2

        deck_top = base + pier_h + deck_t
        deck_bot = base + pier_h

        if deck_outline:
            # 실제 DXF 외곽으로 상판 생성
            pts = [(p[0] - cx, p[1] - cy) for p in deck_outline["points"]]
            if deck_outline["closed"]:
                result = _extrude_polygon(pts, deck_bot, deck_t, "#95A5A6")
                if result:
                    meshes.append(result)
            else:
                # 열린 폴리라인 → 폭 있는 상판 (벽체 방식)
                result = _extrude_polyline_wall(pts, deck_bot, deck_t,
                                                 thickness=deck_w, color="#95A5A6")
                if result:
                    meshes.append(result)

            # 교각: DXF에서 추출한 위치
            if pier_positions:
                for p in pier_positions:
                    px, py = p["centroid"]
                    px -= cx
                    py -= cy
                    pier = make_box(
                        px - pier_w / 2, px + pier_w / 2,
                        py - pier_w / 2, py + pier_w / 2,
                        base, deck_bot,
                    )
                    meshes.append((pier, "#A8A59B", 1.0))
        else:
            # 폴백: 기본 교량
            n_spans = int(params.get("n_fallback_spans", 3))
            span_L = 30.0
            total_L = span_L * n_spans
            deck = make_box(0, total_L, -deck_w / 2, deck_w / 2, deck_bot, deck_top)
            meshes.append((deck, "#95A5A6", 1.0))
            for i in range(n_spans + 1):
                px = i * span_L
                pier = make_box(
                    px - pier_w / 2, px + pier_w / 2,
                    -pier_w / 2, pier_w / 2,
                    base, deck_bot,
                )
                meshes.append((pier, "#A8A59B", 1.0))

        # 지반
        bounds_centered = (bounds[0] - cx, bounds[1] - cy, bounds[2] - cx, bounds[3] - cy)
        meshes.append(_build_ground_plane(bounds_centered, base))

        return meshes


# ---------------------------------------------------------------------------
# 템플릿 구현: 터널 갱구 (Tunnel Portal)
# ---------------------------------------------------------------------------

class TunnelPortalTemplate(StructureTemplate):
    template_id = "tunnel_portal"
    name_ko = "터널 갱구"
    description = "DXF 외곽(갱구 형태) + 터널 단면(원형/박스) 자동 추출"
    required_files = (1, 1, 1)

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("length", "터널 연장", "m", 30.0, 5.0, 500.0),
            ParamField("base_el", "갱구 바닥 EL.", "m", 0.0, -100, 500),
            ParamField("portal_thickness", "갱구 벽체 두께", "m", 2.0, 0.5, 8.0),
            ParamField("extrude_depth", "갱구 돌출 깊이", "m", 3.0, 0.5, 20.0),
            ParamField("use_view_based", "뷰 기반 재구성 사용", "", 1, 0, 1, "choice",
                       choices=["끄기", "켜기"]),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from detail_parser import DetailParser
        from dxf_geometry import extract_all

        params = StructureParams(template_id=self.template_id, name="터널 갱구")
        params.source_files = dxf_paths
        params.params = self.default_params()

        # 뷰 검출
        view_info = self.try_view_based_parse(dxf_paths)
        params.set("_views", view_info["views"])
        params.set("_views_detected", view_info["detected"])

        # 치수 파싱
        parser = DetailParser()
        all_dims = []
        for p in dxf_paths:
            try:
                result = parser.parse(p)
                all_dims.extend(result.dimensions)
                params.raw_annotations.extend(
                    [(d.raw_text, d.position[0], d.position[1]) for d in result.dimensions]
                )
            except Exception:
                pass
        for d in all_dims:
            if d.param == "length" and d.value > 5:
                params.set("length", d.value)

        # 지오메트리
        geom = extract_all(dxf_paths)
        params.set("_geom_bounds", geom.total_bounds)

        # 갱구 외곽: 가장 큰 closed shape
        largest = geom.largest_closed()
        if largest:
            params.set("portal_outline", {
                "points": largest.points,
                "closed": True,
            })

        # 터널 단면: 두 번째로 큰 closed shape (또는 원)
        sorted_closed = sorted(geom.closed_shapes, key=lambda s: -s.area)
        tunnel_section = None
        if len(sorted_closed) >= 2:
            tunnel_section = sorted_closed[1]
        elif len(sorted_closed) == 1:
            tunnel_section = sorted_closed[0]

        if tunnel_section:
            params.set("tunnel_section", {
                "points": tunnel_section.points,
                "centroid": tunnel_section.centroid,
                "area": tunnel_section.area,
            })

        return params

    def build_meshes(self, params: StructureParams):
        # 뷰 기반 재구성 우선
        use_view = int(params.get("use_view_based", 1)) == 1
        if use_view and params.get("_views_detected", False):
            view_meshes = self.try_view_based_meshes(params)
            if view_meshes and len(view_meshes) >= 2:
                return view_meshes

        length = params.get("length", 30.0)
        base = params.get("base_el", 0.0)
        ext_depth = params.get("extrude_depth", 3.0)
        portal = params.get("portal_outline")
        tunnel = params.get("tunnel_section")
        bounds = params.get("_geom_bounds", (0, 0, 20, 15))

        meshes = []
        cx = (bounds[0] + bounds[2]) / 2

        if portal:
            # 갱구 외곽을 Y 방향으로 extrude (depth만큼)
            # 2D 평면도 → XZ 평면으로 재배치 (외곽이 정면도라 가정)
            pts_2d = portal["points"]
            # Z를 원본 Y로, Y를 extrude 방향으로
            centered = [(p[0] - cx, 0, p[1]) for p in pts_2d]  # (X, Y=0, Z=원본Y)
            # depth만큼 복제
            centered_back = [(p[0], ext_depth, p[2]) for p in centered]

            # 앞/뒤 면을 잇는 prism 생성
            if len(centered) >= 3:
                front_pts = np.array(centered)
                back_pts = np.array(centered_back)
                all_pts = np.vstack([front_pts, back_pts])
                n = len(front_pts)

                faces = []
                # 측면
                for i in range(n):
                    ni = (i + 1) % n
                    faces.append([3, i, ni, ni + n])
                    faces.append([3, i, ni + n, i + n])
                # 앞/뒤 면
                for i in range(1, n - 1):
                    faces.append([3, 0, i + 1, i])
                    faces.append([3, n, n + i, n + i + 1])

                try:
                    mesh = pv.PolyData(all_pts, np.concatenate(faces))
                    meshes.append((mesh, "#A8A59B", 1.0))
                except Exception:
                    pass

        # 터널 단면을 갱구에서 length만큼 Y 방향으로 extrude
        if tunnel:
            ts_pts = tunnel["points"]

            # 단면을 Y 방향으로 extrude (ext_depth → ext_depth + length)
            front = [(p[0] - cx, ext_depth, p[1]) for p in ts_pts]
            back = [(p[0] - cx, ext_depth + length, p[1]) for p in ts_pts]

            if len(front) >= 3:
                all_pts = np.vstack([np.array(front), np.array(back)])
                n = len(front)
                faces = []
                for i in range(n):
                    ni = (i + 1) % n
                    faces.append([3, i, ni, ni + n])
                    faces.append([3, i, ni + n, i + n])
                # 앞면만 닫음 (터널 어두운 입구)
                for i in range(1, n - 1):
                    faces.append([3, 0, i + 1, i])
                try:
                    mesh = pv.PolyData(all_pts, np.concatenate(faces))
                    meshes.append((mesh, "#2C3E50", 1.0))
                except Exception:
                    pass

        # 폴백: 지오메트리 없으면 단순 박스 갱구 + 원형 터널
        if not portal and not tunnel:
            pw = 20.0
            ph = 12.0
            portal_wall = make_box(-pw / 2, pw / 2, 0, ext_depth, base, base + ph)
            meshes.append((portal_wall, "#A8A59B", 1.0))
            cyl = pv.Cylinder(
                center=(0, ext_depth + length / 2, base + 5),
                direction=(0, 1, 0),
                radius=4.0, height=length, resolution=32,
            ).extract_surface()
            meshes.append((cyl, "#2C3E50", 1.0))

        # 지반
        bounds_centered = (bounds[0] - cx, 0, bounds[2] - cx, ext_depth + length)
        meshes.append(_build_ground_plane(bounds_centered, base))

        return meshes


# ---------------------------------------------------------------------------
# 템플릿 구현: 일반 (범용)
# ---------------------------------------------------------------------------

class GenericStructureTemplate(StructureTemplate):
    template_id = "generic"
    name_ko = "일반 / 범용"
    description = "DXF 모든 외곽을 높이만큼 extrude (단위 자동 감지)"
    required_files = (1, 2, 1)

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("height", "높이", "m", 5.0, 0.1, 500.0),
            ParamField("base_el", "바닥 EL.", "m", 0.0, -100, 500),
            ParamField("render_mode", "렌더 모드", "", 0, 0, 2, "choice",
                       choices=["closed만 extrude", "open도 벽체", "모든 요소 선"]),
            ParamField("min_area", "무시할 최소 면적 (m²)", "", 1.0, 0.0, 100.0),
            ParamField("wall_thickness", "열린 폴리라인 두께", "m", 0.3, 0.1, 2.0),
            ParamField("max_shapes", "최대 렌더 요소 수", "", 30, 1, 500, "int"),
            ParamField("use_view_based", "뷰 기반 재구성 사용", "", 1, 0, 1, "choice",
                       choices=["끄기", "켜기"]),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from detail_parser import DetailParser
        from dxf_geometry import extract_all

        params = StructureParams(template_id=self.template_id, name="일반 구조물")
        params.source_files = dxf_paths
        params.params = self.default_params()

        # 뷰 검출
        view_info = self.try_view_based_parse(dxf_paths)
        params.set("_views", view_info["views"])
        params.set("_views_detected", view_info["detected"])

        # 치수 파싱
        parser = DetailParser()
        all_dims = []
        for p in dxf_paths:
            try:
                result = parser.parse(p)
                all_dims.extend(result.dimensions)
                params.raw_annotations.extend(
                    [(d.raw_text, d.position[0], d.position[1]) for d in result.dimensions]
                )
            except Exception:
                pass
        # 높이 후보: 0.5~100m 범위
        h_candidates = [d.value for d in all_dims
                        if d.param == "height" and 0.5 <= d.value <= 100.0]
        if h_candidates:
            params.set("height", float(np.median(h_candidates)))

        # 지오메트리 (단위 자동 정규화)
        geom = extract_all(dxf_paths)
        params.set("_geom_bounds", geom.total_bounds)
        params.set("_geom_unit", geom.detected_unit)

        # 모든 shape 저장
        shapes_data = [{
            "kind": s.kind,
            "points": s.points,
            "closed": s.closed,
            "layer": s.layer,
            "area": s.area,
            "length": s.length,
        } for s in geom.shapes]
        params.set("shapes", shapes_data)
        return params

    def build_meshes(self, params: StructureParams):
        # 뷰 기반 재구성 우선
        use_view = int(params.get("use_view_based", 1)) == 1
        if use_view and params.get("_views_detected", False):
            view_meshes = self.try_view_based_meshes(params)
            if view_meshes and len(view_meshes) >= 2:
                return view_meshes

        h = params.get("height", 5.0)
        base = params.get("base_el", 0.0)
        render_mode = int(params.get("render_mode", 0))
        min_area = params.get("min_area", 1.0)
        wall_t = params.get("wall_thickness", 0.3)
        max_shapes = int(params.get("max_shapes", 30))
        shapes = params.get("shapes") or []
        bounds = params.get("_geom_bounds", (0, 0, 20, 20))

        meshes = []
        cx = (bounds[0] + bounds[2]) / 2
        cy = (bounds[1] + bounds[3]) / 2

        # 중요도 기준 정렬 (면적 또는 길이) → 상위 max_shapes개만
        def _importance(s):
            return s["area"] if s["closed"] else s["length"]
        shapes_sorted = sorted(shapes, key=_importance, reverse=True)[:max_shapes]

        # 색상 팔레트 (레이어별 구분)
        colors = ["#B8B5A8", "#A8C4D0", "#D4A373", "#8FBC8F",
                  "#CDB79E", "#B0A59F", "#9A968C", "#BDC3C7"]

        for i, s in enumerate(shapes_sorted):
            centered = [(p[0] - cx, p[1] - cy) for p in s["points"]]
            color = colors[i % len(colors)]

            if render_mode == 0:
                # closed만 extrude
                if s["closed"] and s["area"] >= min_area:
                    result = _extrude_polygon(centered, base, h, color)
                    if result:
                        meshes.append(result)
            elif render_mode == 1:
                # closed는 extrude, open은 벽체
                if s["closed"] and s["area"] >= min_area:
                    result = _extrude_polygon(centered, base, h, color)
                    if result:
                        meshes.append(result)
                elif not s["closed"] and len(centered) >= 2 and s["length"] >= 1.0:
                    result = _extrude_polyline_wall(centered, base, h, wall_t, color)
                    if result:
                        meshes.append(result)
            # 모든 요소 선으로만
            elif len(centered) >= 2:
                pts_3d = np.array([[p[0], p[1], base + h / 2] for p in centered])
                n = len(pts_3d)
                line = pv.PolyData(pts_3d, lines=np.concatenate([[n], np.arange(n)]))
                meshes.append((line, color, 1.0))

        if not meshes:
            # 폴백
            box = make_box(-5, 5, -5, 5, base, base + h)
            meshes.append((box, "#B8B5A8", 1.0))

        # 지반
        bounds_centered = (bounds[0] - cx, bounds[1] - cy, bounds[2] - cx, bounds[3] - cy)
        meshes.append(_build_ground_plane(bounds_centered, base))

        return meshes


# ---------------------------------------------------------------------------
# 상세 템플릿: 취수탑 (Intake Tower)
# ---------------------------------------------------------------------------

class IntakeTowerTemplate(StructureTemplate):
    template_id = "intake_tower"
    name_ko = "취수탑"
    description = "L자 본체 + 수문N개 + 개폐장치 + 호이스트 + 점검구 + 계단"
    required_files = (1, 2, 2)
    supports_view_based = False  # 자체 파서 사용

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("body_width", "본체 폭(X)", "m", 11.2, 3.0, 50.0),
            ParamField("body_depth", "본체 깊이(Y)", "m", 6.4, 2.0, 40.0),
            ParamField("body_bottom_el", "본체 바닥 EL.", "m", 39.0, 0, 500),
            ParamField("body_top_el", "본체 상단 EL.", "m", 57.2, 0, 500),
            ParamField("n_gates", "수문 개수", "문", 3, 1, 10, "int"),
            ParamField("gate_spacing_z", "수문 수직 간격", "m", 2.5, 0.5, 10.0),
            ParamField("gate_width", "수문 폭", "m", 2.0, 0.5, 8.0),
            ParamField("gate_height", "수문 높이", "m", 2.0, 0.5, 8.0),
            ParamField("actuator_radius", "개폐장치 반경", "m", 0.6, 0.2, 2.0),
            ParamField("has_hoist", "호이스트 포함", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("hoist_rail_el", "호이스트 레일 EL.", "m", 56.0, 0, 500),
            ParamField("has_l_extension", "L자 연장부", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("extension_length", "연장부 길이", "m", 14.5, 0, 100),
            ParamField("extension_width", "연장부 폭", "m", 6.4, 1, 40),
            ParamField("has_entry_stairs", "외부 출입계단", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("stairs_width", "계단 폭", "m", 1.5, 0.5, 5.0),
            ParamField("stairs_side", "계단 위치", "", 0, 0, 3, "choice",
                       choices=["좌", "우", "전", "후"]),
            ParamField("has_inspection_cover", "상단 점검구", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("inspection_cover_size", "점검구 크기", "m", 2.5, 0.5, 10.0),
            ParamField("has_parapet", "상단 난간", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("parapet_height", "난간 높이", "m", 1.1, 0.5, 2.5),
            ParamField("roof_thickness", "지붕 두께", "m", 0.5, 0.1, 2.0),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from intake_tower_parser import parse_intake_tower

        it_params = parse_intake_tower(dxf_paths)

        params = StructureParams(template_id=self.template_id, name="취수탑")
        params.source_files = it_params.source_files
        params.raw_annotations = it_params.raw_annotations

        params.params = {
            "body_width": it_params.body_width,
            "body_depth": it_params.body_depth,
            "body_bottom_el": it_params.body_bottom_el,
            "body_top_el": it_params.body_top_el,
            "n_gates": len(it_params.gates),
            "gate_spacing_z": 2.5,
            "gate_width": it_params.gates[0].gate_width if it_params.gates else 2.0,
            "gate_height": it_params.gates[0].gate_height if it_params.gates else 2.0,
            "actuator_radius": it_params.gates[0].actuator_radius if it_params.gates else 0.6,
            "has_hoist": 1 if it_params.has_hoist else 0,
            "hoist_rail_el": it_params.hoist_rail_el,
            "has_l_extension": 1 if it_params.has_l_extension else 0,
            "extension_length": it_params.extension_length,
            "extension_width": it_params.extension_width,
            "has_entry_stairs": 1 if it_params.has_entry_stairs else 0,
            "stairs_width": it_params.stairs_width,
            "stairs_side": 0,
            "has_inspection_cover": 1 if it_params.has_inspection_cover else 0,
            "inspection_cover_size": it_params.inspection_cover_size,
            "has_parapet": 1 if it_params.has_parapet else 0,
            "parapet_height": it_params.parapet_height,
            "roof_thickness": it_params.roof_thickness,
            "_gates": it_params.gates,
            "_floor_elevations": it_params.floor_elevations,
        }
        return params

    def build_meshes(self, params: StructureParams):
        from intake_tower_parser import IntakeTowerParams, GatePosition
        from intake_tower_3d_builder import IntakeTowerBuilder

        it = IntakeTowerParams()
        it.body_width = params.get("body_width", 11.2)
        it.body_depth = params.get("body_depth", 6.4)
        it.body_bottom_el = params.get("body_bottom_el", 39.0)
        it.body_top_el = params.get("body_top_el", 57.2)
        it.has_hoist = bool(int(params.get("has_hoist", 1)))
        it.hoist_rail_el = params.get("hoist_rail_el", 56.0)
        it.has_l_extension = bool(int(params.get("has_l_extension", 1)))
        it.extension_length = params.get("extension_length", 14.5)
        it.extension_width = params.get("extension_width", 6.4)
        it.has_entry_stairs = bool(int(params.get("has_entry_stairs", 1)))
        it.stairs_width = params.get("stairs_width", 1.5)
        it.stairs_side = ["left", "right", "front", "back"][int(params.get("stairs_side", 0))]
        it.has_inspection_cover = bool(int(params.get("has_inspection_cover", 1)))
        it.inspection_cover_size = params.get("inspection_cover_size", 2.5)
        it.has_parapet = bool(int(params.get("has_parapet", 1)))
        it.parapet_height = params.get("parapet_height", 1.1)
        it.roof_thickness = params.get("roof_thickness", 0.5)

        # Gates: 기존 값 재사용 또는 파라미터로 재생성
        existing_gates = params.get("_gates")
        if existing_gates and len(existing_gates) == int(params.get("n_gates", 3)):
            # Width/Height가 편집되었을 수 있음 → 업데이트
            for g in existing_gates:
                g.gate_width = params.get("gate_width", g.gate_width)
                g.gate_height = params.get("gate_height", g.gate_height)
                g.actuator_radius = params.get("actuator_radius", g.actuator_radius)
            it.gates = existing_gates
        else:
            n = int(params.get("n_gates", 3))
            dz = params.get("gate_spacing_z", 2.5)
            gw = params.get("gate_width", 2.0)
            gh = params.get("gate_height", 2.0)
            ar = params.get("actuator_radius", 0.6)
            el_base = it.body_bottom_el + 4
            it.gates = [
                GatePosition(
                    index=i, center_x=(i - (n - 1) / 2) * 3,
                    elevation=el_base + i * dz,
                    actuator_radius=ar,
                    gate_width=gw, gate_height=gh,
                    label=f"수문{i+1}",
                )
                for i in range(n)
            ]

        floor_els = params.get("_floor_elevations", [])
        it.floor_elevations = floor_els

        return IntakeTowerBuilder(it).build_all()


# ---------------------------------------------------------------------------
# 상세 템플릿: 제수변실 + 도수관로
# ---------------------------------------------------------------------------

class ValveChamberTemplate(StructureTemplate):
    template_id = "valve_chamber"
    name_ko = "제수변실 / 도수관로"
    description = "실 본체 + 밸브N개 + 도수관 + 송수관 + 상단 뚜껑"
    required_files = (1, 2, 1)
    supports_view_based = False

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("chamber_width", "실 폭(X)", "m", 27.0, 3.0, 80.0),
            ParamField("chamber_depth", "실 깊이(Y)", "m", 9.0, 2.0, 40.0),
            ParamField("wall_thickness", "벽 두께", "m", 0.6, 0.2, 2.0),
            ParamField("bottom_el", "바닥 EL.", "m", 21.0, 0, 500),
            ParamField("top_el", "상판 EL.", "m", 28.5, 0, 500),
            ParamField("n_valves", "밸브 개수", "개", 5, 1, 20, "int"),
            ParamField("main_conduit_diameter", "도수관 관경", "m", 1.0, 0.2, 3.0),
            ParamField("main_conduit_el", "도수관 EL.", "m", 22.0, 0, 500),
            ParamField("main_conduit_direction", "도수관 방향", "", 0, 0, 1, "choice",
                       choices=["X방향", "Y방향"]),
            ParamField("external_pipe_length", "외부 관로 길이(legacy)", "m", 5.0, 0, 100),
            ParamField("upstream_pipe_length", "상류 도수관 길이", "m", 3.0, 0, 50),
            ParamField("downstream_pipe_length", "하류 송수관 길이", "m", 4.0, 0, 50),
            ParamField("has_inlet_branch", "상류 Y-분기", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("branch_spread_m", "분기 상·하단 간격", "m", 4.4, 0.5, 20),
            ParamField("branch_angle_deg", "분기 합류각", "°", 35.0, 10, 70),
            ParamField("branch_trunk_length", "분기점 이전 도수관 길이", "m", 3.0, 0, 30),
            ParamField("has_hatch", "상단 뚜껑", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("hatch_count", "뚜껑 개수", "개", 1, 1, 10, "int"),
            ParamField("hatch_size", "뚜껑 크기", "m", 1.0, 0.3, 3.0),
            ParamField("has_entry_stairs", "출입 계단", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from valve_chamber_parser import parse_valve_chamber

        vc = parse_valve_chamber(dxf_paths)

        params = StructureParams(template_id=self.template_id, name="제수변실")
        params.source_files = vc.source_files
        params.raw_annotations = vc.raw_annotations

        params.params = {
            "chamber_width": vc.chamber_width,
            "chamber_depth": vc.chamber_depth,
            "wall_thickness": vc.chamber_wall_thickness,
            "bottom_el": vc.bottom_el,
            "top_el": vc.top_el,
            "n_valves": len(vc.valves),
            "main_conduit_diameter": vc.main_conduit_diameter,
            "main_conduit_el": vc.main_conduit_el,
            "main_conduit_direction": 0 if vc.main_conduit_direction == "X" else 1,
            "external_pipe_length": vc.external_pipe_length,
            "upstream_pipe_length": vc.upstream_pipe_length,
            "downstream_pipe_length": vc.downstream_pipe_length,
            "has_inlet_branch": 1 if vc.has_inlet_branch else 0,
            "branch_spread_m": vc.branch_spread_m,
            "branch_angle_deg": vc.branch_angle_deg,
            "branch_trunk_length": vc.branch_trunk_length,
            "has_hatch": 1 if vc.has_hatch else 0,
            "hatch_count": vc.hatch_count,
            "hatch_size": vc.hatch_size,
            "has_entry_stairs": 1 if vc.has_entry_stairs else 0,
            "_valves": vc.valves,
            "_pipes": vc.pipes,
            "_floor_elevations": vc.floor_elevations,
        }
        return params

    def build_meshes(self, params: StructureParams):
        from valve_chamber_parser import ValveChamberParams, Valve
        from valve_chamber_3d_builder import ValveChamberBuilder

        vc = ValveChamberParams()
        vc.chamber_width = params.get("chamber_width", 27.0)
        vc.chamber_depth = params.get("chamber_depth", 9.0)
        vc.chamber_wall_thickness = params.get("wall_thickness", 0.6)
        vc.bottom_el = params.get("bottom_el", 21.0)
        vc.top_el = params.get("top_el", 28.5)
        vc.main_conduit_diameter = params.get("main_conduit_diameter", 1.0)
        vc.main_conduit_el = params.get("main_conduit_el", 22.0)
        vc.main_conduit_direction = "X" if int(params.get("main_conduit_direction", 0)) == 0 else "Y"
        vc.external_pipe_length = params.get("external_pipe_length", 5.0)
        vc.upstream_pipe_length = params.get("upstream_pipe_length", 3.0)
        vc.downstream_pipe_length = params.get("downstream_pipe_length", 4.0)
        vc.has_inlet_branch = bool(int(params.get("has_inlet_branch", 1)))
        vc.branch_spread_m = params.get("branch_spread_m", 4.4)
        vc.branch_angle_deg = params.get("branch_angle_deg", 35.0)
        vc.branch_trunk_length = params.get("branch_trunk_length", 3.0)
        vc.has_hatch = bool(int(params.get("has_hatch", 1)))
        vc.hatch_count = int(params.get("hatch_count", 1))
        vc.hatch_size = params.get("hatch_size", 1.0)
        vc.has_entry_stairs = bool(int(params.get("has_entry_stairs", 1)))

        existing_valves = params.get("_valves") or []
        n_valves = int(params.get("n_valves", 5))
        if existing_valves and len(existing_valves) == n_valves:
            vc.valves = existing_valves
        else:
            # n_valves에 맞춰 재생성
            vc.valves = []
            for i in range(n_valves):
                t = (i + 0.5) / n_valves - 0.5
                v = Valve(
                    index=i, name=f"V-{i+1}",
                    valve_type="GATE",
                    center_x=t * vc.chamber_width * 0.7,
                    center_y=0,
                    elevation=vc.bottom_el + 2,
                    diameter=0.4,
                    label=f"밸브{i+1}",
                )
                vc.valves.append(v)

        # _pipes는 **도면 파싱 결과만** 반영. UI에서 시드를 추가해 없던 도수관을
        # 만들어내지 않는다(사용자 지적: 도면에 없는 관로가 계속 생성되던 버그).
        existing_pipes = list(params.get("_pipes") or [])
        # UI 토글로 분기 기능을 끈 경우, 기존 M-301 분기 pipe들까지 완전히 제거.
        if not vc.has_inlet_branch:
            existing_pipes = [pp for pp in existing_pipes
                              if not ("도수관" in pp.name or pp.name.startswith("M-301"))]
        vc.pipes = existing_pipes
        # 파싱된 도수관이 있을 때만 finalize를 돌려 분기 재생성 (시드 주입 없음).
        has_any_main = any(("도수관" in pp.name or pp.name.startswith("M-301"))
                           for pp in vc.pipes)
        if has_any_main:
            from valve_chamber_parser import ValveChamberParser
            ValveChamberParser()._finalize(vc)
        vc.floor_elevations = params.get("_floor_elevations", [])

        return ValveChamberBuilder(vc).build_all()


# ---------------------------------------------------------------------------
# 상세 템플릿: 옹벽 (교체)
# ---------------------------------------------------------------------------

class DetailedRetainingWallTemplate(StructureTemplate):
    template_id = "retaining_wall"
    name_ko = "옹벽 (상세)"
    description = "사다리꼴 본체 sweep + 기초 slab + 앵커바 격자 + 파라펫 + 배수공"
    required_files = (1, 2, 1)
    supports_view_based = False

    def get_parameter_schema(self) -> list[ParamField]:
        return [
            ParamField("total_length", "총 연장", "m", 100.0, 5.0, 500.0),
            ParamField("bottom_el", "바닥 EL.", "m", 41.5, 0, 500),
            ParamField("top_el", "상단 EL.", "m", 60.0, 0, 500),
            ParamField("avg_top_width", "상단 폭", "m", 0.6, 0.2, 3.0),
            ParamField("avg_bottom_width", "하단 폭", "m", 3.0, 0.5, 10.0),
            ParamField("base_slab_width", "기초 slab 폭", "m", 5.0, 1.0, 15.0),
            ParamField("base_slab_thickness", "기초 slab 두께", "m", 1.0, 0.3, 3.0),
            ParamField("front_batter_ratio", "전면 경사비 (1:N 중 1)", "", 0.05, 0.0, 0.3),
            ParamField("has_anchors", "배면 앵커바", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("anchor_count", "앵커 개수", "개", 78, 0, 500, "int"),
            ParamField("anchor_spacing_h", "앵커 수평간격", "m", 3.0, 0.5, 10.0),
            ParamField("anchor_spacing_v", "앵커 수직간격", "m", 3.0, 0.5, 10.0),
            ParamField("anchor_length", "앵커 매입길이", "m", 12.0, 1, 30),
            ParamField("anchor_angle_deg", "앵커 경사각(°)", "", 15, 0, 45),
            ParamField("has_parapet", "상단 파라펫", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("parapet_height", "파라펫 높이", "m", 1.1, 0.5, 2.5),
            ParamField("has_contraction_joints", "수축이음", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("joint_spacing", "수축이음 간격", "m", 10.0, 3.0, 30.0),
            ParamField("has_weep_holes", "배수공", "", 1, 0, 1, "choice",
                       choices=["X", "O"]),
            ParamField("weep_hole_spacing", "배수공 간격", "m", 3.0, 1.0, 10.0),
            ParamField("ground_level", "전면 지반 EL.", "m", 41.5, 0, 500),
        ]

    def parse(self, dxf_paths: list[str]) -> StructureParams:
        from retaining_wall_parser import parse_retaining_wall

        rw = parse_retaining_wall(dxf_paths)

        params = StructureParams(template_id=self.template_id, name="옹벽")
        params.source_files = rw.source_files
        params.raw_annotations = rw.raw_annotations

        params.params = {
            "total_length": rw.total_length,
            "bottom_el": rw.bottom_el,
            "top_el": rw.top_el,
            "avg_top_width": rw.avg_top_width,
            "avg_bottom_width": rw.avg_bottom_width,
            "base_slab_width": rw.base_slab_width,
            "base_slab_thickness": rw.base_slab_thickness,
            "front_batter_ratio": rw.front_batter_ratio,
            "has_anchors": 1 if rw.has_anchors else 0,
            "anchor_count": rw.anchor_count,
            "anchor_spacing_h": rw.anchor_spacing_h,
            "anchor_spacing_v": rw.anchor_spacing_v,
            "anchor_length": rw.anchor_length,
            "anchor_angle_deg": rw.anchor_angle_deg,
            "has_parapet": 1 if rw.has_parapet else 0,
            "parapet_height": rw.parapet_height,
            "has_contraction_joints": 1 if rw.has_contraction_joints else 0,
            "joint_spacing": rw.joint_spacing,
            "has_weep_holes": 1 if rw.has_weep_holes else 0,
            "weep_hole_spacing": rw.weep_hole_spacing,
            "ground_level": rw.ground_level,
        }
        return params

    def build_meshes(self, params: StructureParams):
        from retaining_wall_parser import RetainingWallParams
        from retaining_wall_3d_builder import RetainingWallBuilder

        rw = RetainingWallParams()
        for k in ["total_length", "bottom_el", "top_el", "avg_top_width",
                  "avg_bottom_width", "base_slab_width", "base_slab_thickness",
                  "front_batter_ratio", "anchor_count", "anchor_spacing_h",
                  "anchor_spacing_v", "anchor_length", "anchor_angle_deg",
                  "parapet_height", "joint_spacing", "weep_hole_spacing",
                  "ground_level"]:
            if k in params.params:
                setattr(rw, k, params.get(k))

        rw.has_anchors = bool(int(params.get("has_anchors", 1)))
        rw.has_parapet = bool(int(params.get("has_parapet", 1)))
        rw.has_contraction_joints = bool(int(params.get("has_contraction_joints", 1)))
        rw.has_weep_holes = bool(int(params.get("has_weep_holes", 1)))

        return RetainingWallBuilder(rw).build_all()


# ---------------------------------------------------------------------------
# 레지스트리
# ---------------------------------------------------------------------------

class TemplateRegistry:
    """구조물 템플릿 레지스트리 (싱글톤 패턴)."""

    _instance: ClassVar[TemplateRegistry | None] = None
    _templates: ClassVar[dict[str, StructureTemplate]] = {}

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._templates = {}
            cls._instance._register_defaults()
        return cls._instance

    def _register_defaults(self):
        # 상세 템플릿 우선 등록 (같은 template_id면 덮어씀)
        for tpl_cls in [
            SpillwayGateTemplate,
            IntakeTowerTemplate,
            ValveChamberTemplate,
            DetailedRetainingWallTemplate,  # 기존 RetainingWallTemplate 대체
            BuildingTemplate,
            BridgeTemplate,
            TunnelPortalTemplate,
            GenericStructureTemplate,
        ]:
            tpl = tpl_cls()
            self._templates[tpl.template_id] = tpl

    def get(self, template_id: str) -> StructureTemplate | None:
        return self._templates.get(template_id)

    def list_all(self) -> list[StructureTemplate]:
        return list(self._templates.values())

    def list_choices(self) -> list[tuple[str, str]]:
        """(template_id, name_ko) 쌍 목록."""
        return [(t.template_id, t.name_ko) for t in self._templates.values()]


# 모듈 레벨 편의 인스턴스
REGISTRY = TemplateRegistry()


if __name__ == "__main__":
    print("등록된 구조물 템플릿:")
    for tid, name in REGISTRY.list_choices():
        tpl = REGISTRY.get(tid)
        print(f"  [{tid}] {name}")
        print(f"    {tpl.description}")
        print(f"    파라미터 {len(tpl.get_parameter_schema())}개")
