"""validate_gate_params.py — Gate(여수로 수문) Params JSON 정적 검증기.

Blender 헤드리스 실행 *전*에 JSON 파일 하나만 가지고:
  - 필수 필드 / 타입 / 물리적 일관성 검사
  - 빌더(gate_3d_builder_bpy.py)가 어떤 분기로 갈지 예측
    (pier: Phase B' polygon vs parametric / bridge: user vs extracted vs parametric)
  - 예상 객체 수 계산
  - 잠재 이슈 경고

bpy를 import하지 않으므로 S-CANVAS env / 일반 Python / 어디서든 실행 가능.

검증 로직은 빌더의 `_validate_pier_polys` / `_validate_bridge_bbox` 과 정확히 동일.
즉 본 검증이 통과하면 빌더에서도 같은 분기 선택 보장.

----------------------------------------------------------------------
사용법
----------------------------------------------------------------------
  python validate_gate_params.py gate_params.json

Exit code:
  0  PASS  — 빌드 진행 가능, 모든 분기가 의도대로 작동
  1  WARN  — 빌드는 되나 일부 폴백 적용 (parametric 경로 등)
  2  FAIL  — 빌드 시 빈 결과/예외 가능성 높음 (필수 필드 누락 등)
"""

from __future__ import annotations

import json
import math
import sys
from pathlib import Path
from typing import Any


# ===========================================================================
# 검증 로직 — 빌더와 정확히 동일한 sanity check
# ===========================================================================

def validate_pier_polys(pier_polys: list,
                        pier_width: float,
                        pier_length: float,
                        tol_ratio: float = 0.5) -> tuple[bool, list[str]]:
    """gate_3d_builder_bpy.GateBuilderBpy._validate_pier_polys 와 동일.

    Returns: (passed, [개별 사유 메시지])
    """
    reasons = []
    w_lo = pier_width * (1 - tol_ratio)
    w_hi = pier_width * (1 + tol_ratio)
    l_lo = pier_length * 0.4
    l_hi = pier_length * 1.5
    all_ok = True
    for i, poly in enumerate(pier_polys):
        xs = [p[0] for p in poly]
        ys = [p[1] for p in poly]
        if len(xs) < 3:
            reasons.append(f"pier#{i}: vertex < 3 ({len(xs)})")
            all_ok = False
            continue
        w = max(xs) - min(xs)
        l = max(ys) - min(ys)
        if not (w_lo <= w <= w_hi):
            reasons.append(f"pier#{i}: width {w:.2f}m 범위 [{w_lo:.2f},{w_hi:.2f}] 벗어남")
            all_ok = False
        if not (l_lo <= l <= l_hi):
            reasons.append(f"pier#{i}: length {l:.2f}m 범위 [{l_lo:.2f},{l_hi:.2f}] 벗어남")
            all_ok = False
    return all_ok, reasons


def validate_bridge_bbox(bbox: tuple | None,
                          total_span: float,
                          pier_length: float) -> tuple[bool, str]:
    """gate_3d_builder_bpy.GateBuilderBpy._validate_bridge_bbox 와 동일.

    Returns: (passed, 사유 메시지)
    """
    if bbox is None:
        return False, "bbox None"
    if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
        return False, f"bbox 형식 오류 (len={len(bbox) if hasattr(bbox, '__len__') else '?'})"
    x0, y0, x1, y1 = bbox
    w = x1 - x0
    h = y1 - y0
    if w < 1.0 or h < 0.5:
        return False, f"너무 작음 (W={w:.2f}m, H={h:.2f}m)"
    if not (total_span * 0.2 <= w <= total_span * 1.5):
        return False, (
            f"width {w:.2f}m 가 total_span 범위 "
            f"[{total_span * 0.2:.2f}, {total_span * 1.5:.2f}] 벗어남"
        )
    if not (pier_length * 0.05 <= h <= pier_length * 1.2):
        return False, (
            f"depth {h:.2f}m 가 pier_length 범위 "
            f"[{pier_length * 0.05:.2f}, {pier_length * 1.2:.2f}] 벗어남"
        )
    return True, "OK"


# ===========================================================================
# 색상 출력 (Windows cmd / PowerShell / Linux 모두 지원)
# ===========================================================================

class C:
    """ANSI 컬러 — Windows 10+ cmd는 자동 활성화. 미지원 환경은 빈 문자열."""
    RESET  = "\033[0m"
    BOLD   = "\033[1m"
    GRAY   = "\033[90m"
    RED    = "\033[91m"
    GREEN  = "\033[92m"
    YELLOW = "\033[93m"
    BLUE   = "\033[94m"
    CYAN   = "\033[96m"


def _supports_color() -> bool:
    if sys.platform == "win32":
        # Windows 10 1607+ 에서 ANSI 지원. 환경변수로 비활성화 가능.
        if "NO_COLOR" in __import__("os").environ:
            return False
        try:
            import ctypes
            kernel32 = ctypes.windll.kernel32
            kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
            return True
        except Exception:
            return False
    return sys.stdout.isatty()


if not _supports_color():
    for _attr in ("RESET", "BOLD", "GRAY", "RED", "GREEN", "YELLOW", "BLUE", "CYAN"):
        setattr(C, _attr, "")


def _ok(msg: str)   -> str: return f"{C.GREEN}✓{C.RESET} {msg}"
def _warn(msg: str) -> str: return f"{C.YELLOW}⚠{C.RESET} {msg}"
def _fail(msg: str) -> str: return f"{C.RED}✗{C.RESET} {msg}"
def _info(msg: str) -> str: return f"{C.CYAN}·{C.RESET} {msg}"
def _h(title: str) -> str:
    return f"\n{C.BOLD}{C.BLUE}── {title} {'─' * max(2, 60 - len(title))}{C.RESET}"


# ===========================================================================
# 검증 컨텍스트
# ===========================================================================

class ReportLevel:
    PASS = 0
    WARN = 1
    FAIL = 2


class Report:
    def __init__(self):
        self.level = ReportLevel.PASS
        self.lines: list[str] = []
        self.fail_count = 0
        self.warn_count = 0

    def ok(self, msg: str):
        self.lines.append(_ok(msg))

    def info(self, msg: str):
        self.lines.append(_info(msg))

    def warn(self, msg: str):
        self.lines.append(_warn(msg))
        self.warn_count += 1
        self.level = max(self.level, ReportLevel.WARN)

    def fail(self, msg: str):
        self.lines.append(_fail(msg))
        self.fail_count += 1
        self.level = ReportLevel.FAIL

    def header(self, title: str):
        self.lines.append(_h(title))

    def blank(self):
        self.lines.append("")

    def raw(self, text: str):
        self.lines.append(text)

    def emit(self):
        for line in self.lines:
            print(line)


# ===========================================================================
# 검증 단계들
# ===========================================================================

def _is_number(x: Any) -> bool:
    return isinstance(x, (int, float)) and not isinstance(x, bool)


def check_required_fields(d: dict, R: Report) -> bool:
    """필수 필드 존재 + 타입 검증. False 반환 시 후속 검사 skip."""
    R.header("1. 필수 필드 / 타입")

    required_numeric = [
        "n_gates", "gate_width", "gate_height",
        "pier_width", "pier_length",
        "el_gate_sill", "el_weir_crest", "el_gate_top",
        "el_trunnion_pin", "el_mwl", "el_nhwl", "el_lwl",
        "el_downstream", "el_upstream_bed", "el_bridge_top",
        "total_span", "total_length",
    ]
    required_lists = ["ogee_profile", "gate_centers_x"]

    fatal = False
    for key in required_numeric:
        if key not in d:
            R.fail(f"필드 누락: {key}")
            fatal = True
        elif not _is_number(d[key]):
            R.fail(f"필드 {key}={d[key]!r} (number 아님)")
            fatal = True
    for key in required_lists:
        if key not in d:
            R.fail(f"필드 누락: {key}")
            fatal = True
        elif not isinstance(d[key], list):
            R.fail(f"필드 {key}={d[key]!r} (list 아님)")
            fatal = True

    if not fatal:
        R.ok(f"필수 numeric 필드 {len(required_numeric)}개 존재")
        R.ok(f"필수 list 필드 {len(required_lists)}개 존재")
    return not fatal


def check_physical_consistency(d: dict, R: Report) -> None:
    """표고 순서 / 치수 양수 / 게이트 개수 일관성."""
    R.header("2. 물리적 일관성")

    # 표고 순서
    bed = d["el_upstream_bed"]
    sill = d["el_gate_sill"]
    crest = d["el_weir_crest"]
    top = d["el_gate_top"]
    bridge = d["el_bridge_top"]

    el_chain = [
        ("el_upstream_bed", bed),
        ("el_gate_sill", sill),
        ("el_weir_crest", crest),
        ("el_gate_top", top),
        ("el_bridge_top", bridge),
    ]
    from itertools import pairwise
    chain_ok = True
    for (n_a, v_a), (n_b, v_b) in pairwise(el_chain):
        if v_a > v_b + 1e-6:
            R.fail(f"표고 역전: {n_a}={v_a:.3f} > {n_b}={v_b:.3f}")
            chain_ok = False
    if chain_ok:
        R.ok(f"표고 단조 증가: {bed:.2f} ≤ {sill:.2f} ≤ {crest:.2f} ≤ {top:.2f} ≤ {bridge:.2f}")

    # 수위 표고
    lwl = d["el_lwl"]; nhwl = d["el_nhwl"]; mwl = d["el_mwl"]
    wl_ok = True
    if not (lwl <= nhwl + 1e-6):
        R.fail(f"수위 역전: LWL={lwl:.2f} > NHWL={nhwl:.2f}")
        wl_ok = False
    if not (nhwl <= mwl + 1e-6):
        R.fail(f"수위 역전: NHWL={nhwl:.2f} > MWL={mwl:.2f}")
        wl_ok = False
    if wl_ok:
        R.ok(f"수위 단조: LWL {lwl:.2f} ≤ NHWL {nhwl:.2f} ≤ MWL {mwl:.2f}")

    # NHWL이 게이트 sill 위쪽인지 (수면이 수문 아래면 본체가 물에 잠기지 않는 경우)
    if nhwl <= sill:
        R.warn(f"NHWL={nhwl:.2f}이 sill={sill:.2f}보다 낮음 — 상류 수면이 수문 아래로 그려짐")

    # 양의 치수
    n_gates = d["n_gates"]
    gate_w = d["gate_width"]
    gate_h_param = d["gate_height"]
    pier_w = d["pier_width"]
    pier_l = d["pier_length"]
    total_span = d["total_span"]

    if n_gates < 1:
        R.fail(f"n_gates={n_gates} (1 이상이어야 함)")
    elif n_gates > 20:
        R.warn(f"n_gates={n_gates} (>20, 비정상적으로 많음)")
    else:
        R.ok(f"n_gates={n_gates}")

    for name, val in [("gate_width", gate_w), ("gate_height", gate_h_param),
                      ("pier_width", pier_w), ("pier_length", pier_l),
                      ("total_span", total_span)]:
        if val <= 0:
            R.fail(f"{name}={val} ≤ 0")

    # gate_height 와 sill→top 일관성
    gate_h_calc = top - sill
    if abs(gate_h_param - gate_h_calc) > 0.5:
        R.warn(
            f"gate_height={gate_h_param:.2f} 가 (top - sill)={gate_h_calc:.2f} 와 0.5m 이상 차이"
        )
    else:
        R.ok(f"gate_height={gate_h_param:.2f}m ≈ (top - sill)={gate_h_calc:.2f}m")

    # gate_centers_x 와 n_gates 일관성
    gate_centers = d["gate_centers_x"]
    if len(gate_centers) != n_gates:
        R.warn(
            f"len(gate_centers_x)={len(gate_centers)} ≠ n_gates={n_gates} "
            f"— builder 내부에서 보정될 수 있음"
        )
    else:
        R.ok(f"gate_centers_x: {n_gates}개 일치")

    # gate_centers_x 단조 증가 (sorted)
    if len(gate_centers) >= 2:
        if not all(gate_centers[i] <= gate_centers[i + 1] + 1e-6
                   for i in range(len(gate_centers) - 1)):
            R.warn("gate_centers_x 가 정렬되지 않음 — pier_x_centers 계산이 비정상이 될 수 있음")
        else:
            spacings = [gate_centers[i+1] - gate_centers[i]
                        for i in range(len(gate_centers) - 1)]
            avg = sum(spacings) / len(spacings)
            min_s = min(spacings); max_s = max(spacings)
            R.ok(
                f"gate_centers_x 정렬 OK — 평균 간격 {avg:.2f}m "
                f"(범위 {min_s:.2f}~{max_s:.2f})"
            )

    # pier_count
    pier_count = d.get("pier_count", n_gates + 1)
    if pier_count != n_gates + 1:
        R.warn(f"pier_count={pier_count} ≠ n_gates+1={n_gates + 1}")
    else:
        R.ok(f"pier_count = n_gates+1 = {pier_count}")


def check_ogee_profile(d: dict, R: Report) -> bool:
    """ogee_profile 점 개수 / 단조 증가 / 표고 범위.

    Returns: True if 본체 빌드가 가능 (점 ≥ 3)
    """
    R.header("3. Ogee 프로파일 (여수로 본체)")

    profile = d.get("ogee_profile", [])
    n = len(profile)

    if n < 3:
        R.fail(f"ogee_profile 점 {n}개 (< 3) — 빌더가 본체를 그리지 못함")
        return False

    # 각 점이 (x, z) 쌍인지
    bad = 0
    for i, pt in enumerate(profile):
        if not (isinstance(pt, (list, tuple)) and len(pt) == 2
                and _is_number(pt[0]) and _is_number(pt[1])):
            bad += 1
    if bad:
        R.fail(f"ogee_profile 에서 {bad}개 점이 (x, z) 형식 아님")
        return False

    R.ok(f"ogee_profile: {n}개 점, 모두 (x, z) 형식 OK")

    xs = [pt[0] for pt in profile]
    zs = [pt[1] for pt in profile]

    # x 단조 증가 (엄격하지 않음 — ogee의 상류 수직 부분은 x 동일점 허용)
    decreasing = sum(1 for i in range(n - 1) if xs[i + 1] < xs[i] - 1e-6)
    if decreasing > 0:
        R.warn(
            f"ogee_profile.x 가 {decreasing}회 감소 — "
            f"prism 단면이 자기교차할 수 있음"
        )
    else:
        R.ok(f"ogee_profile.x 단조 증가 (range {min(xs):.2f}~{max(xs):.2f}m)")

    # z 범위 표고와 일치
    z_min = min(zs); z_max = max(zs)
    sill = d["el_gate_sill"]; crest = d["el_weir_crest"]
    bed = d["el_upstream_bed"]
    if z_min > sill - 0.1:
        R.warn(
            f"ogee z_min={z_min:.2f} 가 sill={sill:.2f}보다 높음 — "
            f"본체 바닥이 게이트 sill 위로 올라감"
        )
    if z_max < crest - 0.5:
        R.warn(
            f"ogee z_max={z_max:.2f} 가 crest={crest:.2f}보다 낮음 — "
            f"본체가 weir crest에 미치지 못함"
        )
    if z_min < bed - 5.0:
        R.warn(
            f"ogee z_min={z_min:.2f}가 upstream_bed={bed:.2f}보다 5m 이상 깊음"
        )

    R.info(f"ogee z-range: {z_min:.2f} ~ {z_max:.2f}m (span {z_max - z_min:.2f}m)")
    return True


def predict_pier_branch(d: dict, R: Report) -> tuple[str, int]:
    """빌더의 pier 빌드 분기 예측.

    Returns: (branch_name, expected_pier_object_count)
        branch_name ∈ {"phase_b_polygon", "parametric"}
    """
    R.header("4. Pier 빌드 분기 예측")

    pier_polys = d.get("pier_plan_polygons", [])
    n_gates = d["n_gates"]
    expected_n_piers = n_gates + 1
    pier_w = d["pier_width"]
    pier_l = d["pier_length"]

    R.info(f"기대 pier 개수: n_gates+1 = {expected_n_piers}")
    R.info(f"sanity 기준: pier_width={pier_w:.2f}m × {0.5}~{1.5}, "
           f"pier_length={pier_l:.2f}m × {0.4}~{1.5}")

    # Phase B' 경로 후보
    if not pier_polys:
        R.warn("pier_plan_polygons 비어있음 → parametric 폴백")
        # parametric: body + nose × n_piers
        return "parametric", expected_n_piers * 2

    if len(pier_polys) != expected_n_piers:
        R.warn(
            f"pier_plan_polygons 개수={len(pier_polys)} ≠ {expected_n_piers} "
            f"→ parametric 폴백"
        )
        return "parametric", expected_n_piers * 2

    # 각 폴리곤 sanity
    passed, reasons = validate_pier_polys(pier_polys, pier_w, pier_l)
    if not passed:
        R.warn("pier 폴리곤 sanity 실패 → parametric 폴백:")
        for r in reasons:
            R.raw(f"   - {r}")
        return "parametric", expected_n_piers * 2

    R.ok(f"Phase B' 경로 통과 — 폴리곤 {len(pier_polys)}개 sanity OK")
    return "phase_b_polygon", expected_n_piers


def predict_bridge_branch(d: dict, R: Report) -> tuple[str, int]:
    """빌더의 bridge 빌드 분기 예측.

    Returns: (branch_name, expected_object_count)
        branch_name ∈ {"none", "user", "extracted", "parametric"}
        (deck 1 + rail 2 = 3 또는 0)
    """
    R.header("5. Bridge 빌드 분기 예측")

    if not d.get("has_service_bridge", False):
        R.info("has_service_bridge=False → 공도교 빌드 안 함")
        return "none", 0

    total_span = d["total_span"]
    pier_l = d["pier_length"]

    # 1순위: 사용자 명시
    ux0 = d.get("bridge_x_start")
    ux1 = d.get("bridge_x_end")
    uy0 = d.get("bridge_y_start")
    uy1 = d.get("bridge_y_end")
    user_complete = (
        ux0 is not None and ux1 is not None and ux1 > ux0
        and uy0 is not None and uy1 is not None and uy1 > uy0
    )
    if user_complete:
        cand = (float(ux0), float(uy0), float(ux1), float(uy1))
        ok, reason = validate_bridge_bbox(cand, total_span, pier_l)
        if ok:
            R.ok(
                f"User override 통과: bbox=({cand[0]:.2f},{cand[1]:.2f}) "
                f"→ ({cand[2]:.2f},{cand[3]:.2f})"
            )
            return "user", 3
        else:
            R.warn(f"User override 실패 ({reason}) → 다음 단계 검사")

    # 2순위: 파서 추출
    bbox = d.get("bridge_plan_bbox")
    if bbox is not None:
        if isinstance(bbox, list):
            bbox = tuple(bbox)
        ok, reason = validate_bridge_bbox(bbox, total_span, pier_l)
        if ok:
            R.ok(
                f"파서 추출 bbox 통과: ({bbox[0]:.2f},{bbox[1]:.2f}) "
                f"→ ({bbox[2]:.2f},{bbox[3]:.2f})"
            )
            return "extracted", 3
        else:
            R.warn(f"파서 추출 bbox 실패 ({reason}) → parametric 폴백")
    else:
        R.info("bridge_plan_bbox=None → parametric 폴백")

    # 3순위: parametric
    pier_w = d["pier_width"]
    px0 = -pier_w * 0.5
    px1 = total_span + pier_w * 0.5
    py0 = pier_l * 0.3
    py1 = pier_l * 0.55
    R.info(
        f"Parametric bbox 사용: ({px0:.2f},{py0:.2f}) → ({px1:.2f},{py1:.2f}) "
        f"= W{px1-px0:.1f}m × H{py1-py0:.1f}m"
    )
    return "parametric", 3


def predict_object_count(d: dict, R: Report,
                         pier_branch: str, pier_objs: int,
                         bridge_branch: str, bridge_objs: int) -> int:
    """전체 예상 객체 수 합산."""
    R.header("6. 예상 객체 수")

    n_gates = d["n_gates"]
    n_piers = n_gates + 1
    has_hoist = d.get("has_hoist_housings", True)
    has_water = d.get("has_water_surface", True)
    has_apron = d.get("has_downstream_apron", True)

    # ogee_profile이 있어야 본체 빌드됨
    ogee_ok = len(d.get("ogee_profile", [])) >= 3
    body = 1 if ogee_ok else 0

    # gates: skin + arms × 2 = 3 per gate
    gates = n_gates * 3 if d.get("gate_centers_x") else 0

    # hoists: body + roof = 2 per pier
    hoists = n_piers * 2 if has_hoist else 0

    water = 1 if has_water else 0
    apron = 1 if has_apron else 0

    rows = [
        ("SpillwayBody",       body,         "(ogee_profile ≥ 3pts)" if ogee_ok else "(ogee 부족)"),
        ("Piers",              pier_objs,    f"({pier_branch})"),
        ("Gates (skin+arms)",  gates,        f"({n_gates} × 3)"),
        ("ServiceBridge",      bridge_objs,  f"({bridge_branch})"),
        ("Hoists",             hoists,       f"({n_piers} × 2)" if has_hoist else "(disabled)"),
        ("Water",              water,        "" if has_water else "(disabled)"),
        ("Apron",              apron,        "" if has_apron else "(disabled)"),
    ]
    total = sum(n for _, n, _ in rows)

    R.raw("")
    R.raw(f"   {C.BOLD}{'Component':<22}{'Count':>6}   Notes{C.RESET}")
    R.raw(f"   {'─' * 22}{'─' * 6}   {'─' * 30}")
    for name, count, note in rows:
        color = C.GRAY if (count == 0 and "disabled" not in note and "부족" not in note) or count == 0 else C.RESET
        R.raw(f"   {color}{name:<22}{count:>6}   {note}{C.RESET}")
    R.raw(f"   {'─' * 22}{'─' * 6}")
    R.raw(f"   {C.BOLD}{'Total':<22}{total:>6}{C.RESET}")
    R.raw("")
    R.raw(f"   {C.CYAN}→ Blender 콘솔에서 '[bpy-gate] Created {total} objects' "
          f"가 보여야 정상{C.RESET}")

    return total


def check_secondary(d: dict, R: Report) -> None:
    """추가 sanity / 정보성 검사."""
    R.header("7. 추가 검사 (정보·경고)")

    # flow_direction_2d 단위벡터 확인
    fd = d.get("flow_direction_2d")
    if fd is not None:
        if isinstance(fd, list):
            fd = tuple(fd)
        if len(fd) == 2 and _is_number(fd[0]) and _is_number(fd[1]):
            mag = math.sqrt(fd[0] ** 2 + fd[1] ** 2)
            if abs(mag - 1.0) > 0.05:
                R.warn(f"flow_direction_2d magnitude={mag:.4f} (1.0과 0.05 이상 차이)")
            else:
                R.ok(f"flow_direction_2d=({fd[0]:+.3f},{fd[1]:+.3f}), |v|={mag:.4f}")
        else:
            R.warn(f"flow_direction_2d 형식 비정상: {fd!r}")

    # plan_outline_polygon
    outline = d.get("plan_outline_polygon", [])
    if outline:
        n = len(outline)
        if n < 4:
            R.warn(f"plan_outline_polygon 점 {n}개 (< 4) — 평면 외곽이 너무 단순")
        else:
            R.ok(f"plan_outline_polygon: {n}개 점")

    # Trunnion이 mid_el과 너무 다르면 빌더가 mid로 강제
    sill = d["el_gate_sill"]; top = d["el_gate_top"]
    mid_el = (sill + top) / 2
    trun = d["el_trunnion_pin"]
    if abs(trun - mid_el) > 0.5:
        R.info(
            f"trunnion EL.{trun:.2f} ↔ mid EL.{mid_el:.2f} (차이 {abs(trun-mid_el):.2f}m) "
            f"→ 빌더가 trunnion_el = mid_el로 강제 사용"
        )
    else:
        R.ok(f"trunnion EL.{trun:.2f} ≈ mid EL.{mid_el:.2f} (그대로 사용)")

    # source_files 정보
    sf = d.get("source_files", [])
    if sf:
        R.info(f"source_files: {len(sf)}개")
        for f in sf:
            R.raw(f"     - {Path(f).name if isinstance(f, str) else f}")

    # raw_text_annotations 양 (참고용)
    rta = d.get("raw_text_annotations", [])
    R.info(f"raw_text_annotations: {len(rta)}개 (디버그 정보)")


# ===========================================================================
# 진입점
# ===========================================================================

def validate_file(json_path: str) -> int:
    R = Report()
    R.raw("")
    R.raw(f"{C.BOLD}Gate Params 검증{C.RESET} — {C.GRAY}{json_path}{C.RESET}")

    p = Path(json_path)
    if not p.exists():
        R.fail(f"파일 없음: {json_path}")
        R.emit()
        return ReportLevel.FAIL

    try:
        text = p.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        text = p.read_text(encoding="utf-8-sig")

    try:
        d = json.loads(text)
    except json.JSONDecodeError as e:
        R.fail(f"JSON 파싱 실패: {e}")
        R.emit()
        return ReportLevel.FAIL

    if not isinstance(d, dict):
        R.fail(f"최상위가 dict 아님: {type(d).__name__}")
        R.emit()
        return ReportLevel.FAIL

    R.info(f"필드 수: {len(d)}, 파일 크기: {p.stat().st_size:,} bytes")

    # 1. 필수 필드
    if not check_required_fields(d, R):
        R.header("결론")
        R.fail("필수 필드 누락 — 빌더 실행 불가")
        R.emit()
        return ReportLevel.FAIL

    # 2. 물리적 일관성
    check_physical_consistency(d, R)

    # 3. ogee_profile
    check_ogee_profile(d, R)

    # 4. pier 분기 예측
    pier_branch, pier_objs = predict_pier_branch(d, R)

    # 5. bridge 분기 예측
    bridge_branch, bridge_objs = predict_bridge_branch(d, R)

    # 6. 예상 객체 수
    total = predict_object_count(d, R, pier_branch, pier_objs, bridge_branch, bridge_objs)

    # 7. 추가 검사
    check_secondary(d, R)

    # 결론
    R.header("결론")
    if R.level == ReportLevel.PASS:
        R.raw(f"{C.GREEN}{C.BOLD}✓ PASS{C.RESET} — 빌드 진행 가능. "
              f"예상 객체 {total}개")
    elif R.level == ReportLevel.WARN:
        R.raw(f"{C.YELLOW}{C.BOLD}⚠ WARN{C.RESET} — "
              f"빌드는 가능하나 폴백/주의 사항 {R.warn_count}건. "
              f"예상 객체 {total}개")
    else:
        R.raw(f"{C.RED}{C.BOLD}✗ FAIL{C.RESET} — "
              f"FAIL {R.fail_count}건 / WARN {R.warn_count}건. 빌드 전 수정 권장")
    R.raw("")

    R.emit()
    return R.level


def main():
    if len(sys.argv) < 2:
        print("Usage: python validate_gate_params.py <gate_params.json>")
        sys.exit(2)
    json_path = sys.argv[1]
    sys.exit(validate_file(json_path))


if __name__ == "__main__":
    main()
