"""background_unmanned — 배경 렌더 무인(NO PEOPLE) 계약 (2026-07-20 이식 ①).

E2E9 L09B01 실측: LLM 저작 배경 프롬프트가 씬 인물 서술을 포함하면
플레이트에 인물이 구워진다(무인 계약 위반) — 그 플레이트를 참조하는
하류 스틸 전체가 오염되는 상류 결함. 강제 지점=렌더 경로 단일 seam
(`render_one_background` 서두):

  ① 프롬프트 계약(no_people_clause) 강제 부착 — 단롤 gpt 경로와
     plate_multiroll(nb2) 위임 경로가 같은 계약을 상속.
  ② (opt-in) 무인 VLM 게이트: 렌더 성공 후 Gemini 구조화 판정
     {people_visible, evidence_ko} → 위반 시 재시도 절(no_people_retry)
     을 덧붙여 1회 재렌더 → 재판정. 그래도 위반 = fail-open + 감사
     기록(`people_detected=True`) — 플레이트 실패가 에피소드 전체를
     막지 않는다(후속 라운드 개선 근거·리뷰 갤러리 노출용).

판정은 전부 VLM(gate_judge_sys 팩) — 글자/substring 의미 판단 없음.
flag OFF(default) = 기존 경로 byte-identical.
"""
from __future__ import annotations

import logging
import shutil
from pathlib import Path
from typing import Any, Callable, Dict, Optional

from app.modules.prompt_loader import load_prompt

logger = logging.getLogger(__name__)

_MODULE = "background_unmanned"

PROMPT_VERSION_MAP = {
    "1": "1.202607201540",
}

GATE_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "people_visible": {"type": "boolean"},
        "evidence_ko": {"type": "string"},
    },
    "required": ["people_visible", "evidence_ko"],
    "additionalProperties": False,
}

# judge_fn(png_path) -> {"people_visible": bool, "evidence_ko": str}
UnmannedJudgeFn = Callable[[Path], Dict[str, Any]]


def resolve_prompt_version(selector: str) -> str:
    try:
        return PROMPT_VERSION_MAP[selector]
    except KeyError:
        raise ValueError(
            f"unknown background_unmanned prompt version selector "
            f"{selector!r} (known: {sorted(PROMPT_VERSION_MAP)})"
        )


def build_no_people_clause(prompt_version: str = "1") -> str:
    return load_prompt(
        _MODULE, "no_people_clause",
        version=resolve_prompt_version(prompt_version),
    ).strip()


def build_retry_clause(prompt_version: str = "1") -> str:
    return load_prompt(
        _MODULE, "no_people_retry",
        version=resolve_prompt_version(prompt_version),
    ).strip()


AUDIT_KEYS = ("no_people_clause_attached", "unmanned_gate", "people_detected")


def audit_fields(info: Dict[str, Any]) -> Dict[str, Any]:
    """render info 의 무인 감사 필드 subset (CP 영속용, E2E10 실측 fix).

    E2E10: step 의 CP entry whitelist 가 감사 3필드를 탈락시켜
    people_detected 분포 acceptance 를 CP 로 검증할 수 없었다. 존재하는
    키만 반환 — OFF/부재=빈 dict 라 기존 CP shape byte-identical.
    """
    return {k: info[k] for k in AUDIT_KEYS if k in info}


def make_unmanned_judge_fn(
    project_config: Optional[Dict[str, Any]] = None,
    prompt_version: str = "1",
) -> UnmannedJudgeFn:
    """Gemini 구조화 무인 판정 — multiroll_gemini 판정 어댑터와 동일 배선."""
    from app.modules.llm.llm_client import call_structured
    from app.modules.pipeline.multiroll_gemini import JUDGE_MODEL, png_part

    step_tag = "background_unmanned_gate"
    pc = {**(project_config or {}), step_tag: {"model": JUDGE_MODEL}}
    judge_sys = load_prompt(
        _MODULE, "gate_judge_sys",
        version=resolve_prompt_version(prompt_version),
    ).strip()

    def judge_fn(png_path: Path) -> Dict[str, Any]:
        parts = [
            {"type": "text", "text": "BACKGROUND PLATE:"},
            png_part(png_path),
        ]
        return call_structured(
            step_tag, judge_sys, parts, GATE_SCHEMA,
            project_config=pc, schema_name=step_tag,
        )

    return judge_fn


def run_unmanned_gate(
    *,
    info: Dict[str, Any],
    out_path: Path,
    retry_fn: Callable[[], Dict[str, Any]],
    judge_fn: Optional[UnmannedJudgeFn] = None,
    project_config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """렌더 결과 무인 게이트 — 위반 시 1회 재렌더 + 재판정.

    계약:
      - status!="ok" = 판정 비대상(no-op).
      - 판정 콜 자체 실패 = 게이트 무효(fail-open, `unmanned_gate.error`)
        — 렌더는 유지(게이트가 렌더를 파괴하지 않는다).
      - 위반 → 원본을 `<stem>.unmanned_v1.png` 로 백업 후 retry_fn()
        (강화 절 재렌더, 같은 out_path). 재렌더 실패·**예외** = 백업 복원
        + `people_detected=True` (원본 유지가 산출 0 보다 낫다 — 감사로
        노출). 예산 예외(ImageCallBudgetExceeded)만 복원 후 재전파
        (Codex 리뷰 4: retry callable 예외가 게이트의 '렌더 비파괴'
        계약을 깨지 않도록 예외 경계 명시).
      - 재렌더 성공 → 재판정. 여전히 위반 = `people_detected=True`
        fail-open (2회 이상 재렌더는 비용 폭주 — 후속 라운드 개선 근거).
    """
    if info.get("status") != "ok":
        return info
    if judge_fn is None:
        judge_fn = make_unmanned_judge_fn(project_config=project_config)
    try:
        v1 = judge_fn(out_path)
    except Exception as exc:  # noqa: BLE001 — 게이트 실패는 렌더 비치명
        logger.warning("background_unmanned gate 판정 실패: %s", exc)
        info["unmanned_gate"] = {"error": str(exc)[:200]}
        return info
    info["unmanned_gate"] = {"attempt1": v1}
    if not v1.get("people_visible"):
        return info

    logger.warning(
        "background_unmanned: 무인 계약 위반 감지(%s) — 강화 재렌더 1회",
        v1.get("evidence_ko", ""),
    )
    backup = out_path.with_name(f"{out_path.stem}.unmanned_v1{out_path.suffix}")
    try:
        shutil.copy(out_path, backup)
    except Exception as exc:  # noqa: BLE001
        logger.warning("background_unmanned: 백업 실패(%s) — 재렌더 생략", exc)
        info["people_detected"] = True
        return info

    def _restore() -> None:
        try:
            shutil.copy(backup, out_path)
        except Exception:  # noqa: BLE001
            pass

    try:
        retry_info = retry_fn()
    except Exception as exc:  # noqa: BLE001 — 원본 복원이 최우선
        from app.core.image_call_budget import ImageCallBudgetExceeded

        _restore()
        if isinstance(exc, ImageCallBudgetExceeded):
            raise  # 예산 소진은 상위 정책 — 복원만 하고 전파
        logger.warning("background_unmanned: 재렌더 예외(%s) — 원본 복원", exc)
        info["unmanned_gate"]["retry_error"] = str(exc)[:200]
        info["people_detected"] = True
        return info
    if retry_info.get("status") != "ok" or not out_path.exists():
        # 재렌더 실패 — 원본 복원(산출 0 방지) + 감사
        _restore()
        info["unmanned_gate"]["retry_status"] = retry_info.get("status")
        info["people_detected"] = True
        return info

    retry_info["unmanned_gate"] = {"attempt1": v1}
    try:
        v2 = judge_fn(out_path)
        retry_info["unmanned_gate"]["attempt2"] = v2
        if v2.get("people_visible"):
            retry_info["people_detected"] = True
    except Exception as exc:  # noqa: BLE001 — 확인 불가=보수적으로 감사
        retry_info["unmanned_gate"]["retry_judge_error"] = str(exc)[:200]
        retry_info["people_detected"] = True
    return retry_info
