"""magazine-ppt-ko / html_to_pptx — manifest+HTML → 단일 PPTX 컴파일러.

IDS Phase 2의 G2 모듈. ``python-pptx``로 1920x1080 16:9 PPTX를 생성하며,
모든 텍스트박스에 한글 폰트(Pretendard 우선, 없을 시 Noto Sans KR)를 강제 지정한다.

외부 API/SDK는 일절 임포트/호출하지 않는다 (IDS §0.5).

Public API:
    compile_pptx(html_dir, manifest_path, output_pptx) -> Path
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any


# python-pptx 임포트는 함수 진입 시점에 lazy로 시도한다 (테스트 skip 호환).


PRIMARY_FONT: str = "Pretendard"
FALLBACK_FONT: str = "Noto Sans KR"
SLIDE_WIDTH_EMU: int = 12192000  # 1920px @ 96dpi → EMU
SLIDE_HEIGHT_EMU: int = 6858000  # 1080px @ 96dpi → EMU


def _import_python_pptx() -> Any:
    """python-pptx를 lazy import. 실패 시 명확한 ImportError 를 raise.

    Raises:
        ImportError: python-pptx 미설치 시.
    """
    try:
        import pptx  # type: ignore[import-not-found]
    except ImportError as exc:  # pragma: no cover — 테스트에서 skip
        raise ImportError(
            "python-pptx is required for compile_pptx; install via "
            "`pip install python-pptx`"
        ) from exc
    return pptx


def _hex_to_rgb_tuple(hex_color: str) -> tuple[int, int, int]:
    """``#RRGGBB`` → (r, g, b) tuple. 잘못된 입력은 흰색."""
    s = hex_color.lstrip("#")
    if len(s) >= 6:
        try:
            return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))
        except ValueError:
            return (255, 255, 255)
    return (255, 255, 255)


def _apply_korean_font(text_frame: Any) -> None:
    """텍스트 프레임의 모든 run에 한글 폰트 강제.

    - run.font.name = "Pretendard" (없을 시 PowerPoint가 fallback chain 사용)
    - east_asia 속성도 Pretendard로 지정 (한글 글리프 보장)
    """
    for paragraph in text_frame.paragraphs:
        for run in paragraph.runs:
            run.font.name = PRIMARY_FONT
            # rPr/eastAsia 직접 조작 — 한글 east-asia 폰트 강제
            try:
                rpr = run._r.get_or_add_rPr()  # type: ignore[attr-defined]
                # east asia 폰트 element 찾기/추가
                from pptx.oxml.ns import qn  # type: ignore[import-not-found]

                ea = rpr.find(qn("a:ea"))
                if ea is None:
                    from lxml import etree  # type: ignore[import-not-found]

                    ea = etree.SubElement(rpr, qn("a:ea"))
                ea.set("typeface", PRIMARY_FONT)
            except Exception:
                # east_asia 처리 실패해도 latin 폰트는 적용됨 → 진행
                pass


def compile_pptx(
    html_dir: str | Path,
    manifest_path: str | Path,
    output_pptx: str | Path,
) -> Path:
    """manifest.json의 슬라이드 정보 + HTML 텍스트로 단일 PPTX 빌드.

    Args:
        html_dir: build_deck.py가 출력한 HTML/manifest 디렉토리.
        manifest_path: manifest.json 경로.
        output_pptx: 출력 .pptx 파일 경로.

    Returns:
        생성된 PPTX 파일의 Path.

    Raises:
        ImportError: python-pptx 미설치 시.
        FileNotFoundError: manifest_path가 존재하지 않을 때.
    """
    pptx_module = _import_python_pptx()
    from pptx.util import Emu, Pt  # type: ignore[import-not-found]
    from pptx.dml.color import RGBColor  # type: ignore[import-not-found]

    manifest_p = Path(manifest_path)
    if not manifest_p.exists():
        raise FileNotFoundError(f"manifest not found: {manifest_p}")

    with manifest_p.open("r", encoding="utf-8") as f:
        manifest: dict[str, Any] = json.load(f)

    tokens: dict[str, Any] = manifest.get("tokens", {})
    bg_rgb = _hex_to_rgb_tuple(str(tokens.get("background", "#ffffff")))
    text_rgb = _hex_to_rgb_tuple(str(tokens.get("text_primary", "#111111")))
    accent_rgb = _hex_to_rgb_tuple(str(tokens.get("accent", "#3ecf8e")))

    presentation = pptx_module.Presentation()
    presentation.slide_width = Emu(SLIDE_WIDTH_EMU)
    presentation.slide_height = Emu(SLIDE_HEIGHT_EMU)

    blank_layout = presentation.slide_layouts[6]  # blank

    out_path = Path(output_pptx)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    html_dir_p = Path(html_dir)

    for slide_meta in manifest.get("slides", []):
        slide = presentation.slides.add_slide(blank_layout)

        # 배경 색상 (bg fill)
        try:
            bg_fill = slide.background.fill
            bg_fill.solid()
            bg_fill.fore_color.rgb = RGBColor(*bg_rgb)
        except Exception:
            # 일부 layout에서 background 직접 fill 미지원 — 박스로 보강
            pass

        # 풀-페이지 배경 박스 (안전 보강)
        bg_box = slide.shapes.add_shape(
            1,  # MSO_SHAPE.RECTANGLE
            Emu(0),
            Emu(0),
            Emu(SLIDE_WIDTH_EMU),
            Emu(SLIDE_HEIGHT_EMU),
        )
        bg_box.fill.solid()
        bg_box.fill.fore_color.rgb = RGBColor(*bg_rgb)
        bg_box.line.fill.background()
        try:
            sp_pr = bg_box._element.spPr  # type: ignore[attr-defined]
            sp_pr.set("alphaEffectsApplied", "0")
        except Exception:
            pass

        # 슬라이드 제목 박스 (layout 이름 + 인덱스)
        title_box = slide.shapes.add_textbox(
            Emu(640000), Emu(640000), Emu(11000000), Emu(900000)
        )
        title_tf = title_box.text_frame
        title_tf.word_wrap = True
        layout_name = str(slide_meta.get("layout", ""))
        title_tf.text = f"{layout_name}"
        for run in title_tf.paragraphs[0].runs:
            run.font.size = Pt(20)
            run.font.bold = True
            run.font.color.rgb = RGBColor(*accent_rgb)
        _apply_korean_font(title_tf)

        # 본문 텍스트박스 — manifest의 모든 한글/사용자 텍스트 슬롯을 줄바꿈으로 결합
        body_box = slide.shapes.add_textbox(
            Emu(640000), Emu(1700000), Emu(11000000), Emu(4800000)
        )
        body_tf = body_box.text_frame
        body_tf.word_wrap = True

        variables: dict[str, str] = slide_meta.get("variables", {}) or {}
        korean_strings: list[str] = list(slide_meta.get("korean_strings", []) or [])

        # Variables 모든 값을 슬라이드 텍스트로 — 한글 텍스트는 그대로 보존
        body_text_lines: list[str] = []
        seen: set[str] = set()
        # 한글 슬롯을 우선 순위로 추가
        for s in korean_strings:
            if s and s not in seen:
                body_text_lines.append(s)
                seen.add(s)
        for v in variables.values():
            sval = str(v)
            if sval and sval not in seen:
                body_text_lines.append(sval)
                seen.add(sval)

        if not body_text_lines:
            body_text_lines = [layout_name]

        body_tf.text = body_text_lines[0]
        for line in body_text_lines[1:]:
            p = body_tf.add_paragraph()
            p.text = line

        for paragraph in body_tf.paragraphs:
            for run in paragraph.runs:
                run.font.size = Pt(18)
                run.font.color.rgb = RGBColor(*text_rgb)
        _apply_korean_font(body_tf)

        # html_dir에 해당 HTML이 존재하는지 확인 (디버깅 용 — 본문엔 영향 없음)
        html_filename = str(slide_meta.get("file", ""))
        _ = (html_dir_p / html_filename).exists() if html_filename else False

    presentation.save(str(out_path))
    return out_path


__all__ = ["compile_pptx", "PRIMARY_FONT", "FALLBACK_FONT"]
