"""뷰 기반 통합 3D 재구성.

핵심 원칙:
  여러 뷰(평면도/정면도/측면도)는 같은 하나의 구조물을 다른 방향에서 본
  서로 다른 투영(projection)이다. 따라서 N개의 뷰 → 1개의 통합 3D 구조물.

좌표계 매핑 (표준 토목 정투영):
  ┌──────────┬───────────┬───────────┬───────────────┐
  │ 뷰 타입  │ 도면의 X  │ 도면의 Y  │ 3D 대응 (실물) │
  ├──────────┼───────────┼───────────┼───────────────┤
  │ 평면도   │ 실물 X    │ 실물 Y    │ XY (위에서 봄) │
  │ 정면도   │ 실물 X    │ 실물 Z    │ XZ (앞에서 봄) │
  │ 측면도   │ 실물 Y    │ 실물 Z    │ YZ (옆에서 봄) │
  │ 단면도   │ 실물 X/Y  │ 실물 Z    │ 단면 프로파일  │
  └──────────┴───────────┴───────────┴───────────────┘

알고리즘:
  1. 각 뷰 종류별로 가장 대표적인 뷰 1개씩 선택 (라벨/면적 기준)
  2. 각 뷰에서 메인 실루엣(silhouette) 추출 (단일 폴리곤)
  3. 실루엣들로부터 3D 구조물의 (X폭, Y깊이, Z높이) 결정
  4. 가장 정보량이 많은 뷰를 base로 단일 메쉬 생성
     - 평면도 있으면: 평면 외곽 → Z방향 extrude
     - 정면만: 정면 외곽 → Y방향 extrude (얕은 두께)
     - 측면만: 측면 외곽 → X방향 extrude
"""

from __future__ import annotations


import numpy as np
import pyvista as pv

from view_detector import ViewRegion


# ---------------------------------------------------------------------------
# 템플릿별 색상 / 키워드
# ---------------------------------------------------------------------------

TEMPLATE_COLORS = {
    "spillway_gate": "#B8B5A8",
    "building": "#BDC3C7",
    "retaining_wall": "#7F8C8D",
    "bridge": "#95A5A6",
    "tunnel_portal": "#A8A59B",
    "generic": "#B8B5A8",
}

# 템플릿별 키워드 (메인 뷰 식별용)
TEMPLATE_KEYWORDS = {
    "spillway_gate": ["여수로", "수문", "spillway", "gate"],
    "building": ["취수탑", "탑", "건물", "사무소", "관리", "tower", "building",
                  "제수변실", "valve"],
    "retaining_wall": ["옹벽", "벽", "방벽", "wall"],
    "bridge": ["교량", "교", "bridge", "공도교"],
    "tunnel_portal": ["터널", "갱구", "tunnel", "portal"],
}


# ---------------------------------------------------------------------------
# 기본 헬퍼: 폴리곤 면적/길이/외곽 추출
# ---------------------------------------------------------------------------

def _polygon_area(points: list) -> float:
    """Shoelace 면적 (closed polygon 가정)."""
    if len(points) < 3:
        return 0.0
    n = len(points)
    s = 0.0
    for i in range(n):
        x1, y1 = points[i][0], points[i][1]
        x2, y2 = points[(i + 1) % n][0], points[(i + 1) % n][1]
        s += x1 * y2 - x2 * y1
    return abs(s) / 2


def _polyline_length(points: list) -> float:
    if len(points) < 2:
        return 0.0
    arr = np.array(points, dtype=np.float64)
    diffs = np.diff(arr, axis=0)
    return float(np.sum(np.linalg.norm(diffs, axis=1)))


# ---------------------------------------------------------------------------
# 메인 뷰 선택 (여러 평면도 중 어떤 게 진짜 평면도?)
# ---------------------------------------------------------------------------

def pick_main_view(views: list[ViewRegion], view_type: str,
                    template_id: str | None = None) -> ViewRegion | None:
    """주어진 타입의 뷰 중 가장 대표적인 것 선택.

    선택 기준:
    1. 해당 타입이 1개면 그것
    2. 라벨이 템플릿 키워드를 포함하면 우선
    3. 그 외엔 가장 큰 영역
    """
    candidates = [v for v in views if v.view_type == view_type]
    if not candidates:
        return None
    if len(candidates) == 1:
        return candidates[0]

    # 키워드 매칭 우선
    if template_id and template_id in TEMPLATE_KEYWORDS:
        keywords = TEMPLATE_KEYWORDS[template_id]
        for v in candidates:
            label = v.label_text.lower()
            for kw in keywords:
                if kw.lower() in label:
                    return v

    # 가장 큰 영역
    return max(candidates, key=lambda v: v.width * v.height)


# ---------------------------------------------------------------------------
# 뷰에서 메인 실루엣(silhouette) 추출
# ---------------------------------------------------------------------------

def extract_main_silhouette(view: ViewRegion) -> list | None:
    """뷰에서 구조물의 외곽선을 단일 폴리곤으로 추출.

    전략 (실루엣 면적이 뷰 면적의 일정 비율 이상이어야 진짜 외곽으로 간주):
    1. 충분히 큰 closed 폴리곤 (뷰 면적의 10% 이상) 중 최대
    2. 그것도 없으면 작은 closed 폴리곤들의 합집합 bbox
    3. convex hull (모든 점 기준) — 모든 LINE/ARC가 그리는 외곽
    4. 위 모두 실패 시 뷰 영역 자체 사각형

    반환은 로컬 좌표 (뷰 bbox 좌하단을 원점으로).
    """
    local_shapes = view.get_local_shapes()
    if not local_shapes:
        return _view_rect_silhouette(view)

    view_area = max(view.width * view.height, 1.0)

    # === 1) 충분히 큰 closed 폴리곤 우선 ===
    closed_candidates = []
    for s in local_shapes:
        if not s.closed or len(s.points) < 3:
            continue
        a = _polygon_area(s.points)
        # 뷰 면적의 10% 이상 + 95% 이하 (프레임 잔재 제외)
        if view_area * 0.10 <= a <= view_area * 0.95:
            closed_candidates.append((a, s.points))

    if closed_candidates:
        closed_candidates.sort(key=lambda x: -x[0])
        return closed_candidates[0][1]

    # === 2) Convex hull 폴백: 모든 LINE/ARC/POLYLINE 점들의 외곽 ===
    # 노이즈를 줄이기 위해 너무 작은 shape는 제외
    significant_shapes = []
    for s in local_shapes:
        if s.kind in ("polyline", "line", "arc", "circle"):
            # 길이/면적이 충분히 큰 것만
            if s.closed:
                if _polygon_area(s.points) >= view_area * 0.001:
                    significant_shapes.append(s)
            elif _polyline_length(s.points) >= max(view.width, view.height) * 0.05:
                significant_shapes.append(s)

    if significant_shapes:
        try:
            from scipy.spatial import ConvexHull
            all_pts = []
            for s in significant_shapes:
                all_pts.extend(s.points)
            if len(all_pts) >= 3:
                arr = np.array(all_pts)
                # 중복 제거
                arr = np.unique(arr, axis=0)
                if len(arr) >= 3:
                    hull = ConvexHull(arr)
                    hull_pts = [tuple(arr[i]) for i in hull.vertices]
                    # Hull이 너무 작으면 뷰 사각형 사용
                    hull_area = _polygon_area(hull_pts)
                    if hull_area >= view_area * 0.10:
                        return hull_pts
        except Exception:
            pass

    # === 3) 작은 closed들의 union bbox (마지막 수단 전 단계) ===
    small_closed = [s.points for s in local_shapes
                    if s.closed and len(s.points) >= 3
                    and _polygon_area(s.points) >= view_area * 0.001]
    if small_closed:
        # 모든 작은 closed의 점들을 모아 hull
        try:
            from scipy.spatial import ConvexHull
            all_pts = []
            for pts in small_closed:
                all_pts.extend(pts)
            if len(all_pts) >= 3:
                arr = np.array(all_pts)
                arr = np.unique(arr, axis=0)
                if len(arr) >= 3:
                    hull = ConvexHull(arr)
                    return [tuple(arr[i]) for i in hull.vertices]
        except Exception:
            pass

    # === 4) 최종 폴백: 뷰 영역 사각형 자체 ===
    return _view_rect_silhouette(view)


def _view_rect_silhouette(view: ViewRegion) -> list:
    """뷰 영역 사각형을 실루엣으로 반환 (로컬 좌표)."""
    return [(0, 0), (view.width, 0),
            (view.width, view.height), (0, view.height)]


# ---------------------------------------------------------------------------
# Oriented Bounding Box (PCA 기반) — 가늘고 긴 구조 감지용
# ---------------------------------------------------------------------------

def compute_oriented_bbox(points: list) -> dict | None:
    """점들의 PCA로 oriented bounding box 계산.

    Returns:
        {
          "center": (x, y),
          "axis_long": (dx, dy),     # 길이방향 단위벡터
          "axis_short": (dx, dy),    # 폭방향 단위벡터
          "length": float,            # 길이방향 길이
          "width": float,             # 폭방향 길이
          "corners": [(x,y) × 4],     # 사각형 4꼭지점 (반시계)
          "aspect_ratio": length/width,
        }
    """
    if len(points) < 3:
        return None

    arr = np.array(points, dtype=np.float64)
    # 중복 점 제거
    arr = np.unique(arr, axis=0)
    if len(arr) < 3:
        return None

    center = arr.mean(axis=0)
    centered = arr - center

    # 공분산 + 고유벡터
    try:
        cov = np.cov(centered.T)
        eigenvals, eigenvecs = np.linalg.eigh(cov)
    except Exception:
        return None

    # 고유값 내림차순 정렬 (큰 게 길이축)
    idx = np.argsort(-eigenvals)
    eigenvecs = eigenvecs[:, idx]

    # 주축으로 점 투영
    proj = centered @ eigenvecs
    long_min, long_max = float(proj[:, 0].min()), float(proj[:, 0].max())
    short_min, short_max = float(proj[:, 1].min()), float(proj[:, 1].max())

    length = long_max - long_min
    width = max(short_max - short_min, 0.01)

    # 4꼭지점 (주축 좌표계 → 원본 좌표계)
    local_corners = np.array([
        [long_min, short_min],
        [long_max, short_min],
        [long_max, short_max],
        [long_min, short_max],
    ])
    world_corners = local_corners @ eigenvecs.T + center

    return {
        "center": (float(center[0]), float(center[1])),
        "axis_long": (float(eigenvecs[0, 0]), float(eigenvecs[1, 0])),
        "axis_short": (float(eigenvecs[0, 1]), float(eigenvecs[1, 1])),
        "length": length,
        "width": width,
        "corners": [tuple(c) for c in world_corners],
        "aspect_ratio": length / width,
    }


def is_elongated_structure(plan_view: ViewRegion,
                             min_aspect: float = 2.5) -> dict | None:
    """평면도가 가늘고 긴 구조물(옹벽 등)인지 감지.

    Returns:
        oriented bbox dict (is elongated인 경우)
        None (그렇지 않으면)
    """
    local_shapes = plan_view.get_local_shapes()
    if not local_shapes:
        return None

    char_size = max(plan_view.width, plan_view.height)
    # 임계 길이를 매우 낮게 (전체 크기의 1%만 넘어도 포함)
    min_sig_len = char_size * 0.01
    min_sig_area = (char_size ** 2) * 0.0001

    significant_pts = []
    for s in local_shapes:
        if s.kind not in ("polyline", "line", "arc", "circle"):
            continue
        if s.closed:
            if _polygon_area(s.points) < min_sig_area:
                continue
        elif _polyline_length(s.points) < min_sig_len:
            continue
        significant_pts.extend(s.points)

    # 점이 충분치 않으면 모든 점 사용
    if len(significant_pts) < 10:
        significant_pts = []
        for s in local_shapes:
            significant_pts.extend(s.points)

    if len(significant_pts) < 5:
        return None

    obb = compute_oriented_bbox(significant_pts)
    if obb is None:
        return None

    if obb["aspect_ratio"] >= min_aspect:
        return obb
    return None


def get_centerline_from_obb(obb: dict, n_samples: int = 2) -> list:
    """OBB의 길이방향 중심선 점 목록 반환."""
    cx, cy = obb["center"]
    ax, ay = obb["axis_long"]
    half_L = obb["length"] / 2

    centerline = []
    for i in range(n_samples):
        t = -1 + 2 * i / max(n_samples - 1, 1)  # -1 ~ 1
        x = cx + ax * t * half_L
        y = cy + ay * t * half_L
        centerline.append((x, y))
    return centerline


def get_obb_polygon(obb: dict) -> list:
    """OBB 사각형을 polygon 점 목록으로 반환."""
    return obb["corners"]


def silhouette_bbox(silhouette: list) -> tuple:
    """실루엣의 bbox 반환 (xmin, ymin, xmax, ymax)."""
    arr = np.array(silhouette)
    return (float(arr[:, 0].min()), float(arr[:, 1].min()),
            float(arr[:, 0].max()), float(arr[:, 1].max()))


# ---------------------------------------------------------------------------
# 차원 결정: 뷰들로부터 실제 3D 구조물 (X, Y, Z) 크기 계산
# ---------------------------------------------------------------------------

def compute_3d_dimensions(silhouettes: dict) -> dict:
    """뷰별 실루엣들로부터 3D 구조물의 실제 크기를 추정.

    Args:
        silhouettes: {"plan": [...], "front": [...], "side": [...]}

    Returns:
        {"width": X폭, "depth": Y깊이, "height": Z높이}
    """
    width = depth = height = None

    # 평면도: 도면X=실물X, 도면Y=실물Y
    if "plan" in silhouettes:
        bb = silhouette_bbox(silhouettes["plan"])
        width = bb[2] - bb[0]
        depth = bb[3] - bb[1]

    # 정면도: 도면X=실물X, 도면Y=실물Z
    if "front" in silhouettes:
        bb = silhouette_bbox(silhouettes["front"])
        if width is None:
            width = bb[2] - bb[0]
        height = bb[3] - bb[1]

    # 측면도: 도면X=실물Y, 도면Y=실물Z
    if "side" in silhouettes:
        bb = silhouette_bbox(silhouettes["side"])
        if depth is None:
            depth = bb[2] - bb[0]
        if height is None:
            height = bb[3] - bb[1]

    # 일관성 검증: width가 plan과 front 모두 있으면 더 큰 값 사용 (노이즈 대비)
    # (실제로는 같아야 함. 다르면 둘 중 더 큰 것이 정확할 가능성)
    # 그러나 단순함을 위해 우선 적용된 값 유지

    return {
        "width": width if width and width > 0 else 10.0,
        "depth": depth if depth and depth > 0 else 10.0,
        "height": height if height and height > 0 else 5.0,
    }


# ---------------------------------------------------------------------------
# 메쉬 빌더 (저수준)
# ---------------------------------------------------------------------------

def _build_extrude_mesh(profile: list, base_z: float, height: float,
                         color: str = "#BDC3C7", opacity: float = 1.0
                         ) -> tuple | None:
    """폴리곤(2D)을 Z방향으로 extrude.

    profile: [(x, y), ...]  로컬 좌표
    """
    if not profile or len(profile) < 3:
        return None

    arr = np.array(profile)
    # 끝점 중복 제거
    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])
    for i in range(1, n - 1):
        faces.append([3, 0, i + 1, i])
        faces.append([3, n, n + i, n + i + 1])

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


def _build_extrude_along_y(profile_xz: list, depth: float, base_y: float = 0,
                            color: str = "#BDC3C7", opacity: float = 1.0
                            ) -> tuple | None:
    """X-Z 프로파일을 Y방향으로 depth만큼 extrude.

    profile_xz: [(x, z), ...]  로컬 좌표
    """
    if not profile_xz or len(profile_xz) < 3:
        return None
    arr = np.array(profile_xz)
    if np.allclose(arr[0], arr[-1]):
        arr = arr[:-1]
    n = len(arr)
    if n < 3:
        return None

    front = np.zeros((n, 3))
    back = np.zeros((n, 3))
    for i, (x, z) in enumerate(arr):
        front[i] = [x, base_y, z]
        back[i] = [x, base_y + depth, z]

    all_pts = np.vstack([front, back])
    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:
        return (pv.PolyData(all_pts, np.concatenate(faces)), color, opacity)
    except Exception:
        return None


def _build_extrude_along_x(profile_yz: list, width: float, base_x: float = 0,
                            color: str = "#BDC3C7", opacity: float = 1.0
                            ) -> tuple | None:
    """Y-Z 프로파일을 X방향으로 width만큼 extrude."""
    if not profile_yz or len(profile_yz) < 3:
        return None
    arr = np.array(profile_yz)
    if np.allclose(arr[0], arr[-1]):
        arr = arr[:-1]
    n = len(arr)
    if n < 3:
        return None

    left = np.zeros((n, 3))
    right = np.zeros((n, 3))
    for i, (y, z) in enumerate(arr):
        left[i] = [base_x, y, z]
        right[i] = [base_x + width, y, z]

    all_pts = np.vstack([left, right])
    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:
        return (pv.PolyData(all_pts, np.concatenate(faces)), color, opacity)
    except Exception:
        return None


def _build_wall_swept(obb: dict, section_2d: list,
                       fallback_height: float,
                       color: str = "#7F8C8D",
                       opacity: float = 1.0) -> tuple | None:
    """OBB 길이방향을 따라 단면(section)을 sweep.

    section_2d: [(perp_offset, z_height), ...] 단면 프로파일 점들
                폭 방향 = OBB short axis, 높이 방향 = Z
    """
    if not section_2d or len(section_2d) < 3:
        # 단면 없음 → 단순 OBB extrude
        return None

    # 단면 프로파일 정규화: x = 폭 (중심 0), y = 높이 (바닥 0)
    sec_arr = np.array(section_2d, dtype=np.float64)
    if np.allclose(sec_arr[0], sec_arr[-1]):
        sec_arr = sec_arr[:-1]
    sec_cx = (sec_arr[:, 0].min() + sec_arr[:, 0].max()) / 2
    sec_y_min = sec_arr[:, 1].min()
    sec_normalized = [(p[0] - sec_cx, p[1] - sec_y_min) for p in sec_arr]

    # OBB의 길이방향 양 끝점 (월드 좌표)
    cx, cy = obb["center"]
    ax, ay = obb["axis_long"]
    nx, ny = obb["axis_short"]  # 폭 방향 (단면 폭 방향)
    half_L = obb["length"] / 2

    # 경로: 길이방향 양 끝
    path_pts = [
        (cx - ax * half_L, cy - ay * half_L),
        (cx + ax * half_L, cy + ay * half_L),
    ]

    # 각 경로 점에서 단면을 배치
    n_sec = len(sec_normalized)
    n_path = len(path_pts)

    all_pts = []
    for i, (px, py) in enumerate(path_pts):
        for u, z in sec_normalized:
            # 폭 방향 = OBB short axis
            wx = px + nx * u
            wy = py + ny * u
            all_pts.append([wx, wy, z])

    pts_3d = np.array(all_pts)

    # 면 생성
    faces = []
    for i in range(n_path - 1):
        for j in range(n_sec):
            a = i * n_sec + j
            b = i * n_sec + (j + 1) % n_sec
            c = (i + 1) * n_sec + (j + 1) % n_sec
            d = (i + 1) * n_sec + j
            faces.append([3, a, b, c])
            faces.append([3, a, c, d])
    # 양끝 단면 닫기 (fan)
    for j in range(1, n_sec - 1):
        faces.append([3, 0, j, j + 1])
        last_base = (n_path - 1) * n_sec
        faces.append([3, last_base, last_base + j + 1, last_base + j])

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


def _make_ground(width: float, depth: float, z: float = 0.0) -> tuple:
    """구조물 주변 지반."""
    margin = max(width, depth) * 0.3
    half_w = width / 2 + margin
    half_d = depth / 2 + margin
    pts = np.array([
        [-half_w, -half_d, z], [half_w, -half_d, z],
        [half_w, half_d, z], [-half_w, half_d, z],
    ])
    return (pv.PolyData(pts, np.array([4, 0, 1, 2, 3])), "#8B7D6B", 1.0)


# ---------------------------------------------------------------------------
# 실루엣 정규화 (좌표를 중심 원점으로)
# ---------------------------------------------------------------------------

def _center_silhouette(silhouette: list, axis: str = "xy") -> list:
    """실루엣의 좌표 원점을 정규화.

    axis="xy": 둘 다 중심으로 이동 (평면도용)
    axis="x_only": X만 중심, Y는 최소값을 0으로 (정면/측면 - Z가 바닥부터 시작)
    """
    if not silhouette:
        return silhouette
    arr = np.array(silhouette)
    cx = (arr[:, 0].min() + arr[:, 0].max()) / 2
    if axis == "xy":
        cy = (arr[:, 1].min() + arr[:, 1].max()) / 2
        return [(p[0] - cx, p[1] - cy) for p in silhouette]
    else:  # x_only: Y는 0부터
        y_min = arr[:, 1].min()
        return [(p[0] - cx, p[1] - y_min) for p in silhouette]


# ---------------------------------------------------------------------------
# 메인 통합 재구성 함수
# ---------------------------------------------------------------------------

def reconstruct_from_views(views: list[ViewRegion],
                            template_id: str | None = None
                            ) -> list[tuple]:
    """검출된 뷰들로부터 단일 통합 3D 구조물 메쉬 생성.

    핵심: 입력 뷰가 N개여도 출력은 하나의 구조물 (+ 지반).

    Args:
        views: detect_view_regions() 결과
        template_id: "spillway_gate", "building", "retaining_wall" 등

    Returns:
        [(mesh, color, opacity), (ground, color, opacity)] — 보통 2개
        실패 시 빈 리스트
    """
    if not views:
        return []

    # === 1) 메인 뷰 선택 (각 타입별 1개씩) ===
    plan_view = pick_main_view(views, "plan", template_id)
    front_view = pick_main_view(views, "front", template_id)
    side_view = pick_main_view(views, "side", template_id)
    section_view = pick_main_view(views, "section", template_id)
    # cross_section / longitudinal도 section 카테고리로 폴백
    if section_view is None:
        section_view = (pick_main_view(views, "cross_section", template_id) or
                        pick_main_view(views, "longitudinal", template_id))
    # 정면도 없으면 elevation_generic 또는 입면도 시도
    if front_view is None:
        front_view = pick_main_view(views, "elevation_generic", template_id)

    # === 2) 각 뷰에서 메인 실루엣 추출 ===
    silhouettes = {}
    if plan_view:
        sil = extract_main_silhouette(plan_view)
        if sil:
            silhouettes["plan"] = sil
    if front_view:
        sil = extract_main_silhouette(front_view)
        if sil:
            silhouettes["front"] = sil
    if side_view:
        sil = extract_main_silhouette(side_view)
        if sil:
            silhouettes["side"] = sil
    if section_view:
        sil = extract_main_silhouette(section_view)
        if sil:
            silhouettes["section"] = sil

    if not silhouettes:
        return []

    # === 3) 3D 차원 계산 ===
    dims = compute_3d_dimensions(silhouettes)

    # === 4) 단일 메쉬 빌드 ===
    color = TEMPLATE_COLORS.get(template_id, "#B8B5A8")

    section_sil = silhouettes.get("section")
    main_mesh = _build_unified_structure(silhouettes, dims, color, template_id,
                                          plan_view=plan_view,
                                          section_silhouette=section_sil)
    if main_mesh is None:
        return []

    # === 5) 지반 추가 ===
    meshes = [main_mesh]
    # OBB이면 ground도 OBB 길이 기준
    obb = is_elongated_structure(plan_view) if plan_view else None
    if obb:
        ground_w = obb["length"] * 1.2
        ground_d = max(obb["width"] * 5, dims["height"] * 1.5)
    else:
        ground_w = dims["width"]
        ground_d = dims["depth"]
    meshes.append(_make_ground(ground_w, ground_d, z=-0.05))

    return meshes


def _build_unified_structure(silhouettes: dict, dims: dict,
                              color: str, template_id: str | None,
                              plan_view: ViewRegion | None = None,
                              section_silhouette: list | None = None,
                              ) -> tuple | None:
    """가용 실루엣들로부터 단일 3D 구조물 메쉬 생성.

    우선순위:
    1. 평면도 있음 + elongated (옹벽) → sweep 또는 OBB extrude
    2. 평면도 있음 → 평면 외곽 Z 방향 extrude
    3. 정면도만 → Y방향 extrude
    4. 측면도만 → X방향 extrude
    """
    # === 케이스 1a: 옹벽 등 elongated 구조 (PCA로 감지) ===
    # retaining_wall 템플릿이면 임계 낮춰 더 적극적으로 OBB 적용
    obb = None
    if plan_view is not None:
        if template_id == "retaining_wall":
            obb = is_elongated_structure(plan_view, min_aspect=1.2)
        else:
            obb = is_elongated_structure(plan_view, min_aspect=2.5)

    if obb is not None:
        # 길이방향 정확한 사각형 OBB로 평면 외곽 사용
        # 단면도 있으면 sweep, 없으면 OBB extrude
        if section_silhouette and template_id == "retaining_wall":
            return _build_wall_swept(obb, section_silhouette,
                                       dims["height"], color)
        else:
            # OBB 사각형을 평면 외곽으로 사용 → Z방향 extrude
            obb_corners = obb["corners"]
            # 중심을 원점으로
            arr = np.array(obb_corners)
            cx = arr[:, 0].mean()
            cy = arr[:, 1].mean()
            centered_obb = [(p[0] - cx, p[1] - cy) for p in obb_corners]
            return _build_extrude_mesh(centered_obb, base_z=0,
                                        height=dims["height"], color=color)

    # === 케이스 1b: 평면도 일반 (closed polygon) ===
    if "plan" in silhouettes:
        plan_pts = silhouettes["plan"]
        plan_centered = _center_silhouette(plan_pts, axis="xy")
        return _build_extrude_mesh(plan_centered, base_z=0,
                                    height=dims["height"], color=color)

    # === 케이스 2: 정면도만 (평면도 없음) ===
    if "front" in silhouettes:
        front_pts = silhouettes["front"]
        front_centered = _center_silhouette(front_pts, axis="x_only")

        # Y 방향(깊이)는 측면도가 있으면 그 폭, 없으면 폭의 절반 정도
        depth_for_extrude = dims["depth"]
        # 측면도가 없으면 폭의 1/3 정도로 추정 (얇은 두께)
        if "side" not in silhouettes:
            depth_for_extrude = max(dims["width"] * 0.3, 2.0)

        # 정면도 중심을 Y=0으로, depth/2 만큼 양쪽으로
        return _build_extrude_along_y(front_centered,
                                       depth=depth_for_extrude,
                                       base_y=-depth_for_extrude / 2,
                                       color=color)

    # === 케이스 3: 측면도만 ===
    if "side" in silhouettes:
        side_pts = silhouettes["side"]
        side_centered = _center_silhouette(side_pts, axis="x_only")

        width_for_extrude = dims["width"]
        if "front" not in silhouettes:
            width_for_extrude = max(dims["depth"] * 0.3, 2.0)

        return _build_extrude_along_x(side_centered,
                                       width=width_for_extrude,
                                       base_x=-width_for_extrude / 2,
                                       color=color)

    # === 케이스 4: 단면도만 ===
    if "section" in silhouettes:
        sec_pts = silhouettes["section"]
        sec_centered = _center_silhouette(sec_pts, axis="x_only")
        # 단면을 X방향으로 width만큼 extrude
        return _build_extrude_along_x(sec_centered,
                                       width=dims["width"],
                                       base_x=-dims["width"] / 2,
                                       color=color)

    return None


# ---------------------------------------------------------------------------
# 진단 정보 (UI에 표시용)
# ---------------------------------------------------------------------------

def diagnose_views(views: list[ViewRegion],
                    template_id: str | None = None) -> dict:
    """뷰 검출/재구성 진단 정보 반환.

    Returns:
        {
          "main_views": {"plan": ..., "front": ..., "side": ...},
          "silhouettes_extracted": {"plan": True, ...},
          "dimensions": {"width": ..., "depth": ..., "height": ...},
          "build_strategy": "plan_extrude" | "front_extrude" | ...,
        }
    """
    info = {
        "main_views": {},
        "silhouettes_extracted": {},
        "dimensions": {"width": 0, "depth": 0, "height": 0},
        "build_strategy": "none",
        "total_views": len(views),
    }

    plan_view = pick_main_view(views, "plan", template_id)
    front_view = pick_main_view(views, "front", template_id)
    side_view = pick_main_view(views, "side", template_id)
    section_view = pick_main_view(views, "section", template_id)

    if plan_view:
        info["main_views"]["plan"] = plan_view.label_text[:40]
    if front_view:
        info["main_views"]["front"] = front_view.label_text[:40]
    if side_view:
        info["main_views"]["side"] = side_view.label_text[:40]
    if section_view:
        info["main_views"]["section"] = section_view.label_text[:40]

    silhouettes = {}
    for name, v in [("plan", plan_view), ("front", front_view),
                     ("side", side_view), ("section", section_view)]:
        if v:
            sil = extract_main_silhouette(v)
            info["silhouettes_extracted"][name] = sil is not None
            if sil:
                silhouettes[name] = sil

    if silhouettes:
        info["dimensions"] = compute_3d_dimensions(silhouettes)

    # OBB 정보 (옹벽 등 elongated)
    obb = None
    if plan_view is not None:
        if template_id == "retaining_wall":
            obb = is_elongated_structure(plan_view, min_aspect=1.2)
        else:
            obb = is_elongated_structure(plan_view, min_aspect=2.5)
    info["is_elongated"] = obb is not None
    if obb:
        info["obb_length"] = obb["length"]
        info["obb_width"] = obb["width"]
        info["obb_aspect"] = obb["aspect_ratio"]
        # OBB가 있으면 차원 갱신
        info["dimensions"]["width"] = obb["length"]
        info["dimensions"]["depth"] = obb["width"]

    # 빌드 전략 결정
    if obb is not None:
        if "section" in silhouettes and template_id == "retaining_wall":
            info["build_strategy"] = f"옹벽 sweep (길이 {obb['length']:.1f}m × 단면)"
        else:
            info["build_strategy"] = f"OBB extrude (길이 {obb['length']:.1f}m × 폭 {obb['width']:.1f}m × 높이)"
    elif "plan" in silhouettes:
        info["build_strategy"] = "평면도 → Z방향 extrude (높이는 정면/측면에서)"
    elif "front" in silhouettes:
        info["build_strategy"] = "정면도 → Y방향 extrude"
    elif "side" in silhouettes:
        info["build_strategy"] = "측면도 → X방향 extrude"
    elif "section" in silhouettes:
        info["build_strategy"] = "단면도 → X방향 extrude"
    else:
        info["build_strategy"] = "실루엣 추출 실패"

    return info


# ---------------------------------------------------------------------------
# 테스트
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    from pathlib import Path
    from view_detector import detect_view_regions

    samples = [
        ("SAMPLE_CAD/1. 좌안옹벽 일반도 작성(2026.0109).dxf", "retaining_wall"),
        ("SAMPLE_CAD/12995740-M40-001 여수로 수문 설치도(1／2).dxf", "spillway_gate"),
        ("SAMPLE_CAD/12996710-M40-001 신설 취수탑 설비 설치도(1／2).dxf", "building"),
        ("SAMPLE_CAD/12996710-M43-002 신설 제수변실 설비 배치도.dxf", "building"),
    ]

    for path, tid in samples:
        print(f"\n=== {Path(path).name} ({tid}) ===")
        try:
            views = detect_view_regions(path)
            info = diagnose_views(views, tid)
            print(f"  뷰 {info['total_views']}개 검출")
            print("  메인 뷰:")
            for vt, label in info["main_views"].items():
                ok = "✓" if info["silhouettes_extracted"].get(vt) else "✗"
                print(f"    [{vt}] {ok} \"{label}\"")
            d = info["dimensions"]
            print(f"  3D 크기: W{d['width']:.1f}m × D{d['depth']:.1f}m × H{d['height']:.1f}m")
            print(f"  전략: {info['build_strategy']}")

            meshes = reconstruct_from_views(views, tid)
            print(f"  결과 메쉬: {len(meshes)}개")
        except Exception as e:
            print(f"  오류: {e}")
            import traceback
            traceback.print_exc()
