# -*- coding: utf-8 -*-
"""
Style Generator v2.1  (Phase 4 — 하드코딩 제거)

template_info의 tools 추출값 → CSS 문자열 생성.

★ v2.1 변경사항:
  - 하드코딩 간격 → 추출값 대체:
    · .doc-header margin-bottom → page.margins.header에서 계산
    · .doc-footer margin-top   → page.margins.footer에서 계산
    · .title-block margin/padding → title paraPr spacing에서 유도
  - .img-wrap, .img-caption CSS 추가 (content_order 이미지 지원)

★ v2.0 변경사항 (v1.0 대비):
  - charPr 28개 전체 → .cpr-{id} CSS 클래스 생성
  - paraPr 23개 전체 → .ppr-{id} CSS 클래스 생성
  - styles 12개 → .sty-{id} CSS 클래스 (charPr + paraPr 조합)
  - fontRef → 실제 폰트명 해석 (font_map 빌드)
  - 제목 블록: 하드코딩 제거 → 실제 추출 데이터 사용
  - 줄간격: paraPr별 line-height 개별 적용
  - 여백: @page는 인쇄용, .page는 화면용 (이중 적용 제거)
  - bf CSS: NONE-only borderFill도 클래스 생성 (border: none 명시)
  - 텍스트 색상: charPr별 color 반영
  - 폰트: charPr별 fontRef → 실제 font-family 해석

★ 원칙: hwpx_domain_guide.md §1~§8 매핑 규칙 100% 준수
★ 원칙: 하드코딩 값 0개. 모든 CSS 값은 template_info에서 유래.
"""

HU_TO_MM = 25.4 / 7200  # 1 HWPUNIT = 1/7200 inch → mm


# ================================================================
#  메인 엔트리포인트
# ================================================================

def generate_css(template_info: dict, semantic_map: dict = None) -> str:
    """template_info + semantic_map → CSS 문자열 전체 생성."""
    # font_map 빌드 (charPr CSS에서 재사용)
    fm = _build_font_map(template_info)

    parts = [
        _page_css(template_info),
        _body_css(template_info, fm),
        _layout_css(template_info),
        _header_footer_css(template_info),
        _title_block_css(template_info, fm, semantic_map),
        _section_css(template_info),
        _table_base_css(template_info),
        _border_fill_css(template_info),
        _char_pr_css(template_info, fm),
        _para_pr_css(template_info),
        _named_style_css(template_info),
        _table_detail_css(template_info, semantic_map),
    ]
    return "\n\n".join(p for p in parts if p)


# ================================================================
#  @page (인쇄 전용)
# ================================================================

def _page_css(ti: dict) -> str:
    page = ti.get("page", {})
    paper = page.get("paper", {})
    margins = page.get("margins", {})

    w = paper.get("width_mm", 210)
    h = paper.get("height_mm", 297)
    mt = margins.get("top", "20mm")
    mb = margins.get("bottom", "20mm")
    ml = margins.get("left", "20mm")
    mr = margins.get("right", "20mm")

    return (
        "@page {\n"
        f"  size: {w}mm {h}mm;\n"
        f"  margin: {mt} {mr} {mb} {ml};\n"
        "}\n"
        "@media screen {\n"
        "  @page { margin: 0; }\n"  # 화면에서는 .page padding만 사용
        "}"
    )


# ================================================================
#  body
# ================================================================

def _body_css(ti: dict, fm: dict) -> str:
    """바탕글 스타일 기준 body CSS"""
    # '바탕글' 스타일 → charPr → fontRef → 실제 폰트
    base_charpr = _resolve_style_charpr(ti, "바탕글")
    base_parapr = _resolve_style_parapr(ti, "바탕글")

    # 폰트
    font_family = _charpr_font_family(base_charpr, fm)
    # 크기
    size_pt = base_charpr.get("height_pt", 10.0)
    # 색상
    color = base_charpr.get("textColor", "#000000")
    # 줄간격
    line_height = _parapr_line_height(base_parapr)
    # 정렬
    # body에는 정렬 넣지 않음 (paraPr별로)

    return (
        "body {\n"
        f"  font-family: {font_family};\n"
        f"  font-size: {size_pt}pt;\n"
        f"  line-height: {line_height};\n"
        f"  color: {color};\n"
        "  margin: 0; padding: 0;\n"
        "}"
    )


# ================================================================
#  .page 레이아웃 (화면 전용 — 여백은 여기서만)
# ================================================================

def _layout_css(ti: dict) -> str:
    page = ti.get("page", {})
    paper = page.get("paper", {})
    margins = page.get("margins", {})

    w = paper.get("width_mm", 210)
    ml = _mm(margins.get("left", "20mm"))
    mr = _mm(margins.get("right", "20mm"))
    body_w = w - ml - mr

    mt = margins.get("top", "20mm")
    mb = margins.get("bottom", "20mm")
    m_left = margins.get("left", "20mm")
    m_right = margins.get("right", "20mm")

    return (
        ".page {\n"
        f"  width: {body_w:.0f}mm;\n"
        "  margin: 0 auto;\n"
        f"  padding: {mt} {m_right} {mb} {m_left};\n"
        "}"
    )


# ================================================================
#  헤더 / 푸터
# ================================================================

def _header_footer_css(ti: dict) -> str:
    page = ti.get("page", {})
    margins = page.get("margins", {})

    # 헤더 margin-bottom: page.margins.header에서 유도
    # 푸터 margin-top: page.margins.footer에서 유도
    hdr_margin = margins.get("header", "")
    ftr_margin = margins.get("footer", "")

    hdr_mb = f"{_mm(hdr_margin) * 0.3:.1f}mm" if hdr_margin else "4mm"
    ftr_mt = f"{_mm(ftr_margin) * 0.4:.1f}mm" if ftr_margin else "6mm"

    lines = [
        "/* 헤더/푸터 */",
        f".doc-header {{ margin-bottom: {hdr_mb}; }}",
        f".doc-footer {{ margin-top: {ftr_mt}; }}",
        ".doc-header table, .doc-footer table {",
        "  width: 100%; border-collapse: collapse;",
        "}",
    ]

    hdr_padding = _hf_cell_padding(ti.get("header"))
    ftr_padding = _hf_cell_padding(ti.get("footer"))

    lines.append(
        f".doc-header td {{ {hdr_padding} vertical-align: middle; }}"
    )
    lines.append(
        f".doc-footer td {{ {ftr_padding} vertical-align: middle; }}"
    )
    return "\n".join(lines)


# ================================================================
#  제목 블록 — ★ 하드코딩 제거, 실제 데이터 사용
# ================================================================

def _title_block_css(ti: dict, fm: dict, sm: dict = None) -> str:
    """제목 블록 CSS — title_table의 실제 셀 데이터에서 추출"""
    tables = ti.get("tables", [])

    # semantic_map에서 title_table 인덱스 가져오기
    title_idx = None
    if sm:
        title_idx = sm.get("title_table")

    title_tbl = None
    if title_idx is not None:
        title_tbl = next((t for t in tables if t["index"] == title_idx), None)

    # 못 찾으면 1행 표 중 텍스트 있는 것 검색
    if not title_tbl:
        for t in tables:
            rows = t.get("rows", [])
            if rows and len(rows) == 1:
                for cell in rows[0]:
                    if cell.get("text", "").strip():
                        title_tbl = t
                        break
            if title_tbl:
                break

    lines = ["/* 제목 블록 */"]

    if title_tbl:
        # 텍스트 있는 셀에서 charPr, paraPr, bf 추출
        title_charpr = None
        title_parapr = None
        title_bf_id = None

        for row in title_tbl.get("rows", []):
            for cell in row:
                if cell.get("text", "").strip():
                    # ★ primaryCharPrIDRef 사용 (table_v2 추출)
                    cpr_id = cell.get("primaryCharPrIDRef")
                    if cpr_id is not None:
                        title_charpr = next(
                            (c for c in ti.get("char_styles", [])
                             if c.get("id") == cpr_id), None
                        )
                    ppr_id = cell.get("primaryParaPrIDRef")
                    if ppr_id is not None:
                        title_parapr = next(
                            (p for p in ti.get("para_styles", [])
                             if p.get("id") == ppr_id), None
                        )
                    title_bf_id = cell.get("borderFillIDRef")
                    break
            if title_charpr:
                break

        # charPr 못 찾으면 폴백 (charPrIDRef가 없는 구버전 table.py)
        if not title_charpr:
            title_charpr = _find_title_charpr(ti)

        # CSS 생성
        font_family = _charpr_font_family(title_charpr, fm) if title_charpr else "'맑은 고딕', sans-serif"
        size_pt = title_charpr.get("height_pt", 15.0) if title_charpr else 15.0
        bold = title_charpr.get("bold", False) if title_charpr else False
        color = title_charpr.get("textColor", "#000000") if title_charpr else "#000000"

        # 줄간격
        line_height = _parapr_line_height(title_parapr) if title_parapr else "180%"
        align = _parapr_align(title_parapr) if title_parapr else "center"

        # ★ margin/padding — paraPr 또는 page.margins에서 유도
        title_after_mm = "4mm"  # 기본값
        title_padding = "4mm 0"  # 기본값
        if title_parapr:
            margin_info = title_parapr.get("margin", {})
            after_hu = margin_info.get("after_hu", 0)
            if after_hu:
                title_after_mm = f"{after_hu * HU_TO_MM:.1f}mm"
            before_hu = margin_info.get("before_hu", 0)
            if before_hu or after_hu:
                b_mm = before_hu * HU_TO_MM if before_hu else 4
                a_mm = after_hu * HU_TO_MM if after_hu else 0
                title_padding = f"{b_mm:.1f}mm 0 {a_mm:.1f}mm 0"

        lines.append(f".title-block {{ margin-bottom: {title_after_mm}; }}")
        lines.append(".title-table { width: 100%; border-collapse: collapse; }")
        lines.append(
            f".title-block h1 {{\n"
            f"  font-family: {font_family};\n"
            f"  font-size: {size_pt}pt;\n"
            f"  font-weight: {'bold' if bold else 'normal'};\n"
            f"  color: {color};\n"
            f"  text-align: {align};\n"
            f"  line-height: {line_height};\n"
            f"  margin: 0; padding: {title_padding};\n"
            f"}}"
        )

        # bf 적용 (파란 하단선 등)
        if title_bf_id:
            bf_data = ti.get("border_fills", {}).get(str(title_bf_id), {})
            css_dict = bf_data.get("css", {})
            bf_rules = []
            for prop, val in css_dict.items():
                if val and val.lower() != "none":
                    bf_rules.append(f"  {prop}: {val};")
            if bf_rules:
                lines.append(
                    f".title-block {{\n"
                    + "\n".join(bf_rules)
                    + "\n}"
                )
    else:
        lines.append(".title-block { margin-bottom: 4mm; }")
        lines.append(".title-table { width: 100%; border-collapse: collapse; }")
        lines.append(
            ".title-block h1 {\n"
            "  font-size: 15pt; font-weight: normal;\n"
            "  text-align: center; margin: 0; padding: 4mm 0;\n"
            "}"
        )

    return "\n".join(lines)


# ================================================================
#  섹션 — 하드코딩 제거
# ================================================================

def _section_css(ti: dict) -> str:
    """섹션 CSS — '#큰아이콘' 또는 '개요1' 스타일에서 추출"""
    lines = ["/* 섹션 */"]

    # 섹션 제목: '#큰아이콘' 또는 가장 큰 bold charPr
    title_charpr = _resolve_style_charpr(ti, "#큰아이콘")
    if not title_charpr or title_charpr.get("id") == 0:
        title_charpr = _resolve_style_charpr(ti, "개요1")
    if not title_charpr or title_charpr.get("id") == 0:
        # 폴백: bold인 charPr 중 가장 큰 것
        for cs in sorted(ti.get("char_styles", []),
                         key=lambda x: x.get("height_pt", 0), reverse=True):
            if cs.get("bold"):
                title_charpr = cs
                break

    if title_charpr:
        size = title_charpr.get("height_pt", 11)
        bold = title_charpr.get("bold", True)
        color = title_charpr.get("textColor", "#000000")
        lines.append(
            f".section-title {{\n"
            f"  font-size: {size}pt;\n"
            f"  font-weight: {'bold' if bold else 'normal'};\n"
            f"  color: {color};\n"
            f"  margin-bottom: 3mm;\n"
            f"}}"
        )
    else:
        lines.append(
            ".section-title { font-weight: bold; margin-bottom: 3mm; }"
        )

    lines.append(".section { margin-bottom: 6mm; }")
    lines.append(".section-content { text-align: justify; }")

    # content_order 기반 본문용 스타일
    lines.append("/* 이미지/문단 (content_order) */")
    lines.append(
        ".img-wrap { text-align: center; margin: 3mm 0; }"
    )
    lines.append(
        ".img-wrap img { max-width: 100%; height: auto; }"
    )
    lines.append(
        ".img-caption { font-size: 9pt; color: #666; margin-top: 1mm; }"
    )

    return "\n".join(lines)


# ================================================================
#  데이터 표 기본 CSS
# ================================================================

def _table_base_css(ti: dict) -> str:
    """표 기본 — '표내용' 스타일 charPr에서 추출"""
    tbl_charpr = _resolve_style_charpr(ti, "표내용")
    tbl_parapr = _resolve_style_parapr(ti, "표내용")

    size_pt = tbl_charpr.get("height_pt", 9.0) if tbl_charpr else 9.0
    line_height = _parapr_line_height(tbl_parapr) if tbl_parapr else "160%"
    align = _parapr_align(tbl_parapr) if tbl_parapr else "justify"

    border_fills = ti.get("border_fills", {})
    if border_fills:
        # bf-{id} 클래스가 셀별 테두리를 담당 → 기본값은 none
        # (하드코딩 border를 넣으면 bf 클래스보다 specificity가 높아 덮어씀)
        border_rule = "border: none;"
    else:
        # border_fills 추출 실패 시에만 폴백
        border_rule = "border: 1px solid #000;"

    return (
        "/* 데이터 표 */\n"
        ".data-table {\n"
        "  width: 100%; border-collapse: collapse; margin: 4mm 0;\n"
        "}\n"
        ".data-table th, .data-table td {\n"
        f"  {border_rule}\n"
        f"  font-size: {size_pt}pt;\n"
        f"  line-height: {line_height};\n"
        f"  text-align: {align};\n"
        "  vertical-align: middle;\n"
        "}\n"
        ".data-table th {\n"
        "  font-weight: bold; text-align: center;\n"
        "}"
    )


# ================================================================
#  borderFill → .bf-{id} CSS 클래스
# ================================================================

def _border_fill_css(ti: dict) -> str:
    """★ v2.0: NONE-only bf도 클래스 생성 (border: none 명시)"""
    border_fills = ti.get("border_fills", {})
    if not border_fills:
        return ""

    parts = ["/* borderFill → CSS 클래스 */"]

    for bf_id, bf in border_fills.items():
        rules = []

        css_dict = bf.get("css", {})
        for prop, val in css_dict.items():
            if val:
                # NONE도 포함 (border: none 명시)
                rules.append(f"  {prop}: {val};")

        # background
        if "background-color" not in css_dict:
            bg = bf.get("background", "")
            if bg and bg.lower() not in ("", "none", "transparent",
                                          "#ffffff", "#fff"):
                rules.append(f"  background-color: {bg};")

        if rules:
            parts.append(f".bf-{bf_id} {{\n" + "\n".join(rules) + "\n}")

    return "\n".join(parts) if len(parts) > 1 else ""


# ================================================================
#  ★ NEW: charPr → .cpr-{id} CSS 클래스
# ================================================================

def _char_pr_css(ti: dict, fm: dict) -> str:
    """charPr 전체 → 개별 CSS 클래스 생성.
    
    각 .cpr-{id}에 font-family, font-size, font-weight, color 등 포함.
    HTML에서 <span class="cpr-5"> 등으로 참조.
    """
    char_styles = ti.get("char_styles", [])
    if not char_styles:
        return ""

    parts = ["/* charPr → CSS 클래스 (글자 모양) */"]

    for cs in char_styles:
        cid = cs.get("id")
        rules = []

        # font-family
        ff = _charpr_font_family(cs, fm)
        if ff:
            rules.append(f"  font-family: {ff};")

        # font-size
        pt = cs.get("height_pt")
        if pt:
            rules.append(f"  font-size: {pt}pt;")

        # bold
        if cs.get("bold"):
            rules.append("  font-weight: bold;")

        # italic
        if cs.get("italic"):
            rules.append("  font-style: italic;")

        # color
        color = cs.get("textColor", "#000000")
        if color and color.lower() != "#000000":
            rules.append(f"  color: {color};")

        # underline — type이 NONE이 아닌 실제 밑줄만
        underline = cs.get("underline", "NONE")
        ACTIVE_UNDERLINE = {"BOTTOM", "CENTER", "TOP", "SIDE"}
        if underline in ACTIVE_UNDERLINE:
            rules.append("  text-decoration: underline;")

        # strikeout — shape="NONE" 또는 "3D"는 취소선 아님
        # 실제 취소선: CONTINUOUS, DASH, DOT 등 선 스타일만
        strikeout = cs.get("strikeout", "NONE")
        ACTIVE_STRIKEOUT = {"CONTINUOUS", "DASH", "DOT", "DASH_DOT",
                            "DASH_DOT_DOT", "LONG_DASH", "DOUBLE"}
        if strikeout in ACTIVE_STRIKEOUT:
            rules.append("  text-decoration: line-through;")

        # ── 자간 (letter-spacing) ──
        # HWPX spacing은 % 단위: letter-spacing = height_pt × spacing / 100
        spacing_pct = cs.get("spacing", {}).get("hangul", 0)
        if spacing_pct != 0 and pt:
            ls_val = round(pt * spacing_pct / 100, 2)
            rules.append(f"  letter-spacing: {ls_val}pt;")

        # ── 장평 (scaleX) ──
        # HWPX ratio는 글자 폭 비율 (100=기본). CSS transform으로 변환
        ratio_pct = cs.get("ratio", {}).get("hangul", 100)
        if ratio_pct != 100:
            rules.append(f"  transform: scaleX({ratio_pct / 100});")
            rules.append("  display: inline-block;")  # scaleX 적용 필수

        if rules:
            parts.append(f".cpr-{cid} {{\n" + "\n".join(rules) + "\n}")

    return "\n".join(parts) if len(parts) > 1 else ""


# ================================================================
#  ★ NEW: paraPr → .ppr-{id} CSS 클래스
# ================================================================

def _para_pr_css(ti: dict) -> str:
    """paraPr 전체 → 개별 CSS 클래스 생성.
    
    각 .ppr-{id}에 text-align, line-height, text-indent, margin 등 포함.
    HTML에서 <p class="ppr-3"> 등으로 참조.
    """
    para_styles = ti.get("para_styles", [])
    if not para_styles:
        return ""

    parts = ["/* paraPr → CSS 클래스 (문단 모양) */"]

    for ps in para_styles:
        pid = ps.get("id")
        rules = []

        # text-align
        align = _parapr_align(ps)
        if align:
            rules.append(f"  text-align: {align};")

        # line-height
        lh = _parapr_line_height(ps)
        if lh:
            rules.append(f"  line-height: {lh};")

        # text-indent
        margin = ps.get("margin", {})
        indent_hu = margin.get("indent_hu", 0)
        if indent_hu:
            indent_mm = indent_hu * HU_TO_MM
            rules.append(f"  text-indent: {indent_mm:.1f}mm;")

        # margin-left
        left_hu = margin.get("left_hu", 0)
        if left_hu:
            left_mm = left_hu * HU_TO_MM
            rules.append(f"  margin-left: {left_mm:.1f}mm;")

        # margin-right
        right_hu = margin.get("right_hu", 0)
        if right_hu:
            right_mm = right_hu * HU_TO_MM
            rules.append(f"  margin-right: {right_mm:.1f}mm;")

        # spacing before/after
        before = margin.get("before_hu", 0)
        if before:
            rules.append(f"  margin-top: {before * HU_TO_MM:.1f}mm;")
        after = margin.get("after_hu", 0)
        if after:
            rules.append(f"  margin-bottom: {after * HU_TO_MM:.1f}mm;")

        if rules:
            parts.append(f".ppr-{pid} {{\n" + "\n".join(rules) + "\n}")

    return "\n".join(parts) if len(parts) > 1 else ""


# ================================================================
#  ★ NEW: named style → .sty-{id} CSS 클래스
# ================================================================

def _named_style_css(ti: dict) -> str:
    """styles 목록 → .sty-{id} CSS 클래스.
    
    각 style은 charPrIDRef + paraPrIDRef 조합.
    → .sty-{id} = .cpr-{charPrIDRef} + .ppr-{paraPrIDRef} 의미.
    HTML에서 class="sty-0" 또는 class="cpr-5 ppr-11" 로 참조.
    """
    styles = ti.get("styles", [])
    if not styles:
        return ""

    parts = ["/* named styles */"]

    for s in styles:
        sid = s.get("id")
        name = s.get("name", "")
        cpr_id = s.get("charPrIDRef")
        ppr_id = s.get("paraPrIDRef")

        # 주석으로 매핑 기록
        parts.append(
            f"/* .sty-{sid} '{name}' = cpr-{cpr_id} + ppr-{ppr_id} */"
        )

    return "\n".join(parts)


# ================================================================
#  표 상세 CSS (열 너비, 셀 패딩)
# ================================================================

def _table_detail_css(ti: dict, sm: dict = None) -> str:
    if not sm:
        return ""

    body_indices = sm.get("body_tables", [])
    tables = ti.get("tables", [])
    if not body_indices or not tables:
        return ""

    parts = ["/* 표 상세 (tools 추출값) */"]

    for tbl_num, tbl_idx in enumerate(body_indices, 1):
        tbl = next((t for t in tables if t["index"] == tbl_idx), None)
        if not tbl:
            continue

        cls = f"tbl-{tbl_num}"

        # 열 너비
        col_pcts = tbl.get("colWidths_pct", [])
        if col_pcts:
            for c_idx, pct in enumerate(col_pcts):
                parts.append(
                    f".{cls} col:nth-child({c_idx + 1}) {{ width: {pct}%; }}"
                )

        # 셀 패딩
        cm = _first_cell_margin(tbl)
        if cm:
            ct = cm.get("top", 0) * HU_TO_MM
            cb = cm.get("bottom", 0) * HU_TO_MM
            cl = cm.get("left", 0) * HU_TO_MM
            cr = cm.get("right", 0) * HU_TO_MM
            parts.append(
                f".{cls} td, .{cls} th {{\n"
                f"  padding: {ct:.1f}mm {cr:.1f}mm {cb:.1f}mm {cl:.1f}mm;\n"
                f"}}"
            )

        # 헤더행 높이
        first_row = tbl.get("rows", [[]])[0]
        if first_row:
            h_hu = first_row[0].get("height_hu", 0)
            if h_hu > 0:
                h_mm = h_hu * HU_TO_MM
                parts.append(
                    f".{cls} thead th {{ height: {h_mm:.1f}mm; }}"
                )

    return "\n".join(parts) if len(parts) > 1 else ""


# ================================================================
#  보조 함수
# ================================================================

def _build_font_map(ti: dict) -> dict:
    """fonts → {(lang, id): face_name} 딕셔너리"""
    fm = {}
    for lang, flist in ti.get("fonts", {}).items():
        if isinstance(flist, list):
            for f in flist:
                fm[(lang, f.get("id", 0))] = f.get("face", "")
    return fm


def _charpr_font_family(charpr: dict, fm: dict) -> str:
    """charPr의 fontRef → 실제 font-family CSS 값"""
    if not charpr:
        return "'맑은 고딕', sans-serif"

    fr = charpr.get("fontRef", {})
    hangul_id = fr.get("hangul", 0)
    latin_id = fr.get("latin", 0)

    hangul_face = fm.get(("HANGUL", hangul_id), "")
    latin_face = fm.get(("LATIN", latin_id), "")

    faces = []
    if hangul_face:
        faces.append(f"'{hangul_face}'")
    if latin_face and latin_face != hangul_face:
        faces.append(f"'{latin_face}'")
    faces.append("sans-serif")

    return ", ".join(faces)


def _resolve_style_charpr(ti: dict, style_name: str) -> dict:
    """스타일 이름 → charPr dict 해석"""
    styles = ti.get("styles", [])
    char_styles = ti.get("char_styles", [])

    for s in styles:
        if s.get("name") == style_name:
            cpr_id = s.get("charPrIDRef")
            for cs in char_styles:
                if cs.get("id") == cpr_id:
                    return cs

    # 못 찾으면 charPr[0] (바탕글 기본)
    return char_styles[0] if char_styles else {}


def _resolve_style_parapr(ti: dict, style_name: str) -> dict:
    """스타일 이름 → paraPr dict 해석"""
    styles = ti.get("styles", [])
    para_styles = ti.get("para_styles", [])

    for s in styles:
        if s.get("name") == style_name:
            ppr_id = s.get("paraPrIDRef")
            for ps in para_styles:
                if ps.get("id") == ppr_id:
                    return ps

    return para_styles[0] if para_styles else {}


def _find_title_charpr(ti: dict) -> dict:
    """제목용 charPr 추론 (primaryCharPrIDRef 없을 때 폴백).
    
    헤드라인 폰트 or 가장 큰 크기 기준.
    """
    headline_keywords = ["헤드라인", "headline", "제목", "title"]
    fm = _build_font_map(ti)
    
    best = {}
    best_pt = 0
    for cs in ti.get("char_styles", []):
        pt = cs.get("height_pt", 0)
        fr = cs.get("fontRef", {})
        hangul_id = fr.get("hangul", 0)
        face = fm.get(("HANGUL", hangul_id), "").lower()
        
        # 헤드라인 폰트면 우선
        if any(kw in face for kw in headline_keywords):
            if pt > best_pt:
                best_pt = pt
                best = cs
    
    # 헤드라인 폰트 못 찾으면 가장 큰 것
    if not best:
        for cs in ti.get("char_styles", []):
            pt = cs.get("height_pt", 0)
            if pt > best_pt:
                best_pt = pt
                best = cs
    
    return best


def _parapr_line_height(parapr: dict) -> str:
    """paraPr → CSS line-height"""
    if not parapr:
        return "160%"
    ls = parapr.get("lineSpacing", {})
    ls_type = ls.get("type", "PERCENT")
    ls_val = ls.get("value", 160)
    if ls_type == "PERCENT":
        return f"{ls_val}%"
    elif ls_type == "FIXED":
        return f"{ls_val / 100:.1f}pt"
    else:
        return f"{ls_val}%"


def _parapr_align(parapr: dict) -> str:
    """paraPr → CSS text-align"""
    if not parapr:
        return "justify"
    align = parapr.get("align", "JUSTIFY")
    return {
        "JUSTIFY": "justify", "LEFT": "left", "RIGHT": "right",
        "CENTER": "center", "DISTRIBUTE": "justify",
        "DISTRIBUTE_SPACE": "justify"
    }.get(align, "justify")


def _hf_cell_padding(hf_info: dict | None) -> str:
    if not hf_info or not hf_info.get("table"):
        return "padding: 2px 4px;"
    rows = hf_info["table"].get("rows", [])
    if not rows or not rows[0]:
        return "padding: 2px 4px;"
    cm = rows[0][0].get("cellMargin", {})
    if not cm:
        return "padding: 2px 4px;"
    ct = cm.get("top", 0) * HU_TO_MM
    cb = cm.get("bottom", 0) * HU_TO_MM
    cl = cm.get("left", 0) * HU_TO_MM
    cr = cm.get("right", 0) * HU_TO_MM
    return f"padding: {ct:.1f}mm {cr:.1f}mm {cb:.1f}mm {cl:.1f}mm;"


def _first_cell_margin(tbl: dict) -> dict | None:
    for row in tbl.get("rows", []):
        for cell in row:
            cm = cell.get("cellMargin")
            if cm:
                return cm
    return None


def _mm(val) -> float:
    if isinstance(val, (int, float)):
        return float(val)
    try:
        return float(str(val).replace("mm", "").strip())
    except (ValueError, TypeError):
        return 20.0