"""Phase V — Stage 1.8: 콘텐츠-컨테이너 적합성 검증.

꼭지별 블록 선택 후, 각 컨테이너에 콘텐츠가 실제로 들어가는지 검증.
안 들어가면 재배분 시도 → 그래도 안 되면 Kei 에스컬레이션.

모든 수치는 동적 계산. font-size / font metric / line-height 외 하드코딩 없음.
레이아웃 수치는 tokens.css(디자인 토큰) 또는 catalog.yaml에서 읽어옴.
"""
from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


# ──────────────────────────────────────
# 디자인 토큰 로딩 (tokens.css에서)
# ──────────────────────────────────────

_tokens_cache: dict[str, int] | None = None


def _load_design_tokens() -> dict[str, int]:
    """tokens.css에서 spacing 변수를 읽어옴."""
    global _tokens_cache
    if _tokens_cache is not None:
        return _tokens_cache

    tokens_path = Path(__file__).parent.parent / "static" / "tokens.css"
    if not tokens_path.exists():
        raise FileNotFoundError(f"디자인 토큰 파일 없음: {tokens_path}")

    css = tokens_path.read_text(encoding="utf-8")
    tokens: dict[str, int] = {}
    for match in re.finditer(r"--spacing-(\w+):\s*(\d+)px", css):
        key = f"spacing_{match.group(1)}"
        tokens[key] = int(match.group(2))

    # border-width도 읽기
    for match in re.finditer(r"--border-width:\s*(\d+)px", css):
        tokens["border_width"] = int(match.group(1))
    if "border_width" not in tokens:
        # tokens.css에 --border-width가 있는지 확인
        for match in re.finditer(r"--accent-border:\s*(\d+)px", css):
            tokens["accent_border"] = int(match.group(1))

    _tokens_cache = tokens
    return tokens


# ──────────────────────────────────────
# 텍스트 높이 추정 (space_allocator 실측 기반)
# ──────────────────────────────────────

CHAR_WIDTH_RATIO = 0.947  # Pretendard 한글 실측 (font metric — font-size 계열)


def estimate_text_height(
    text_chars: int,
    font_size: float,
    available_width: float,
    line_height_ratio: float = 1.5,
) -> float:
    """텍스트를 주어진 폭에 넣으면 몇 px 높이가 필요한가."""
    if text_chars <= 0:
        return 0
    char_width = font_size * CHAR_WIDTH_RATIO
    # 최소 폭: spacing_page (슬라이드 패딩) 이상이어야 유효
    tokens = _load_design_tokens()
    min_width = tokens.get("spacing_page", 1)
    inner_width = max(min_width, available_width)
    chars_per_line = max(1, int(inner_width / char_width))
    total_lines = max(1, -(-text_chars // chars_per_line))  # ceil division
    return total_lines * (font_size * line_height_ratio)


# ──────────────────────────────────────
# 블록 오버헤드: 블록 구조별 정확 계산
# ──────────────────────────────────────

def estimate_block_overhead(block_id: str, catalog_entry: dict, item_count: int = 1) -> float:
    """블록의 텍스트 외 오버헤드(padding, 아이콘, 번호, 테두리 등).

    catalog.yaml의 padding_overhead_px 필드에서 읽음 — 하드코딩 아님.
    카드형 블록은 아이템 수에 비례하여 오버헤드 증가.
    """
    # catalog.yaml에서 padding_overhead_px 읽기
    base = catalog_entry.get("padding_overhead_px", 0)  # catalog에 없으면 0 (오버헤드 없음으로 처리)

    # 카드형 블록: per-item 오버헤드 (catalog의 값은 1아이템 기준)
    category = catalog_entry.get("category", "")
    if category == "cards" and item_count > 1:
        tokens = _load_design_tokens()
        card_gap = tokens["spacing_small"]  # 카드 간 간격 = --spacing-small
        return base * item_count + card_gap * (item_count - 1)

    return base


def estimate_image_height(
    topic_source_data: str,
    available_width: float,
    image_sizes: dict[str, dict] | None = None,
) -> float:
    """이 꼭지에 이미지가 있으면 높이를 추정.

    source_data에 '[이미지:' 참조가 있는 꼭지만 이미지를 가짐.
    image_sizes가 있으면 실제 이미지 크기에서 계산, 없으면 SVG 기준 추정.
    """
    if "[이미지:" not in topic_source_data:
        return 0

    # 실제 이미지 크기가 있으면 사용
    if image_sizes:
        for img_name, size_info in image_sizes.items():
            if isinstance(size_info, dict) and "width" in size_info and "height" in size_info:
                orig_w = size_info["width"]
                orig_h = size_info["height"]
                # 이미지 디스플레이 폭: 원본과 가용폭 중 작은 값
                img_display_width = min(available_width, orig_w)
                img_height = orig_h * (img_display_width / max(1, orig_w))
                return img_height

    # 이미지 크기 없으면 SVG 다이어그램으로 추정
    # catalog.yaml의 해당 블록 min_height_px를 SVG 높이로 사용
    # (venn-diagram min_height=300 등 — catalog에서 동적으로 가져옴)
    from src.block_reference import _load_catalog
    for b in _load_catalog():
        if b.get("category") == "visuals" and b.get("min_height_px", 0) > 0:
            # 시각화 블록의 min_height를 SVG 추정 높이로 사용
            # 실제 배치 시 available_width에 맞춰 조정됨
            return min(b["min_height_px"], available_width)
    # catalog에도 없으면 가용 폭 기준 정사각형
    return available_width
    return img_height


def estimate_keymsg_height(core_message: str, font_size: float) -> float:
    """key-msg 배너 높이. tokens.css의 spacing에서 padding 읽음."""
    if not core_message:
        return 0
    tokens = _load_design_tokens()
    # key-msg padding = --spacing-small (상하) + border (--border-width × 2)
    padding_v = tokens["spacing_small"] * 2  # 상 + 하
    border_w = tokens.get("border_width", tokens.get("accent_border", 1))
    border_v = border_w * 2  # 상 + 하
    text_h = font_size * 1.4  # line-height (typography constant)
    return text_h + padding_v + border_v


def count_items_in_topic(topic: dict, role: str, block_schema: dict | None = None) -> int:
    """꼭지의 아이템 수 추정 — 블록 schema 기반.

    블록 schema에 max_items/max_cards/max_steps 등 리스트형 슬롯이 있으면
    source_data에서 구분 가능한 항목 수를 파싱.
    리스트형 슬롯이 없으면 1 반환.

    하드코딩 없음: 블록 schema + source_data 패턴으로만 판단.
    """
    source_data = topic.get("source_data", "")

    # 블록 schema에서 리스트형 슬롯 확인
    if block_schema:
        max_keys = [k for k in block_schema if k.startswith("max_")]
        if max_keys:
            # 리스트형 블록 — source_data에서 항목 수 파싱
            return _count_items_from_source(source_data)

    return 1


def _count_items_from_source(source_data: str) -> int:
    """source_data에서 구분 가능한 항목 수를 파싱.

    패턴 우선순위:
    1. "이름(설명), 이름(설명)" → 괄호+쉼표로 구분
    2. "- 항목\n- 항목" → 줄바꿈+불릿으로 구분
    3. "항목, 항목, 항목" → 쉼표로 구분
    """
    import re

    # 패턴 1: "이름(설명), 이름(설명)"
    paren_items = re.findall(r'[^,()]+\([^)]+\)', source_data)
    if len(paren_items) >= 2:
        return len(paren_items)

    # 패턴 2: 줄바꿈 + 불릿 ("- ", "• ", "* ")
    bullet_lines = [l.strip() for l in source_data.split("\n")
                    if l.strip() and l.strip()[0] in "-•*"]
    if len(bullet_lines) >= 2:
        return len(bullet_lines)

    # 패턴 3: 쉼표 구분 (단, 괄호 안의 쉼표는 제외)
    # 괄호 안 내용 제거 후 쉼표로 분리
    cleaned = re.sub(r'\([^)]*\)', '', source_data)
    comma_items = [s.strip() for s in cleaned.split(",") if s.strip()]
    if len(comma_items) >= 2:
        return len(comma_items)

    return 1


def get_actual_text_chars(
    topic: dict,
    normalized: dict,
    role: str,
) -> int:
    """꼭지에 해당하는 실제 텍스트 분량.

    structured_text(Kei가 원본 85% 보존 구조화)를 우선 사용.
    없으면 source_data 길이로 fallback.
    하드코딩 키워드 매칭 없음.
    """
    source_data = topic.get("source_data", "")
    structured_text = topic.get("structured_text", "")

    # structured_text가 있으면 그 길이가 실제 텍스트 분량
    if structured_text:
        # [팝업:], [이미지:] 마커는 실제 텍스트가 아니므로 제외
        import re
        clean = re.sub(r'\[팝업:\s*[^\]]+\]', '', structured_text)
        clean = re.sub(r'\[이미지:\s*[^\]]+\]', '', clean)
        estimated = len(clean.strip())
    else:
        # fallback: source_data 길이
        estimated = len(source_data)

    # 팝업 링크 추가 (본문에는 "상세보기 →" 링크 1줄)
    popup_link_chars = 0
    if "[팝업:" in source_data or "[팝업:" in structured_text:
        popups = normalized.get("popups", [])
        text_to_search = structured_text or source_data
        for p in popups:
            if p.get("title", "") in text_to_search:
                popup_link_chars += len(p.get("title", "")) + len("상세보기 →") + 2

    estimated += popup_link_chars

    return estimated


# ──────────────────────────────────────
# 데이터 클래스
# ──────────────────────────────────────

@dataclass
class TopicFit:
    """한 꼭지의 적합성 분석."""
    topic_id: int
    role: str
    block_id: str
    text_chars: int
    text_height_px: float
    image_height_px: float      # SVG/이미지 높이
    block_overhead_px: float
    required_height_px: float   # max(text+overhead, image+overhead) 또는 합산
    font_size: float
    item_count: int = 1         # 카드형 아이템 수
    has_image: bool = False
    has_keymsg: bool = False
    keymsg_height_px: float = 0


@dataclass
class RoleFit:
    """한 역할(영역)의 적합성 분석."""
    role: str
    topic_fits: list[TopicFit] = field(default_factory=list)
    total_required_px: float = 0
    allocated_px: float = 0
    gap_between_px: float = 0  # 실행 시 tokens에서 설정
    shortfall_px: float = 0
    fit_status: str = "OK"      # OK / TIGHT / OVERFLOW


@dataclass
class FitAnalysis:
    """전체 적합성 분석."""
    roles: dict[str, RoleFit] = field(default_factory=dict)
    can_redistribute: bool = False
    redistribution: dict[str, float] | None = None
    needs_escalation: bool = False


# ──────────────────────────────────────
# V-2: 적합성 검증 메인
# ──────────────────────────────────────

def calculate_fit(
    topics: list[dict[str, Any]],
    page_structure: dict[str, Any],
    containers: dict[str, Any],
    references: dict[str, list[dict[str, Any]]],
    font_hierarchy: dict[str, float],
    normalized: dict[str, Any] | None = None,
    core_message: str = "",
) -> FitAnalysis:
    """각 컨테이너에 콘텐츠가 들어가는지 정확하게 검증."""
    topic_map = {t.get("id"): t for t in topics}
    if normalized is None:
        normalized = {}

    role_font_map = {"본심": "core", "배경": "bg", "첨부": "sidebar", "결론": "key_msg"}
    role_line_height = {"본심": 1.5, "배경": 1.4, "첨부": 1.4, "결론": 1.3}

    analysis = FitAnalysis()

    for role, info in page_structure.items():
        if not isinstance(info, dict):
            continue
        topic_ids = info.get("topic_ids", [])
        if not topic_ids:
            continue

        container = containers.get(role)
        if container is None:
            continue

        if hasattr(container, "height_px"):
            allocated_h = container.height_px
            width_px = container.width_px
        else:
            allocated_h = container.get("height_px", 0)
            width_px = container.get("width_px", 0)

        font_key = role_font_map.get(role, "core")
        font_size = font_hierarchy.get(font_key, 12)
        line_h = role_line_height.get(role, 1.5)

        # V-1 출력: 꼭지별 블록 리스트
        ref_list = references.get(role, [])
        ref_map = {}

        # 블록 수평 padding — catalog.yaml의 padding_h_px에서 가져옴
        max_h_padding = 0
        for r in ref_list:
            if isinstance(r, dict):
                ce = r.get("catalog_entry", {})
                max_h_padding = max(max_h_padding, ce.get("padding_h_px", 0))
        tokens_cw = _load_design_tokens()
        content_width = max(tokens_cw["spacing_page"], width_px - max_h_padding)
        for r in ref_list:
            if isinstance(r, dict):
                ref_map[r.get("topic_id")] = r

        topic_count = len(topic_ids)
        tokens = _load_design_tokens()
        block_gap = tokens["spacing_small"]  # --spacing-small: 블록 간 간격

        role_fit = RoleFit(role=role, allocated_px=allocated_h, gap_between_px=block_gap)
        total_required = 0

        for i, tid in enumerate(topic_ids):
            topic = topic_map.get(tid, {})
            source_data = topic.get("source_data", "")

            ref = ref_map.get(tid, {})
            block_id = ref.get("block_id", "unknown") if isinstance(ref, dict) else "unknown"
            catalog_entry = ref.get("catalog_entry", {}) if isinstance(ref, dict) else {}

            # ── 1. 실제 텍스트 분량 ──
            text_chars = get_actual_text_chars(topic, normalized, role)

            # ── 2. 아이템 수 (블록 schema의 max_* 키 기반) ──
            block_schema = catalog_entry.get("schema", {}) if isinstance(catalog_entry, dict) else {}
            item_count = count_items_in_topic(topic, role, block_schema=block_schema)

            # ── 3. 이미지 높이 (이 꼭지의 source_data에 [이미지:] 참조가 있을 때만) ──
            image_sizes = {}  # analysis.image_sizes가 있으면 전달
            img_h = estimate_image_height(source_data, content_width, image_sizes)
            has_image = img_h > 0

            # ── 4. key-msg (본심에만) ──
            has_keymsg = (role == "본심" and core_message)
            keymsg_h = estimate_keymsg_height(core_message, font_hierarchy.get("key_msg", 14)) if has_keymsg else 0

            # ── 5. 블록 오버헤드 ──
            overhead = estimate_block_overhead(block_id, catalog_entry, item_count)

            # ── 6. 텍스트 높이 ──
            if has_image:
                # 이미지가 있으면 텍스트는 이미지 옆에 배치 (flex row)
                # 이미지 최소 폭: catalog의 min_display_width_px (SVG 가독성 기반)
                tokens = _load_design_tokens()
                img_text_gap = tokens["spacing_inner"]
                img_min_width = tokens["spacing_page"]  # 기본 최소
                for r in ref_list:
                    if isinstance(r, dict):
                        ce = r.get("catalog_entry", {})
                        w = ce.get("min_display_width_px", 0)
                        if w > img_min_width:
                            img_min_width = w
                img_display_width = img_min_width
                text_width = content_width - img_display_width - img_text_gap
                text_width = max(tokens["spacing_page"], text_width)
                text_h = estimate_text_height(text_chars, font_size, text_width, line_h)
                # 본심 높이 = max(이미지, 텍스트) + key-msg + overhead
                content_h = max(img_h, text_h)
                required = content_h + keymsg_h + overhead
            else:
                text_h = estimate_text_height(text_chars, font_size, content_width, line_h)
                required = text_h + keymsg_h + overhead

            # 제목 높이 (첫 번째 꼭지만 — 영역 제목)
            if i == 0:
                title_extra = font_size * 1.5 + tokens["spacing_small"]
                required += title_extra

            topic_fit = TopicFit(
                topic_id=tid,
                role=role,
                block_id=block_id,
                text_chars=text_chars,
                text_height_px=round(text_h, 1),
                image_height_px=round(img_h, 1),
                block_overhead_px=round(overhead, 1),
                required_height_px=round(required, 1),
                font_size=font_size,
                item_count=item_count,
                has_image=has_image,
                has_keymsg=has_keymsg,
                keymsg_height_px=round(keymsg_h, 1),
            )
            role_fit.topic_fits.append(topic_fit)
            total_required += required

            if i < topic_count - 1:
                total_required += block_gap

        role_fit.total_required_px = round(total_required, 1)
        role_fit.shortfall_px = round(total_required - allocated_h, 1)

        tokens = _load_design_tokens()
        tight_threshold = tokens["spacing_block"]  # --spacing-block: TIGHT 판정 기준
        if role_fit.shortfall_px <= 0:
            role_fit.fit_status = "OK"
        elif role_fit.shortfall_px <= tight_threshold:
            role_fit.fit_status = "TIGHT"
        else:
            role_fit.fit_status = "OVERFLOW"

        analysis.roles[role] = role_fit

        logger.info(
            f"[V-2] {role}: 필요={role_fit.total_required_px}px, "
            f"배정={allocated_h}px, 차이={role_fit.shortfall_px}px → {role_fit.fit_status}"
        )

    return analysis


# ──────────────────────────────────────
# V-3: 재배분
# ──────────────────────────────────────

def build_escalation_report(analysis: FitAnalysis) -> str:
    """Kei에게 보낼 에스컬레이션 보고서 생성."""
    lines = []
    for role, rf in analysis.roles.items():
        icon = {"OK": "✅", "TIGHT": "⚠️", "OVERFLOW": "❌"}[rf.fit_status]
        lines.append(f"{icon} {role}: 필요 {rf.total_required_px}px / 배정 {rf.allocated_px}px → {rf.fit_status} (차이 {rf.shortfall_px:+.0f}px)")
        for tf in rf.topic_fits:
            parts = [f"텍스트 {tf.text_chars}자→{tf.text_height_px}px"]
            if tf.has_image:
                parts.append(f"이미지 {tf.image_height_px}px")
            if tf.has_keymsg:
                parts.append(f"key-msg {tf.keymsg_height_px}px")
            parts.append(f"overhead {tf.block_overhead_px}px")
            lines.append(f"  꼭지{tf.topic_id} ({tf.block_id}): {', '.join(parts)} → {tf.required_height_px}px")

    if analysis.redistribution:
        lines.append("")
        lines.append("재배분 시도 결과:")
        for role, new_h in analysis.redistribution.items():
            rf = analysis.roles.get(role)
            if rf:
                old_h = rf.allocated_px
                gap = new_h - rf.total_required_px
                lines.append(f"  {role}: {old_h}→{new_h:.0f}px / 필요 {rf.total_required_px}px → {'해결' if gap >= 0 else f'부족 {abs(gap):.0f}px'}")

    return "\n".join(lines)


ROLE_ZONE_MAP = {
    "본심": "body",
    "배경": "body",
    "첨부": "sidebar",
    "결론": "footer",
}


def redistribute(
    analysis: FitAnalysis,
    containers: dict[str, Any],
    min_margin_px: float | None = None,
) -> FitAnalysis:
    """부족 영역에 여유 영역의 공간을 재배분.

    같은 zone 내에서만 재배분 가능 (body 안의 배경↔본심).
    유형 B: containers의 zone 속성에서 동적으로 매핑.
    """
    zone_roles: dict[str, list[str]] = {}
    for role in analysis.roles:
        # containers에 zone 정보가 있으면 그걸 사용, 없으면 ROLE_ZONE_MAP fallback
        ci = containers.get(role)
        if ci is not None:
            zone = ci.get("zone") if isinstance(ci, dict) else getattr(ci, "zone", None)
        else:
            zone = None
        if not zone:
            zone = ROLE_ZONE_MAP.get(role, "body")
        if zone not in zone_roles:
            zone_roles[zone] = []
        zone_roles[zone].append(role)

    # min_margin을 tokens에서 가져옴
    if min_margin_px is None:
        tokens = _load_design_tokens()
        min_margin_px = tokens["spacing_small"]  # --spacing-small

    redistribution: dict[str, float] = {}
    all_resolved = True

    for zone, roles_in_zone in zone_roles.items():
        if len(roles_in_zone) < 2:
            for role in roles_in_zone:
                rf = analysis.roles[role]
                redistribution[role] = rf.allocated_px
                if rf.shortfall_px > 0:
                    all_resolved = False
            continue

        deficit_roles = []
        surplus_roles = []

        for role in roles_in_zone:
            rf = analysis.roles[role]
            if rf.shortfall_px > 0:
                deficit_roles.append((role, rf.shortfall_px))
            elif rf.shortfall_px < -min_margin_px:
                available = abs(rf.shortfall_px) - min_margin_px
                if available > 0:
                    surplus_roles.append((role, available))

        total_deficit = sum(d for _, d in deficit_roles)
        total_surplus = sum(s for _, s in surplus_roles)

        if total_deficit <= 0:
            for role in roles_in_zone:
                redistribution[role] = analysis.roles[role].allocated_px
            continue

        if total_surplus <= 0:
            for role in roles_in_zone:
                redistribution[role] = analysis.roles[role].allocated_px
            all_resolved = False
            continue

        transfer = min(total_deficit, total_surplus)

        for role, deficit in deficit_roles:
            rf = analysis.roles[role]
            share = (deficit / total_deficit) * transfer
            new_height = rf.allocated_px + share
            redistribution[role] = round(new_height, 1)
            logger.info(f"[V-3] {role}: {rf.allocated_px}px → {new_height:.0f}px (+{share:.0f}px)")

        for role, surplus in surplus_roles:
            rf = analysis.roles[role]
            share = (surplus / total_surplus) * transfer
            new_height = rf.allocated_px - share
            redistribution[role] = round(new_height, 1)
            logger.info(f"[V-3] {role}: {rf.allocated_px}px → {new_height:.0f}px (-{share:.0f}px)")

        if total_surplus < total_deficit:
            all_resolved = False

    for role, rf in analysis.roles.items():
        if role not in redistribution:
            redistribution[role] = rf.allocated_px

    analysis.redistribution = redistribution
    analysis.can_redistribute = all_resolved
    analysis.needs_escalation = not all_resolved

    return analysis


# ──────────────────────────────────────
# V-7~V-10: 콘텐츠 품질 강화 분석
# ──────────────────────────────────────

@dataclass
class Enhancement:
    """하나의 개선 제안."""
    role: str
    type: str           # "subordinate" | "fill_space" | "emphasis" | "bold_keywords"
    description: str    # Kei에게 보여줄 설명
    detail: dict = field(default_factory=dict)  # 구체적 데이터


@dataclass
class SupplementBlock:
    """여유 공간에 추가할 보충 블록."""
    role: str
    block_id: str
    variant: str
    content_source: str      # "popup:DX와 BIM의 구분" 등
    estimated_height_px: float
    available_px: float


@dataclass
class EnhancementAnalysis:
    """V-7~V-10 전체 개선 제안 + Kei 확인 후 보충 블록."""
    enhancements: list[Enhancement] = field(default_factory=list)
    supplement_blocks: list[SupplementBlock] = field(default_factory=list)
    emphasis_blocks: list[dict] = field(default_factory=list)  # 강조 블록 정보
    bold_keywords: dict[str, list[str]] = field(default_factory=dict)  # role → keywords


def analyze_enhancements(
    topics: list[dict[str, Any]],
    page_structure: dict[str, Any],
    references: dict[str, list[dict[str, Any]]],
    analysis: FitAnalysis,
    normalized: dict[str, Any],
    core_message: str = "",
) -> EnhancementAnalysis:
    """재배분 후 콘텐츠 품질 강화 제안을 생성.

    AI가 분석, Kei가 확인하는 구조. 하드코딩 없음.
    모든 판단은 이전 Stage 데이터(topic purpose/layer, fit 결과, popup 내용)에서 동적 도출.
    """
    topic_map = {t.get("id"): t for t in topics}
    tokens = _load_design_tokens()
    result = EnhancementAnalysis()

    for role, ref_list in references.items():
        rf = analysis.roles.get(role)
        if not rf:
            continue

        # ── V-7: 종속 꼭지 처리 제안 ──
        for ref in ref_list:
            supporting_tids = ref.get("supporting_topic_ids", [])
            if not supporting_tids:
                continue

            for s_tid in supporting_tids:
                s_topic = topic_map.get(s_tid, {})
                s_source = s_topic.get("source_data", "")
                s_purpose = s_topic.get("purpose", "")

                # 종속 꼭지의 분량으로 처리 방식 결정
                # 팝업 참조가 있고 source_data가 짧으면 → 인라인
                has_popup_ref = "[팝업:" in s_source
                source_len = len(s_source)

                # 팝업 참조 시 실제 팝업 내용 길이 확인
                popup_content_len = 0
                popup_title = ""
                if has_popup_ref:
                    for p in normalized.get("popups", []):
                        if p.get("title", "") in s_source:
                            popup_content_len = len(p.get("content", ""))
                            popup_title = p.get("title", "")
                            break

                # 판단: 분량 기준은 spacing 값에서 유도
                # 인라인 = 1~2줄 분량 → chars_per_line * 2 이내
                # chars_per_line은 font_size와 width에서 계산
                font_size = rf.topic_fits[0].font_size if rf.topic_fits else 12  # font-size (허용)
                container_width = rf.allocated_px  # 이건 height인데... width가 필요
                # 간략 판단: source_data 자체가 짧으면 인라인
                if has_popup_ref and source_len < font_size * CHAR_WIDTH_RATIO * 5:  # ~5줄 미만
                    treatment = "inline"
                    desc = f"종속 꼭지{s_tid}({s_purpose}): 팝업 \"{popup_title}\" 참조 ({popup_content_len}자). 본문에는 인라인 1줄 + 링크"
                elif source_len > font_size * CHAR_WIDTH_RATIO * 10:  # ~10줄 이상
                    treatment = "sub_block"
                    desc = f"종속 꼭지{s_tid}({s_purpose}): 콘텐츠 {source_len}자. 하위 블록으로 분리 권장"
                else:
                    treatment = "inline"
                    desc = f"종속 꼭지{s_tid}({s_purpose}): 인라인 처리 ({source_len}자)"

                result.enhancements.append(Enhancement(
                    role=role,
                    type="subordinate",
                    description=desc,
                    detail={
                        "supporting_topic_id": s_tid,
                        "treatment": treatment,
                        "source_len": source_len,
                        "has_popup": has_popup_ref,
                        "popup_title": popup_title,
                        "popup_content_len": popup_content_len,
                    },
                ))

        # ── V-8: 여유 공간 콘텐츠 보충 ──
        new_h = analysis.redistribution.get(role, rf.allocated_px) if analysis.redistribution else rf.allocated_px
        surplus = new_h - rf.total_required_px
        # 여유 기준: spacing_block 이상이면 의미 있는 여유
        if surplus > tokens["spacing_block"]:
            # 이 영역의 꼭지에 관련 팝업이 있는지
            info = page_structure.get(role, {})
            topic_ids = info.get("topic_ids", []) if isinstance(info, dict) else []

            for tid in topic_ids:
                topic = topic_map.get(tid, {})
                source_data = topic.get("source_data", "")
                structured_text = topic.get("structured_text", "")
                search_text = structured_text + " " + source_data

                if "[팝업:" not in search_text:
                    continue

                for p in normalized.get("popups", []):
                    p_title = p.get("title", "")
                    p_content = p.get("content", "")

                    if p_title not in search_text:
                        continue

                    # 팝업에 구조화 콘텐츠가 있는지 (표 = |, 목록 = *)
                    has_table = "|" in p_content and p_content.count("|") > 3
                    has_list = p_content.count("*") > 2

                    if has_table or has_list:
                        content_type = "표" if has_table else "목록"
                        result.enhancements.append(Enhancement(
                            role=role,
                            type="fill_space",
                            description=f"{role} 여유 {surplus:.0f}px. 팝업 \"{p_title}\"에 {content_type}({len(p_content)}자) 있음. 핵심 요약을 넣을까요?",
                            detail={
                                "surplus_px": surplus,
                                "popup_title": p_title,
                                "popup_content_len": len(p_content),
                                "content_type": content_type,
                                "has_table": has_table,
                            },
                        ))

        # ── V-9: 영역 핵심 결론 강조 블록 ──
        info = page_structure.get(role, {})
        topic_ids = info.get("topic_ids", []) if isinstance(info, dict) else []

        for tid in topic_ids:
            topic = topic_map.get(tid, {})
            purpose = topic.get("purpose", "")
            source_data = topic.get("source_data", "")

            # 결론적 패턴 감지: purpose가 문제제기이고 텍스트에 "필요", "해야" 등
            conclusion_patterns = ["필요", "해야", "되어야", "요구됨", "시급"]
            structured_text = topic.get("structured_text", "")
            # structured_text 우선, 없으면 source_data + sections에서 검색
            all_text = structured_text if structured_text else source_data
            if not structured_text:
                for s in normalized.get("sections", []):
                    s_content = s.get("content", "") if isinstance(s, dict) else ""
                    if any(kw in s_content for kw in source_data.split()[:3]):
                        all_text += " " + s_content
                        break
            has_conclusion = any(pat in all_text for pat in conclusion_patterns)

            if has_conclusion and purpose in ("문제제기", "핵심전달"):
                # 결론 문장 추출: 전체 텍스트에서 패턴 포함 문장
                sentences = [s.strip() for s in all_text.replace("\n", ". ").replace(".", ". ").split(". ") if s.strip()]
                conclusion_sentence = ""
                for sent in reversed(sentences):
                    if any(pat in sent for pat in conclusion_patterns):
                        conclusion_sentence = sent
                        break

                if conclusion_sentence:
                    result.enhancements.append(Enhancement(
                        role=role,
                        type="emphasis",
                        description=f"{role} 꼭지{tid}({purpose}): \"{conclusion_sentence[:50]}...\" → 강조 블록으로 처리할까요?",
                        detail={
                            "topic_id": tid,
                            "conclusion_sentence": conclusion_sentence,
                            "purpose": purpose,
                        },
                    ))

        # ── V-10: bold 키워드 — Kei가 문맥 기반으로 판단 (pipeline.py에서 호출) ──
        # 기계적 키워드 추출 제거. Kei 판단 결과가 pipeline.py에서 주입됨.

    return result


def build_enhancement_report(enhancements: EnhancementAnalysis) -> str:
    """Kei에게 보여줄 개선 제안 보고서."""
    lines = ["=== 콘텐츠 품질 강화 제안 ===", ""]

    by_type = {}
    for e in enhancements.enhancements:
        if e.type not in by_type:
            by_type[e.type] = []
        by_type[e.type].append(e)

    type_labels = {
        "subordinate": "V-7 종속 꼭지 처리",
        "fill_space": "V-8 여유 공간 보충",
        "emphasis": "V-9 강조 블록",
        "bold_keywords": "V-10 bold 키워드",
    }

    for etype, label in type_labels.items():
        items = by_type.get(etype, [])
        if not items:
            continue
        lines.append(f"── {label} ({len(items)}건) ──")
        for e in items:
            lines.append(f"  [{e.role}] {e.description}")
        lines.append("")

    return "\n".join(lines)


def apply_enhancements(
    enhancements: EnhancementAnalysis,
    analysis: FitAnalysis,
) -> EnhancementAnalysis:
    """Step 6: Kei 확인 후 보충 블록 선택 + fit 재검증.

    Kei가 승인한 제안에 대해:
    - fill_space → catalog에서 여유 공간에 맞는 블록 선택
    - emphasis → 강조 문장 확정
    - bold_keywords → 키워드 목록 확정

    하드코딩 없음. 블록 선택은 catalog의 min_height_px로 판단.
    """
    from src.block_reference import _load_catalog

    catalog = _load_catalog()

    for e in enhancements.enhancements:
        if e.type == "fill_space":
            surplus_px = e.detail.get("surplus_px", 0)
            has_table = e.detail.get("has_table", False)
            popup_title = e.detail.get("popup_title", "")

            # 여유 공간에 맞는 블록: catalog에서 min_height_px <= surplus_px
            target_categories = ["tables"] if has_table else ["cards", "emphasis"]
            candidates = [
                b for b in catalog
                if b.get("category") in target_categories
                and b.get("min_height_px", 0) <= surplus_px
            ]

            if candidates:
                # 여유에 가장 가까운(크지만 넘지 않는) 블록
                candidates.sort(key=lambda b: b.get("min_height_px", 0), reverse=True)
                selected = candidates[0]

                enhancements.supplement_blocks.append(SupplementBlock(
                    role=e.role,
                    block_id=selected["id"],
                    variant="default",
                    content_source=f"popup:{popup_title}",
                    estimated_height_px=selected.get("min_height_px", 0),
                    available_px=surplus_px,
                ))

                logger.info(
                    f"[V-8] {e.role}: 보충 블록 {selected['id']} "
                    f"(min_h={selected.get('min_height_px')}px, 여유={surplus_px}px)"
                )

        elif e.type == "emphasis":
            conclusion = e.detail.get("conclusion_sentence", "")
            if conclusion:
                enhancements.emphasis_blocks.append({
                    "role": e.role,
                    "topic_id": e.detail.get("topic_id"),
                    "sentence": conclusion,
                })

        elif e.type == "bold_keywords":
            role = e.role
            keywords = e.detail.get("keywords", [])
            if keywords:
                if role not in enhancements.bold_keywords:
                    enhancements.bold_keywords[role] = []
                enhancements.bold_keywords[role].extend(keywords)

    # fit 재검증: 보충 블록이 실제로 들어가는지
    valid_supplements = []
    for sb in enhancements.supplement_blocks:
        rf = analysis.roles.get(sb.role)
        if not rf:
            continue
        new_h = analysis.redistribution.get(sb.role, rf.allocated_px) if analysis.redistribution else rf.allocated_px
        remaining = new_h - rf.total_required_px
        if sb.estimated_height_px <= remaining:
            valid_supplements.append(sb)
            logger.info(f"[V-8] {sb.role}: 보충 {sb.block_id} 확정 ({sb.estimated_height_px}px <= 여유 {remaining}px)")
        else:
            logger.warning(f"[V-8] {sb.role}: 보충 {sb.block_id} 제외 ({sb.estimated_height_px}px > 여유 {remaining}px)")

    enhancements.supplement_blocks = valid_supplements
    return enhancements


# ──────────────────────────────────────
# Step 7: 세부 컨테이너 배치 계산
# ──────────────────────────────────────

@dataclass
class SubContainer:
    """세부 컨테이너 정보."""
    name: str           # "svg", "text", "table", "keymsg", "emphasis" 등
    width_px: float
    height_px: float
    align: str = "stretch"  # "stretch" | "center"


@dataclass
class ContainerLayout:
    """하나의 메인 컨테이너 안의 세부 배치."""
    role: str
    main_height_px: float
    main_width_px: float
    sub_containers: list[SubContainer] = field(default_factory=list)
    table_rows: int = 0     # 보충 표 행 수


def calculate_sub_layout(
    role: str,
    main_height_px: float,
    main_width_px: float,
    topic_fits: list[TopicFit],
    enhancements: EnhancementAnalysis,
    font_hierarchy: dict[str, float],
) -> ContainerLayout:
    """메인 컨테이너 안에서 세부 컨테이너 배치를 계산.

    이미지/텍스트/표/key-msg 등의 크기를 동적으로 결정.
    """
    tokens = _load_design_tokens()
    layout = ContainerLayout(role=role, main_height_px=main_height_px, main_width_px=main_width_px)

    # 제목 높이: font_size * line_height + margin
    role_font_map = {"본심": "core", "배경": "bg", "첨부": "sidebar", "결론": "key_msg"}
    font_key = role_font_map.get(role, "core")
    title_font = font_hierarchy.get(font_key, 12)
    title_h = title_font * 1.5 + tokens["spacing_small"]

    # key-msg (본심에만)
    keymsg_h = 0
    for tf in topic_fits:
        if tf.has_keymsg:
            keymsg_h = tf.keymsg_height_px
            break

    # 강조 블록 (배경 등)
    emphasis_h = 0
    for eb in enhancements.emphasis_blocks:
        if eb.get("role") == role:
            emphasis_h = title_font * 1.4 + tokens["spacing_small"] * 2
            break

    # 이미지 있는지
    has_image = any(tf.has_image for tf in topic_fits)

    # 블록 padding overhead (선택된 블록의 padding/border 등)
    block_overhead = max((tf.block_overhead_px for tf in topic_fits), default=0)

    # 사용 가능 높이: 메인 - 제목 - key-msg - 강조 - 블록 padding - 간격
    gap = tokens["spacing_small"]
    used_h = title_h + keymsg_h + emphasis_h + block_overhead
    used_h += gap * (2 if keymsg_h > 0 else 1)  # 제목-콘텐츠 gap + 콘텐츠-keymsg gap
    content_h = max(0, main_height_px - used_h)

    if has_image:
        # SVG(좌) + 텍스트+표(우) 구조
        # 이미지 최소 폭: catalog의 min_display_width_px
        from src.block_reference import _load_catalog
        img_min_w = tokens["spacing_page"]
        for b in _load_catalog():
            if b.get("category") == "visuals" and b.get("min_display_width_px", 0) > img_min_w:
                img_min_w = b["min_display_width_px"]
                break  # 첫 번째 visual 블록 기준
        img_width = img_min_w
        text_width = main_width_px - img_width - gap

        # 텍스트 높이 (topic_fits에서)
        text_h = sum(tf.text_height_px for tf in topic_fits if not tf.has_keymsg)

        # 보충 표 사용 가능 높이
        table_available = content_h - text_h - gap  # 텍스트 아래 여유
        table_rows = 0

        for sb in enhancements.supplement_blocks:
            if sb.role == role:
                # 표 1행 높이: catalog의 padding_overhead_px / 3 (헤더+3행 기준)
                from src.block_reference import _load_catalog
                block = next((b for b in _load_catalog() if b["id"] == sb.block_id), None)
                if block:
                    overhead = block.get("padding_overhead_px", 0)
                    # 헤더 높이 ≈ overhead의 절반
                    header_h = overhead / 2
                    row_h = title_font * 1.5 + tokens["spacing_small"]  # 1행 높이
                    # 가용 높이에서 헤더 빼고 행 수 계산
                    if table_available > header_h:
                        table_rows = max(0, int((table_available - header_h) / row_h))

        layout.sub_containers = [
            SubContainer("svg", img_width, content_h, align="stretch"),
            SubContainer("text_and_table", text_width, content_h, align="center"),
        ]
        if keymsg_h > 0:
            layout.sub_containers.append(SubContainer("keymsg", main_width_px, keymsg_h))
        layout.table_rows = table_rows

    else:
        # 이미지 없는 구조 (텍스트만)
        # 카드형: item_count > 1이면 카드당 높이 예산 계산
        total_items = sum(tf.item_count for tf in topic_fits)
        if total_items > 1:
            card_gap = gap * (total_items - 1)
            per_card_h = max(0, (content_h - card_gap) / total_items)
            layout.sub_containers = [
                SubContainer(f"card_{i+1}", main_width_px, per_card_h)
                for i in range(total_items)
            ]
        else:
            layout.sub_containers = [
                SubContainer("text", main_width_px, content_h),
            ]
        if keymsg_h > 0:
            layout.sub_containers.append(SubContainer("keymsg", main_width_px, keymsg_h))
        if emphasis_h > 0:
            layout.sub_containers.append(SubContainer("emphasis", main_width_px, emphasis_h))

    logger.info(
        f"[V-Step7] {role}: main={main_height_px}px, "
        + ", ".join(f"{sc.name}={sc.width_px:.0f}×{sc.height_px:.0f}" for sc in layout.sub_containers)
        + (f", 표 {layout.table_rows}행" if layout.table_rows > 0 else "")
    )

    return layout
