"""retry_loop.py — IDS Phase 1+2 retry-until-pass 루프.

목적:
  quality_evaluator.evaluate_image() FAIL 시 자동 재시도하여
  PASS 이미지만 최종 산출. silent pass 절대 금지.

회장 3원칙:
  - 더 가볍게: 외부 API 호출 0
  - 더 정확하게: 코드 수치 기반 PASS/FAIL 판정
  - 더 퀄리티 높게: 5회 retry-until-pass, 실패 시 RuntimeError 발생
                     (운영팀 알림 의무)

CRITICAL: silent pass 절대 금지.
  5회 retry 후 PASS 미달 시 반드시 RuntimeError를 raise한다.
  예외를 삼키거나 부분 결과를 반환하는 것은 금지.
  5회 retry 후 ERROR 발생 시 운영팀 핸들링 의무.

Phase 2 통합 (task-2428): _render_with_seed가 hybrid-image PATTERNS dict를
직접 dispatch한다. 6축 hint는 design_tokens에 'hint_*' prefix로 주입된다.
패턴은 모르는 hint 키를 graceful 무시하므로 회귀 0이 보장된다.

  - seed: random_seed (배경/레이아웃 변형)
  - hints["force_pattern_diversity"]: 단조 패턴 강제 회피
  - hints["force_brand_color"]: design-md primary 색 강제 적용
  - hints["force_pattern_signature"]: 5 hybrid 패턴별 특징 강제
  - hints["min_font_px"]: 폰트 크기 강제 임계
  - hints["force_korean_font"]: 한글 폰트 강제 (fallback 차단)
  - hints["force_spatial_coherence"]: 공간 일관성 강제
"""

from __future__ import annotations

import random
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable

from .quality_evaluator import EvalResult, evaluate_image


# ---------------------------------------------------------------------------
# Pattern dispatch (lazy import to avoid hard dependency at module load)
# ---------------------------------------------------------------------------

# 풀네임 ↔ 짧은 키 매핑 (quality_evaluator.PATTERN_THRESHOLDS는 풀네임만 지원)
_PATTERN_NAME_MAP: dict[str, str] = {
    "h1_photo_card": "h1",
    "h2_illustration_card": "h2",
    "h3_gpt_style_card": "h3",
    "h4_gradient_card": "h4",
    "h5_user_photo_card": "h5",
}


_HYBRID_PATTERNS_DIR = Path("/home/jay/workspace/skills/hybrid-image/patterns")
_PATTERNS_MODULE_CACHE: Any | None = None


def _load_hybrid_patterns_module() -> Any:
    """hybrid-image/patterns 모듈을 file-based importlib로 로드한다.

    skills/hybrid-image 디렉토리 이름에 하이픈이 있어 dotted import 불가능 →
    importlib.util.spec_from_file_location으로 우회한다.
    동일 프로세스 내 1회 캐시.
    """
    global _PATTERNS_MODULE_CACHE
    if _PATTERNS_MODULE_CACHE is not None:
        return _PATTERNS_MODULE_CACHE

    import importlib.util
    import sys

    init_path = _HYBRID_PATTERNS_DIR / "__init__.py"
    if not init_path.exists():
        raise RuntimeError(
            f"hybrid-image patterns 디렉토리 부재: {init_path}"
        )

    # patterns/__init__.py가 'from .h1_photo_card import render' 등 상대 import를 사용하므로
    # 패키지로 등록해야 한다. _hybrid_patterns_pkg 이름으로 sys.modules에 주입.
    pkg_name = "_hybrid_patterns_pkg"
    if pkg_name in sys.modules:
        _PATTERNS_MODULE_CACHE = sys.modules[pkg_name]
        return _PATTERNS_MODULE_CACHE

    spec = importlib.util.spec_from_file_location(
        pkg_name,
        init_path,
        submodule_search_locations=[str(_HYBRID_PATTERNS_DIR)],
    )
    if spec is None or spec.loader is None:
        raise RuntimeError(f"importlib spec 생성 실패: {init_path}")

    module = importlib.util.module_from_spec(spec)
    sys.modules[pkg_name] = module
    spec.loader.exec_module(module)
    _PATTERNS_MODULE_CACHE = module
    return module


def _get_pattern_render_fn(pattern_full_name: str) -> Callable[..., Path]:
    """hybrid-image PATTERNS dict에서 render 함수를 가져온다.

    Lazy import — retry_loop를 단위 테스트 시 hybrid-image 의존 회피.
    """
    if pattern_full_name not in _PATTERN_NAME_MAP:
        raise ValueError(
            f"unknown hybrid_pattern: {pattern_full_name} "
            f"(supported: {list(_PATTERN_NAME_MAP.keys())})"
        )
    short_key = _PATTERN_NAME_MAP[pattern_full_name]
    patterns_module = _load_hybrid_patterns_module()
    return patterns_module.PATTERNS[short_key]


# ---------------------------------------------------------------------------
# Dataclass
# ---------------------------------------------------------------------------

@dataclass
class RetryResult:
    """generate_with_eval 반환 결과."""

    success: bool
    final_path: Path | None
    attempts: int
    final_eval: EvalResult
    history: list[dict[str, Any]] = field(default_factory=list)
    # 각 attempt: {"attempt": int, "seed": int, "hints": dict, "score": int, "fail_reasons": list}


# ---------------------------------------------------------------------------
# 내부: 렌더링 placeholder
# ---------------------------------------------------------------------------

def _render_with_seed(
    content: dict[str, Any],
    design_md: dict[str, Any],
    hybrid_pattern: str,
    target_size: tuple[int, int],
    output_path: Path,
    seed: int,
    hints: dict[str, Any],
) -> None:
    """hybrid 패턴으로 PNG를 렌더링한다 (task-2428 Phase 2-1 통합 완료).

    skills/hybrid-image/patterns/PATTERNS dict를 직접 dispatch한다.
    6축 hint는 design_tokens에 'hint_*' prefix로 주입되며, 패턴은
    모르는 키를 graceful 무시한다. (회귀 0)

    Args:
        content: {"title": str, "body": str, "background_path": Path | None,
                  "prompt_hint": str | None} 카드뉴스 콘텐츠
        design_md: {"primary": str, "secondary": str, "name": str} 브랜드 정보
        hybrid_pattern: 'h1_photo_card' | 'h2_illustration_card' |
                        'h3_gpt_style_card' | 'h4_gradient_card' | 'h5_user_photo_card'
        target_size: (width, height) 픽셀
        output_path: 출력 PNG 경로
        seed: 랜덤 시드 (attempt별 변형값)
        hints: 이전 evaluate_image의 retry_hints (패턴/색상 강제 힌트 포함)

    Raises:
        ValueError: hybrid_pattern이 지원되지 않는 경우.
        RuntimeError: PATTERNS dict 호출 실패 (silent pass 차단).
    """
    render_fn = _get_pattern_render_fn(hybrid_pattern)

    # design_md primary를 brand color로 우선 적용 (force_brand_color hint > design_md)
    brand_color = (
        hints.get("force_brand_color")
        or design_md.get("primary")
        or "#0f1729"
    )

    # design_tokens 조립 — 패턴이 직접 소비하는 표준 키 + 'hint_*' 확장
    design_tokens: dict[str, Any] = {
        # 기존 표준 키 (회귀 0)
        "title_color": design_md.get("title_color", "#fafaf8"),
        "body_color": design_md.get("body_color", "#d4d8e0"),
        "accent_color": brand_color,
        # H4 gradient 테마 — seed 기반 회전 (단조 회피)
        "gradient_theme": _select_gradient_theme(seed),
        # 폰트 크기 — min_font_px hint 반영 (Codex sanitize: hint 미존재 시 기본값)
        "title_size": max(72, int(hints.get("min_font_px", 0) or 0)),
        "body_size": max(38, int(hints.get("min_font_px", 0) or 0) - 28 if hints.get("min_font_px") else 38),
        # 6축 hint 'hint_*' prefix 주입 (패턴이 graceful 무시)
        "hint_seed": seed,
        "hint_force_pattern_diversity": bool(hints.get("force_pattern_diversity", False)),
        "hint_force_brand_color": brand_color,
        "hint_force_pattern_signature": bool(hints.get("force_pattern_signature", False)),
        "hint_min_font_px": int(hints.get("min_font_px", 0) or 0),
        "hint_force_korean_font": bool(hints.get("force_korean_font", True)),
        "hint_force_spatial_coherence": bool(hints.get("force_spatial_coherence", False)),
        # H4 전용: brand 색을 직접 gradient에 inject할 수 있도록 flag
        "primary_hex": brand_color,
    }

    title = str(content.get("title", ""))
    body = str(content.get("body", ""))
    background_path = content.get("background_path")
    prompt_hint = content.get("prompt_hint")

    try:
        render_fn(
            title,
            body,
            output_path,
            size=target_size,
            design_tokens=design_tokens,
            background_path=background_path,
            prompt_hint=prompt_hint,
        )
    except Exception as exc:
        # silent pass 차단 — 렌더링 실패 시 명시 RuntimeError raise
        raise RuntimeError(
            f"hybrid-image 렌더링 실패: pattern={hybrid_pattern}, "
            f"seed={seed}, output={output_path}, error={exc!r}"
        ) from exc

    # PNG 파일 존재 검증 (silent skip 차단)
    if not output_path.exists():
        raise RuntimeError(
            f"렌더링 후 출력 PNG 부재: {output_path} "
            f"(pattern={hybrid_pattern}, seed={seed})"
        )


def _select_gradient_theme(seed: int) -> str:
    """seed 기반으로 gradient theme를 회전 선택한다 (h4 단조 회피).

    H4 gradient_card 패턴은 design_tokens["gradient_theme"]을 소비한다.
    동일 seed → 동일 theme (결정성 보장).
    """
    themes = ["navy", "warm", "mint", "rose", "mono"]
    return themes[seed % len(themes)]


# ---------------------------------------------------------------------------
# 공개 함수
# ---------------------------------------------------------------------------

def generate_with_eval(
    content: dict[str, Any],
    design_md: dict[str, Any],
    hybrid_pattern: str,
    target_size: tuple[int, int],
    output_path: Path,
    max_retry: int = 5,
    initial_seed: int | None = None,
) -> RetryResult:
    """hybrid 패턴으로 PNG를 생성하고 품질 평가 PASS까지 재시도한다.

    CRITICAL — silent pass 절대 금지:
      max_retry 회 내 PASS 미달 시 RuntimeError를 raise한다.
      예외를 삼키는 것은 절대 금지. 5회 retry 후 ERROR 발생 시 운영팀 알림 의무.

    Args:
        content: {"title": str, "body": str, ...} 카드뉴스 콘텐츠
        design_md: {"primary": str, "secondary": str, "name": str} 브랜드 정보
        hybrid_pattern: 'h1_photo_card' | 'h2_illustration_card' |
                        'h3_gpt_style_card' | 'h4_gradient_card' | 'h5_user_photo_card'
        target_size: (width, height) 픽셀
        output_path: 최종 출력 PNG 경로
        max_retry: 최대 재시도 횟수 (기본 5회)
        initial_seed: 초기 랜덤 시드 (None이면 자동 생성)

    Returns:
        RetryResult (success=True 보장, 실패 시 RuntimeError raise)

    Raises:
        RuntimeError: max_retry 회 내 PASS 미달 시 — 운영팀 핸들링 의무
    """
    # Step 1: seed 초기화
    seed = initial_seed if initial_seed is not None else random.randint(0, 2**31)

    history: list[dict[str, Any]] = []
    hints: dict[str, Any] = {}
    last_eval: EvalResult | None = None

    # Step 2: retry loop
    for attempt in range(1, max_retry + 1):
        # attempt별 seed 변형 (동일 seed 반복 방지)
        attempt_seed = seed + attempt * 1000

        # Step 2a: PNG 렌더링 (task-2428 Phase 2-1 통합 완료)
        # _render_with_seed가 hybrid-image PATTERNS dict를 직접 dispatch.
        # 렌더링 실패 시 RuntimeError raise → 본 attempt fail로 기록 후 다음 시도.
        render_error: str | None = None
        try:
            _render_with_seed(
                content=content,
                design_md=design_md,
                hybrid_pattern=hybrid_pattern,
                target_size=target_size,
                output_path=output_path,
                seed=attempt_seed,
                hints=hints,
            )
        except (RuntimeError, ValueError) as exc:
            # 렌더링 자체가 실패 (silent pass 차단) — 본 attempt 실패로 기록
            render_error = f"렌더링 실패: {exc!s}"

        # Step 2b: 렌더링 실패 처리
        if render_error:
            record: dict[str, Any] = {
                "attempt": attempt,
                "seed": attempt_seed,
                "hints": dict(hints),
                "score": 0,
                "fail_reasons": [render_error],
                "render_error": render_error,
            }
            history.append(record)
            # 다음 attempt로 계속
            continue

        # Step 2c: 품질 평가
        eval_result = evaluate_image(
            png_path=output_path,
            design_md=design_md,
            hybrid_pattern=hybrid_pattern,
            target_size=target_size,
        )
        last_eval = eval_result

        record = {
            "attempt": attempt,
            "seed": attempt_seed,
            "hints": dict(hints),
            "score": eval_result.score,
            "fail_reasons": list(eval_result.fail_reasons),
        }
        history.append(record)

        # Step 2d: PASS → 즉시 반환
        if eval_result.passed:
            return RetryResult(
                success=True,
                final_path=output_path,
                attempts=attempt,
                final_eval=eval_result,
                history=history,
            )

        # Step 2e: FAIL → retry_hints 업데이트 후 다음 시도
        hints.update(eval_result.retry_hints)

    # ---------------------------------------------------------------------------
    # CRITICAL: silent pass 절대 금지
    # max_retry 도달 후 PASS 없음 → RuntimeError raise (운영팀 핸들링 의무)
    # ---------------------------------------------------------------------------
    final_fail_reasons = last_eval.fail_reasons if last_eval else ["렌더링 실패로 평가 불가"]
    fail_summary = "; ".join(final_fail_reasons)

    raise RuntimeError(
        f"retry-until-pass FAILED after {max_retry} attempts: {fail_summary}\n"
        f"[운영팀 알림 의무] 5회 retry 후 PASS 미달 — 수동 개입 필요.\n"
        f"마지막 점수: {last_eval.score if last_eval else 0}/100\n"
        f"history: {history}"
    )
