"""Blender 헤드리스 기반 구조물 단독 렌더링 워커.

scanvas_maker.SCanvasApp 안의 구조물 빌드 다이얼로그(_open_structure_template_dialog)
에서 호출되어, 사용자 파라미터로 빌드된 구조물을 Blender Cycles로 고품질 렌더한다.

워크플로 안에서의 위치:
  [A] DXF + 파라미터 → Blender 빌더 → Cycles 렌더 → 구조물 PNG    ← 이 모듈
            ↓
  [B] 별도 트랙: DEM/위성 → 지형 capture map
            ↓
  [C] 합성: 지형 capture에 구조물 PNG 얹기                        ← 추후 작업
            ↓
  [D] AI 트랙: 합성 control map → Gemini → 최종 조감도

따라서:
  - 출력 파일명은 'structure_render.png' (AI 결과 'rendered_birdseye.png'를
    덮어쓰지 않음)
  - 결과 표시는 app._show_rendered_result()가 아니라 app._show_structure_render()
    로 분리 (없으면 OS 기본 뷰어 폴백, GUI 없는 환경에서는 단순 print)
  - transparent_bg=True 면 RGBA PNG → 추후 [C] 합성에 직접 사용 가능
  - transparent_bg=False 면 Sky 배경 → 단독 발표용 그림으로 사용

공개 API:
    run_blender_render(app, blender_exe, params_json, ...)

----------------------------------------------------------------------
v3 (워크플로 분리):
  - 출력 기본값: rendered_birdseye.png → structure_render.png
  - app._show_rendered_result → app._show_structure_render (없으면 OS 폴백)
  - transparent_bg 인자 추가 → 빌더의 setup_lighting_and_camera/render_to_png
    호출 시 동일하게 전달
  - PIL 후처리 분기: 투명 PNG는 RGBA로 리사이즈 (RGB 변환하면 알파 손실)
v2: tkinter / PIL 을 lazy import (GUI 없는 환경에서도 모듈 로드 가능)
v1: 초기 버전
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
import tempfile
import textwrap
import time as _time
from pathlib import Path
import contextlib

# tkinter / PIL 은 GUI 컨텍스트에서만 필요 → lazy import.
# Harness 의존 (선택적 — 없으면 그냥 렌더만 진행)
try:
    from harness.seed_manager import SeedManager
    from harness.logger import get_db_session
    _HARNESS_OK = True
except Exception:
    SeedManager = None  # type: ignore
    get_db_session = None  # type: ignore
    _HARNESS_OK = False


# ===========================================================================
# Lazy import 헬퍼
# ===========================================================================

def _show_error_dialog(title: str, msg: str) -> None:
    """tkinter messagebox로 에러 다이얼로그. 없으면 stderr 폴백."""
    try:
        from tkinter import messagebox
        messagebox.showerror(title, msg)
    except Exception:
        sys.stderr.write(f"[ERROR] {title}: {msg}\n")


def _open_image(path: str):
    """PIL.Image.open 의 lazy wrapper."""
    from PIL import Image
    return Image.open(path)


def _image_lanczos():
    """PIL.Image.LANCZOS 상수 lazy 접근."""
    from PIL import Image
    return Image.LANCZOS


def _open_in_os_default_viewer(path: str) -> bool:
    """플랫폼별 기본 이미지 뷰어로 PNG 열기. 결과 표시 폴백용.

    Returns: True if launched successfully (best-effort)
    """
    try:
        if sys.platform == "win32":
            os.startfile(path)  # type: ignore[attr-defined]
        elif sys.platform == "darwin":
            subprocess.Popen(["open", path])
        else:
            subprocess.Popen(["xdg-open", path])
        return True
    except Exception:
        return False


# ===========================================================================
# Blender 실행파일 자동 탐색
# ===========================================================================

_BLENDER_SEARCH_PATHS = {
    "win32": [
        r"C:\Program Files\Blender Foundation\Blender 4.5\blender.exe",
        r"C:\Program Files\Blender Foundation\Blender 4.4\blender.exe",
        r"C:\Program Files\Blender Foundation\Blender 4.3\blender.exe",
        r"C:\Program Files\Blender Foundation\Blender 4.2\blender.exe",
        r"C:\Program Files\Blender Foundation\Blender 4.1\blender.exe",
        r"C:\Program Files\Blender Foundation\Blender 4.0\blender.exe",
        r"C:\Program Files\Blender Foundation\Blender 3.6\blender.exe",
    ],
    "darwin": [
        "/Applications/Blender.app/Contents/MacOS/Blender",
    ],
    "linux": [
        "/usr/bin/blender",
        "/usr/local/bin/blender",
        "/snap/bin/blender",
    ],
}


def find_blender_executable(explicit: str | None = None) -> str | None:
    """Blender 실행파일 위치 결정.

    우선순위:
      1) 인자로 명시
      2) 환경변수 BLENDER_EXE
      3) PATH 안의 'blender' / 'blender.exe'
      4) 플랫폼별 표준 설치 경로 (win/mac/linux)
      5) Windows의 경우 'C:\\Program Files\\Blender Foundation\\*' glob

    Returns: 절대 경로 (str) 또는 None
    """
    if explicit and Path(explicit).is_file():
        return str(Path(explicit).resolve())

    env_exe = os.environ.get("BLENDER_EXE")
    if env_exe and Path(env_exe).is_file():
        return str(Path(env_exe).resolve())

    which = shutil.which("blender") or shutil.which("blender.exe")
    if which:
        return str(Path(which).resolve())

    candidates = _BLENDER_SEARCH_PATHS.get(sys.platform, [])
    for c in candidates:
        if Path(c).is_file():
            return str(Path(c).resolve())

    if sys.platform == "win32":
        prog_files = Path(r"C:\Program Files\Blender Foundation")
        if prog_files.is_dir():
            for sub in sorted(prog_files.iterdir(), reverse=True):
                exe = sub / "blender.exe"
                if exe.is_file():
                    return str(exe.resolve())

    return None


# ===========================================================================
# 구조물 종류 자동 감지 + 빌더 라우팅
# ===========================================================================

_STRUCTURE_REGISTRY = {
    "gate": {
        "module":   "gate_3d_builder_bpy",
        "klass":    "GateBuilderBpy",
        "marker":   "n_gates",
    },
    "intake_tower": {
        "module":   "intake_tower_3d_builder_bpy",
        "klass":    "IntakeTowerBuilderBpy",
        "marker":   "body_top_el",
    },
}


def detect_structure_kind(params_json_path: str) -> str | None:
    """JSON 안의 marker 필드를 보고 구조물 종류 판별."""
    try:
        with open(params_json_path, encoding="utf-8") as f:
            d = json.load(f)
    except Exception:
        return None

    if not isinstance(d, dict):
        return None

    for kind, spec in _STRUCTURE_REGISTRY.items():
        marker = spec["marker"]
        if marker in d and isinstance(d[marker], (int, float)):
            return kind
    return None


# ===========================================================================
# 동적 wrapper script 생성 (Cycles seed + transparent_bg 주입)
# ===========================================================================

def _generate_blender_runner(
    *,
    s_canvas_dir: str,
    structure_kind: str,
    params_json: str,
    output_path: str,
    seed: int,
    samples: int,
    engine: str,
    time_preset: str,
    width: int,
    height: int,
    transparent_bg: bool = False,
    blend_path: str | None = None,
    glb_path: str | None = None,
) -> str:
    """헤드리스 Blender에 줄 임시 Python 스크립트.

    Cycles seed + transparent_bg 모두 빌더 외부에서 주입.
    빌더 파일은 immutable 유지.
    """
    spec = _STRUCTURE_REGISTRY[structure_kind]
    module = spec["module"]
    klass = spec["klass"]

    src = textwrap.dedent(f"""
    # Auto-generated by blender_renderer.py — do not edit.
    import sys, json, traceback
    from pathlib import Path

    SCANVAS_DIR = {s_canvas_dir!r}
    if SCANVAS_DIR not in sys.path:
        sys.path.insert(0, SCANVAS_DIR)

    try:
        import bpy
        import {module} as B

        # 1) Params 로드
        params_dict = json.loads(Path({params_json!r}).read_text(encoding="utf-8"))
        params = B.params_from_dict(params_dict)

        # 2) 3D scene 빌드
        builder = B.{klass}(params, clear_scene=True)
        builder.build_all()
        print(f"[blender_renderer] objects={{builder._object_count}}")

        # 3) 카메라 + 조명 (transparent_bg 옵션 포함)
        B.setup_lighting_and_camera(
            params,
            time_preset={time_preset!r},
            transparent_bg={transparent_bg!r},
        )

        # 4) Cycles seed (결정론적) — 빌더 외부에서 주입
        scene = bpy.context.scene
        if {engine!r} == "CYCLES":
            scene.cycles.seed = {seed}
            print(f"[blender_renderer] cycles.seed={{scene.cycles.seed}}")

        # 5) (옵션) .blend / .glb 부수 출력
        blend_path = {blend_path!r}
        glb_path = {glb_path!r}
        if blend_path:
            B.save_blend(blend_path)
            print(f"[blender_renderer] saved .blend -> {{blend_path}}")
        if glb_path:
            B.export_glb(glb_path)
            print(f"[blender_renderer] saved .glb -> {{glb_path}}")

        # 6) PNG 렌더 (transparent_bg 전달)
        B.render_to_png(
            {output_path!r},
            resolution=({width}, {height}),
            samples={samples},
            engine={engine!r},
            transparent_bg={transparent_bg!r},
        )
        print(f"[blender_renderer] OK -> {output_path}")

    except Exception as e:
        print("[blender_renderer] FAIL:", e)
        traceback.print_exc()
        sys.exit(2)
    """)
    return src


# ===========================================================================
# subprocess 실행 + stdout 스트리밍
# ===========================================================================

def _run_blender_subprocess(
    blender_exe: str,
    runner_script: str,
    *,
    log_callback,
    timeout_sec: int = 1800,
) -> tuple[int, str]:
    """Blender를 subprocess로 호출. stdout 줄 단위로 log_callback에 전달."""
    cmd = [blender_exe, "--background", "--python", runner_script]
    log_callback(f"  $ {Path(blender_exe).name} --background --python <runner.py>")

    try:
        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            encoding="utf-8",
            errors="replace",
            bufsize=1,
        )
    except FileNotFoundError as e:
        return -1, f"Blender 실행 실패: {e}"

    captured: list[str] = []
    t_start = _time.time()
    try:
        for raw_line in proc.stdout:  # type: ignore
            line = raw_line.rstrip()
            captured.append(line)
            if any(tag in line for tag in
                   ("[blender_renderer]", "[bpy-gate]", "[bpy-builder]",
                    "Saved:", "Render finished")):
                log_callback(f"    {line}")
            if _time.time() - t_start > timeout_sec:
                proc.kill()
                captured.append(f"[timeout] {timeout_sec}초 초과")
                log_callback(f"  ⚠ {timeout_sec}초 초과 — Blender 강제 종료")
                break
        proc.wait(timeout=10)
    except Exception as e:
        proc.kill()
        captured.append(f"[exception] {e}")

    return proc.returncode, "\n".join(captured)


# ===========================================================================
# 메인 진입점
# ===========================================================================

def run_blender_render(
    app,
    blender_exe: str | None,
    params_json: str,
    *,
    time_preset: str = "daytime",
    engine: str = "CYCLES",
    samples: int = 128,
    output_path: str = "structure_render.png",
    transparent_bg: bool = False,
    save_blend: bool = False,
    save_glb: bool = False,
    structure_kind: str | None = None,
    timeout_sec: int = 1800,
) -> None:
    """구조물 단독을 Blender Cycles로 렌더 + Harness 통합.

    AI 워크플로(Step 4)와 **별개의 트랙**입니다. 결과 PNG는 추후 지형 합성([C])
    의 입력으로 사용되거나 단독 발표용으로 사용됩니다.

    Args:
        app: scanvas_maker.SCanvasApp (상태/로그/UI scheduling 접근)
        blender_exe: Blender 실행파일 경로. None이면 자동 탐색.
        params_json: GateParams 또는 IntakeTowerParams JSON 경로.
        time_preset: 'daytime' | 'sunset' | 'overcast'
        engine: 'CYCLES' (사실적) | 'BLENDER_EEVEE_NEXT' (빠른 미리보기)
        samples: Cycles 샘플 수 (CYCLES 외 엔진은 무시)
        output_path: 결과 PNG. 기본 'structure_render.png' (AI 결과와 분리)
        transparent_bg: True면 RGBA + 투명 배경 (지형 합성 입력용)
        save_blend: True면 .blend 추가 저장
        save_glb: True면 .glb 추가 저장 (VR/AR/외부 뷰어용)
        structure_kind: 'gate' | 'intake_tower' | None (auto-detect)
        timeout_sec: subprocess 강제 종료 임계 (기본 30분)

    UI / harness 통합 동작:
        - app.log(...) 로 진행 상황 출력
        - app.set_status(...) 로 상태바 갱신
        - SeedManager로 결정론적 seed 산출 → Cycles seed 주입
        - QualityValidator로 결과 검증
        - JobLogger로 SQLite 이력 기록
        - 완료 후 app._show_structure_render(output_path) 호출
          (없으면 OS 기본 뷰어로 폴백)
    """
    t_start = _time.time()
    job_id = None
    db = None
    runner_path: Path | None = None

    # ── 0) 사전 점검 ────────────────────────────────────────────────────
    blender_exe = find_blender_executable(blender_exe)
    if not blender_exe:
        msg = (
            "Blender 실행파일을 찾을 수 없습니다.\n\n"
            "다음 중 하나로 해결:\n"
            "  1) 환경변수 BLENDER_EXE 설정 (절대경로)\n"
            "  2) 표준 위치에 설치 (Windows: "
            "C:\\Program Files\\Blender Foundation\\Blender X.X\\blender.exe)\n"
            "  3) PATH에 'blender' 추가\n\n"
            "다운로드: https://www.blender.org/download/"
        )
        app.after(0, lambda: app.log(f"  ✗ {msg.splitlines()[0]}"))
        app.after(0, lambda m=msg: _show_error_dialog("Blender 없음", m))
        app.after(0, lambda: app.set_status("Blender 실행파일 없음", "#E74C3C"))
        return

    if not Path(params_json).is_file():
        app.after(0, lambda p=params_json: app.log(f"  ✗ params_json 없음: {p}"))
        app.after(0, lambda p=params_json: _show_error_dialog("파일 없음",
            f"파라미터 JSON 파일을 찾을 수 없습니다:\n{p}\n\n"
            "구조물 상세 3D 빌드 단계에서 먼저 JSON을 생성해야 합니다."))
        app.after(0, lambda: app.set_status("params.json 없음", "#E74C3C"))
        return

    if structure_kind is None:
        structure_kind = detect_structure_kind(params_json)
    if structure_kind not in _STRUCTURE_REGISTRY:
        app.after(0, lambda k=structure_kind: app.log(
            f"  ✗ 구조물 종류 인식 실패 (kind={k!r})"))
        kinds = ', '.join(_STRUCTURE_REGISTRY.keys())
        app.after(0, lambda p=params_json, ks=kinds: _show_error_dialog(
            "구조물 인식 실패",
            f"JSON에서 구조물 종류를 자동 감지할 수 없습니다.\n"
            f"params_json: {p}\n"
            f"지원 종류: {ks}"))
        return

    s_canvas_dir = str(Path(params_json).resolve().parent)
    builder_module = _STRUCTURE_REGISTRY[structure_kind]["module"]
    builder_path = Path(s_canvas_dir) / f"{builder_module}.py"
    if not builder_path.is_file():
        app.after(0, lambda bp=builder_path: app.log(f"  ✗ 빌더 모듈 없음: {bp}"))
        app.after(0, lambda bm=builder_module, bp=builder_path: _show_error_dialog(
            "빌더 모듈 없음",
            f"{bm}.py 가 다음 위치에 없습니다:\n{bp}"))
        return

    # ── 1) Harness — Job 시작 ────────────────────────────────────────────
    dxf_hash = ""
    prompt_hash = ""
    prompt_ver = "blender_v1"
    seed = 0

    try:
        dxf_hash = app._get_dxf_hash()
    except Exception:
        try:
            import hashlib
            with open(params_json, "rb") as f:
                dxf_hash = hashlib.sha256(f.read()).hexdigest()[:16]
        except Exception:
            dxf_hash = ""

    with contextlib.suppress(Exception):
        prompt_hash = app._get_prompt_hash(
            f"engine={engine}|time={time_preset}|samples={samples}|"
            f"transparent={transparent_bg}|kind={structure_kind}"
        )

    if app.job_logger and _HARNESS_OK:
        try:
            db = get_db_session()
            job = app.job_logger.create_job(db, app.dxf_path or params_json, dxf_hash)
            job_id = job.id
            seed = app.seed_mgr.get_or_create_seed(db, job_id, dxf_hash)
            if app.prompt_reg:
                prompt_ver = app.prompt_reg.latest_version() or "blender_v1"
            app.job_logger.start_job(db, job_id, seed, prompt_ver, prompt_hash)
            app.after(0, lambda jid=job_id, s=seed: app.log(
                f"  Harness: job#{jid}, {SeedManager.describe(s)}, "
                f"engine={engine}, samples={samples}, time={time_preset}, "
                f"bg={'transparent' if transparent_bg else 'sky'}"))
        except Exception as e:
            app.after(0, lambda em=str(e): app.log(f"  Harness 초기화 경고: {em}"))

    # ── 2) Wrapper script + subprocess 실행 ──────────────────────────────
    try:
        # 출력 해상도 — app.target_resolution 우선, 없으면 1920×1080
        # (구조물 단독 렌더는 AI 입력 해상도와 다를 수 있어 별도 속성도 검사)
        struct_tgt = getattr(app, "structure_render_resolution", None)
        if struct_tgt and struct_tgt[0] > 0 and struct_tgt[1] > 0:
            width, height = int(struct_tgt[0]), int(struct_tgt[1])
        else:
            tgt = getattr(app, "target_resolution", None)
            if tgt and tgt[0] > 0 and tgt[1] > 0:
                width, height = int(tgt[0]), int(tgt[1])
            else:
                width, height = 1920, 1080

        out_abs = str(Path(output_path).resolve())
        blend_path = (Path(out_abs).with_suffix(".blend").as_posix()
                      if save_blend else None)
        glb_path = (Path(out_abs).with_suffix(".glb").as_posix()
                    if save_glb else None)

        runner_src = _generate_blender_runner(
            s_canvas_dir=s_canvas_dir,
            structure_kind=structure_kind,
            params_json=str(Path(params_json).resolve()),
            output_path=out_abs,
            seed=seed,
            samples=samples,
            engine=engine,
            time_preset=time_preset,
            width=width,
            height=height,
            transparent_bg=transparent_bg,
            blend_path=blend_path,
            glb_path=glb_path,
        )

        tmp_dir = tempfile.gettempdir()
        runner_path = Path(tmp_dir) / f"scanvas_blender_runner_{os.getpid()}.py"
        runner_path.write_text(runner_src, encoding="utf-8")

        bg_label = "투명배경" if transparent_bg else "Sky 배경"
        app.after(0, lambda be=blender_exe, sk=structure_kind, w=width, h=height,
                            s=seed, bl=bg_label: app.log(
            f"  Blender 실행 중... ({Path(be).name}, {sk}, {w}×{h}, "
            f"seed={s}, {bl})"))
        app.after(0, lambda: app.set_status("Blender 렌더링 중...", "#3498DB"))

        def _ui_log(msg: str):
            app.after(0, lambda m=msg: app.log(m))

        rc, full_log = _run_blender_subprocess(
            blender_exe, str(runner_path),
            log_callback=_ui_log,
            timeout_sec=timeout_sec,
        )

        if rc != 0:
            tail = "\n".join(full_log.splitlines()[-30:])
            err_msg = f"Blender 종료 코드 {rc}.\n\n마지막 출력:\n{tail}"
            if app.job_logger and db and job_id:
                with contextlib.suppress(Exception):
                    app.job_logger.fail_job(db, job_id, f"rc={rc}")
            app.after(0, lambda r=rc: app.log(f"  ✗ Blender 실패 (rc={r})"))
            app.after(0, lambda em=err_msg: _show_error_dialog("Blender 오류", em))
            app.after(0, lambda: app.set_status("Blender 렌더링 실패", "#E74C3C"))
            return

        if not Path(out_abs).is_file():
            if app.job_logger and db and job_id:
                with contextlib.suppress(Exception):
                    app.job_logger.fail_job(db, job_id, "출력 PNG 없음")
            app.after(0, lambda o=out_abs: app.log(f"  ✗ 출력 파일이 생성되지 않았습니다: {o}"))
            app.after(0, lambda: app.set_status("출력 파일 없음", "#E74C3C"))
            return

        # ── 3) 출력 후처리 (해상도 강제 시) ─────────────────────────────
        rendered = _open_image(out_abs)

        # 투명 PNG는 RGBA 유지 (RGB 변환하면 알파 손실)
        if transparent_bg and rendered.mode != "RGBA":
            rendered = rendered.convert("RGBA")

        # 사용자가 명시적으로 다른 해상도를 요청한 경우만 리사이즈
        # (struct_tgt 기준; 일반적으론 빌더가 만든 그대로 사용)
        target_size = (width, height)
        if rendered.size != target_size:
            src_size = rendered.size
            app.after(0, lambda s=src_size, t=target_size: app.log(
                f"  화질 리사이즈: {s[0]}x{s[1]} → {t[0]}x{t[1]}"))
            rendered = rendered.resize(target_size, _image_lanczos())
            rendered.save(out_abs)

        latency_ms = (_time.time() - t_start) * 1000

        # ── 4) 품질 검증 ────────────────────────────────────────────────
        quality_score = 0.0
        if app.quality_val:
            try:
                vr = app.quality_val.validate(Path(out_abs))
                quality_score = vr.score
                app.after(0, lambda s=vr.summary: app.log(f"  품질검증: {s}"))
            except Exception as e:
                app.after(0, lambda em=str(e): app.log(f"  품질검증 오류: {em}"))

        # ── 5) Job 완료 ─────────────────────────────────────────────────
        if app.job_logger and db and job_id:
            with contextlib.suppress(Exception):
                app.job_logger.complete_job(
                    db, job_id, out_abs, quality_score, latency_ms
                )

        app.after(0, lambda o=out_abs, sz=rendered.size, lm=latency_ms,
                            q=quality_score, s=seed: app.log(
            f"  Blender 렌더링 완료! → {o} ({sz}) "
            f"[{lm:.0f}ms, 품질={q:.2f}, seed={s}]"))
        app.after(0, lambda: app.set_status("Blender 렌더링 완료", "#2ECC71"))

        # ── 6) 결과 표시 — AI 워크플로와 분리 ────────────────────────────
        # 우선 app._show_structure_render(path) 시도. 없으면 OS 기본 뷰어 폴백.
        def _present_result(path=out_abs):
            shown = False
            shower = getattr(app, "_show_structure_render", None)
            if callable(shower):
                try:
                    shower(path)
                    shown = True
                except Exception as e:
                    app.log(f"  결과 표시 실패 (_show_structure_render): {e}")
            if not shown:
                if _open_in_os_default_viewer(path):
                    app.log(f"  결과 PNG를 기본 뷰어로 열었습니다: {path}")
                else:
                    app.log(f"  결과 PNG: {path} (뷰어 자동 실행 실패 — "
                            f"파일을 직접 열어 확인하세요)")

        app.after(0, _present_result)

    except Exception as e:
        if app.job_logger and db and job_id:
            with contextlib.suppress(Exception):
                app.job_logger.fail_job(db, job_id, str(e))
        err_msg = str(e)[:300]
        app.after(0, lambda em=err_msg: app.log(f"  Blender 워커 오류: {em}"))
        app.after(0, lambda: app.set_status("렌더링 실패", "#E74C3C"))
        app.after(0, lambda em=err_msg: _show_error_dialog(
            "Blender 워커 오류", f"실행 오류:\n{em}"))
    finally:
        if runner_path and runner_path.is_file():
            with contextlib.suppress(Exception):
                runner_path.unlink()
        if db:
            with contextlib.suppress(Exception):
                db.close()


# ===========================================================================
# CLI 진입점 (단독 실행 / 디버그용)
# ===========================================================================

class _StubApp:
    """app 인터페이스 흉내 — CLI 모드에서 가짜 app 객체로 사용."""
    def __init__(self, dxf_path: str = ""):
        self.dxf_path = dxf_path
        self.target_resolution = None
        self.structure_render_resolution = None
        self.job_logger = None
        self.seed_mgr = None
        self.prompt_reg = None
        self.quality_val = None

    def log(self, msg: str):
        print(msg)

    def set_status(self, text: str, color: str = ""):
        print(f"[status] {text}")

    def after(self, delay_ms: int, fn):
        try:
            fn()
        except Exception as e:
            print(f"[after-error] {e}")

    def _get_dxf_hash(self) -> str:
        return ""

    def _get_prompt_hash(self, prompt: str) -> str:
        import hashlib
        return hashlib.sha256(prompt.encode()).hexdigest()[:16]

    def _show_structure_render(self, path: str):
        print(f"[structure-render] {path}")


def _cli():
    import argparse
    ap = argparse.ArgumentParser(
        description="Blender 헤드리스 구조물 렌더 (S-CANVAS 외부 단독 실행)"
    )
    ap.add_argument("--params", required=True, help="GateParams/IntakeTowerParams JSON")
    ap.add_argument("--blender", default=None,
                    help="Blender 실행파일 경로 (없으면 자동 탐색)")
    ap.add_argument("--output", default="structure_render.png",
                    help="출력 PNG (기본 'structure_render.png')")
    ap.add_argument("--time", default="daytime",
                    choices=["daytime", "sunset", "overcast"])
    ap.add_argument("--engine", default="CYCLES",
                    choices=["CYCLES", "BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"])
    ap.add_argument("--samples", type=int, default=128)
    ap.add_argument("--width", type=int, default=1920)
    ap.add_argument("--height", type=int, default=1080)
    ap.add_argument("--transparent", action="store_true",
                    help="투명 배경 RGBA PNG (지형 합성 입력용)")
    ap.add_argument("--save-blend", action="store_true")
    ap.add_argument("--save-glb", action="store_true")
    ap.add_argument("--kind", default=None,
                    choices=list(_STRUCTURE_REGISTRY.keys()),
                    help="구조물 종류 (없으면 JSON에서 자동 감지)")
    ap.add_argument("--timeout", type=int, default=1800,
                    help="subprocess timeout (초)")
    args = ap.parse_args()

    stub = _StubApp()
    stub.structure_render_resolution = (args.width, args.height)

    run_blender_render(
        stub,
        blender_exe=args.blender,
        params_json=args.params,
        time_preset=args.time,
        engine=args.engine,
        samples=args.samples,
        output_path=args.output,
        transparent_bg=args.transparent,
        save_blend=args.save_blend,
        save_glb=args.save_glb,
        structure_kind=args.kind,
        timeout_sec=args.timeout,
    )


if __name__ == "__main__":
    _cli()
