"""파이프라인 단계 게이트 — 모든 단계 전환 시 전 단계 100% 완성 검증.

각 단계가 시작되기 전에 이전 단계의 완성도를 확인하고,
미완성이면 구체적인 에러 메시지와 함께 차단한다.
"""

import json
import logging
import re
from typing import Dict, List, Optional, Tuple

from sqlalchemy.orm import Session as OrmSession

from app.core.errors import AppError
from app.models.project import (
    EntityCanon, EntityEpisodeLink, CharacterOutlook,
    ImageAsset, SceneStill, Episode,
)

logger = logging.getLogger(__name__)


def check_analysis_ready(db: OrmSession, project_id: str, episode_id: str) -> None:
    """분석 시작 전 검증: fulltext 존재."""
    ep = db.query(Episode).filter(Episode.id == episode_id, Episode.project_id == project_id).first()
    if not ep:
        raise AppError(code="episode.not_found", message="에피소드를 찾을 수 없습니다", status_code=404)
    if not ep.fulltext:
        raise AppError(code="gate.no_fulltext", message="시나리오 텍스트가 없습니다. PDF를 먼저 업로드하세요.", status_code=400)


def ensure_analysis_projection_current(
    db: OrmSession, project_id: str, episode_id: str,
) -> None:
    """W20F10 — readiness gate 진입부에서 checkpoint → DB projection 을 최신화.

    image/ref readiness gate 가 stale ``Episode.status`` 만 신뢰해 막히는 회귀
    방지용. ``orchestrate_full_sync`` 는 idempotent — step 경로가 이미 sync 한
    경우에도 안전하게 재실행된다. API/script 경로처럼 sync 없이 gate 만 통과하는
    호출자가 stale ``error`` projection 에 갇히는 것을 막는다.

    실패 시 fail-closed: 주 세션 rollback 후 ``gate.projection_sync_failed``
    AppError 로 상위 호출자에 보고. orchestrator 자체가 별도 세션에 sync 실패를
    이미 기록하므로 여기서는 호출 세션 정리만 책임진다.
    """
    try:
        from app.services.checkpoint_sync import orchestrate_full_sync
        orchestrate_full_sync(project_id, episode_id, db)
    except AppError:
        raise
    except Exception as exc:
        try:
            db.rollback()
        except Exception:
            logger.warning("rollback after sync failure failed", exc_info=True)
        raise AppError(
            code="gate.projection_sync_failed",
            message=f"분석 projection 동기화 실패: {exc}",
            status_code=500,
        ) from exc


def check_ref_images_ready(db: OrmSession, project_id: str, episode_id: str) -> None:
    """참조 이미지 생성 전 검증: 분석 완료 + 요소 존재."""
    ep = db.query(Episode).filter(Episode.id == episode_id, Episode.project_id == project_id).first()
    if not ep:
        raise AppError(code="episode.not_found", message="에피소드를 찾을 수 없습니다", status_code=404)
    # W20F10: stale Episode.status 로 막히지 않도록 checkpoint projection 을
    # 동기화한 뒤 ep 를 refresh 해서 status 를 다시 본다.
    ensure_analysis_projection_current(db, project_id, episode_id)
    db.refresh(ep)
    if ep.status != "analyzed":
        raise AppError(
            code="gate.analysis_incomplete",
            message=f"분석이 완료되지 않았습니다 (현재: {ep.status}). 분석을 먼저 완료하세요.",
            status_code=400,
        )
    entity_count = db.query(EntityEpisodeLink).filter(
        EntityEpisodeLink.project_id == project_id,
        EntityEpisodeLink.episode_id == episode_id,
    ).count()
    if entity_count == 0:
        raise AppError(code="gate.no_entities", message="이 에피소드에 연결된 요소가 없습니다. 분석을 먼저 실행하세요.", status_code=400)


def check_scene_images_ready(db: OrmSession, project_id: str, episode_id: str) -> Dict:
    """씬 이미지 생성 전 검증: 분석 + 참조 이미지 + 합성 이미지 100% 완성.

    Returns: 검증 결과 요약 dict (UI 표시용)
    """
    ep = db.query(Episode).filter(Episode.id == episode_id, Episode.project_id == project_id).first()
    if not ep:
        raise AppError(code="episode.not_found", message="에피소드를 찾을 수 없습니다", status_code=404)

    # W20F10: stale Episode.status 로 막히지 않도록 checkpoint projection 을
    # 동기화한 뒤 ep 를 refresh 해서 status 를 다시 본다.
    ensure_analysis_projection_current(db, project_id, episode_id)
    db.refresh(ep)

    # 1) 분석 완료
    if ep.status != "analyzed":
        raise AppError(
            code="gate.analysis_incomplete",
            message=f"분석이 완료되지 않았습니다 (현재: {ep.status}). 분석을 먼저 완료하세요.",
            status_code=400,
        )

    # 2) 씬 존재
    scene_count = db.query(SceneStill).filter(
        SceneStill.project_id == project_id, SceneStill.episode_id == episode_id,
    ).count()
    if scene_count == 0:
        raise AppError(code="gate.no_scenes", message="씬이 없습니다. 분석을 먼저 완료하세요.", status_code=400)

    # 3) 캐릭터/장소/소품 참조 이미지 전수 확인
    linked_ids = [
        r[0] for r in db.query(EntityEpisodeLink.canon_id).filter(
            EntityEpisodeLink.project_id == project_id,
            EntityEpisodeLink.episode_id == episode_id,
        ).all()
    ]
    non_outlook = db.query(EntityCanon).filter(
        EntityCanon.id.in_(linked_ids),
        # location 제외 — 배경 참조 이미지 생성 비활성화 상태
        # TODO: 배경 참조 이미지 활성화 시 "location" 다시 추가
        EntityCanon.entity_type.in_(["character", "prop"]),
    ).all()

    # 저빈도 스킵 목록 로드 (ref_image_gen에서 저장) — v0.5.17부터 유틸 경유
    from app.core.low_freq_skip import load_low_freq_skip_ids
    low_freq_skipped_ids: set = load_low_freq_skip_ids(project_id, episode_id)

    # DB + 파일 존재 동시 확인
    entities_with_ref = set()
    if non_outlook:
        from app.core.file_paths import resolve_image_path
        ref_assets = db.query(ImageAsset).filter(
            ImageAsset.project_id == project_id,
            ImageAsset.episode_id == episode_id,
            ImageAsset.asset_type == "reference",
            ImageAsset.is_primary == 1,
            ImageAsset.entity_id.in_([e.id for e in non_outlook]),
        ).all()
        for ra in ref_assets:
            p = resolve_image_path(ra.file_path)
            if p and p.exists():
                entities_with_ref.add(ra.entity_id)
            else:
                logger.warning("참조 이미지 파일 누락: entity=%s, path=%s", ra.entity_id, ra.file_path)

    missing_refs = [e for e in non_outlook if e.id not in entities_with_ref and e.id not in low_freq_skipped_ids]
    if missing_refs:
        names = ", ".join(f"{e.name}({e.entity_type})" for e in missing_refs[:5])
        more = f" 외 {len(missing_refs)-5}개" if len(missing_refs) > 5 else ""
        raise AppError(
            code="gate.incomplete_references",
            message=f"참조 이미지 미완성: {len(missing_refs)}개 누락 ({names}{more}). '참조 이미지 생성'을 먼저 완료하세요.",
            status_code=400,
        )

    # 4) 인물+아웃룩 합성 이미지 전수 확인 (이 에피소드에 연결된 캐릭터만)
    # O00 (Null Outlook) 쌍은 composite 불필요 → 제외
    char_ids_in_episode = {e.id for e in non_outlook if e.entity_type == "character"}
    _o00_ids = {
        e.id for e in db.query(EntityCanon).filter(
            EntityCanon.project_id == project_id,
            EntityCanon.short_id == "O00",
            EntityCanon.entity_type == "outlook",
        ).all()
    }
    _all_combos_raw = db.query(CharacterOutlook).filter(
        CharacterOutlook.project_id == project_id,
        CharacterOutlook.character_id.in_(char_ids_in_episode),
    ).all() if char_ids_in_episode else []
    all_combos = [c for c in _all_combos_raw if c.outlook_id not in _o00_ids]

    if all_combos:
        from sqlalchemy import or_
        from app.core.file_paths import resolve_image_path
        composite_assets = db.query(ImageAsset).filter(
            ImageAsset.project_id == project_id,
            ImageAsset.asset_type == "reference",
            or_(
                ImageAsset.prompt_used.like("%composite:%"),
                ImageAsset.prompt_used.like("%outlook_id:%"),
            ),
        ).all()

        existing_keys = set()
        for ca in composite_assets:
            p = ca.prompt_used or ""
            # 새 형식: composite:{char_id}:{outlook_id} 또는 레거시: outlook_id:{outlook_id}
            m = re.search(r'composite:([a-f0-9-]+):([a-f0-9-]+)', p)
            m_legacy = re.search(r'outlook_id:([a-f0-9-]+)', p) if not m else None
            char_id = m.group(1) if m else (ca.entity_id if m_legacy else None)
            outlook_id = m.group(2) if m else (m_legacy.group(1) if m_legacy else None)
            if char_id and outlook_id:
                resolved = resolve_image_path(ca.file_path)
                if resolved and resolved.exists():
                    existing_keys.add(f"{char_id}:{outlook_id}")
                else:
                    logger.warning("합성 이미지 파일 누락: entity=%s, path=%s", ca.entity_id, ca.file_path)

        # 배치 쿼리 — 누락 combo의 entity 이름을 한 번에 로드 (N+1 방지)
        _missing_pairs = [
            combo for combo in all_combos
            if combo.character_id not in low_freq_skipped_ids
            and f"{combo.character_id}:{combo.outlook_id}" not in existing_keys
        ]
        missing_combos: List[str] = []
        if _missing_pairs:
            _needed_ids: set = set()
            for combo in _missing_pairs:
                _needed_ids.add(combo.character_id)
                _needed_ids.add(combo.outlook_id)
            _name_map: Dict[str, str] = {
                e.id: e.name for e in db.query(EntityCanon).filter(EntityCanon.id.in_(_needed_ids)).all()
            }
            for combo in _missing_pairs:
                missing_combos.append(
                    f"{_name_map.get(combo.character_id, '?')}+{_name_map.get(combo.outlook_id, '?')}"
                )

        if missing_combos:
            names = ", ".join(missing_combos[:5])
            more = f" 외 {len(missing_combos)-5}개" if len(missing_combos) > 5 else ""
            raise AppError(
                code="gate.incomplete_composites",
                message=f"인물+아웃룩 합성 이미지 미완성: {len(missing_combos)}개 누락 ({names}{more}). '참조 이미지 생성'을 먼저 완료하세요.",
                status_code=400,
            )

    return {
        "entities": len(non_outlook),
        "refs": len(entities_with_ref),
        "combos": len(all_combos),
        "combo_done": len(all_combos) - len(missing_combos) if all_combos else 0,
        "scenes": scene_count,
    }


def get_pipeline_status(db: OrmSession, project_id: str, episode_id: str) -> Dict:
    """전체 파이프라인 단계별 완성도 조회 (UI 표시용)."""
    ep = db.query(Episode).filter(Episode.id == episode_id, Episode.project_id == project_id).first()
    if not ep:
        return {}

    # 요소
    linked_ids = [
        r[0] for r in db.query(EntityEpisodeLink.canon_id).filter(
            EntityEpisodeLink.project_id == project_id,
            EntityEpisodeLink.episode_id == episode_id,
        ).all()
    ]
    entity_total = db.query(EntityCanon).filter(EntityCanon.id.in_(linked_ids)).count() if linked_ids else 0
    non_outlook_total = db.query(EntityCanon).filter(
        EntityCanon.id.in_(linked_ids), EntityCanon.entity_type.in_(["character", "location", "prop"]),
    ).count() if linked_ids else 0

    # 참조 이미지
    ref_done = db.query(ImageAsset.entity_id).filter(
        ImageAsset.project_id == project_id, ImageAsset.episode_id == episode_id,
        ImageAsset.asset_type == "reference", ImageAsset.is_primary == 1,
    ).distinct().count()

    # 합성 이미지 (O00 Null Outlook 제외)
    _o00_ids_status = {
        e.id for e in db.query(EntityCanon).filter(
            EntityCanon.project_id == project_id,
            EntityCanon.short_id == "O00",
            EntityCanon.entity_type == "outlook",
        ).all()
    }
    _all_combos_status = db.query(CharacterOutlook).filter(CharacterOutlook.project_id == project_id).all()
    combo_total = sum(1 for c in _all_combos_status if c.outlook_id not in _o00_ids_status)
    composite_done = db.query(ImageAsset).filter(
        ImageAsset.project_id == project_id, ImageAsset.asset_type == "reference",
        ImageAsset.prompt_used.like("%outlook_id:%"),
    ).count()

    # 씬
    scene_total = db.query(SceneStill).filter(
        SceneStill.project_id == project_id, SceneStill.episode_id == episode_id,
    ).count()
    scene_img_done = db.query(ImageAsset.still_id).filter(
        ImageAsset.project_id == project_id, ImageAsset.episode_id == episode_id,
        ImageAsset.asset_type == "scene", ImageAsset.is_primary == 1,
    ).distinct().count()

    # 단계별 완성 여부
    analysis_done = ep.status == "analyzed"
    ref_complete = ref_done >= non_outlook_total and non_outlook_total > 0
    combo_complete = composite_done >= combo_total if combo_total > 0 else ref_complete
    scene_complete = scene_img_done >= scene_total and scene_total > 0

    return {
        "analysis": {"status": ep.status, "done": analysis_done},
        "reference_images": {
            "entity_total": non_outlook_total,
            "ref_done": ref_done,
            "complete": ref_complete,
            "can_start": analysis_done,
        },
        "composite_images": {
            "combo_total": combo_total,
            "composite_done": composite_done,
            "complete": combo_complete,
            "can_start": ref_complete,
        },
        "scene_images": {
            "scene_total": scene_total,
            "scene_done": scene_img_done,
            "complete": scene_complete,
            "can_start": ref_complete and combo_complete,
        },
    }
