"""Phase T: AI HTML 생성기 — 동적 프롬프트 생성.

영역별 개별 호출. Phase T context(폰트 위계, 블록 레퍼런스, 디자인 예산)에서
모든 수치를 동적으로 가져와 프롬프트를 조립.

Phase S 하드코딩 프롬프트(BG_PROMPT 등) → build_area_prompt() 동적 생성으로 교체.

역할 분리:
  Kei (1단계): 콘텐츠 분석
  Claude Sonnet (이 모듈): HTML 코드 생성
"""
from __future__ import annotations

import json
import logging
import re
from pathlib import Path
from typing import Any

import anthropic

from src.config import settings

logger = logging.getLogger(__name__)


# ═══════════════════════════════════════════════════════════
# Phase T: 동적 프롬프트 생성
# Phase S 하드코딩 프롬프트 → context 기반 동적 생성으로 교체
# ═══════════════════════════════════════════════════════════

# 공통 텍스트 규칙 (모든 영역 동일)
_COMMON_TEXT_RULES = """## 텍스트 규칙 (반드시 적용)
1. 원본 텍스트의 단어를 한 글자도 빼지 마라. 축약/요약 절대 금지.
2. 마침표(.)로 끝나는 문장이 2개 이상이면 각각 별도 불릿(•)으로 분리.
3. 개조식 어미 변환: 문장 끝 1-2글자만 변환. 그 외 단어는 절대 건드리지 마라.
   - "~있다" → "~있음", "~한다" → "~함", "~이다" → 삭제, "~된다" → "~됨"
4. 원본에 없는 텍스트를 추가하지 마라.
5. 동일한 내용을 다른 형태로 2번 넣지 마라. 상세 내용은 "[상세보기]" 텍스트 링크만 남기고 본문에서 제거."""

# 공통 HTML 규칙
_COMMON_HTML_RULES = """## HTML 규칙
- inline style만 사용. <style> 블록 금지.
- overflow:hidden 금지 (텍스트 잘림 방지).
- 모든 텍스트가 보여야 한다. 잘리는 텍스트가 있으면 안 됨.
- HTML만 반환. 설명 없이 코드만."""


def _calc_indent(font_size: float) -> tuple[int, int]:
    """폰트 크기에 맞는 들여쓰기 px 계산.
    불릿 마커 "• " 폭 ≈ font_size × 1.2.
    Returns: (padding_left, text_indent)
    """
    import math
    pl = math.ceil(font_size * 1.2)
    return pl, -pl


def build_area_prompt(
    role: str,
    content_block: str,
    phase_t: dict,
    height_px: int,
    width_px: int,
    images: list[dict] | None = None,
    core_message: str = "",
) -> str:
    """Phase T context에서 모든 수치를 동적으로 가져와 프롬프트 생성.

    하드코딩 CSS 값 0개. 모든 수치는 phase_t context에서.

    Args:
        role: "배경" | "본심" | "첨부" | "결론"
        content_block: 원본 텍스트 (이 영역에 해당하는)
        phase_t: analysis["phase_t"] dict (font_hierarchy, references, design_budgets, container_ratio)
        height_px: 이 영역의 높이
        width_px: 이 영역의 너비
        images: 이미지 정보 (본심에서만)
        core_message: 핵심 메시지 (본심에서만)
    """
    fh = phase_t.get("font_hierarchy", {})
    refs = phase_t.get("references", {})
    budgets = phase_t.get("design_budgets", {})

    # 역할별 폰트 매핑
    role_font_map = {"배경": "bg", "본심": "core", "첨부": "sidebar", "결론": "key_msg"}
    font_size = fh.get(role_font_map.get(role, "core"), 12)

    # 들여쓰기 (폰트 크기 기반)
    indent_pl, indent_ti = _calc_indent(font_size)

    # V-1: 꼭지별 블록 레퍼런스 (리스트)
    ref_list = refs.get(role, [])
    if isinstance(ref_list, dict):
        # 하위호환: 이전 형식(dict) → 리스트로 변환
        ref_list = [ref_list]
    # 모든 블록의 디자인 레퍼런스 HTML을 결합
    ref_html = "\n\n".join(r.get("design_reference_html", "") for r in ref_list if r)

    # 디자인 예산
    budget = budgets.get(role, {})
    avail_h = budget.get("available_height_px", 0)
    avail_w = budget.get("available_width_px", 0)

    parts = []

    # ── Phase V Step 7: 서브 컨테이너 레이아웃 ──
    sub_layouts = phase_t.get("sub_layouts", {})
    role_layout = sub_layouts.get(role, {})
    sub_containers = role_layout.get("sub_containers", [])
    if sub_containers:
        layout_lines = [f"## 세부 레이아웃 (Phase V Step 7) — 반드시 이 구조를 따르라"]
        for sc in sub_containers:
            sc_name = sc.get("name", "")
            sc_w = int(sc.get("width_px", 0))
            sc_h = int(sc.get("height_px", 0))
            sc_align = sc.get("align", "stretch")
            layout_lines.append(f"- {sc_name}: {sc_w}×{sc_h}px (align: {sc_align})")

        # 서브 컨테이너 조합 지시
        names = [sc.get("name", "") for sc in sub_containers]
        if "svg" in names and "text_and_table" in names:
            svg_sc = next(sc for sc in sub_containers if sc["name"] == "svg")
            txt_sc = next(sc for sc in sub_containers if sc["name"] == "text_and_table")
            layout_lines.append(f"\n구조: SVG({int(svg_sc['width_px'])}px, 좌측) + 텍스트/표({int(txt_sc['width_px'])}px, 우측) — display:flex")
        if "keymsg" in names:
            layout_lines.append("key-msg: 컨테이너 최하단 전체 폭, flex-shrink:0")

        table_rows = role_layout.get("table_rows", 0)
        if table_rows > 0:
            layout_lines.append(f"보충 표: {table_rows}행 (텍스트 아래 여유 공간에 배치)")

        parts.append("\n".join(layout_lines) + "\n")

    # ── Phase V: Stage 1.8 결과를 프롬프트에 반영 ──
    fit_result = phase_t.get("fit_result", {})
    enhancements = phase_t.get("enhancements", {})

    # 재배분된 컨테이너 크기
    redist = fit_result.get("redistribution", {})
    if redist.get(role):
        redistributed_h = int(redist[role])
        parts.append(f"## 컨테이너 크기 (재배분 후)\n- height: {redistributed_h}px, width: {width_px}px\n- 이 크기를 절대 초과하지 마라. overflow 금지.\n")

    # 강조 블록
    for eb in enhancements.get("emphasis_blocks", []):
        if eb.get("role") == role:
            sentence = eb.get("sentence", "")
            parts.append(f"## 강조 (Phase V)\n다음 문장을 강조 블록으로 처리하라 (배경색 반전, bold):\n\"{sentence}\"\n")

    # bold 키워드
    role_bolds = enhancements.get("bold_keywords", {}).get(role, [])
    if role_bolds:
        parts.append(f"## bold 키워드 (Phase V)\n다음 키워드가 본문에 나올 때 <strong>으로 감싸라:\n{role_bolds}\n")

    # 보충 블록 + Step 7 표 행 수
    table_rows = role_layout.get("table_rows", 0)
    for sb in enhancements.get("supplement_blocks", []):
        if sb.get("role") == role:
            row_hint = f"\n- 표 행 수: {table_rows}행 (Step 7 계산 결과)" if table_rows > 0 else ""
            parts.append(f"## 보충 콘텐츠 (Phase V)\n여유 공간에 다음 콘텐츠의 핵심 요약을 넣어라:\n- 출처: {sb.get('content_source', '')}\n- 블록: {sb.get('block_id', '')}{row_hint}\n")

    # V-4: Kei 에스컬레이션 결정 (공간 부족 시 Kei가 내린 판단)
    for kd in enhancements.get("kei_decisions", []):
        if kd.get("role") == role:
            action = kd.get("action", "")
            detail = kd.get("detail", "")
            if action == "inline":
                parts.append(f"## Kei 결정 (V-4): 인라인 축약\n{detail}\n사례/근거를 괄호 한 줄로 축약하라. 상세는 팝업 링크로.\n")
            elif action == "trim":
                parts.append(f"## Kei 결정 (V-4): 텍스트 축약\n{detail}\n핵심만 남기고 분량을 줄여라.\n")
            elif action == "popup":
                parts.append(f"## Kei 결정 (V-4): 팝업 분리\n{detail}\n상세 내용을 제거하고 \"상세보기\" 링크만 남겨라.\n")
            elif action == "merge":
                parts.append(f"## Kei 결정 (V-4): 꼭지 합치기\n{detail}\n여러 꼭지를 하나의 흐름으로 자연스럽게 연결하라.\n")

    # V-7: 종속 꼭지 처리 지시
    for st in enhancements.get("subordinate_treatments", []):
        if st.get("role") == role:
            detail = st.get("detail", {})
            treatment = detail.get("treatment", "inline")
            s_tid = detail.get("supporting_topic_id", "?")
            s_purpose = detail.get("has_popup", False)
            popup_title = detail.get("popup_title", "")
            if treatment == "inline":
                parts.append(f"## 종속 꼭지 처리 (V-7)\n꼭지{s_tid}의 내용을 인라인 1~2줄로 축약하여 주 블록 안에 삽입하라.\n" +
                             (f"팝업 \"{popup_title}\" 참조가 있으면 링크만 남겨라.\n" if popup_title else ""))
            elif treatment == "sub_block":
                parts.append(f"## 종속 꼭지 처리 (V-7)\n꼭지{s_tid}의 내용을 하위 블록(border-left + 들여쓰기)으로 분리하여 주 블록 아래에 배치하라.\n")

    # ── 들여쓰기 예시 HTML (TP-4: Sonnet이 정확히 따르도록 구체적 예시 제공) ──
    indent_example = f"""<div style="padding-left:{indent_pl}px; text-indent:{indent_ti}px; font-size:{font_size}px;">• 첫줄 텍스트가 여기서 시작하고
둘째줄도 정확히 같은 위치에서 시작한다</div>"""

    # ── 역할별 지시 ──
    if role == "배경":
        parts.append(f"""다음 콘텐츠를 배경(보조) 영역 HTML로 만들어라.

## 핵심 원칙
이 영역은 **보조 영역**이다. 본심(핵심)보다 시각적으로 **반드시 약해야** 한다.
- 다크 배경(#1a~#2a 계열) 절대 금지. 밝은 톤만 사용.
- 본심이 슬라이드의 주인공. 이 영역은 조용하고 가벼워야 한다.

## 크기
- width: 100% (body 영역 전체 폭), height: {height_px}px
- 본심과 가로 폭이 반드시 동일해야 한다.

## 폰트
- 이 영역의 폰트: {font_size}px. 제목은 {font_size + 1}px bold.
- 이보다 큰 폰트 사용 금지. (위계: 핵심{fh.get('key_msg',14)}px > 본심{fh.get('core',12)}px >= 배경{font_size}px > 첨부{fh.get('sidebar',10)}px)

## 들여쓰기 — 반드시 아래 예시를 정확히 따라라 (TP-4)
{indent_example}

불릿이 있으면 반드시 위 style을 그대로 사용. padding-left:{indent_pl}px; text-indent:{indent_ti}px;""")

    elif role == "본심":
        img_instruction = ""
        if images:
            for img in images:
                img_id = f"slide-img-{img.get('topic_id', '')}"
                img_instruction = f"""
## 이미지 (TP-2: 텍스트가 주인공, 이미지는 보조)
- <img id="{img_id}" src="placeholder"> (후처리에서 교체)
- 이미지는 반드시 float:right. 텍스트 옆에 배치. 이미지가 전체 폭을 차지하면 안 됨.
- 이미지 width: 최대 250px. 텍스트가 이미지를 감싸도록.
- 이미지가 주인공이 아니다. 텍스트가 주인공이다."""

        parts.append(f"""다음 콘텐츠를 본심(핵심) 영역 HTML로 만들어라.

## 핵심 원칙 (TP-2)
이 영역이 슬라이드의 **주인공**이다. 가장 큰 시각적 비중.
- **텍스트가 주인공**, 이미지/도형은 텍스트를 보조하는 역할.
- 핵심 메시지(key-msg)가 시각적으로 **가장 눈에 띄어야** 함.
- key-msg에 배경색 + 테두리 + 큰 폰트를 적용하여 강조.

## 크기
- width: 100% (body 영역 전체 폭), max-height: {height_px}px
- 배경과 가로 폭이 반드시 동일해야 한다.

## 폰트
- 이 영역의 본문 폰트: {font_size}px. line-height: 1.75.
- 핵심 메시지(key-msg): {fh.get('key_msg', 14)}px bold. 반드시 class="key-msg" 포함.
- 이 영역에서 {fh.get('key_msg', 14)}px보다 큰 폰트 사용 금지.

## 들여쓰기 — 반드시 아래 예시를 정확히 따라라 (TP-4)
{indent_example}

주불릿: padding-left:{indent_pl}px; text-indent:{indent_ti}px;
부불릿: padding-left:{indent_pl * 2}px; text-indent:{indent_ti}px;

## 핵심 메시지 (반드시 포함)
- 하단에 key-msg 영역: "{core_message}"
- HTML: <div class="key-msg" style="font-size:{fh.get('key_msg', 14)}px; font-weight:bold; padding:8px; border-radius:6px; text-align:center; margin-top:10px;">...</div>

## 팝업/상세 내용 (TP-5: 링크 위치)
- 상세 내용(비교표 등)은 본문에 넣지 마라. 별도 첨부 파일로 분리됨.
- "상세보기" 링크를 **해당 섹션 제목 옆 우측**에 작게 배치 (10px, #2563eb).
- 예시:
  <div style="display:flex; justify-content:space-between; align-items:center;">
    <span style="font-weight:bold;">섹션 제목</span>
    <span style="font-size:10px; color:#2563eb;">상세보기 →</span>
  </div>
- 본문 중간에 한 줄로 넣지 마라. 동일 내용을 2번 넣지 마라.
{img_instruction}""")

    elif role == "첨부":
        parts.append(f"""다음 콘텐츠를 sidebar 영역 HTML로 만들어라.

## 크기 (TP-3: 잘림 방지)
- width: 100% (부모 grid cell에 맞춤). height: {height_px}px.
- 최외곽 div에 width:100%를 쓰라. 절대 px 값으로 width를 지정하지 마라.
- 이 크기 안에 **모든 내용이 들어가야** 한다. 넘치면 폰트를 줄여서 맞춰라.
- 각 카드 width: 100%. 컨테이너 밖으로 넘치면 안 됨.
- word-break: break-word (긴 영문도 줄바꿈)

## 폰트
- 이 영역의 폰트: {font_size}px. 제목은 {font_size + 1}px bold.
- 이보다 큰 폰트 사용 금지. (위계: 이 영역은 가장 작은 폰트)

## 들여쓰기 — 반드시 아래 예시를 정확히 따라라 (TP-4)
{indent_example}

## 카드 구조
- 각 용어를 카드로 구분. 카드 내부 padding 포함하여 width 100% 안에 맞출 것.
- 카드 간 간격 8px.
- 출처가 있으면 카드 하단에 작게 ({max(font_size - 2, 8)}px).""")

    elif role == "결론":
        parts.append(f"""다음 콘텐츠를 결론 배너 HTML로 만들어라.

## 크기
- width: 100%, height: {height_px}px

## 폰트
- 핵심 메시지: {font_size}px bold white
- 이 영역은 핵심 메시지 한 줄. 가장 큰 폰트.""")

    # ── 공통: 콘텐츠 ──
    parts.append(f"""
## 콘텐츠 (축약/요약/삭제 금지. 원본 텍스트를 그대로 사용.)
{content_block}""")

    # ── 공통: 텍스트 규칙 ──
    parts.append(_COMMON_TEXT_RULES)

    # ── 블록 레퍼런스 (있으면) ──
    if ref_html:
        if len(ref_html) > 3000:
            ref_html = ref_html[:3000] + "\n<!-- truncated -->"
        parts.append(f"""
## 디자인 레퍼런스 — 이 HTML의 구조와 색상 패턴을 따르되, 콘텐츠를 교체하라.
구조(레이아웃, 색상 배치, 카드/불릿 패턴)를 따르고, 텍스트만 원본으로 교체.
발명하지 마라. 이 구조를 따라라.

{ref_html}""")

    # ── 디자인 예산 (있으면) ──
    if avail_h > 0:
        parts.append(f"""
## 디자인 예산
- 텍스트 영역 확보 후 남은 공간: 높이 {avail_h}px, 너비 {avail_w}px
- 도형/이미지/배경색 영역은 이 예산 안에서 배치.""")

    # ── 공통: HTML 규칙 ──
    parts.append(_COMMON_HTML_RULES)

    return "\n".join(parts)


# Phase S 레거시 프롬프트 — build_area_prompt()로 교체됨. 참고용으로만 보존.
_LEGACY_CORE_PROMPT = """다음 콘텐츠를 본심 영역 HTML로 만들어라.

## 크기: width:100%, max-height: {height}px, overflow: hidden (반드시 적용)

## 참고할 CSS 구조 (이 CSS를 반드시 그대로 사용하라. 특히 .fi의 float:right는 절대 빼지 마라.):
```css
.core {{
    width: 100%;
    max-height: {height}px;
    margin-top: 0;
    font-family: 'Pretendard Variable', sans-serif;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    padding: 14px 18px;
    overflow: hidden;
    word-break: keep-all;
}}
.core-header {{
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}}
.core-label {{
    background: #1e293b;
    color: #ffffff;
    font-size: 12px;
    font-weight: 700;
    padding: 3px 12px;
    border-radius: 4px;
}}
.popup-link {{
    font-size: 10px;
    color: #2563eb;
    font-weight: 700;
    cursor: pointer;
    text-decoration: underline;
}}
.fi {{
    float: right;
    margin: {img_margin_top}px 0 8px 12px;
    width: {img_width}px;
}}
.fi img {{ width: 100%; }}
.fi .cap {{
    font-size: 9px;
    color: #94a3b8;
    text-align: center;
    margin-top: 1px;
}}
.core-text {{
    font-size: 12px;
    color: #1e293b;
    line-height: 1.75;
}}
.bp {{
    padding-left: 14px;
    text-indent: -14px;
    margin-bottom: 5px;
}}
.bp::before {{
    content: '•';
    display: inline-block;
    width: 14px;
    text-indent: 0;
    color: #1e293b;
    font-weight: 700;
}}
.sp {{
    padding-left: 28px;
    text-indent: -14px;
    margin-bottom: 4px;
    font-size: 11px;
    color: #475569;
}}
.sp::before {{
    content: '◦';
    display: inline-block;
    width: 14px;
    text-indent: 0;
    color: #64748b;
}}
.core-text b {{ font-weight: 700; color: #1e293b; }}
.key-msg {{
    background: #f0f9ff;
    border: 2px solid #bae6fd;
    border-radius: 6px;
    padding: 5px 12px;
    text-align: center;
    font-size: 14px;
    font-weight: 700;
    color: #0c4a6e;
    margin-top: 8px;
    clear: both;
}}
.key-msg em {{
    color: #dc2626;
    font-style: normal;
    font-weight: 900;
}}
```

## 참고할 HTML 구조 (이 구조를 그대로 따르되 텍스트만 교체):
```html
<div class="core">
    <div class="core-header">
        <div class="core-label">제목 라벨</div>
    </div>
    <div class="core-text">
        <div class="fi">
            <img id="slide-img-ID" src="placeholder">
            <div class="cap">이미지 캡션 (topic 제목을 사용)</div>
        </div>
        <div class="bp">메인 불릿 1</div>
        <div class="bp">메인 불릿 2</div>
        <div class="sp"><b>하위 항목</b> : 설명</div>
    </div>
    <div class="key-msg">
        핵심 메시지 텍스트
    </div>
</div>
```

## 핵심 메시지 (하단 key-msg 박스에 이 텍스트 그대로 사용. 임의 라벨/요약을 넣지 말고 아래 텍스트 그대로.)
{core_message}

## 콘텐츠 (축약/요약/삭제 금지. 원본 텍스트 80-95% 그대로 사용.)
{content_block}

## 텍스트 규칙 (반드시 적용)
1. 원본 텍스트의 단어를 한 글자도 빼지 마라. 축약/요약 절대 금지.
2. 마침표(.)로 끝나는 문장이 2개 이상이면 각각 별도 불릿(•)으로 분리.
3. 개조식 어미 변환: 문장 끝 1-2글자만 변환. 그 외 단어는 절대 건드리지 마라.
   - "~있다" → "~있음", "~한다" → "~함", "~이다" → 삭제, "~된다" → "~됨"
   예: "활용하는 과정이다" → "활용하는 과정" (끝만 변환)
4. 원본에 없는 텍스트를 추가하지 마라.

{img_instruction}

## 팝업 (<details>/<summary>)
원본 콘텐츠에 비교표/대조 구조가 있으면 <details>/<summary>로 팝업 구현하라.
없으면 팝업을 만들지 마라. 팝업 제목은 콘텐츠에 맞게 자동 결정.
팝업은 우측 상단에 1개만. 중복 금지.

## 절대 규칙
- .core에 margin-top을 넣지 마라 (간격은 외부에서 처리됨). margin-top: 0 필수.
- 원본에 없는 텍스트나 라벨을 임의로 넣지 마라.

HTML + inline <style>만 반환. 위 CSS와 HTML 구조를 정확히 따르라. 설명 없이 코드만."""


_LEGACY_SIDEBAR_PROMPT = """다음 용어 정의를 sidebar 카드로 만들어라. {width}px × {height}px.

## 용어 (축약/요약/삭제 금지. 원본 텍스트를 한 글자도 바꾸지 말고 그대로 사용.)
{definitions_block}

## 디자인 요구사항
1. 최상위 div: width:{width}px, height:{height}px, overflow:hidden (반드시 적용, 넘치면 잘림)
2. 상단에 "용어 정의" 라벨 (카드 좌측 시작점에 맞춰 좌측 정렬, 10px #64748b)
3. 각 용어를 카드로:
   - 배경: #f8fafc, 테두리: 1px solid #e2e8f0, border-radius: 8px, padding: 14px
   - 용어명: 11px bold #1e293b
   - 부제 금지: 원본에 없는 텍스트를 만들어 넣지 마라. 용어명 아래에 임의 설명을 추가하지 마라.
   - 불릿: 10px #475569, line-height: 1.6, 불릿 마커 "•"
   - 들여쓰기: 인라인 style만 사용 (CSS class 사용 금지).
     각 불릿 div에 style="padding-left:14px; text-indent:-14px; margin-bottom:4px; font-size:10px; color:#475569; line-height:1.6;" 적용.
     불릿 마커는 "• " 텍스트로 직접 삽입 (::before 금지).
   - 각 불릿은 원본 텍스트 그대로. 단어를 한 글자도 빼지 마라.
   - 마침표(.)로 끝나는 문장이 2개 이상이면 별도 불릿(•)으로 분리하라.
   - 개조식 어미 변환: 문장 끝 1-2글자만 변환 ("~이다"→삭제, "~있다"→"~있음"). 그 외 단어 절대 건드리지 마라.
4. 카드 간 간격 10px
5. {height}px 안에 맞출 것. 넘치면 폰트를 줄여서 맞출 것.

HTML만 반환. <style> 블록 금지. 모든 스타일은 인라인 style 속성으로. 설명 없이 코드만."""


_LEGACY_FOOTER_PROMPT = """결론 배너 HTML.

## 콘텐츠 (축약/요약/삭제 금지. 원본 텍스트를 그대로 사용.)
{content_block}

## 텍스트 규칙
- 원본 텍스트의 단어를 한 글자도 빼지 마라. 축약 절대 금지.
- 개조식 어미 변환: 문장 끝 1-2글자만 변환. 그 외 단어는 절대 건드리지 마라.
  예: "일부분이다" → "일부분", "필요하다" → "필요" (끝만 변환, 앞 문장 그대로)

## 디자인
- 배너: linear-gradient(135deg, #006aff, #00aaff), border-radius: 8px
- 핵심 메시지: 15px bold white
- 부가 텍스트: 11px, opacity: 0.85
- padding: 14px 30px, text-align: center, height: {height}px

HTML + inline <style>만 반환. 설명 없이 코드만."""


# ═══════════════════════════════════════════════════════════
# Phase T-7: 프롬프트에 레퍼런스 + 수치 주입
# ═══════════════════════════════════════════════════════════

def _build_phase_t_supplement(role: str, analysis: dict) -> str:
    """Phase T context가 있으면 프롬프트 보충 섹션을 생성.

    폰트 위계, 디자인 예산, 레퍼런스 HTML을 구체적 수치로 전달.
    Phase S 교훈: "구체적 프롬프트는 합격, 추상적 프롬프트는 실패"
    → px, 폰트 크기, 줄 수를 숫자로 넣되 context에서 동적으로 가져옴.
    """
    phase_t = analysis.get("phase_t")
    if not phase_t:
        return ""

    parts = []

    # 1. 폰트 위계 (역할별 확정 폰트)
    fh = phase_t.get("font_hierarchy", {})
    role_font_map = {"배경": "bg", "본심": "core", "첨부": "sidebar", "결론": "core"}
    font_key = role_font_map.get(role, "core")
    font_size = fh.get(font_key, 12)
    parts.append(
        f"\n[폰트 위계 — 반드시 준수]\n"
        f"이 영역({role})의 확정 폰트: {font_size}px\n"
        f"전체 위계: 핵심={fh.get('key_msg', 14)}px > 본심={fh.get('core', 12)}px "
        f">= 배경={fh.get('bg', 11)}px > 첨부={fh.get('sidebar', 10)}px\n"
        f"이 영역에서 {font_size}px보다 큰 폰트를 사용하지 마라. 위계가 깨진다."
    )

    # 2. 디자인 예산 (남은 공간)
    budgets = phase_t.get("design_budgets", {})
    budget = budgets.get(role, {})
    if budget:
        parts.append(
            f"\n[디자인 예산]\n"
            f"텍스트 영역 확보 후 남은 공간:\n"
            f"- 가용 높이: {budget.get('available_height_px', 0)}px\n"
            f"- 가용 너비: {budget.get('available_width_px', 0)}px\n"
            f"- 원형 요소 최대: {budget.get('max_circle_diameter', 0)}px\n"
            f"- 이미지 최대: {budget.get('max_img_width', 0)}×{budget.get('max_img_height', 0)}px\n"
            f"디자인 요소(도형, 이미지, 배경색 영역)는 이 예산 안에서 배치하라."
        )

    # 3. V-1: 꼭지별 디자인 레퍼런스 HTML (리스트)
    refs = phase_t.get("references", {})
    ref_list = refs.get(role, [])
    if isinstance(ref_list, dict):
        ref_list = [ref_list]
    ref_html = "\n\n".join(r.get("design_reference_html", "") for r in ref_list if r)
    if ref_html:
        # 너무 길면 잘라서 토큰 절약
        if len(ref_html) > 3000:
            ref_html = ref_html[:3000] + "\n<!-- ... (truncated) -->"
        parts.append(
            f"\n[디자인 레퍼런스 — 구조와 색상 패턴을 참고하되 그대로 복사하지 마라]\n"
            f"{ref_html}"
        )

    return "\n".join(parts) if parts else ""


# ═══════════════════════════════════════════════════════════
# 메인 함수
# ═══════════════════════════════════════════════════════════

async def generate_slide_html(
    content: str,
    analysis: dict[str, Any],
    container_specs: dict,
    preset: dict[str, Any],
    images: list[dict] | None = None,
) -> dict[str, str]:
    """Phase S: 영역별 개별 호출, 검증 합격 프롬프트 템플릿 사용."""
    if images is None:
        images = []

    client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)

    page_struct = analysis.get("page_structure", {})
    topics = analysis.get("topics", [])
    topic_map = {t["id"]: t for t in topics}

    def get_topics_for_role(role: str) -> list[dict]:
        info = page_struct.get(role, {})
        if not isinstance(info, dict):
            return []
        return [topic_map[tid] for tid in info.get("topic_ids", []) if tid in topic_map]

    bg_topics = get_topics_for_role("배경")
    core_topics = get_topics_for_role("본심")
    ref_topics = get_topics_for_role("첨부")
    conclusion_topics = get_topics_for_role("결론")

    bg_spec = container_specs.get("배경")
    core_spec = container_specs.get("본심")
    ref_spec = container_specs.get("첨부")
    concl_spec = container_specs.get("결론")

    result = {"body_html": "", "sidebar_html": "", "footer_html": "", "reasoning": ""}

    # ── 실제 zone 높이: containers에서 온 값 사용 (하드코딩 아님) ──
    from src.fit_verifier import _load_design_tokens
    tokens = _load_design_tokens()
    bg_h = bg_spec.height_px if bg_spec else 0
    core_h = core_spec.height_px if core_spec else 0
    footer_h = concl_spec.height_px if concl_spec else 0
    sidebar_h = ref_spec.height_px if ref_spec else 0
    # body zone = 배경 + 본심 + gap
    bg_core_gap = tokens["spacing_small"]
    body_zone_h = bg_h + core_h + (bg_core_gap if bg_topics and core_topics else 0)
    sidebar_zone_h = sidebar_h if sidebar_h > 0 else body_zone_h
    # core_max_h: 본심 컨테이너 높이에서 key-msg 높이를 빼야 Sonnet이 넘치지 않음
    phase_t = analysis.get("phase_t", {})
    core_sub = phase_t.get("sub_layouts", {}).get("본심", {})
    keymsg_sub_h = 0
    for sc in core_sub.get("sub_containers", []):
        if sc.get("name") == "keymsg":
            keymsg_sub_h = sc.get("height_px", 0)
    core_max_h = core_h - keymsg_sub_h if core_h > 0 else (body_zone_h - bg_h - bg_core_gap if bg_topics else body_zone_h)
    logger.info(f"[Phase S] zone 계산: body={body_zone_h}px, sidebar={sidebar_zone_h}px, bg={bg_h}px, core_max={core_max_h}px (keymsg={keymsg_sub_h}px 제외)")

    # Phase T context
    phase_t = analysis.get("phase_t", {})

    # 원본 텍스트 매핑
    sections = _slice_mdx_sections(content)

    # ── 콘텐츠 텍스트 가져오기: structured_text 우선, 없으면 sections 매칭 fallback ──
    def _get_role_content(role_topics):
        """structured_text를 우선 사용. 없으면 기존 sections 매칭."""
        texts = []
        for t in role_topics:
            st = t.get("structured_text", "")
            if st:
                texts.append(st)
            else:
                # fallback: source_hint 키워드로 sections에서 매칭
                keywords = _extract_keywords_from_hints([t])
                matched = _map_sections_for_role(sections, [t], keywords)
                if matched:
                    texts.append(matched)
        return "\n\n".join(texts) if texts else ""

    # ── 배경 ──
    if bg_topics:
        logger.info("[Phase T] 배경 생성...")
        bg_content = _get_role_content(bg_topics)
        body_width = bg_spec.width_px if bg_spec else (core_spec.width_px if core_spec else 0)
        prompt = build_area_prompt(
            role="배경",
            content_block=bg_content,
            phase_t=phase_t,
            height_px=bg_h,
            width_px=body_width,
        )
        html = await _call_claude(client, prompt)
        if html:
            result["body_html"] += html + f'\n<div style="height:{bg_core_gap}px;"></div>\n'
            logger.info(f"[Phase T] 배경 완료: {len(html)}자")

    # ── 본심 ──
    if core_topics:
        logger.info("[Phase T] 본심 생성...")
        core_content = _get_role_content(core_topics)
        core_images = [img for img in images if img.get("topic_id") in [t["id"] for t in core_topics]]
        body_width = core_spec.width_px if core_spec else (bg_spec.width_px if bg_spec else 0)
        prompt = build_area_prompt(
            role="본심",
            content_block=core_content,
            phase_t=phase_t,
            height_px=core_max_h,
            width_px=body_width,
            images=core_images,
            core_message=analysis.get("core_message", ""),
        )
        html = await _call_claude(client, prompt)
        if html:
            html = _replace_img_placeholder(html, images)
            result["body_html"] += html + "\n"
            logger.info(f"[Phase T] 본심 완료: {len(html)}자")

    # ── sidebar ──
    if ref_topics:
        logger.info("[Phase T] sidebar 생성...")
        sidebar_content = _get_role_content(ref_topics)
        sidebar_width = ref_spec.width_px if ref_spec else 0
        prompt = build_area_prompt(
            role="첨부",
            content_block=sidebar_content,
            phase_t=phase_t,
            height_px=sidebar_zone_h,
            width_px=sidebar_width,
        )
        html = await _call_claude(client, prompt)
        if html:
            result["sidebar_html"] = html
            logger.info(f"[Phase T] sidebar 완료: {len(html)}자")

    # ── footer ──
    if conclusion_topics:
        logger.info("[Phase T] footer 생성...")
        footer_content = _get_role_content(conclusion_topics) or _get_conclusion(content)
        footer_width = concl_spec.width_px if concl_spec else 0
        prompt = build_area_prompt(
            role="결론",
            content_block=footer_content.strip(),
            phase_t=phase_t,
            height_px=concl_spec.height_px if concl_spec else 0,
            width_px=footer_width,
        )
        html = await _call_claude(client, prompt)
        if html:
            result["footer_html"] = html
            logger.info(f"[Phase T] footer 완료: {len(html)}자")

    result["reasoning"] = "영역별 개별 호출, 검증 합격 프롬프트 템플릿 사용."
    return result


# ═══════════════════════════════════════════════════════════
# 콘텐츠 추출 함수
# ═══════════════════════════════════════════════════════════

def normalize_mdx(raw_mdx: str) -> str:
    """MDX를 ## 섹션 기반 표준 구조로 정규화 (0단계).

    다양한 MDX 포맷을 ## 섹션 + 순수 텍스트로 통일.
    패턴은 계속 추가될 수 있음 — MDX 문법 기반 범용 처리.

    처리 패턴:
    - frontmatter (---...---) 제거
    - import 문 제거
    - <br/>, 장식용 --- 제거
    - JSX div style 태그 → 내부 텍스트만
    - 커스텀 컴포넌트 (<Component />) 제거
    - <details><summary> → 태그 제거, 내용 유지
    - :::directive[제목] → ## 승격
    - ## N. 제목 → ## 제목 (번호 제거)
    - ### N.N 제목 → ### 제목 (번호 제거)
    - * **제목** (## 전 도입부) → ## 승격
    - ![alt](path) → [이미지] 참조 보존
    - *이탤릭 출처* → 출처: 텍스트
    """
    text = raw_mdx

    # frontmatter 제거
    text = re.sub(r"^---\n.*?\n---\n*", "", text, flags=re.DOTALL)

    # import 문 제거
    text = re.sub(r"^import\s+.+$", "", text, flags=re.MULTILINE)

    # <br/> 제거
    text = re.sub(r"<br\s*/?>", "", text)

    # JSX div style → 태그만 제거
    text = re.sub(r"<div\s+style=\{\{[^}]*\}\}>", "", text)
    text = text.replace("</div>", "")

    # 커스텀 컴포넌트 태그 제거 (<Component />, <Component>...</Component>)
    text = re.sub(r"<[A-Z]\w+\s*/>", "", text)
    text = re.sub(r"<[A-Z]\w+[^>]*>.*?</[A-Z]\w+>", "", text, flags=re.DOTALL)

    # <details>/<summary> → 태그 제거, 내용 유지
    text = re.sub(r"<details>\s*", "", text)
    text = re.sub(r"<summary[^>]*>(.+?)</summary>", r"[\1]", text)
    text = re.sub(r"</details>", "", text)

    # :::directive[제목] → ## 승격
    text = re.sub(r":::(\w+)\[(.+?)\]", r"## \2", text)
    text = re.sub(r"^:::\s*$", "", text, flags=re.MULTILINE)

    # ## N. 제목 → ## 제목 (번호 제거)
    text = re.sub(r"^## \d+\.\s*", "## ", text, flags=re.MULTILINE)

    # ### N.N 제목 → ### 제목 (번호 제거)
    text = re.sub(r"^### \d+\.\d+\s*", "### ", text, flags=re.MULTILINE)

    # * **제목** → ## 승격 (## 전 도입부에서만)
    first_hash = text.find("\n## ")
    if first_hash == -1:
        first_hash = len(text)
    intro = text[:first_hash]
    rest = text[first_hash:]
    intro = re.sub(r"^\* \*\*(.+?)\*\*\s*$", r"## \1", intro, flags=re.MULTILINE)
    text = intro + rest

    # 이미지 참조 보존
    text = re.sub(r"!\[(.+?)\]\((.+?)\)", r"[이미지: \1, 경로: \2]", text)

    # 이탤릭 출처 (단독 줄)
    text = re.sub(r"^\s*\*([^*\n]+)\*\s*$", r"출처: \1", text, flags=re.MULTILINE)

    # 장식용 --- 제거
    text = re.sub(r"^---\s*$", "", text, flags=re.MULTILINE)

    # 연속 빈 줄 정리
    text = re.sub(r"\n{3,}", "\n\n", text)

    return text.strip()


def _slice_mdx_sections(content: str) -> dict[str, str]:
    """원본 MDX를 정규화 후 ## 기준으로 섹션별 슬라이싱.

    0단계: normalize_mdx()로 MDX 표준화
    1단계: ## 기준으로 분할
    """
    # 0단계: MDX 정규화
    normalized = normalize_mdx(content)

    sections = {}
    current_section = None
    current_lines = []

    for line in normalized.split("\n"):
        if line.startswith("## "):
            if current_section:
                sections[current_section] = "\n".join(current_lines).strip()
            current_section = line[3:].strip()
            current_lines = []
        elif current_section:
            current_lines.append(line)

    if current_section:
        sections[current_section] = "\n".join(current_lines).strip()

    return sections


def _map_sections_for_role(
    sections: dict[str, str],
    role_topics: list[dict],
    fallback_keywords: list[str],
) -> str:
    """역할의 topics에 해당하는 원본 MDX 섹션을 매핑하여 반환.

    1차: topic의 source_hint에서 섹션명 매칭
    2차: fallback_keywords로 섹션명 검색

    source_data는 사용하지 않음 (Kei 메모 포함 가능).
    원본 MDX 텍스트만 반환.
    """
    matched = []
    matched_names = set()

    # 1차: source_hint 기반 매칭
    for t in role_topics:
        hint = t.get("source_hint", "")
        if hint:
            for sec_name, sec_text in sections.items():
                if sec_name in hint and sec_name not in matched_names:
                    matched.append(f"### {sec_name}")
                    matched.append(sec_text)
                    matched.append("")
                    matched_names.add(sec_name)

    # 2차: fallback keywords
    if not matched:
        for sec_name, sec_text in sections.items():
            if any(kw in sec_name for kw in fallback_keywords) and sec_name not in matched_names:
                matched.append(f"### {sec_name}")
                matched.append(sec_text)
                matched.append("")
                matched_names.add(sec_name)

    # topic 메타정보 추가 (제목, 표현 의도 — source_data 제외)
    meta = []
    for t in role_topics:
        meta.append(f"제목 라벨: \"{t.get('title', '')}\"")
        hint = t.get("expression_hint", "")
        if hint:
            meta.append(f"표현 의도: {hint}")

    result = "\n".join(meta) + "\n\n" + "\n".join(matched) if matched else "\n".join(meta)
    return result.strip()


def _extract_keywords_from_hints(topics: list[dict]) -> list[str]:
    """source_hint에서 섹션명 키워드를 동적 추출.

    예: "원본의 '용어의 혼용' 섹션 전체 내용" → ["용어의", "혼용"]
    하드코딩 fallback 키워드를 대체한다.
    """
    keywords = []
    for t in topics:
        hint = t.get("source_hint", "")
        if not hint:
            continue
        # 따옴표 안의 섹션명 추출
        match = re.search(r"['\"]([^'\"]+)['\"]", hint)
        if match:
            section_name = match.group(1)
            words = [w for w in section_name.split() if len(w) >= 2]
            keywords.extend(words)
        else:
            # 따옴표 없으면 hint에서 2글자 이상 단어 추출
            words = [w for w in hint.split() if len(w) >= 2]
            keywords.extend(words[:3])  # 최대 3개
    return keywords


def _get_definitions(content: str) -> str:
    """용어 정의: 원본 MDX에서 용어별 정의 섹션을 그대로 추출."""
    sections = _slice_mdx_sections(content)
    for sec_name, sec_text in sections.items():
        if "용어" in sec_name and "정의" in sec_name:
            return sec_text
    # fallback: "정의" 포함 섹션
    for sec_name, sec_text in sections.items():
        if "정의" in sec_name:
            return sec_text
    return "(원본에서 용어 정의를 찾지 못함)"


def _get_conclusion(content: str) -> str:
    """결론: 원본 MDX에서 핵심 요약/결론 섹션을 그대로 추출."""
    sections = _slice_mdx_sections(content)
    # "요약"이 포함된 섹션을 우선 매칭 (핵심만 포함된 섹션과 구분)
    for sec_name, sec_text in sections.items():
        if "요약" in sec_name:
            return sec_text
    for sec_name, sec_text in sections.items():
        if "결론" in sec_name:
            return sec_text
    # fallback: 마지막 섹션
    if sections:
        return list(sections.values())[-1]
    return ""


# ═══════════════════════════════════════════════════════════
# Claude 호출 + 이미지 교체
# ═══════════════════════════════════════════════════════════

async def _call_claude(client, prompt: str) -> str | None:
    """Claude Sonnet 호출 → HTML 추출."""
    try:
        response = await client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=8192,
            messages=[{"role": "user", "content": prompt}],
        )
        text = response.content[0].text if response.content else ""
        if not text:
            return None

        match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
        if match:
            return match.group(1).strip()

        match = re.search(r"(<(?:div|style|section)[^>]*>.*)", text, re.DOTALL)
        if match:
            return match.group(1).strip()

        return text.strip()
    except Exception as e:
        logger.error(f"[Phase S] Claude 호출 실패: {e}")
        return None


async def regenerate_area(
    area_name: str,
    errors: list[str],
    content: str,
    analysis: dict[str, Any],
    container_specs: dict,
    preset: dict[str, Any],
    images: list[dict] | None = None,
) -> str | None:
    """실패한 영역을 에러 피드백과 함께 재생성.

    원래 프롬프트 끝에 검증 실패 사유를 추가하여 Claude에게 재생성 요청.
    전체를 재생성하지 않고 해당 영역만 재생성.
    """
    client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)

    # 에러 피드백 블록
    error_feedback = "\n\n## 이전 생성 결과 검증 실패. 다음 문제를 반드시 수정하라:\n"
    for i, err in enumerate(errors[:5], 1):
        error_feedback += f"{i}. {err}\n"
    error_feedback += "\n위 문제들을 해결한 새 HTML을 생성하라. 원본 텍스트를 축약/요약하지 마라."

    sections = _slice_mdx_sections(content)
    page_struct = analysis.get("page_structure", {})
    topics = analysis.get("topics", [])
    topic_map = {t["id"]: t for t in topics}

    def get_topics_for_role(role: str) -> list[dict]:
        info = page_struct.get(role, {})
        if not isinstance(info, dict):
            return []
        return [topic_map[tid] for tid in info.get("topic_ids", []) if tid in topic_map]

    if area_name == "sidebar":
        ref_spec = container_specs.get("첨부")
        # 실제 zone 높이 계산
        footer_h = container_specs.get("결론")
        footer_h = footer_h.height_px if footer_h else 0
        sidebar_zone_h = ref_spec.height_px if ref_spec else 0

        defs = _get_definitions(content)
        prompt = _LEGACY_SIDEBAR_PROMPT.format(
            width=ref_spec.width_px if ref_spec else 0,
            height=sidebar_zone_h,
            definitions_block=defs,
        ) + error_feedback

        logger.info(f"[재생성] sidebar 재생성 (에러 {len(errors)}건)")
        return await _call_claude(client, prompt)

    elif area_name == "footer":
        concl_spec = container_specs.get("결론")
        footer_content = _get_conclusion(content)
        prompt = _LEGACY_FOOTER_PROMPT.format(
            height=concl_spec.height_px if concl_spec else 0,
            content_block=footer_content.strip(),
        ) + error_feedback

        logger.info(f"[재생성] footer 재생성 (에러 {len(errors)}건)")
        return await _call_claude(client, prompt)

    else:
        # body_bg, body_core는 합본이므로 None 반환 → 호출자가 전체 body 재생성
        logger.info(f"[재생성] {area_name}은 body 전체 재생성 필요")
        return None


def _replace_img_placeholder(html: str, images: list[dict]) -> str:
    """placeholder 이미지를 base64로 교체."""
    for img in images:
        b64 = img.get("b64", "")
        if not b64:
            continue
        img_id = f"slide-img-{img.get('topic_id', 0)}"
        if img_id in html:
            data_uri = f"data:image/png;base64,{b64}"
            html = html.replace('src="placeholder"', f'src="{data_uri}"')
            html = html.replace("src='placeholder'", f"src='{data_uri}'")
            logger.info(f"[Phase S] 이미지 교체: {img_id}")
    return html

