"""DA-11 + DA-21: 슬라이드 조합 렌더러.

블록 배치 명세(JSON)를 받아 Jinja2로 HTML을 생성한다.
- 다중 페이지 지원
- 카테고리 경로 지원 (blocks/{category}/{name}.html)
- _legacy fallback (기존 경로 호환)
- 같은 area 블록 그룹핑 (겹침 방지)
"""
from __future__ import annotations

import logging
from collections import OrderedDict
from pathlib import Path
from typing import Any

import yaml
from jinja2 import Environment, FileSystemLoader

logger = logging.getLogger(__name__)

TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
STATIC_DIR = Path(__file__).parent.parent / "static"
CATALOG_PATH = TEMPLATES_DIR / "catalog.yaml"

# 카테고리 검색 순서
BLOCK_CATEGORIES = ["headers", "cards", "tables", "visuals", "emphasis", "media"]

# catalog.yaml에서 id → template 경로 매핑 로드 (BF-10: mtime 체크로 자동 갱신)
_CATALOG_MAP: dict[str, str] | None = None
_CATALOG_MTIME: float = 0.0

def _load_catalog_map() -> dict[str, str]:
    """catalog.yaml에서 블록 id → template 경로 매핑을 로드한다.

    파일 수정시간(mtime)을 확인하여, 변경 시에만 재로드한다.
    """
    global _CATALOG_MAP, _CATALOG_MTIME

    current_mtime = CATALOG_PATH.stat().st_mtime if CATALOG_PATH.exists() else 0.0

    if _CATALOG_MAP is not None and _CATALOG_MTIME == current_mtime:
        return _CATALOG_MAP  # 파일 변경 없음 → 캐시 재사용

    # 변경 감지 또는 첫 로드 → 새로 읽기
    _CATALOG_MTIME = current_mtime
    _CATALOG_MAP = {}
    if CATALOG_PATH.exists():
        try:
            with open(CATALOG_PATH, encoding="utf-8") as f:
                catalog = yaml.safe_load(f)
            for block in catalog.get("blocks", []):
                block_id = block.get("id", "")
                template = block.get("template", "")
                if block_id and template:
                    _CATALOG_MAP[block_id] = template
            logger.info(f"catalog.yaml 로드: {len(_CATALOG_MAP)}개 블록 매핑")
        except Exception as e:
            logger.warning(f"catalog.yaml 로드 실패: {e}")
    else:
        logger.warning(f"catalog.yaml 미발견: {CATALOG_PATH}")

    return _CATALOG_MAP


# Phase R: variant별 template 경로 캐시
_CATALOG_VARIANT_MAP: dict[str, str] | None = None


def _load_catalog_map_with_variants() -> dict[str, str]:
    """catalog.yaml에서 variant별 template 경로 매핑을 로드한다.

    키: "block_id--variant_id" → 값: template 경로
    """
    global _CATALOG_VARIANT_MAP

    # _load_catalog_map이 이미 캐시 관리하므로 같은 mtime 사용
    _load_catalog_map()  # 캐시 갱신 보장

    if _CATALOG_VARIANT_MAP is not None and _CATALOG_MTIME == (CATALOG_PATH.stat().st_mtime if CATALOG_PATH.exists() else 0.0):
        return _CATALOG_VARIANT_MAP

    _CATALOG_VARIANT_MAP = {}
    if CATALOG_PATH.exists():
        try:
            with open(CATALOG_PATH, encoding="utf-8") as f:
                catalog = yaml.safe_load(f)
            for block in catalog.get("blocks", []):
                block_id = block.get("id", "")
                for variant in block.get("variants", []):
                    vid = variant.get("id", "default")
                    vtemplate = variant.get("template", "")
                    if vid != "default" and vtemplate:
                        _CATALOG_VARIANT_MAP[f"{block_id}--{vid}"] = vtemplate
        except Exception as e:
            logger.warning(f"catalog variant 로드 실패: {e}")

    return _CATALOG_VARIANT_MAP


def create_jinja_env() -> Environment:
    """Jinja2 환경 생성."""
    return Environment(
        loader=FileSystemLoader(str(TEMPLATES_DIR)),
        autoescape=False,
    )


def _resolve_template_path(env: Environment, block_type: str, variant: str = "default") -> str | None:
    """블록 타입 + variant로 템플릿 경로를 찾는다.

    Phase R: variant가 지정되면 variant 전용 템플릿을 우선 탐색.
    variant 템플릿이 없으면 기존 블록 템플릿으로 fallback.

    검색 순서:
    0-v. catalog.yaml의 variant별 template 경로 (Phase R, 최우선)
    0. catalog.yaml 매핑 (id → template 경로)
    1. 정확한 경로 (blocks/cards/card-icon-desc.html 등)
    2. 카테고리 폴더 검색 (blocks/{category}/{block_type}.html)
    3. _legacy fallback (blocks/_legacy/{block_type}.html)
    4. 루트 fallback (blocks/{block_type}.html)
    """
    candidates = []

    # 0-v. Phase R: variant 전용 템플릿 우선 탐색
    if variant and variant != "default":
        catalog_map_full = _load_catalog_map_with_variants()
        variant_key = f"{block_type}--{variant}"
        if variant_key in catalog_map_full:
            candidates.append(catalog_map_full[variant_key])
        # 카테고리 폴더에서 variant 파일 탐색
        for category in BLOCK_CATEGORIES:
            candidates.append(f"blocks/{category}/{block_type}--{variant}.html")

    # 0. catalog.yaml에서 id → template 매핑 조회
    catalog_map = _load_catalog_map()
    if block_type in catalog_map:
        catalog_path = catalog_map[block_type]
        candidates.append(catalog_path)
        # .html 확장자 없는 경우 대비
        if not catalog_path.endswith(".html"):
            candidates.append(f"{catalog_path}.html")

    # 1. 이미 카테고리 경로가 포함된 경우 (예: "cards/card-icon-desc")
    if "/" in block_type:
        candidates.append(f"blocks/{block_type}.html")
        candidates.append(f"blocks/{block_type}")  # .html 이미 포함된 경우

    # 2. 카테고리 폴더 검색
    for category in BLOCK_CATEGORIES:
        candidates.append(f"blocks/{category}/{block_type}.html")

    # 3. _legacy fallback
    candidates.append(f"blocks/_legacy/{block_type}.html")

    # 4. 루트 fallback
    candidates.append(f"blocks/{block_type}.html")

    for path in candidates:
        try:
            env.get_template(path)
            return path
        except Exception:
            continue

    return None



def _preprocess_svg_data(block_type: str, block_data: dict[str, Any]) -> dict[str, Any]:
    """P2-B: SVG 시각화 블록의 좌표를 사전 계산한다.

    venn-diagram: items[]에 cx, cy, r 좌표 추가 + outer_r 등 레이아웃 데이터
    다른 블록: 변경 없이 그대로 반환
    """
    SVG_BLOCKS = {"venn-diagram", "relationship"}

    if block_type not in SVG_BLOCKS:
        return block_data

    items = block_data.get("items", [])
    if not items:
        return block_data

    # items에 이미 cx가 있으면 (수동 지정) 그대로 사용
    if items[0].get("cx") is not None:
        return block_data

    try:
        from src.svg_calculator import prepare_venn_data

        prepared = prepare_venn_data(
            items=items,
            center_label=block_data.get("center_label", ""),
            center_sub=block_data.get("center_sub", ""),
            description=block_data.get("description", ""),
        )
        # 기존 block_data에 계산 결과 병합
        block_data.update(prepared)
        logger.info(
            f"SVG 좌표 계산 완료: {block_type}, "
            f"{len(items)}개 원소, outer_r={prepared.get('outer_r')}"
        )
    except Exception as e:
        logger.warning(f"SVG 좌표 계산 실패 ({block_type}): {e}. Phase 1 fallback.")
        # fallback: 좌표 없이 Jinja2에 전달 → Phase 1 고정 SVG

    return block_data


def _group_blocks_by_area(
    blocks: list[dict[str, Any]],
    container_specs: dict | None = None,
) -> list[dict[str, Any]]:
    """Phase O: 같은 area의 블록들을 비중 기반 컨테이너로 그룹핑한다.

    container_specs가 있으면 body zone 안에 역할별 고정 높이 컨테이너를 생성.
    """
    grouped = OrderedDict()
    for block in blocks:
        area = block["area"]
        if area not in grouped:
            grouped[area] = {"area": area, "blocks": []}
        grouped[area]["blocks"].append(block)

    result = []
    for area, data in grouped.items():
        block_list = data["blocks"]

        # Phase O: body zone에 컨테이너 스펙 적용
        if container_specs and area in ("body", "left", "right", "hero", "detail"):
            container_htmls = []
            assigned_ids = set()

            role_order = ["배경", "본심"]
            for role in role_order:
                spec = container_specs.get(role)
                if not spec or spec.zone != area:
                    continue

                # 이 역할에 속하는 블록 찾기 (topic_id로 매칭)
                role_blocks = [
                    b for b in block_list
                    if b.get("_topic_id") in spec.topic_ids
                    and id(b) not in assigned_ids
                ]

                # topic_id 매칭 안 되면 순서로 매칭
                if not role_blocks:
                    for b in block_list:
                        if id(b) not in assigned_ids:
                            role_blocks.append(b)
                            if len(role_blocks) >= len(spec.topic_ids):
                                break

                for b in role_blocks:
                    assigned_ids.add(id(b))

                if not role_blocks:
                    continue

                inner_html = "\n".join(b["html"] for b in role_blocks)
                font_size = spec.block_constraints.get("font_size_px", 15.2)
                padding = spec.block_constraints.get("padding_px", 20)

                container_htmls.append(
                    f'<div class="container-{role}" style="'
                    f'height:{spec.height_px}px; '
                    f'overflow:visible; '
                    f'display:flex; flex-direction:column; gap:8px; '
                    f'font-size:{font_size}px; '
                    f'--spacing-inner:{padding}px; '
                    f'--font-body:{font_size / 16:.3f}rem;">\n'
                    f'{inner_html}\n</div>'
                )

            # 미배정 블록
            for b in block_list:
                if id(b) not in assigned_ids:
                    container_htmls.append(b["html"])

            html = "\n".join(container_htmls)

        elif len(block_list) == 1:
            html = block_list[0]["html"]
        else:
            inner = "\n".join(b["html"] for b in block_list)
            html = (
                f'<div style="display:flex; flex-direction:column; '
                f'gap:var(--spacing-block); height:100%;">\n'
                f'{inner}\n</div>'
            )

        result.append({"area": area, "html": html})

    return result


def render_multi_page(layout_concept: dict[str, Any]) -> str:
    """다중 페이지 레이아웃 컨셉으로 완성 HTML을 생성한다."""
    env = create_jinja_env()
    title = layout_concept.get("title", "슬라이드")
    pages = layout_concept.get("pages", [])

    if not pages:
        logger.warning("페이지가 없습니다. 빈 HTML 반환.")
        return "<html><body><p>페이지가 없습니다.</p></body></html>"

    pages_rendered = []
    for page_idx, page in enumerate(pages):
        blocks_raw = []
        for block in page.get("blocks", []):
            block_type = block.get("type", "")
            block_data = block.get("data", {})

            # 높이 자동 조치: _strip_sub_text 플래그 처리
            if block_data.get("_strip_sub_text"):
                block_data.pop("sub_text", None)
                block_data.pop("_strip_sub_text", None)

            # P2-B: SVG 시각화 블록은 좌표 사전 계산
            block_data = _preprocess_svg_data(block_type, block_data)

            # DA-21: 카테고리 경로 검색
            template_path = _resolve_template_path(env, block_type, block.get("_variant", "default") if isinstance(block, dict) else "default")

            if template_path:
                try:
                    block_template = env.get_template(template_path)
                    rendered_html = block_template.render(**block_data)
                except Exception as e:
                    logger.warning(f"블록 렌더링 실패 ({block_type}): {e}")
                    rendered_html = (
                        f'<div class="body-text">블록 렌더링 실패: {block_type}</div>'
                    )
            else:
                logger.warning(f"블록 템플릿 미발견: {block_type}")
                rendered_html = (
                    f'<div class="body-text">블록 템플릿 미발견: {block_type}</div>'
                )

            # Phase N-3: max-height CSS 래퍼 제거.
            # 콘텐츠는 렌더링 전에 _max_chars로 맞춘다. CSS로 사후에 자르지 않는다.
            # overflow는 slide_measurer가 scrollHeight > clientHeight로 감지한다.

            blocks_raw.append({
                "area": block.get("area", "main"),
                "html": rendered_html,
                "_topic_id": block.get("topic_id"),  # Phase O: 컨테이너 매칭용
            })

        # Phase O: 비중 기반 컨테이너 그룹핑
        page_container_specs = layout_concept.get("_container_specs")
        blocks_grouped = _group_blocks_by_area(blocks_raw, container_specs=page_container_specs)

        # A-1: area별 CSS 변수 override 주입
        area_styles = page.get("area_styles", {})
        for grouped_block in blocks_grouped:
            grouped_block["style_override"] = area_styles.get(
                grouped_block["area"], ""
            )

        pages_rendered.append({
            "grid_areas": page.get("grid_areas", "'main'"),
            "grid_columns": page.get("grid_columns", "1fr"),
            "grid_rows": page.get("grid_rows", "auto"),
            "blocks": blocks_grouped,
            "page_number": page_idx + 1,
        })

    base_template = env.get_template("slide-base.html")
    html = base_template.render(
        slide_title=title,
        pages=pages_rendered,
        total_pages=len(pages_rendered),
    )

    # CSS 인라인 삽입
    tokens_css = (STATIC_DIR / "tokens.css").read_text(encoding="utf-8")
    base_css = (STATIC_DIR / "base.css").read_text(encoding="utf-8")
    base_css = base_css.replace("@import url('./tokens.css');", "")

    inline_css = f"<style>\n{tokens_css}\n{base_css}\n</style>"
    html = html.replace(
        '<link rel="stylesheet" href="/static/base.css">',
        inline_css,
    )

    logger.info(f"슬라이드 렌더링 완료: {title}, {len(pages_rendered)}페이지")
    return html


def render_slide(layout: dict[str, Any]) -> str:
    """하위 호환 렌더링. pages 구조가 있으면 render_multi_page로 위임."""
    if "pages" in layout:
        return render_multi_page(layout)

    env = create_jinja_env()

    blocks_raw = []
    for block in layout.get("blocks", []):
        block_type = block["type"]
        block_data = block.get("data", {})

        template_path = _resolve_template_path(env, block_type, block.get("_variant", "default") if isinstance(block, dict) else "default")

        if template_path:
            try:
                block_template = env.get_template(template_path)
                rendered_html = block_template.render(**block_data)
            except Exception as e:
                logger.warning(f"블록 렌더링 실패 ({block_type}): {e}")
                rendered_html = (
                    f'<div class="body-text">블록 렌더링 실패: {block_type}</div>'
                )
        else:
            logger.warning(f"블록 템플릿 미발견: {block_type}")
            rendered_html = (
                f'<div class="body-text">블록 템플릿 미발견: {block_type}</div>'
            )

        blocks_raw.append({
            "area": block["area"],
            "html": rendered_html,
        })

    blocks_grouped = _group_blocks_by_area(blocks_raw)

    base_template = env.get_template("slide-base.html")
    html = base_template.render(
        slide_title=layout.get("title", ""),
        pages=[{
            "grid_areas": layout.get("grid_areas", "'header' 'main' 'footer'"),
            "grid_columns": layout.get("grid_columns", "1fr"),
            "grid_rows": layout.get("grid_rows", "auto 1fr auto"),
            "blocks": blocks_grouped,
            "page_number": 1,
        }],
        total_pages=1,
    )

    tokens_css = (STATIC_DIR / "tokens.css").read_text(encoding="utf-8")
    base_css = (STATIC_DIR / "base.css").read_text(encoding="utf-8")
    base_css = base_css.replace("@import url('./tokens.css');", "")

    inline_css = f"<style>\n{tokens_css}\n{base_css}\n</style>"
    html = html.replace(
        '<link rel="stylesheet" href="/static/base.css">',
        inline_css,
    )

    logger.info(f"슬라이드 렌더링 완료: {layout.get('title', 'untitled')}")
    return html


def render_block_in_container(
    block_type: str,
    data: dict[str, Any],
    container_height_px: int,
    container_width_px: int,
    font_size_px: float = 15.2,
    padding_px: int = 20,
) -> str:
    """Phase P: 단일 블록을 컨테이너 안에서 렌더링한다.

    컨테이너 크기에 맞게 CSS가 적용된 완전한 HTML을 반환.
    Selenium으로 스크린샷 캡처 + 높이 측정에 사용.
    """
    block_html = render_standalone_block(block_type, data)

    tokens_css = (STATIC_DIR / "tokens.css").read_text(encoding="utf-8")
    base_css = (STATIC_DIR / "base.css").read_text(encoding="utf-8")
    base_css = base_css.replace("@import url('./tokens.css');", "")

    return f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<style>
{tokens_css}
{base_css}
.candidate-container {{
    width: {container_width_px}px;
    height: {container_height_px}px;
    overflow: visible;
    font-size: {font_size_px}px;
    --font-body: {font_size_px / 16:.3f}rem;
    --spacing-inner: {padding_px}px;
    padding: {padding_px}px;
    font-family: 'Pretendard Variable', 'Pretendard', sans-serif;
    line-height: 1.7;
    word-break: keep-all;
}}
</style>
</head>
<body>
<div class="candidate-container">
{block_html}
</div>
</body>
</html>"""


def render_slide_from_html(
    generated: dict[str, str],
    analysis: dict[str, Any],
    preset: dict[str, Any],
) -> str:
    """Phase R': AI가 생성한 HTML 조각을 슬라이드 프레임에 삽입하여 완성 HTML 반환.

    블록 템플릿을 렌더링하지 않는다. AI가 생성한 body/sidebar/footer HTML을 직접 삽입.

    Args:
        generated: {"body_html": "...", "sidebar_html": "...", "footer_html": "..."}
        analysis: 1단계 분석 결과 (title)
        preset: 프리셋 정보 (grid_areas, grid_columns, grid_rows)
    """
    tokens_css = ""
    tokens_path = Path(__file__).parent.parent / "static" / "tokens.css"
    if tokens_path.exists():
        tokens_css = tokens_path.read_text(encoding="utf-8")

    base_css = ""
    base_path = Path(__file__).parent.parent / "static" / "base.css"
    if base_path.exists():
        # @import 제거 (inline으로 포함하므로)
        raw = base_path.read_text(encoding="utf-8")
        base_css = "\n".join(
            line for line in raw.split("\n")
            if not line.strip().startswith("@import")
        )

    title = analysis.get("title", "슬라이드")
    grid_areas = preset.get("grid_areas", "'header header' 'body sidebar' 'footer footer'")

    # Phase T: 동적 비율 반영 — container_ratio가 있으면 프리셋 고정값 대신 사용
    container_ratio = analysis.get("_container_ratio")
    if container_ratio and len(container_ratio) == 2:
        grid_columns = f"{container_ratio[0]}fr {container_ratio[1]}fr"
    else:
        grid_columns = preset.get("grid_columns", "65fr 35fr")

    # Phase W: AFTER 컨테이너 크기를 grid-template-rows에 반영
    # header=auto, body/sidebar=AFTER 높이에 맞춤, footer=AFTER 높이
    containers = analysis.get("_containers", {})
    redist = analysis.get("_fit_redistribution", {})
    from src.fit_verifier import _load_design_tokens as _ldt
    _tokens = _ldt()
    _header_h = _tokens.get("header_height", 66)
    _gap_small = _tokens["spacing_small"]
    # zone 기반으로 body/footer 높이를 동적 탐색 (유형 A: 배경+본심, 유형 B: zone별)
    def _find_h(role_name, zone_name=None):
        """redist → containers 순으로 높이 탐색. role_name 없으면 zone으로 fallback."""
        h = redist.get(role_name, 0)
        if h:
            return int(h)
        ci = containers.get(role_name, {})
        if ci:
            return int(ci.get("height_px", 0))
        if zone_name:
            for _r, _c in containers.items():
                if isinstance(_c, dict) and _c.get("zone") == zone_name:
                    return int(redist.get(_r, _c.get("height_px", 0)))
        return 0

    _bg_h = _find_h("배경")
    _core_h = _find_h("본심")
    _footer_h = _find_h("결론", "footer")
    _body_row_h = _bg_h + _core_h + _gap_small if _bg_h and _core_h else 0
    if _body_row_h > 0 and _footer_h > 0:
        grid_rows = f"auto {_body_row_h}px {_footer_h}px"
    else:
        grid_rows = preset.get("grid_rows", "auto auto auto").replace("1fr", "auto")

    body_html = generated.get("body_html", "")
    sidebar_html = generated.get("sidebar_html", "")
    footer_html = generated.get("footer_html", "")

    # ── 후처리 ──
    import re as _re
    # 1) sidebar 최외곽 wrapper div만 width:100% (grid cell에 맞추기)
    # 첫 번째 태그의 style에서만 변경. 내부 요소(카드 번호 등)는 건드리지 않음.
    sidebar_html = _re.sub(
        r'^(\s*<div\s+style="[^"]*?)width:\s*\d+px',
        r'\1width:100%',
        sidebar_html,
        count=1,
    )
    # 2) overflow-y:auto/scroll → overflow:hidden (스크롤 절대 금지)
    body_html = _re.sub(r'overflow-y:\s*(auto|scroll)', 'overflow:hidden', body_html)
    body_html = _re.sub(r'overflow:\s*(?:auto|scroll)(?!bar)', 'overflow:hidden', body_html)
    sidebar_html = _re.sub(r'overflow-y:\s*(auto|scroll)', 'overflow:hidden', sidebar_html)
    sidebar_html = _re.sub(r'overflow:\s*(?:auto|scroll)(?!bar)', 'overflow:hidden', sidebar_html)
    # 3) markdown **bold** → <strong>
    body_html = _re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', body_html)
    sidebar_html = _re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', sidebar_html)
    # 4) 폰트 위계 강제: 배경 영역 font-size가 본심(core)보다 크면 안 됨
    font_h = analysis.get("_font_hierarchy", {})
    bg_max = font_h.get("bg", 11.0)
    core_max = font_h.get("core", 12.0)
    sidebar_max = font_h.get("sidebar", 10.0)

    def _cap_font(html_str: str, max_size: float) -> str:
        """font-size: NNpx 중 max_size 초과하는 것을 max_size로 캡."""
        def _repl(m):
            val = float(m.group(1))
            if val > max_size:
                return f"font-size:{max_size}px"
            return m.group(0)
        return _re.sub(r'font-size:\s*(\d+(?:\.\d+)?)px', _repl, html_str)

    # body_html의 첫 번째 주요 div(배경)만 캡: height:117px or 배경 색상이 있는 div
    # 배경 div 끝(</div> + spacer) 이후가 본심
    bg_end = body_html.find('<div style="height:12px;')  # spacer between bg and core
    if bg_end > 0:
        bg_part = body_html[:bg_end]
        core_part = body_html[bg_end:]
        bg_part = _cap_font(bg_part, bg_max)
        body_html = bg_part + core_part
    # sidebar 전체 캡
    sidebar_html = _cap_font(sidebar_html, sidebar_max)

    html = f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>
{tokens_css}
{base_css}

.slide-1 {{
    grid-template-areas: {grid_areas};
    grid-template-columns: {grid_columns};
    grid-template-rows: {grid_rows};
}}
.slide-1 .area-body {{ grid-area: body; }}
.slide-1 .area-sidebar {{ grid-area: sidebar; }}
.slide-1 .area-footer {{ grid-area: footer; }}

.slide + .slide {{ margin-top: 40px; }}
@media print {{
    .slide {{ page-break-after: always; }}
    .slide + .slide {{ margin-top: 0; }}
}}
</style>
</head>
<body>

<div class="slide slide-1">
    <div class="slide-title" style="grid-area: header;">{title}</div>

    <div class="area-body" style="overflow:hidden;">
        {body_html}
    </div>

    <div class="area-sidebar" style="overflow:hidden;">
        {sidebar_html}
    </div>

    <div class="area-footer" style="overflow:hidden;">
        {footer_html}
    </div>
</div>

<script>
window.onbeforeprint = function() {{
    document.querySelectorAll('details').forEach(function(d) {{ d.open = true; }});
}};
window.onafterprint = function() {{
    document.querySelectorAll('details').forEach(function(d) {{ d.open = false; }});
}};
</script>
</body>
</html>"""

    logger.info(f"[R'] 슬라이드 렌더링 완료: {title}")
    return html


def render_standalone_block(block_type: str, data: dict[str, Any], variant: str = "default") -> str:
    """단일 블록을 독립 HTML로 렌더링 (테스트/미리보기용)."""
    env = create_jinja_env()
    template_path = _resolve_template_path(env, block_type, variant)
    if not template_path:
        return f"<div>블록 미발견: {block_type}</div>"
    template = env.get_template(template_path)
    return template.render(**data)
