"""T2I 프롬프트 검수 — 감지 only (mutation=0, signal-only).

entity_t2i: 한국어 설명과 비교하여 T2I 검증 (배치 1회)
scene_detail: shot 원본과 비교하여 T2I 검증 (배치 N회, BATCH_SIZE개씩)

v1 (Area #6, 2026-05-18+): blind substring mutation 폐기 + 5 core fields
structured diagnostic emit. 외부 force signal only — in-step regen 0.
"""
import json
import logging
from typing import Any, Dict, List, Optional, Tuple

from app.core.errors import AppError
from app.core.steps._evidence_helpers import _normalize_scene_detail_result
from app.modules.llm.llm_client import call_structured
from app.modules.llm.safety import SchemaValidationError
from app.modules.prompt_loader import load_prompt, load_schema

logger = logging.getLogger(__name__)

BATCH_SIZE = 10


def _build_item_id_map(scene_detail_data: Dict) -> Dict[str, Tuple[int, int]]:
    """t2i_variations 모두 순회 — item_id (deterministic) → (scene_list_idx, variation_idx) map 생성.

    item_id format: f"S{scene_index}_L{scene_list_idx}_V{variation_idx}"
    LLM 이 prompt 의 item_id 를 echo 하면 application 이 map 으로 정확 위치 찾음.

    AC-A6 (V3 patch I1, spec §3.3): fan-out 시나리오 (같은 scene_index 다수) 모호성 차단.
    """
    m: Dict[str, Tuple[int, int]] = {}
    for s_list_idx, scene in enumerate(scene_detail_data.get("scenes", [])):
        scene_index = scene.get("scene_index")
        for v_idx, _variation in enumerate(scene.get("t2i_variations", [])):
            item_id = f"S{scene_index}_L{s_list_idx}_V{v_idx}"
            m[item_id] = (s_list_idx, v_idx)
    return m


def run_t2i_review(
    entity_t2i_data: Dict[str, Any],
    scene_detail_data: Dict[str, Any],
    entity_merge_data: Dict[str, Any],
    entity_detail_data: Dict[str, Any],
    shot_extract_data: Dict[str, Any],
    vwr_data: Dict[str, Any],
    shot_staging_data: Optional[Dict[str, Any]] = None,
    opik_metadata: Optional[Dict] = None,
) -> Dict[str, Any]:
    """T2I 검수 실행 — 감지 only, mutation 0.

    Area #6 v1: target/suggestion blind substring replacement 폐기.
    LLM 이 5 core fields ({id, scope, issue_code, action, hint_diagnostic})
    structured diagnostic 만 emit. 코드는 emit 누적 → 외부 force signal only.

    Returns:
        {
          "entity_fixes": int (detected count, entity scope),
          "scene_fixes":  int (detected count, scene scope),
          "entity_applied": 0  (mutation=0 invariant, canary),
          "scene_applied":  0  (mutation=0 invariant, canary),
          "scene_applied_indices": []  (always empty, canary),
          "diagnostics": list[dict]  (entity + scene 통합 5 core fields list),
        }
    """

    t2i_context = vwr_data.get("t2i_context", "")
    char_names = [c["name"] for c in entity_merge_data.get("characters", [])]
    char_names_str = ", ".join(char_names)

    # entity short_id → 원문 description 맵
    entity_desc_map = _build_entity_desc_map(entity_detail_data, entity_merge_data)

    # shot description 맵
    shot_map = _build_shot_map(shot_extract_data)

    # Phase 9.2: shot_staging → (si, shi) → camera_direction 맵
    staging_map = _build_staging_map(shot_staging_data or {})

    # ── Part 1: entity_t2i 검증 ──
    entity_diagnostics = _review_entity_t2i(
        entity_t2i_data, entity_desc_map, t2i_context, opik_metadata,
    )

    # ── Part 2: scene_detail 검증 ──
    scene_diagnostics = _review_scene_detail(
        scene_detail_data, shot_map, staging_map, t2i_context, char_names_str, opik_metadata,
    )

    # ── Area #6 v1: mutation=0 invariant. item_id_map 은 Patch A V3 patch I1
    # defensive guard 로 보존 (deterministic id 검증, 미래 mutation 재도입 시
    # fan-out 차단). _apply_*_fixes 호출 0.
    _build_item_id_map(scene_detail_data)  # invoked for defensive validation; result discarded
    diagnostics = list(entity_diagnostics) + list(scene_diagnostics)

    logger.info(
        "t2i_review v1: entity %d diagnostics, scene %d diagnostics (mutation=0)",
        len(entity_diagnostics), len(scene_diagnostics),
    )

    return {
        "entity_fixes": len(entity_diagnostics),
        "scene_fixes":  len(scene_diagnostics),
        "entity_applied": 0,          # canary invariant
        "scene_applied":  0,          # canary invariant
        "scene_applied_indices": [],  # canary invariant
        "diagnostics": diagnostics,
    }


def _build_entity_desc_map(
    entity_detail_data: Dict, entity_merge_data: Dict,
) -> Dict[str, Dict]:
    """short_id → {name, description}."""
    desc_map = {}
    details = entity_detail_data.get("entity_details", {})
    for key, val in details.items():
        parts = key.split(":")
        name = parts[0]
        for cat in ["characters", "locations", "props"]:
            for e in entity_merge_data.get(cat, []):
                if e.get("name") == name:
                    desc_map[e.get("short_id", "")] = {
                        "name": name,
                        "description": val.get("description", ""),
                    }
    return desc_map


def _build_shot_map(shot_extract_data: Dict) -> Dict[int, List[Dict]]:
    """scene_index → [{shot_index, description}]."""
    shot_map = {}
    for s in shot_extract_data.get("scenes", []):
        si = s.get("scene_index")
        shots = []
        for sh in s.get("shots", []):
            shots.append({
                "shot_index": sh.get("shot_index", 0),
                "description": sh.get("description", ""),
            })
        shot_map[si] = shots
    return shot_map


def _build_staging_map(shot_staging_data: Dict) -> Dict[tuple, str]:
    """(scene_index, shot_index) → camera_direction string.

    Phase 9.2: 신규 close_framing_existing_ref / physical_inconsistency 검증 룰이
    camera_direction을 비교 대상으로 요구. shot_staging cp의 shots[] 배열에서
    각 (si, shi)별 camera_direction 자연어를 추출.
    """
    result = {}
    for sh in shot_staging_data.get("shots", []) or []:
        si = sh.get("scene_index")
        shi = sh.get("shot_index")
        if si is None or shi is None:
            continue
        cam = sh.get("camera_direction") or ""
        if cam:
            result[(si, shi)] = cam
    return result


# ══════════════════════════════════════════════════════════
# Part 1: entity_t2i
# ══════════════════════════════════════════════════════════

def _review_entity_t2i(
    entity_t2i_data: Dict,
    entity_desc_map: Dict[str, Dict],
    t2i_context: str,
    opik_metadata: Optional[Dict],
) -> List[Dict]:
    """entity_t2i 배치 검증 — 한국어 설명과 비교. diagnostic 누적 (mutation=0)."""

    system = load_prompt("t2i_review", "entity_system")
    schema = load_schema("t2i_review", "entity_schema")
    system = system.format(t2i_context=t2i_context)

    # 배치 구성
    items = []
    for cat in ["characters", "locations", "props"]:
        for c in entity_t2i_data.get(cat, []):
            sid = c.get("short_id", "")
            t2i = c.get("t2i_prompt", "")
            if not t2i:
                continue
            desc_info = entity_desc_map.get(sid, {})
            items.append({
                "short_id": sid,
                "name": c.get("name", ""),
                "korean_desc": desc_info.get("description", ""),
                "t2i_prompt": t2i,
            })

    if not items:
        return []

    user_lines = []
    for ei in items:
        user_lines.append(
            f"[{ei['short_id']}] {ei['name']}\n"
            f"  한국어 설명: {ei['korean_desc']}\n"
            f"  T2I: {ei['t2i_prompt']}"
        )
    user_prompt = "아래 엔티티들의 T2I 프롬프트를 검수하세요:\n\n" + "\n\n".join(user_lines)

    try:
        result = call_structured(
            step="t2i_review",
            system_prompt=system,
            user_prompt=user_prompt,
            response_schema=schema,
            opik_metadata=opik_metadata,
        )
    except (AppError, SchemaValidationError):
        # B2 patch (Block A closure stabilization, Codex BLOCKING #2): contract
        # violation — fail-fast. silent fallback (return []) 은
        # feedback_no_silent_fallback.md 정책 위반 (검수 실패를 "문제 없음" 으로 변환).
        # T5 v2 의 _review_scene_detail 와 동일 정책 — entity path 도 정합.
        raise
    except Exception as e:
        # B2 patch: transient 도 partial success 금지 — fail-fast (T5 v2 패턴 mirror).
        # entity batch 는 single batch 라 try/except 안에서 직접 raise (scene path
        # 의 multi-batch failed_batches 후처리 패턴과 다름).
        logger.error("t2i_review entity batch failed: %s", e)
        raise AppError(
            code="t2i_review.entity_batch_failed",
            message=(
                f"t2i_review entity batch transient failure — partial success 금지 "
                f"(silent fallback 정책). original={type(e).__name__}: {e}"
            ),
        ) from e

    diagnostics: List[Dict] = []
    for r in result.get("results", []):
        if r.get("has_issues") and r.get("issues"):
            short_id = r.get("short_id", "")
            for iss in r["issues"]:
                # Codex Round 1 Important #4 fix-up: alias equality 강제 (spec §3 Q8
                # line 211 "alias = id value 동일"). silent default 금지 — id /
                # short_id / result-level short_id 셋이 모두 같은 값이어야 함.
                iss_short_id = iss["short_id"]
                if not (iss["id"] == short_id == iss_short_id):
                    raise AppError(
                        code="t2i_review.diagnostic_alias_mismatch",
                        message=(
                            f"entity issue id/short_id alias mismatch — schema 위반. "
                            f"iss['id']={iss['id']!r} "
                            f"iss['short_id']={iss_short_id!r} "
                            f"result short_id={short_id!r} "
                            f"(spec §3 Q8 alias=id strict)"
                        ),
                    )
                diagnostics.append({
                    "id": iss["id"],
                    "scope": iss["scope"],
                    "issue_code": iss["issue_code"],
                    "action": iss["action"],
                    "hint_diagnostic": iss["hint_diagnostic"],
                    "short_id": iss_short_id,
                })
                logger.info(
                    "t2i_review entity %s [%s]: %s",
                    iss["id"], iss["issue_code"], iss["hint_diagnostic"][:80],
                )

    return diagnostics


# ══════════════════════════════════════════════════════════
# Part 2: scene_detail
# ══════════════════════════════════════════════════════════

def _review_scene_detail(
    scene_detail_data: Dict,
    shot_map: Dict[int, List[Dict]],
    staging_map: Dict[tuple, str],
    t2i_context: str,
    char_names_str: str,
    opik_metadata: Optional[Dict],
) -> List[Dict]:
    """scene_detail 배치 검증 — shot 원본 + camera_direction과 비교 (Phase 9.2).

    각 t2i_variation 별로 `_shot_index`(scene_detail v3+)가 있으면 그 단일 shot의
    description + camera_direction을 inject. 없으면 (legacy) scene 내 모든 shot
    description fallback.

    Area #6 v1: 5 core fields diagnostic 누적 (mutation=0).
    """

    system = load_prompt("t2i_review", "scene_system")
    schema = load_schema("t2i_review", "scene_schema")
    system = system.format(t2i_context=t2i_context, char_names=char_names_str)

    # 전체 아이템 수집 — T5/AC-A6: outer enumerate + deterministic item_id
    items = []
    for s_list_idx, s in enumerate(scene_detail_data.get("scenes", [])):
        # G3.1: 옛 cp 4-field 누락 lazy backfill (마킹=legacy).
        _normalize_scene_detail_result(s, where="t2i_review._review_scene_detail")
        si = s.get("scene_index")
        shi = s.get("_shot_index")  # scene_detail v3+ shot 단위 fan_out
        shots = shot_map.get(si, [])

        # 단일 shot description (있으면), 없으면 모든 shot fallback (legacy)
        if shi is not None:
            shot_desc = next(
                (sh["description"] for sh in shots if sh["shot_index"] == shi),
                "",
            )
            shot_text = f"  shot{shi}: {shot_desc}" if shot_desc else "(없음)"
        else:
            shot_text = "\n".join(
                f"  shot{sh['shot_index']}: {sh['description']}" for sh in shots
            ) if shots else "(없음)"

        # Phase 9.2: camera_direction inject — 신규 검증 룰 데이터 입력
        cam_dir = staging_map.get((si, shi), "") if shi is not None else ""

        for vi, v in enumerate(s.get("t2i_variations", [])):
            t2i = v.get("t2i_prompt", "")
            if t2i:
                items.append({
                    "item_id": f"S{si}_L{s_list_idx}_V{vi}",
                    "scene_index": si,
                    "var_index": vi,
                    "shot_text": shot_text,
                    "camera_direction": cam_dir,
                    "t2i_prompt": t2i,
                })

    if not items:
        return []

    fixes = []
    failed_batches = 0
    for batch_start in range(0, len(items), BATCH_SIZE):
        batch = items[batch_start:batch_start + BATCH_SIZE]
        batch_num = batch_start // BATCH_SIZE + 1
        total_batches = (len(items) - 1) // BATCH_SIZE + 1

        user_lines = []
        for bi in batch:
            cam_block = (
                f"  camera_direction: {bi['camera_direction']}\n"
                if bi['camera_direction'] else ""
            )
            user_lines.append(
                f"[item_id: {bi['item_id']}] [씬{bi['scene_index']} var{bi['var_index']}]\n"
                f"  shot 원본:\n{bi['shot_text']}\n"
                f"{cam_block}"
                f"  T2I: {bi['t2i_prompt']}"
            )
        user_prompt = "아래 씬들의 T2I 프롬프트를 검수하세요:\n\n" + "\n\n".join(user_lines)

        try:
            result = call_structured(
                step="t2i_review",
                system_prompt=system,
                user_prompt=user_prompt,
                response_schema=schema,
                opik_metadata=opik_metadata,
            )
            for r in result.get("results", []):
                if r.get("has_issues") and r.get("issues"):
                    # T5/AC-A6: result level item_id 를 issue 단위 diagnostic 에
                    # propagate. T5 stabilization v2 (A+): schema required 누락
                    # 시 fail-fast (silent skip 정책 위반).
                    result_item_id = r.get("item_id")
                    if not result_item_id:
                        raise AppError(
                            code="t2i_review.missing_item_id",
                            message=(
                                f"LLM result missing item_id — schema 위반. "
                                f"scene_index={r.get('scene_index')} "
                                f"var_index={r.get('var_index')} "
                                f"issues={len(r.get('issues', []))}"
                            ),
                        )
                    for iss in r["issues"]:
                        # Codex Round 1 Important #4 fix-up: alias equality 강제
                        # (spec §3 Q8 line 211 "alias = id value 동일"). silent
                        # default 금지 — id / item_id / result-level item_id 셋이
                        # 모두 같은 값이어야 함.
                        iss_item_id = iss["item_id"]
                        if not (iss["id"] == result_item_id == iss_item_id):
                            raise AppError(
                                code="t2i_review.diagnostic_alias_mismatch",
                                message=(
                                    f"scene issue id/item_id alias mismatch — schema 위반. "
                                    f"iss['id']={iss['id']!r} "
                                    f"iss['item_id']={iss_item_id!r} "
                                    f"result item_id={result_item_id!r} "
                                    f"(spec §3 Q8 alias=id strict)"
                                ),
                            )
                        fixes.append({
                            "id": iss["id"],
                            "scope": iss["scope"],
                            "issue_code": iss["issue_code"],
                            "action": iss["action"],
                            "hint_diagnostic": iss["hint_diagnostic"],
                            "item_id": iss_item_id,
                        })
            logger.info("t2i_review scene batch %d/%d: %d items, %d issues",
                        batch_num, total_batches, len(batch),
                        sum(1 for r in result.get("results", [])
                            if r.get("has_issues") and r.get("issues")))
        except (AppError, SchemaValidationError):
            # T5 stabilization v2 (A+): contract violation (item_id 누락 / schema
            # 위반) 은 fail-fast — broad except 가 흡수 시 step 성공처럼 종료
            # (feedback_no_silent_fallback.md 정책 위반).
            raise
        except Exception as e:
            failed_batches += 1
            logger.warning("t2i_review scene batch %d/%d failed: %s", batch_num, total_batches, e)

    if failed_batches:
        # T5 stabilization v2 (A+): partial success 금지. transient 실패도
        # silent fallback 의 한 형태라 fail-fast (feedback_no_silent_fallback.md).
        logger.warning("t2i_review: %d/%d scene batches failed", failed_batches, total_batches)
        raise AppError(
            code="t2i_review.batch_failed",
            message=(
                f"t2i_review {failed_batches}/{total_batches} scene batches failed — "
                f"transient errors. partial success 금지 (silent fallback 정책)."
            ),
        )
    return fixes


# ══════════════════════════════════════════════════════════
# Area #6 v1 — mutation 폐기. 본 모듈은 detection-only.
# _apply_entity_fixes / _apply_scene_fixes 폐기 (mutation 0 invariant).
# Patch A V3 patch I1 의 _build_item_id_map 만 deterministic id 보장으로 보존.
# ══════════════════════════════════════════════════════════
