"""magazine-ppt-ko / build_deck — 슬라이드별 HTML + manifest.json 빌더.

IDS Phase 2의 G2 구현 게이트 모듈. 외부 API/SDK는 일절 임포트/호출하지 않으며,
Phase 1 ``satori-cardnews`` 스킬의 ``design_md_loader``를 import하여 디자인 토큰을
HTML/CSS에 주입한다.

핵심 계약 (IDS):
- §0.1 한글 100%: manifest 의 한글 슬롯 값이 HTML에 string-match로 존재
- §0.5 외부 API 차단: openai/anthropic/google/urlopen/requests/httpx 사용 금지

Public API:
    build(layout_names, variables_list, brand=None, output_dir=None) -> BuildResult
"""

from __future__ import annotations

import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

# --- Phase 1 design-md loader import (sys.path 주입 후 직접 import) -----------
_SATORI_CARDNEWS_PATH = "/home/jay/workspace/skills/satori-cardnews"
if _SATORI_CARDNEWS_PATH not in sys.path:
    sys.path.insert(0, _SATORI_CARDNEWS_PATH)

try:
    from design_md_loader import (  # type: ignore[import-not-found]
        fallback_tokens,
        load_design_md,
    )
except Exception:  # pragma: no cover — Phase 1 모듈 부재 시 graceful fallback
    def load_design_md(brand: str) -> dict[str, Any]:  # type: ignore[no-redef]
        raise FileNotFoundError(f"design_md_loader unavailable: {brand}")

    def fallback_tokens() -> dict[str, Any]:  # type: ignore[no-redef]
        return {
            "primary": "#0f0f0f",
            "secondary": "#171717",
            "accent": "#3ecf8e",
            "background": "#ffffff",
            "text_primary": "#111111",
            "text_secondary": "#666666",
            "font_display": "Pretendard",
            "font_body": "Pretendard",
            "spacing": {"sm": 8, "md": 16, "lg": 32, "xl": 64},
            "border_radius": "8px",
            "shadow": [],
        }


# --- 상수 -----------------------------------------------------------------
SKILL_ROOT = Path("/home/jay/workspace/skills/magazine-ppt-ko")
TEMPLATES_DIR = SKILL_ROOT / "templates"
REGISTRY_PATH = TEMPLATES_DIR / "registry.json"

# 한글 폰트 fallback chain (변경 금지)
FONT_STACK: str = "'Pretendard', 'Noto Sans KR', sans-serif"

# Jinja2-style placeholder 정규식 ({{ var_name }} 또는 {{var_name}})
_PLACEHOLDER_RE = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")

# 한글 문자 범위 (가-힣, 자모, 호환 자모)
_KOREAN_RE = re.compile(r"[가-힣ᄀ-ᇿ㄰-㆏]")


# --- 데이터클래스 ---------------------------------------------------------


@dataclass
class SlideManifestEntry:
    """슬라이드별 manifest 항목."""

    file: str
    layout: str
    family: str
    korean_strings: list[str] = field(default_factory=list)
    variables: dict[str, str] = field(default_factory=dict)


@dataclass
class BuildResult:
    """build() 반환 객체."""

    output_dir: Path
    html_paths: list[Path]
    manifest_path: Path
    manifest: dict[str, Any]
    tokens: dict[str, Any]


# --- 내부 헬퍼 -----------------------------------------------------------


def _load_registry() -> dict[str, Any]:
    """templates/registry.json 로드."""
    with REGISTRY_PATH.open("r", encoding="utf-8") as f:
        data: dict[str, Any] = json.load(f)
    return data


def _resolve_tokens(brand: str | None) -> dict[str, Any]:
    """brand 인자에 따라 design-md 토큰 해석. 실패 시 fallback.

    Args:
        brand: ``resources/design-md/{brand}/`` 디렉토리 이름. None이면 fallback.

    Returns:
        DesignTokens dict — 항상 유효한 토큰 dict 반환.
    """
    if not brand:
        return dict(fallback_tokens())
    try:
        tokens = load_design_md(brand)
        return dict(tokens)
    except (FileNotFoundError, ValueError):
        return dict(fallback_tokens())


def _render_placeholders(template: str, mapping: dict[str, str]) -> str:
    """``{{ key }}`` placeholder를 mapping[key]로 치환. 누락 시 빈 문자열.

    Args:
        template: HTML/CSS 원본 텍스트.
        mapping: 변수명 → 치환값 dict.

    Returns:
        모든 placeholder가 치환된 텍스트.
    """

    def repl(match: re.Match[str]) -> str:
        key = match.group(1)
        value = mapping.get(key, "")
        return str(value)

    return _PLACEHOLDER_RE.sub(repl, template)


def _token_mapping(tokens: dict[str, Any]) -> dict[str, str]:
    """디자인 토큰을 ``tk_*`` placeholder mapping으로 변환."""
    return {
        "tk_primary": str(tokens.get("primary", "#0f0f0f")),
        "tk_secondary": str(tokens.get("secondary", "#171717")),
        "tk_accent": str(tokens.get("accent", "#3ecf8e")),
        "tk_background": str(tokens.get("background", "#ffffff")),
        "tk_text_primary": str(tokens.get("text_primary", "#111111")),
        "tk_text_secondary": str(tokens.get("text_secondary", "#666666")),
        "tk_radius": str(tokens.get("border_radius", "8px")),
    }


def _has_korean(value: str) -> bool:
    """문자열에 한글 문자가 포함되어 있는지 확인."""
    return bool(_KOREAN_RE.search(value))


def _force_font_stack(css: str) -> str:
    """CSS 내 모든 ``font-family`` 선언을 강제 폰트 스택으로 치환.

    - 임의로 추가된 fallback 차단
    - 치환 후 ``'Pretendard', 'Noto Sans KR', sans-serif`` 만 존재
    """
    pattern = re.compile(r"font-family\s*:\s*[^;]+;", re.IGNORECASE)
    return pattern.sub(f"font-family: {FONT_STACK};", css)


def _render_slide_html(
    layout_meta: dict[str, Any],
    variables: dict[str, str],
    tokens: dict[str, Any],
) -> tuple[str, list[str]]:
    """단일 슬라이드 HTML(인라인 CSS) 렌더링.

    Args:
        layout_meta: registry.json 의 layout 메타.
        variables: 사용자 입력 변수 dict (한글 가능).
        tokens: 디자인 토큰 dict.

    Returns:
        (rendered_html, korean_strings_list) — HTML 본문 + 한글 슬롯 리스트.
    """
    html_path = TEMPLATES_DIR / layout_meta["path"]
    css_path = TEMPLATES_DIR / layout_meta["css"]
    raw_html = html_path.read_text(encoding="utf-8")
    raw_css = css_path.read_text(encoding="utf-8")

    token_map = _token_mapping(tokens)

    # CSS 먼저 치환 (디자인 토큰만 사용)
    rendered_css = _render_placeholders(raw_css, token_map)
    rendered_css = _force_font_stack(rendered_css)

    # HTML은 사용자 변수 + 토큰 (HTML에서도 토큰 placeholder 가능)
    html_map: dict[str, str] = {}
    html_map.update(token_map)
    html_map.update({k: str(v) for k, v in variables.items()})
    rendered_html = _render_placeholders(raw_html, html_map)

    # external <link rel="stylesheet"> 제거하고 inline <style>로 변환
    # (PPTX 파이프라인에서 단일 파일로 다루기 위함)
    rendered_html = re.sub(
        r"<link[^>]*?rel=[\"']stylesheet[\"'][^>]*?>",
        f"<style>\n{rendered_css}\n</style>",
        rendered_html,
        count=1,
    )

    # 만약 link 태그가 없었다면 <head> 끝에 style 삽입
    if "<style>" not in rendered_html:
        rendered_html = rendered_html.replace(
            "</head>", f"<style>\n{rendered_css}\n</style>\n</head>", 1
        )

    # HTML 본문 자체에 한글 폰트 스택이 안전하게 들어갔는지 확인 (보강 inject)
    if FONT_STACK not in rendered_html:
        rendered_html = rendered_html.replace(
            "<style>",
            f"<style>\nbody {{ font-family: {FONT_STACK}; }}\n",
            1,
        )

    # 한글 슬롯 추출
    korean_slots = layout_meta.get("korean_slots", [])
    korean_strings: list[str] = []
    for slot in korean_slots:
        value = variables.get(slot, "")
        if value and _has_korean(value):
            korean_strings.append(value)
    return rendered_html, korean_strings


# --- Public API -----------------------------------------------------------


def list_layouts() -> list[str]:
    """registry.json에 등록된 모든 layout 이름 반환."""
    registry = _load_registry()
    layouts = registry.get("layouts", {})
    return sorted(layouts.keys())


def get_layout_meta(layout_name: str) -> dict[str, Any]:
    """layout_name의 registry 메타 반환.

    Raises:
        KeyError: layout_name이 registry에 존재하지 않을 때.
    """
    registry = _load_registry()
    layouts: dict[str, Any] = registry.get("layouts", {})
    if layout_name not in layouts:
        raise KeyError(f"unknown layout: {layout_name}")
    meta: dict[str, Any] = layouts[layout_name]
    return meta


def build(
    layout_names: list[str],
    variables_list: list[dict[str, str]],
    brand: str | None = None,
    output_dir: str | Path | None = None,
) -> BuildResult:
    """슬라이드별 HTML + manifest.json 빌드.

    Args:
        layout_names: registry.json의 layout 이름 리스트 (순서대로 슬라이드 생성).
        variables_list: 각 슬라이드에 주입할 변수 dict 리스트. ``len`` == ``layout_names``.
        brand: ``resources/design-md/{brand}/DESIGN.md`` 의 brand 이름. None이면 fallback.
        output_dir: 슬라이드 HTML/manifest 출력 경로. None이면 ``/tmp/magazine-ppt-ko-build``.

    Returns:
        BuildResult — output_dir, html_paths, manifest_path, manifest dict, tokens.

    Raises:
        ValueError: layout_names와 variables_list 길이 불일치.
        KeyError: 미등록 layout 사용.
    """
    if len(layout_names) != len(variables_list):
        raise ValueError(
            f"layout_names ({len(layout_names)}) and variables_list "
            f"({len(variables_list)}) must have the same length"
        )

    out = Path(output_dir) if output_dir else Path("/tmp/magazine-ppt-ko-build")
    out.mkdir(parents=True, exist_ok=True)

    tokens = _resolve_tokens(brand)

    manifest_slides: list[dict[str, Any]] = []
    html_paths: list[Path] = []

    for idx, (layout_name, variables) in enumerate(
        zip(layout_names, variables_list), start=1
    ):
        meta = get_layout_meta(layout_name)
        rendered_html, korean_strings = _render_slide_html(meta, variables, tokens)

        filename = f"{idx:02d}_{layout_name}.html"
        slide_path = out / filename
        slide_path.write_text(rendered_html, encoding="utf-8")
        html_paths.append(slide_path)

        manifest_slides.append(
            {
                "index": idx,
                "file": filename,
                "layout": layout_name,
                "family": meta.get("family", ""),
                "korean_strings": korean_strings,
                "variables": {k: str(v) for k, v in variables.items()},
            }
        )

    manifest: dict[str, Any] = {
        "skill": "magazine-ppt-ko",
        "version": "1.0.0",
        "brand": brand,
        "tokens": tokens,
        "font_stack": FONT_STACK,
        "slides": manifest_slides,
    }
    manifest_path = out / "manifest.json"
    manifest_path.write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
    )

    return BuildResult(
        output_dir=out,
        html_paths=html_paths,
        manifest_path=manifest_path,
        manifest=manifest,
        tokens=tokens,
    )


__all__ = [
    "BuildResult",
    "FONT_STACK",
    "SlideManifestEntry",
    "build",
    "get_layout_meta",
    "list_layouts",
]
