"""요소 참조 이미지 파이프라인 — T2I 생성 + GPT LVM 검증 + 비교 선택.

흐름:
1) 의존성 순서대로 T2I 생성 (Gemini) — 변형은 기본 참조이미지 포함
2) GPT LVM 검증 → 심각도 판단
3) 심각하면 재생성 → GPT LVM이 (1)과 (2) 비교하여 더 좋은 것 선택
4) 최종 참조 이미지 확정
"""

import base64
import json
import logging
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from app.core.config import settings
from app.modules.prompt_loader import load_prompt
from app.modules.llm.gemini_image_client import GeminiImageClient, ModerationError

# entity_type → prompt template filename (without .md)
_REF_PROMPT_MAP = {
    "character": "character_ref",
    "character_nonhuman": "character_nonhuman_ref",
    "location": "location_ref",
    "prop": "prop_ref",
    "outlook": "character_outlook_ref",
    "composite": "character_composite_ref",
    # ★몸이 곧 신원인 인물(로봇·사이보그 등)의 합성 — 몸을 새로 그리지 않고
    #  기본 형태 위에 덧입힌다 (2026-09-18).
    "composite_derive": "character_composite_derive_ref",
}

# entity_type → aspect ratio
_ASPECT_MAP = {
    "character": "3:4",
    "character_nonhuman": "1:1",
    "outlook": "3:4",
    "prop": "1:1",
    "location": "16:9",
    "composite": "16:9",
    "composite_derive": "16:9",
}


def _load_ref_image_prompt(entity_type: str, **kwargs) -> Optional[str]:
    """Load type-specific reference image prompt template from external files.

    Returns the formatted prompt string, or None if no template is found
    (backward-compatible: caller falls back to raw t2i_prompt).
    """
    template_name = _REF_PROMPT_MAP.get(entity_type)
    if not template_name:
        return None
    try:
        return load_prompt("ref_image_prompts", template_name, **kwargs)
    except (FileNotFoundError, KeyError):
        return None


def _load_lvm_prompt(name: str, **kwargs) -> str:
    return load_prompt("lvm_prompts", name, **kwargs)

logger = logging.getLogger(__name__)

OPENAI_API_URL = "https://api.openai.com/v1/responses"

# ── GPT LVM 스키마 ──

_VALIDATION_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "matches_description": {"type": "boolean"},
        "severity": {
            "type": "string",
            "description": "ok | minor | severe",
        },
        "issues": {
            "type": "array",
            "items": {"type": "string"},
        },
        "score": {"type": "integer", "description": "0-100"},
    },
    "required": ["matches_description", "severity", "issues", "score"],
}

_COMPARISON_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "winner": {
            "type": "string",
            "description": "image_1 | image_2",
        },
        "reason": {"type": "string"},
    },
    "required": ["winner", "reason"],
}


def _lvm_parts(
    image_bytes: bytes, image_bytes_2: Optional[bytes] = None,
) -> List[Dict[str, Any]]:
    """텍스트 + 그림 1~2장 — **조립만 한다.**

    ★`_call_gpt_lvm` 안에 있던 것을 꺼냈다. 그 함수는 「명시 모델로 한 번
     보내는」 운반층이고, 이중 호출은 `dual_vlm.ask_both` 가 소유한다 —
     둘을 한 함수에 두면 업무마다 다른 합의가 운반층으로 새어 든다
     (2026-08-27 Codex 판정).
    """
    def _url(raw: bytes) -> Dict[str, Any]:
        b64 = base64.b64encode(raw).decode("ascii")
        return {"type": "image_url",
                "image_url": {"url": f"data:image/png;base64,{b64}"}}

    parts: List[Dict[str, Any]] = [_url(image_bytes)]
    if image_bytes_2:
        parts.append(_url(image_bytes_2))
    return parts


def _call_gpt_lvm(
    image_bytes: bytes,
    text_prompt: str,
    response_schema: Dict[str, Any],
    schema_name: str = "lvm_result",
    image_bytes_2: Optional[bytes] = None,
    opik_tags: Optional[List[str]] = None,
) -> Dict[str, Any]:
    """GPT LVM (Vision) 호출 — LiteLLM Router 경유, Opik 자동 추적."""
    from app.modules.llm.llm_client import (build_call_metadata,
                                            router_completion)

    content = [
        {"type": "text", "text": text_prompt},
        {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64.b64encode(image_bytes).decode('ascii')}"}},
    ]
    if image_bytes_2:
        content.append(
            {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64.b64encode(image_bytes_2).decode('ascii')}"}}
        )

    tags = opik_tags if opik_tags else [schema_name]

    # Phase 4 iter 7 W1 — temperature param 명시 전달 제거. gpt-5*/gpt-5.4*
    # family 는 temperature=1 만 허용하고 0.2 같은 값은 OpenAI 측에서 직접
    # 400 BadRequest 로 거부함. router/litellm 의 drop_params 가 response_format
    # path 에서 일관되게 적용되지 않아 silent BadRequest → silent absorb 사고
    # 가 발생했음 (운영 결함, scene/ref 모든 호출). 호출 측에서 보내지 않는
    # 것이 안전 — vision validation 의 의미상 default 가 적합.
    response = router_completion(
        model="gpt",
        messages=[{"role": "user", "content": content}],
        response_format={
            "type": "json_schema",
            "json_schema": {"name": schema_name, "schema": response_schema, "strict": True},
        },
        timeout=settings.llm_timeout_validation,
        # ★표준 조립부를 탄다 — 맨 이름만 실으면 축이 안 갈리고 부모
        #   trace 에도 안 붙어 `chat.completion` 으로 홀로 남는다.
        metadata=build_call_metadata(schema_name, tags),
    )

    raw = response.choices[0].message.content
    if not raw:
        raise RuntimeError(f"GPT LVM returned empty response for schema={schema_name}")
    return json.loads(raw)


def _validation_description(
    entity_description: str, outlook_description: Optional[str],
) -> str:
    """검사에 넘길 설명 — **옷 상세를 싣는다** (2026-09-20 Codex 감사).

    합성 갈래에서 `entity_description` 은 `<인물> wearing <옷 이름>`
    한 줄이다. 생성 문안은 `outlook_description`(옷의 실제 상세)을 받는데
    **검사는 그 한 줄만** 받았다. 그래서 「커다란 밀짚모자와 거대한 장화」가
    빠진 그림도 검사를 통과했다 — 검사가 그런 요구가 있는 줄 몰랐다.

    ★새 유료 검사를 더하는 것이 아니다. **같은 호출에 요구사항을 싣는다.**
    ★그래도 「검사 통과」가 「모자·장화·비율·층 순서까지 충족」의 증거는
     아니다. 그 판단은 다음 판 그림을 사람이 보는 자리에 남는다.
    """
    base = (entity_description or "").strip()
    extra = (outlook_description or "").strip()
    if not extra or extra == base:
        return base
    return f"{base}\n입은 것의 상세: {extra}"


def validate_reference_image(
    image_bytes: bytes,
    entity_name: str,
    entity_description: str,
    entity_type: str,
) -> Dict[str, Any]:
    """참조 이미지 검증 — **두 모델이 각각 한 번**, 둘 다 severe 여야 재생성.

    Returns:
        `{"matches_description", "severity", "issues", "score", "_dual"}`.
        모양은 종전 그대로다 — 호출부가 `severity` 를 읽는 계약이 살아 있다.

    ## 합의 규칙 — **이 호출자가 소유한다**

    `severity == "severe"` 면 호출부가 **그림을 하나 더 산다**(`:324`).
    그래서 이 자리의 합의는 **돈을 쓰는 문턱**이다:

        둘 다 severe    → severe   (재생성한다)
        엇갈림          → split    (**안 산다**)
        한쪽 실패       → incomplete (**안 산다**)

    ★엇갈릴 때 안 사는 쪽으로 간다. 한 모델이 「괜찮다」고 하면 그 그림을
     버릴 근거가 약하고, 틀렸을 때 잃는 것은 참조 한 장의 품질인데
     맞았을 때 아끼는 것은 **유료 이미지 한 장**이다.
    ★`issues` 는 **합치지 않고 이어 붙인다** — 서로 다른 뜻의 자유문장을
     글자로 거르면 안 된다(이 저장소 금지 규칙).
    """
    from app.modules.llm.dual_vlm import ask_both

    prompt = _load_lvm_prompt("ref_validation",
        entity_name=entity_name, entity_type=entity_type, entity_description=entity_description)
    dual = ask_both("ref_validation", prompt,
                    _lvm_parts(image_bytes), _VALIDATION_SCHEMA,
                    schema_name="ref_validation")
    return combine_validation(dual)


def combine_validation(dual) -> Dict[str, Any]:
    """모델별 검증 → 재생성 문턱. **판단은 여기 있고 실행부엔 없다.**"""
    sev = [str((c.payload or {}).get("severity") or "") for c in dual.calls
           if c.ok]
    if not dual.complete:
        agreed = "incomplete"
    elif all(x == "severe" for x in sev):
        agreed = "severe"
    elif any(x == "severe" for x in sev):
        agreed = "split"          # 엇갈렸다 — 돈을 더 쓰지 않는다
    else:
        agreed = sev[0] if len(set(sev)) == 1 else "split"
    issues: List[str] = []
    for c in dual.calls:
        for it in ((c.payload or {}).get("issues") or []):
            issues.append(f"[{c.alias}] {it}")
    scores = [int((c.payload or {}).get("score") or 0) for c in dual.calls
              if c.ok]
    return {
        "matches_description": all(
            bool((c.payload or {}).get("matches_description"))
            for c in dual.calls if c.ok) if dual.complete else False,
        "severity": agreed,
        "issues": issues,
        # 점수는 **평균 내지 않는다** — 둘이 다른 뜻으로 쓴 눈금일 수 있다.
        "score": min(scores) if scores else 0,
        "_dual": dual.provenance(),
    }


def compare_two_images(
    image_1: bytes,
    image_2: bytes,
    entity_name: str,
    entity_description: str,
) -> Dict[str, Any]:
    """두 이미지 비교 — **두 모델이 각각 한 번**, 둘 다 바꾸자고 해야 바꾼다.

    Returns: `{"winner": "image_1"|"image_2", "reason", "_dual"}`

    ## 합의 규칙 — **위 검증과 다르다**

    여기서는 **이미 산 두 장** 중 하나를 고른다. 돈이 더 나가지 않으므로
    문턱의 뜻이 다르다:

        둘 다 image_2  → image_2  (바꾼다)
        그 밖          → image_1  (원래 것을 지킨다)

    ★이진 선택이라 **점수 합산 같은 것을 넣지 않는다.** 엇갈리면 새로
     만든 것으로 바꿀 근거가 약하니 원래 것을 지킨다.
    """
    from app.modules.llm.dual_vlm import ask_both

    prompt = _load_lvm_prompt("ref_comparison",
        entity_name=entity_name, entity_description=entity_description)
    dual = ask_both("ref_comparison", prompt,
                    _lvm_parts(image_1, image_2), _COMPARISON_SCHEMA,
                    schema_name="ref_comparison")
    return combine_comparison(dual)


def combine_comparison(dual) -> Dict[str, Any]:
    """모델별 승자 → 최종 승자. 엇갈리면 **원래 것을 지킨다.**"""
    picks = [str((c.payload or {}).get("winner") or "") for c in dual.calls
             if c.ok]
    winner = ("image_2" if dual.complete and picks
              and all(p == "image_2" for p in picks) else "image_1")
    reasons = [f"[{c.alias}] {(c.payload or {}).get('reason') or ''}"
               for c in dual.calls if c.ok]
    return {"winner": winner, "reason": " / ".join(reasons),
            "_dual": dual.provenance()}


def generate_and_validate_reference(
    gemini_client: GeminiImageClient,
    entity_name: str,
    entity_description: str,
    entity_type: str,
    t2i_prompt: str,
    output_dir: Path,
    extra_references: Optional[List[Tuple[str, bytes]]] = None,
    style_context: str = "",
    trace_meta: Optional[Dict[str, Any]] = None,
    state_hint: Optional[str] = None,
    outlook_description: Optional[str] = None,
) -> Dict[str, Any]:
    """참조 이미지 생성 + GPT LVM 검증 + 필요 시 재생성 비교.

    Args:
        trace_meta (Phase 4 iter 7 W3): observability metadata —
            {project_id, episode_id, entity_id, scene_index, shot_index}.
            llm_call_log 의 PID/EID NULL 사고 차단용.

    Returns:
        {
            "file_path": str,
            "image_bytes": bytes,
            "validation": {...},
            "was_regenerated": bool,
            "generation_model": str,
        }
    """
    output_dir.mkdir(parents=True, exist_ok=True)
    import uuid

    # Opik context — W3: trace_meta 를 set_context 에 forward.
    ctx_kwargs: Dict[str, Any] = {
        "step": "ref_image_gen",
        "entity_name": entity_name,
        "entity_type": entity_type,
    }
    if trace_meta:
        ctx_kwargs.update(trace_meta)
    gemini_client.set_context(**ctx_kwargs)

    # 1) T2I 생성 — ModerationError 시 sanitizer로 프롬프트 수정 후 재시도 (최대 3회)
    logger.info("Generating reference image for: %s (type=%s)", entity_name, entity_type)
    from app.modules.prompt_sanitizer import PromptSanitizer
    from app.modules.llm.openai_client import OpenAIClient

    # 타입별 프롬프트 템플릿 로드 (외부 파일 우선, 없으면 raw t2i_prompt 사용)
    # ★★`outlook_description` 은 **옷의 설명**이다 (2026-09-19).
    #  종전에는 `entity_description`(=「찰리 wearing 모포망토」)을 두 칸에
    #  똑같이 넣어서, 옷을 글로 설명하는 문안에 **옷 설명이 안 들어갔다.**
    #  몸이 곧 신원인 인물은 옷 사진을 안 주고 글로만 주므로 이 칸이 비면
    #  모델이 그릴 재료가 없다.
    ref_prompt = _load_ref_image_prompt(
        entity_type,
        entity_description=entity_description,
        outlook_description=(outlook_description or entity_description),
    )
    if ref_prompt:
        current_prompt = ref_prompt
        logger.info("Using type-specific ref prompt template for %s (%s)", entity_name, entity_type)
    else:
        current_prompt = t2i_prompt

    # 스타일 컨텍스트를 프롬프트 앞에 추가
    if style_context:
        current_prompt = f"{style_context}\n\n{current_prompt}"
    aspect = _ASPECT_MAP.get(entity_type, "3:4")
    img_bytes_1 = None
    sanitization_info = None

    #: 검열로 막혔을 때 **글을 바꾸기 전에 제공자를 바꾼다** (2026-09-19,
    #: 사용자 지시 「안 되면 grok, 그것도 안 되면 seedream 최신으로」).
    #:
    #: 사니타이저는 글을 부드럽게 만들어 **요청한 상태를 지운다** — 실측:
    #: 앰버(10세) severely_injured 를 세 판 만들었는데 세 판 모두 안 다친
    #: 그림이 나왔고 어른이 같이 그려졌다. 제공자마다 막는 선이 달라서,
    #: **원문 그대로** 다른 제공자에게 보내는 쪽이 먼저다.
    #:
    #: ★cine 쪽에 이미 있던 장치를 쓴다(`build_cine_client`) — 새로 만들지
    #:  않는다. 그 슬롯은 `generate_image(prompt, labeled_references=...)`
    #:  하나뿐이라 호출부가 제공자를 몰라도 된다.
    _fallback_used: Optional[str] = None

    def _try_other_providers() -> Optional[bytes]:
        from app.core.config import settings as _st
        from app.modules.pipeline.cine_provider import build_cine_client

        names = [x.strip() for x in str(getattr(
            _st, "still_cine_moderation_fallback_providers", "")).split(",")
            if x.strip()]
        for name in names:
            try:
                client = build_cine_client(name)
                out, _ = client.generate_image(
                    prompt=current_prompt,
                    labeled_references=extra_references,
                    aspect_ratio=aspect,
                )
                if out:
                    logger.warning(
                        "Reference T2I: %s 가 검열로 막혀 **%s 로 만들었다** "
                        "(글은 원문 그대로)", entity_name, name)
                    return out, name
            except Exception as exc:                       # noqa: BLE001
                logger.warning("대체 제공자 %s 도 실패: %s", name, exc)
        return None

    for attempt in range(4):  # 1 original + 3 sanitized retries
        try:
            img_bytes_1, elapsed = gemini_client.generate_image(
                prompt=current_prompt,
                labeled_references=extra_references,
                aspect_ratio=aspect,
            )
            break
        except ModerationError as exc:
            logger.warning("Reference T2I blocked (attempt %d) for %s: %s",
                           attempt + 1, entity_name, exc.block_reason)
            # ★첫 거부에서 **먼저 다른 제공자에게 원문 그대로** 보낸다.
            #  글을 고치는 것은 그다음이다 — 고치면 요청한 것이 사라진다.
            if attempt == 0:
                _alt = _try_other_providers()
                if _alt:
                    img_bytes_1, _fallback_used = _alt
                    break
            if attempt >= 3:
                raise
            # Sanitize prompt and retry
            try:
                sanitizer = PromptSanitizer(OpenAIClient())
                # ★★**참조는 사람이 하나다** (2026-09-19).
                #  사니타이저 팩에는 「아동 위험 상황 → 보호자와 함께 있는
                #  안전한 장면으로」가 있다. 씬 그림이면 맞는 정책이지만
                #  참조는 **신원을 정하는 그림**이라, 그대로 먹으면 사람이
                #  둘 들어가 하류 샷에서 신원이 섞인다.
                #  실측: 앰버(10세)의 severely_injured 참조에 어른 여자가
                #  같이 그려졌고, 부상 묘사도 통째로 지워졌는데 성공으로
                #  세어졌다.
                #  ★`semantic_constraints` 는 **이미 있던 통로**다 —
                #   참조 경로만 그걸 안 넘기고 있었다.
                _constraints: Dict[str, Any] = {
                    "semantic_mode": "reference_identity",
                    "single_subject_only": True,
                    "override_strategy_prefix": True,
                    "subject": entity_name,
                }
                if state_hint:
                    _constraints["source_states"] = {entity_name: state_hint}
                    _constraints["forbid_unharmed_rewrite"] = True
                    _constraints["preserve_subject_state"] = True
                sanitize_result = sanitizer.sanitize(
                    current_prompt, exc.block_reason, exc.block_categories,
                    attempt=attempt+1, semantic_constraints=_constraints)
                current_prompt = sanitize_result.get("sanitized_prompt", current_prompt)
                sanitization_info = sanitize_result
                logger.info("Sanitized prompt for %s (strategy: %s): %s",
                            entity_name, sanitize_result.get("strategy", ""), current_prompt[:80])
            except Exception as san_exc:
                logger.warning("Sanitization failed: %s", san_exc)
                raise exc

    if img_bytes_1 is None:
        raise RuntimeError(f"Failed to generate reference image for {entity_name}")

    path_1 = output_dir / f"{uuid.uuid4()}.png"
    path_1.write_bytes(img_bytes_1)

    # PNG 메타데이터 삽입
    from app.modules.png_metadata import embed_png_metadata
    from datetime import datetime, timezone

    embed_png_metadata(path_1, {
        "prompt": current_prompt,
        "entity_name": entity_name,
        "entity_type": entity_type,
        "model": settings.gemini_image_model,
        "created_at": datetime.now(timezone.utc).isoformat(),
    })

    # ★검사·비교가 **같은 설명**을 쓴다 (2026-09-20). 한 번 만들어 둘에
    #  똑같이 넘긴다 — 두 곳에서 따로 만들면 한쪽만 고쳐진다.
    _checked_description = _validation_description(
        entity_description, outlook_description)

    # 2) GPT LVM 검증
    # Phase 4 iter 7 W1 — silent OK fallback 제거 (scene_image_pipeline 와 동일).
    # default AppError fail-fast — silent absorb 차단. 운영자 override env =
    # ALLOW_LVM_VALIDATION_UNAVAILABLE=true.
    try:
        validation = validate_reference_image(
            img_bytes_1, entity_name, _checked_description, entity_type,
        )
    except Exception as exc:
        if settings.allow_lvm_validation_unavailable:
            logger.warning(
                "Reference LVM validation unavailable for %s: %s — explicit "
                "override (ALLOW_LVM_VALIDATION_UNAVAILABLE=true). Proceeding "
                "with _validation_unavailable marker.",
                entity_name, exc,
            )
            # Phase 4 iter 7 I2 — matches_description=False 로 strong negative
            # signal. 옛 True 값은 silent 성공 오판 위험.
            validation = {
                "matches_description": False,
                "severity": "unavailable",
                "issues": ["LVM validation unavailable — explicit override active"],
                "score": 0,
                "_validation_unavailable": True,
            }
        else:
            from app.core.errors import AppError
            raise AppError(
                code="step.ref_image_pipeline.lvm_validation_failed",
                message=(
                    f"Reference LVM validation failed for {entity_name!r}: "
                    f"{exc!r}. To proceed without LVM (e.g. transient outage) "
                    f"set ALLOW_LVM_VALIDATION_UNAVAILABLE=true env. Silent "
                    f"absorb 금지 (feedback_no_silent_fallback)."
                ),
                status_code=500,
            )

    # 3) 심각하면 재생성 + 비교
    if validation.get("severity") == "severe":
        logger.info("Severe issue for %s — regenerating", entity_name)
        try:
            img_bytes_2, _ = gemini_client.generate_image(
                prompt=current_prompt,
                labeled_references=extra_references,
                aspect_ratio=aspect,
            )
            path_2 = output_dir / f"{uuid.uuid4()}.png"
            path_2.write_bytes(img_bytes_2)

            # GPT LVM 비교
            # ★**비교에도 같은 설명이 가야 한다** (2026-09-20 Codex BLOCK).
            #  첫 검사만 옷 상세를 받고 이 비교는 이름 한 줄을 받으면,
            #  모자·장화 결손을 알아채 다시 산 직후 **그 요구를 모르는
            #  비교자**가 원본을 다시 고르거나 다른 불완전본을 고른다.
            #  같은 문자열을 한 번 만들어 두 호출이 함께 쓴다.
            comparison = compare_two_images(
                img_bytes_1, img_bytes_2, entity_name, _checked_description,
            )
            winner = comparison.get("winner", "image_1")
            logger.info("Comparison for %s: winner=%s reason=%s",
                        entity_name, winner, comparison.get("reason", ""))
            # ★**유료 판정 두 건의 기록이 사라지던 자리** (2026-08-27 Codex).
            #  `winner == "image_1"` 이면 아래 반환에 `comparison` 키가
            #  **아예 없어** 두 모델의 raw·물리 모델·토큰·비용이 통째로
            #  없어졌다. 승리 갈래에서도 하류 소비자는 `validation` 만
            #  `review_notes` 로 영속한다.
            #  ★그래서 **이미 영속되는 칸에 얹는다** — 새 테이블도 배선도
            #   필요 없다. 「비교를 안 했다」와 「비교했는데 안 바꿨다」는
            #   다른 사건이고, 뒤엣것은 돈이 나간 사건이다.
            validation["comparison"] = comparison

            if winner == "image_2":
                return {
                    "file_path": str(path_2),
                    "image_bytes": img_bytes_2,
                    "prompt_used": current_prompt,
                    "validation": validation,
                    "comparison": comparison,
                    "was_regenerated": True,
                    "generation_model": settings.gemini_image_model,
                    "sanitization_info": sanitization_info,
                    "fallback_provider": _fallback_used,
                }
        except Exception as exc:
            logger.warning("Regeneration failed for %s: %s", entity_name, exc)

    return {
        "file_path": str(path_1),
        "image_bytes": img_bytes_1,
        "prompt_used": current_prompt,
        "validation": validation,
        "was_regenerated": False,
        "generation_model": (
            f"{settings.gemini_image_model}→{_fallback_used}"
            if _fallback_used else settings.gemini_image_model),
        "sanitization_info": sanitization_info,
        "fallback_provider": _fallback_used,
    }
