"""Satori 카드뉴스 렌더링 핵심 모듈.

silent fallback 0건 보장:
- Node.js Satori 강제 호출 (Pillow fallback 절대 금지)
- hybrid-image silent fallback monkey-patch (install_silent_fallback_guard)
- 한글 폰트 명시 로드 (Pretendard + NotoSansCJK)

렌더링 방식:
- skills/satori-cardnews/render_html.mjs (전용 래퍼)를 primary로 사용
- satori_cli.js 존재 확인 (변경 금지 파일 — 참조/확인 전용)
- HTML 문자열 → Python html.parser → Satori virtual DOM JSON → render_html.mjs

IDS Phase 1 task-2401 — silent corruption 근본 수정.
"""

from __future__ import annotations

import html.parser as _html_parser
import importlib.util
import json
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any

# ─── 폰트 스택 상수 (system fallback 절대 포함 금지) ──────────────────────
KOREAN_FONT_STACK = "'Pretendard', 'Noto Sans KR'"

# ─── Pretendard 경로 후보 ───────────────────────────────────────────────────
PRETENDARD_PATHS: list[Path] = [
    Path("/home/jay/.local/share/fonts/Pretendard-Regular.otf"),
    Path("/home/jay/.local/share/fonts/Pretendard-Bold.otf"),
    Path("/home/jay/.local/share/fonts/Pretendard-SemiBold.otf"),
    Path("/home/jay/.local/share/fonts/Pretendard-Black.otf"),
]

# ─── NotoSansCJK 경로 후보 ─────────────────────────────────────────────────
NOTO_CJK_PATHS: list[Path] = [
    Path("/home/jay/.local/share/fonts/NotoSansCJKKR.otf"),
    Path("/home/jay/.local/share/fonts/NotoSansCJKKR-Bold.otf"),
    Path("/home/jay/workspace/tools/ai-image-gen/satori-test/fonts/NotoSansCJKkr-Regular.otf"),
    Path("/home/jay/workspace/tools/ai-image-gen/satori-test/fonts/NotoSansCJKkr-Bold.otf"),
]

# ─── Satori 스크립트 위치 ──────────────────────────────────────────────────
SATORI_SCRIPT_DIR = Path("/home/jay/workspace/tools/ai-image-gen/satori-test")
SATORI_CLI_SCRIPT = SATORI_SCRIPT_DIR / "satori_cli.js"

# ─── 전용 렌더링 래퍼 위치 ────────────────────────────────────────────────
RENDER_HTML_SCRIPT = Path("/home/jay/workspace/skills/satori-cardnews/render_html.mjs")

# ─── monkey-patch sentinel ─────────────────────────────────────────────────
_GUARD_INSTALLED: bool = False


# ─── guard 함수 (monkey-patch 대체용) ─────────────────────────────────────

def _blocked_fallback(*args: Any, **kwargs: Any) -> Any:
    """hybrid-image _pillow_fallback 차단 — silent corruption 방지."""
    del args, kwargs
    raise RuntimeError(
        "silent fallback blocked by satori-cardnews guard — "
        "investigate Node.js Satori failure"
    )


def _blocked_write(*args: Any, **kwargs: Any) -> Any:
    """hybrid-image _write_blank_png 차단 — silent corruption 방지."""
    del args, kwargs
    raise RuntimeError(
        "silent fallback blocked by satori-cardnews guard — "
        "investigate Node.js Satori failure"
    )


# ─── public API ────────────────────────────────────────────────────────────

def find_korean_fonts() -> list[dict[str, object]]:
    """시스템에서 Pretendard + NotoSansCJK 폰트를 검색하여 satori fonts 옵션 dict 리스트로 반환.

    하나라도 없으면 FileNotFoundError raise.

    Returns:
        [{"name": str, "path": str, "weight": int, "style": str}, ...] 형태의 리스트.

    Raises:
        FileNotFoundError: Pretendard 또는 NotoSansCJK 폰트 파일을 찾을 수 없을 때.
    """
    result: list[dict[str, object]] = []

    # Pretendard Regular
    pretendard_regular: Path | None = None
    pretendard_bold: Path | None = None
    for p in PRETENDARD_PATHS:
        if p.exists():
            name_lower = p.name.lower()
            if "bold" in name_lower or "semibold" in name_lower or "black" in name_lower:
                if pretendard_bold is None:
                    pretendard_bold = p
            else:
                if pretendard_regular is None:
                    pretendard_regular = p

    if pretendard_regular is None:
        raise FileNotFoundError(
            "Pretendard Regular font not found. "
            f"Searched: {[str(p) for p in PRETENDARD_PATHS]}"
        )
    result.append({
        "name": "Pretendard",
        "path": str(pretendard_regular),
        "weight": 400,
        "style": "normal",
    })

    if pretendard_bold is not None:
        result.append({
            "name": "Pretendard",
            "path": str(pretendard_bold),
            "weight": 700,
            "style": "normal",
        })

    # NotoSansCJK Regular
    noto_regular: Path | None = None
    noto_bold: Path | None = None
    for p in NOTO_CJK_PATHS:
        if p.exists():
            name_lower = p.name.lower()
            if "bold" in name_lower:
                if noto_bold is None:
                    noto_bold = p
            else:
                if noto_regular is None:
                    noto_regular = p

    if noto_regular is None:
        raise FileNotFoundError(
            "NotoSansCJK Regular font not found. "
            f"Searched: {[str(p) for p in NOTO_CJK_PATHS]}"
        )
    result.append({
        "name": "Noto Sans KR",
        "path": str(noto_regular),
        "weight": 400,
        "style": "normal",
    })

    if noto_bold is not None:
        result.append({
            "name": "Noto Sans KR",
            "path": str(noto_bold),
            "weight": 700,
            "style": "normal",
        })

    return result


def _html_to_vdom(html_str: str, width: int, height: int) -> dict[str, Any]:
    """HTML 문자열을 Satori virtual DOM 객체로 변환.

    Python html.parser를 사용하여 HTML을 파싱하고
    Satori가 소비할 수 있는 virtual DOM JSON 구조로 변환합니다.

    지원: 단순 HTML (div, span, 인라인 style 속성).
    Satori는 inline style만 지원 (CSS class 미지원).
    """

    class _VdomBuilder(_html_parser.HTMLParser):
        def __init__(self) -> None:
            super().__init__()
            self._stack: list[dict[str, Any]] = []
            self.root: dict[str, Any] | None = None

        def _parse_inline_style(self, style_str: str) -> dict[str, Any]:
            """인라인 CSS 문자열을 camelCase dict로 변환 (Satori 호환)."""
            result: dict[str, Any] = {}
            for declaration in style_str.split(";"):
                declaration = declaration.strip()
                if ":" not in declaration:
                    continue
                prop, _, val = declaration.partition(":")
                prop = prop.strip()
                val = val.strip()
                if not prop or not val:
                    continue
                # CSS property → camelCase 변환
                camel = re.sub(r"-([a-z])", lambda m: m.group(1).upper(), prop)
                # 숫자 값 변환 (px 제거 시도하지 않음 — Satori는 px 문자열 허용)
                result[camel] = val
            return result

        def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
            attr_dict: dict[str, Any] = {}
            style_dict: dict[str, Any] = {}

            for name, value in attrs:
                if name == "style" and value:
                    style_dict = self._parse_inline_style(value)
                elif value is not None:
                    attr_dict[name] = value

            if style_dict:
                attr_dict["style"] = style_dict

            node: dict[str, Any] = {
                "type": tag,
                "props": attr_dict,
            }

            if self._stack:
                parent = self._stack[-1]
                if "children" not in parent["props"]:
                    parent["props"]["children"] = []
                children = parent["props"]["children"]
                if isinstance(children, list):
                    children.append(node)
                elif isinstance(children, str):
                    parent["props"]["children"] = [children, node]
                else:
                    parent["props"]["children"] = [node]

            self._stack.append(node)

        def handle_endtag(self, tag: str) -> None:
            if self._stack and self._stack[-1]["type"] == tag:
                node = self._stack.pop()
                if not self._stack:
                    self.root = node

        def handle_data(self, data: str) -> None:
            text = data.strip()
            if not text or not self._stack:
                return
            parent = self._stack[-1]
            if "children" not in parent["props"]:
                parent["props"]["children"] = text
            else:
                existing = parent["props"]["children"]
                if isinstance(existing, str):
                    parent["props"]["children"] = existing + text
                elif isinstance(existing, list):
                    existing.append(text)

    parser = _VdomBuilder()
    parser.feed(html_str)

    if parser.root is not None:
        return parser.root

    # 파싱 실패 시 fallback wrapper
    return {
        "type": "div",
        "props": {
            "style": {
                "display": "flex",
                "width": f"{width}px",
                "height": f"{height}px",
                "background": "#1a1a2e",
                "fontFamily": KOREAN_FONT_STACK,
                "alignItems": "center",
                "justifyContent": "center",
                "color": "#ffffff",
                "fontSize": "48px",
            },
            "children": "렌더링 오류 — HTML 파싱 실패",
        },
    }


def safe_render_html_to_png(
    html: str,
    output_path: Path,
    *,
    width: int,
    height: int,
) -> Path:
    """Satori (Node.js) 강제 렌더. 실패 시 RuntimeError 또는 FileNotFoundError raise.

    silent fallback 절대 금지:
    - 폰트 파일 존재 확인 → 없으면 FileNotFoundError
    - node binary 존재 확인 → 없으면 FileNotFoundError
    - satori_cli.js 존재 확인 → 없으면 FileNotFoundError (참조용)
    - render_html.mjs 존재 확인 → 없으면 FileNotFoundError
    - 출력 PNG 크기 검증 (≥ 10KB) → 미달 시 RuntimeError

    Args:
        html: Satori-호환 HTML 문자열.
        output_path: 출력 PNG 파일 경로.
        width: 이미지 폭(px).
        height: 이미지 높이(px).

    Returns:
        실제 저장된 PNG 파일 경로.

    Raises:
        FileNotFoundError: 폰트 / node binary / satori_cli.js / render_html.mjs 부재.
        RuntimeError: Satori 호출 실패 또는 출력 PNG 크기 미달.
    """
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # 1. 폰트 존재 확인
    find_korean_fonts()  # FileNotFoundError raise if missing

    # 2. node binary 확인
    node_bin = shutil.which("node")
    if node_bin is None:
        raise FileNotFoundError("node binary not found — install Node.js (e.g. nvm)")

    # 3. satori_cli.js 존재 확인 (참조용 — 변경 금지 파일)
    if not SATORI_CLI_SCRIPT.exists():
        raise FileNotFoundError(
            f"satori_cli.js not found at {SATORI_CLI_SCRIPT} — "
            "check tools/ai-image-gen/satori-test/ directory"
        )

    # 4. render_html.mjs 존재 확인 (실제 렌더링 래퍼)
    if not RENDER_HTML_SCRIPT.exists():
        raise FileNotFoundError(
            f"render_html.mjs not found at {RENDER_HTML_SCRIPT} — "
            "skills/satori-cardnews/render_html.mjs 필요"
        )

    # 5. HTML → Satori virtual DOM JSON 변환
    vdom = _html_to_vdom(html, width, height)

    # 6. JSON payload 구성
    payload: dict[str, Any] = {
        "element": vdom,
        "output": str(output_path),
        "width": width,
        "height": height,
    }
    json_str = json.dumps(payload, ensure_ascii=False)

    # 7. subprocess 호출 (render_html.mjs)
    # render_html.mjs는 satori-test의 node_modules를 사용하므로 cwd=SATORI_SCRIPT_DIR
    try:
        result = subprocess.run(
            [node_bin, str(RENDER_HTML_SCRIPT), "--json", json_str],
            capture_output=True,
            text=True,
            cwd=str(SATORI_SCRIPT_DIR),
            timeout=120,
        )
    except subprocess.TimeoutExpired as exc:
        raise RuntimeError(
            f"render_html.mjs timed out after 120s for output={output_path}"
        ) from exc
    except FileNotFoundError as exc:
        raise FileNotFoundError(
            f"Failed to launch node: {exc}"
        ) from exc

    if result.returncode != 0:
        stderr_snippet = result.stderr[:2000] if result.stderr else "(empty)"
        raise RuntimeError(
            f"render_html.mjs exited with code {result.returncode}.\n"
            f"stderr: {stderr_snippet}"
        )

    # 8. 출력 파일 검증
    if not output_path.exists():
        stdout_snippet = result.stdout[:500] if result.stdout else "(empty)"
        raise RuntimeError(
            f"render_html.mjs reported success but output file not found: {output_path}\n"
            f"stdout: {stdout_snippet}"
        )

    file_size = output_path.stat().st_size
    min_size_bytes = 10 * 1024  # 10KB
    if file_size < min_size_bytes:
        raise RuntimeError(
            f"Output PNG too small ({file_size} bytes < {min_size_bytes} bytes). "
            f"Likely a placeholder or blank image: {output_path}"
        )

    return output_path


def install_silent_fallback_guard() -> None:
    """import-time monkey-patch: hybrid-image의 _pillow_fallback과 _write_blank_png를 RuntimeError로 차단.

    회귀 시 silent corruption 발생을 즉시 stack trace로 표면화.
    멱등성: 이미 패치되었으면 재패치 스킵.

    패치 전략:
    1. sys.modules에서 이미 로드된 hybrid-image _satori 모듈 탐색 (동일 인스턴스 패치)
    2. 없으면 importlib으로 새 모듈 로드 후 패치 + sys.modules 등록
    3. hybrid-image 미존재 환경(CI, 테스트)에서는 silently return
    """
    import sys as _sys

    global _GUARD_INSTALLED

    if _GUARD_INSTALLED:
        return

    hi_satori_path = Path("/home/jay/workspace/skills/hybrid-image/patterns/_satori.py")
    if not hi_satori_path.exists():
        # hybrid-image 미존재 환경 — silently skip
        _GUARD_INSTALLED = True
        return

    # sys.modules에서 hybrid-image _satori 탐색 (다양한 등록명 시도)
    _HI_MOD_KEYS = [
        "skills.hybrid_image.patterns._satori",
        "hybrid_image.patterns._satori",
        "hi_satori",
        "_hybrid_image_satori_guard",
        "skills.hybrid-image.patterns._satori",
    ]

    hi_mod = None
    for key in _HI_MOD_KEYS:
        if key in _sys.modules:
            hi_mod = _sys.modules[key]
            break

    # sys.modules에 없으면 importlib으로 로드
    if hi_mod is None:
        try:
            spec = importlib.util.spec_from_file_location(
                "_hybrid_image_satori_guard",
                str(hi_satori_path),
            )
            if spec is None or spec.loader is None:
                _GUARD_INSTALLED = True
                return

            hi_mod = importlib.util.module_from_spec(spec)
            _sys.modules["_hybrid_image_satori_guard"] = hi_mod
            spec.loader.exec_module(hi_mod)  # type: ignore[union-attr]
        except Exception as exc:
            # 마아트 권고: silent return 금지 — 명시적 stderr 기록
            import sys as _diag_sys
            _diag_sys.stderr.write(
                f"[satori-cardnews guard] WARNING: hybrid-image _satori import failed; "
                f"silent fallback monkey-patch NOT installed (file={hi_satori_path}, error={exc!r}). "
                f"If hybrid-image is in use, silent corruption may occur until guard is reinstalled.\n"
            )
            _GUARD_INSTALLED = True
            return

    # 이미 패치된 경우 스킵 (함수 이름으로 판단)
    existing_fallback = getattr(hi_mod, "_pillow_fallback", None)
    if existing_fallback is not None and getattr(existing_fallback, "__name__", "") == "_blocked_fallback":
        _GUARD_INSTALLED = True
        return

    # monkey-patch 적용
    hi_mod._pillow_fallback = _blocked_fallback  # type: ignore[attr-defined]
    hi_mod._write_blank_png = _blocked_write  # type: ignore[attr-defined]

    _GUARD_INSTALLED = True
