"""이미지 관리 API 라우터."""

import logging
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, Depends, Query, Request, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
from sqlalchemy.orm import Session as OrmSession

from app.api.deps import get_db, get_current_user, verify_project_access
from app.core.config import settings
from app.core.database import SessionLocal
from app.core.errors import AppError
from app.core.job_manager import submit_background_job
from app.i18n.loader import t
from app.models.catalog import UserAccount
from app.schemas.image import ImageResponse, ReviewUpdate, ValidationResponse
from app.schemas.trace import GenerationTraceResponse, RejectionStatsResponse
from app.modules.llm.gemini_key_pool import key_count as gemini_key_count
from app.services.image_service import ImageService
from app.services.reference_image_service import ReferenceImageService
from app.services.scene_image_service import SceneImageService

logger = logging.getLogger(__name__)


class GenerateSingleRequest(BaseModel):
    custom_prompt: Optional[str] = None
    variation_index: Optional[int] = None  # 특정 variation만 생성 (0-based)


class EditAngleRequest(BaseModel):
    horizontal: int = 0
    vertical: int = 0
    zoom: float = 1.0


class EditColorRequest(BaseModel):
    color_prompt: str


class SelectVariantRequest(BaseModel):
    variant: str  # "original" | "A" | "B"


class SelectOriginalRequest(BaseModel):
    image_id: str


router = APIRouter(
    prefix="/api/v1/projects/{project_id}",
    tags=["images"],
)


def _make_service(
    project_id: str,
    db: OrmSession,
    user: UserAccount,
) -> ImageService:
    return ImageService(
        db=db,
        project_id=project_id,
        actor_id=user.id,
    )


def _make_reference_service(
    project_id: str,
    db: OrmSession,
    user: UserAccount,
) -> ReferenceImageService:
    return ReferenceImageService(
        db=db,
        project_id=project_id,
        actor_id=user.id,
    )


def _make_scene_service(
    project_id: str,
    db: OrmSession,
    user: UserAccount,
) -> SceneImageService:
    return SceneImageService(
        db=db,
        project_id=project_id,
        actor_id=user.id,
    )


@router.get("/images", response_model=list[ImageResponse])
def list_images(
    type: str | None = Query(default=None, alias="type"),
    episode_id: str | None = Query(default=None),
    entity_id: str | None = Query(default=None),
    still_id: str | None = Query(default=None),
    outlook_id: str | None = Query(default=None),
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """List images, optionally filtered by asset type, episode, entity, or still.

    outlook_id: when provided, returns images whose prompt_used contains
    'outlook_id:{outlook_id}' (composite images referencing this outlook).
    """
    service = _make_service(project_id, db, current_user)
    return service.list_images(
        episode_id=episode_id, asset_type=type,
        entity_id=entity_id, still_id=still_id,
        outlook_id=outlook_id,
    )


@router.get("/images/{image_id}", response_model=ImageResponse)
def get_image(
    image_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Get image detail."""
    service = _make_service(project_id, db, current_user)
    return service.get_image(image_id)


@router.get("/images/{image_id}/file")
def get_image_file(
    image_id: str,
    thumb: int = 0,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Serve image file. ?thumb=1 로 축소 JPG 썸네일 반환."""
    from app.core.file_paths import resolve_image_path

    service = _make_service(project_id, db, current_user)
    img = service.get_image(image_id)
    file_path = resolve_image_path(img.file_path)
    if file_path is None:
        raise AppError(
            code="image.not_found",
            message=t("image.not_found"),
            status_code=404,
        )

    # Path traversal guard: resolved path must be within the projects directory
    resolved = file_path.resolve()
    project_dir = Path(settings.projects_dir).resolve()
    if not str(resolved).startswith(str(project_dir) + "/") and resolved != project_dir:
        raise AppError(
            code="image.path_forbidden",
            message=t("image.not_found"),
            status_code=403,
        )

    if not file_path.exists():
        raise AppError(
            code="image.not_found",
            message=t("image.not_found"),
            status_code=404,
        )

    # 썸네일 모드: 축소 JPG 반환 (캐시 포함)
    if thumb:
        thumb_path = file_path.parent / (file_path.stem + ".thumb.jpg")
        needs_regen = not thumb_path.exists() or thumb_path.stat().st_mtime < file_path.stat().st_mtime
        if needs_regen:
            try:
                from PIL import Image as PILImage
                with PILImage.open(file_path) as pil_img:
                    pil_img.thumbnail((400, 400))
                    pil_img = pil_img.convert("RGB")
                    pil_img.save(thumb_path, "JPEG", quality=75)
            except Exception:
                # Pillow 실패 시 원본 반환
                return FileResponse(path=str(file_path), media_type="image/png", filename=file_path.name)
        return FileResponse(
            path=str(thumb_path),
            media_type="image/jpeg",
            filename=file_path.stem + ".thumb.jpg",
            headers={"Cache-Control": "public, max-age=86400"},
        )

    return FileResponse(
        path=str(file_path),
        media_type="image/png",
        filename=file_path.name,
    )


@router.patch("/images/{image_id}/review", response_model=ImageResponse)
def update_review(
    image_id: str,
    body: ReviewUpdate,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Update review status of an image."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    return service.update_review(image_id, body.status, body.notes, ip=ip)


@router.post("/images/{image_id}/validate", response_model=ValidationResponse)
def validate_image(
    image_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Re-validate a single image using GPT-5.4 vision."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    result = service.validate_image(image_id, ip=ip)
    return {
        "image_id": image_id,
        "score": result.get("score"),
        "passed": result.get("passed"),
        "issues": result.get("issues", []),
        "description": result.get("description", ""),
    }


@router.get("/images/{image_id}/validation", response_model=ValidationResponse)
def get_validation(
    image_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Get validation result for a single image."""
    service = _make_service(project_id, db, current_user)
    return service.get_validation(image_id)


@router.post("/images/{image_id}/regenerate")
def regenerate_image(
    image_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Regenerate a single image."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    service.regenerate_image(image_id, ip=ip)
    return {"ok": True, "message": t("image.regeneration_started")}


@router.post("/images/{image_id}/set-primary", response_model=ImageResponse)
def set_primary(
    image_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Set an image as the primary (representative) image for its entity/still."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    return service.set_primary_image(image_id, ip=ip)


@router.post("/entities/{entity_id}/generate-image", response_model=ImageResponse)
def generate_entity_image(
    entity_id: str,
    body: GenerateSingleRequest,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Generate a single reference image for an entity."""
    from app.core.pipeline_gate import check_ref_images_ready
    # 엔티티에서 episode_id 찾기
    from app.models.project import EntityEpisodeLink
    link = db.query(EntityEpisodeLink).filter(
        EntityEpisodeLink.project_id == project_id, EntityEpisodeLink.canon_id == entity_id,
    ).first()
    if not link:
        raise AppError(code="gate.no_episode_link", message="이 요소가 연결된 에피소드가 없습니다. 분석을 먼저 실행하세요.", status_code=400)
    check_ref_images_ready(db, project_id, link.episode_id)
    ip = request.client.host if request.client else None
    service = _make_reference_service(project_id, db, current_user)
    result = service.generate_single_entity_image(entity_id, custom_prompt=body.custom_prompt, ip=ip)
    return result


def _trigger_composite_regen(project_id: str, entity_id: str, actor_id: str, ip: str | None):
    """백그라운드 스레드에서 관련 합성 이미지 재생성."""
    import threading

    def _run():
        from app.core.database import SessionLocal
        db2 = SessionLocal()
        try:
            svc = ReferenceImageService(db2, project_id, actor_id=actor_id)
            svc.regenerate_composites_for_entity(entity_id, ip=ip)
        except Exception as exc:
            import logging
            logging.getLogger(__name__).warning("Auto composite regen failed: %s", exc)
        finally:
            db2.close()

    threading.Thread(target=_run, daemon=True).start()


@router.post("/composites/generate", response_model=ImageResponse)
def generate_composite_image(
    request: Request,
    character_id: str = Query(...),
    outlook_id: str = Query(...),
    episode_id: str | None = Query(
        None, description="어느 화의 배정인가 — 안 주면 인물이 한 화에만 있을 "
                          "때만 그 화로, 여러 화면 `shared` 아래에 적는다"),
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """합성 이미지 개별 생성 (인물 + 아웃룩)."""
    from app.core.pipeline_gate import check_ref_images_ready
    from app.models.project import EntityEpisodeLink

    # ★★임의의 첫 링크로 게이트를 보지 않는다 (Codex BLOCK 2026-09-04).
    #  공유 인물이 여러 화에 있으면 **어느 화의 준비도를 본 것인지** 알 수
    #  없고, 그 화가 저장 자리로도 쓰였다 — 계보가 어긋난다.
    _eps = sorted({
        r[0] for r in db.query(EntityEpisodeLink.episode_id).filter(
            EntityEpisodeLink.project_id == project_id,
            EntityEpisodeLink.canon_id == character_id,
        ).all()
    })
    if episode_id is not None and episode_id not in _eps:
        raise AppError(
            code="image.episode_not_linked",
            message=f"이 인물은 그 화에 없습니다 (걸린 화 {len(_eps)}개)",
            status_code=400)
    _gate_ep = episode_id or (_eps[0] if len(_eps) == 1 else None)
    if _gate_ep:
        check_ref_images_ready(db, project_id, _gate_ep)
    ip = request.client.host if request.client else None
    service = _make_reference_service(project_id, db, current_user)
    return service.generate_composite_image(
        character_id, outlook_id, ip=ip, episode_id=episode_id)


@router.get("/entities/{entity_id}/affected-composites")
def get_affected_composites(
    entity_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """이 엔티티의 이미지가 변경되면 영향받는 합성 이미지 수 조회."""
    from app.models.project import EntityCanon, CharacterOutlook
    entity = db.query(EntityCanon).filter(EntityCanon.id == entity_id, EntityCanon.project_id == project_id).first()
    if not entity:
        return {"count": 0, "combos": []}

    combos = []
    if entity.entity_type == "character":
        rows = db.query(CharacterOutlook).filter(
            CharacterOutlook.character_id == entity_id, CharacterOutlook.project_id == project_id,
        ).all()
        for co in rows:
            ol = db.query(EntityCanon).filter(EntityCanon.id == co.outlook_id).first()
            combos.append({"character_id": entity_id, "character_name": entity.name,
                          "outlook_id": co.outlook_id, "outlook_name": ol.name if ol else "?"})
    elif entity.entity_type == "outlook":
        rows = db.query(CharacterOutlook).filter(
            CharacterOutlook.outlook_id == entity_id, CharacterOutlook.project_id == project_id,
        ).all()
        for co in rows:
            ch = db.query(EntityCanon).filter(EntityCanon.id == co.character_id).first()
            combos.append({"character_id": co.character_id, "character_name": ch.name if ch else "?",
                          "outlook_id": entity_id, "outlook_name": entity.name})

    return {"count": len(combos), "combos": combos}


@router.post("/entities/{entity_id}/regenerate-composites")
def regenerate_entity_composites(
    entity_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """이 엔티티(인물/아웃룩)의 관련 합성 이미지를 모두 재생성 (백그라운드)."""
    ip = request.client.host if request.client else None
    _trigger_composite_regen(project_id, entity_id, current_user.id, ip)
    return {"ok": True, "message": "합성 이미지 재생성이 시작되었습니다"}


@router.post("/stills/{still_id}/generate-image", response_model=ImageResponse)
def generate_still_image(
    still_id: str,
    body: GenerateSingleRequest,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Generate a single scene image for a still."""
    from app.core.pipeline_gate import check_scene_images_ready
    from app.services.dispatcher_preflight import check_bg_catalog_freshness
    from app.models.project import SceneStill as _SS

    # Normalize custom_prompt: blank/whitespace-only → None. service 가
    # `if custom_prompt:` (`scene_image_service.py:655`) + `custom_prompt or
    # still.still_frame_prompt` (line 634) 로 falsy 를 automatic path 로 fallback
    # 하므로, endpoint 도 같은 의미론을 따라야 enforce_shot_binding 결정과 service
    # 호출 path 가 정합. (review iter1 BLOCKING — `{"custom_prompt": ""}` 로
    # binding 강제 우회 차단.)
    custom_prompt = body.custom_prompt
    if custom_prompt is not None:
        custom_prompt = custom_prompt.strip() or None

    still = db.query(_SS).filter(_SS.id == still_id, _SS.project_id == project_id).first()
    if still:
        # D6 §4.8 preflight — first signal (catalog freshness 가 다른 gate 보다
        # 먼저 검증되어야 운영자가 STALE_UPSTREAM 을 명확히 받음). custom_prompt
        # path 는 binding stale 허용 (operator 가 one-off prompt 작성 의도 — D6
        # binding 은 자동 재바인딩 대상이지만 catalog freshness 는 강제).
        check_bg_catalog_freshness(
            project_id, still.episode_id,
            enforce_shot_binding=(custom_prompt is None),
        )
        check_scene_images_ready(db, project_id, still.episode_id)
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    return service.generate_single_scene_image(
        still_id, custom_prompt=custom_prompt, ip=ip,
    )


class ComposerPromptUpdate(BaseModel):
    composer_system_prompt: Optional[str] = None
    composer_user_prompt: Optional[str] = None


@router.post("/stills/{still_id}/compose-prompts")
def compose_prompts(
    still_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Run T2I composer on a still, save results, return both prompts."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    return service.compose_prompts(still_id, ip=ip)


@router.get("/settings/composer-prompt")
def get_composer_prompt(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Get current composer prompt settings for a project."""
    service = _make_service(project_id, db, current_user)
    return service.get_composer_prompt()


@router.patch("/settings/composer-prompt")
def update_composer_prompt(
    body: ComposerPromptUpdate,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Update project-level composer prompt override."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    return service.update_composer_prompt(
        system_prompt=body.composer_system_prompt,
        user_prompt=body.composer_user_prompt,
        ip=ip,
    )


@router.post("/images/upload", response_model=ImageResponse)
async def upload_image(
    file: UploadFile = File(...),
    entity_id: Optional[str] = Form(default=None),
    still_id: Optional[str] = Form(default=None),
    episode_id: Optional[str] = Form(default=None),
    request: Request = None,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Upload a custom reference/scene image."""
    ip = request.client.host if request and request.client else None
    file_bytes = await file.read()
    filename = file.filename or "uploaded_image.png"
    service = _make_service(project_id, db, current_user)
    result = service.upload_custom_image(
        file_bytes=file_bytes,
        filename=filename,
        entity_id=entity_id,
        still_id=still_id,
        episode_id=episode_id,
        ip=ip,
    )
    return result


def _run_image_gen_in_background(
    project_id: str,
    actor_id: str,
    episode_id: str,
    ip: str | None,
    mode: str = "resume",
) -> None:
    """Run image generation in a background thread with its own DB session."""
    db = SessionLocal()
    try:
        svc = SceneImageService(
            db=db,
            project_id=project_id,
            actor_id=actor_id,
        )
        svc.generate_images(episode_id, ip=ip, mode=mode)
    except Exception as exc:
        # Mark any stuck 'running' progress as failed so UI doesn't stay in "generating..." state
        import logging
        logging.getLogger(__name__).error("Image generation failed: %s", exc)
        try:
            from app.models.project import PipelineProgress
            stuck = (
                db.query(PipelineProgress)
                .filter(
                    PipelineProgress.project_id == project_id,
                    PipelineProgress.episode_id == episode_id,
                    PipelineProgress.operation == "image_generation",
                    PipelineProgress.status == "running",
                )
                .order_by(PipelineProgress.started_at.desc())
                .first()
            )
            if stuck:
                from datetime import datetime, timezone
                now = datetime.now(timezone.utc).isoformat()
                stuck.status = "error"
                stuck.error_message = str(exc)[:2000]
                stuck.updated_at = now
                db.commit()
        except Exception as mark_exc:
            logger.error("Failed to mark progress as error (still alive): %s", mark_exc)
        raise  # re-raise so job_manager logs it
    finally:
        db.close()


@router.post("/episodes/{episode_id}/generate-reference-images")
def generate_reference_images(
    episode_id: str,
    request: Request,
    mode: str = Query("resume", pattern="^resume$"),
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """참조 이미지(엔티티)만 생성 (씬 이미지 제외). 백그라운드 스레드.

    mode: resume (only) — 미완성분만 추가 생성. 'full' 모드는
    feedback_never_delete_images 규칙 위반 (ref ImageAsset 일괄 DELETE +
    checkpoint clear) 으로 제거됨. 강제 재생성은 RefImageGenStep mode='force'
    (UPDATE is_primary=0 + 새 row is_primary=1, PNG 보존) 사용.
    """
    ip = request.client.host if request.client else None
    from app.core.pipeline_gate import check_ref_images_ready
    check_ref_images_ready(db, project_id, episode_id)
    if not settings.gemini_api_key and gemini_key_count() == 0:
        raise AppError(code="image.gemini_key_missing", message=t("image.gemini_key_missing"), status_code=400)

    def _run_ref_gen(pid, actor_id, ep_id, ip_addr, gen_mode):
        db2 = SessionLocal()
        try:
            svc = ReferenceImageService(db=db2, project_id=pid, actor_id=actor_id)
            svc.generate_reference_images_only(ep_id, ip=ip_addr, mode=gen_mode)
        except Exception as exc:
            import logging
            logging.getLogger(__name__).error("Reference image generation failed: %s", exc)
            try:
                from app.models.project import PipelineProgress
                stuck = (
                    db2.query(PipelineProgress)
                    .filter(
                        PipelineProgress.project_id == pid,
                        PipelineProgress.episode_id == ep_id,
                        PipelineProgress.operation == "reference_image_generation",
                        PipelineProgress.status == "running",
                    )
                    .order_by(PipelineProgress.started_at.desc())
                    .first()
                )
                if stuck:
                    from datetime import datetime, timezone
                    now = datetime.now(timezone.utc).isoformat()
                    stuck.status = "error"
                    stuck.error_message = str(exc)[:2000]
                    stuck.updated_at = now
                    db2.commit()
            except Exception as mark_exc:
                logger.error("Failed to mark reference progress as error: %s", mark_exc)
            raise  # re-raise so job_manager logs it
        finally:
            db2.close()

    started = submit_background_job(
        job_key=f"ref_gen:{episode_id}",
        target=_run_ref_gen,
        args=(project_id, current_user.id, episode_id, ip, mode),
        description=f"Reference image generation for episode {episode_id}",
    )
    if not started:
        raise AppError(code="image.generation_already_running", message="참조 이미지 생성이 이미 진행 중입니다", status_code=409)
    return {"ok": True, "message": "참조 이미지 생성이 시작되었습니다"}


@router.post("/episodes/{episode_id}/generate-images")
def generate_images(
    episode_id: str,
    request: Request,
    mode: str = Query("resume", pattern="^resume$"),
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Start image generation for an episode (background thread).

    mode: resume (only) — 미완성분만 추가 생성. 'full' 모드는
    feedback_never_delete_images 규칙 위반으로 제거됨. 강제 재생성은
    StepRunner SceneImagePipelineStep mode='force' (기존 row/PNG 보존
    + UPDATE is_primary=0 + 새 row is_primary=1) 를 사용.
    """
    ip = request.client.host if request.client else None

    # Validate preconditions
    from app.models.project import Episode
    episode = (
        db.query(Episode)
        .filter(Episode.id == episode_id, Episode.project_id == project_id)
        .first()
    )
    if not episode:
        raise AppError(
            code="episode.not_found",
            message=t("episode.not_found"),
            status_code=404,
        )
    if not episode.fulltext:
        raise AppError(
            code="analysis.no_text",
            message=t("analysis.no_text"),
            status_code=400,
        )
    if not settings.gemini_api_key and gemini_key_count() == 0:
        raise AppError(
            code="image.gemini_key_missing",
            message=t("image.gemini_key_missing"),
            status_code=400,
        )

    # ── 전 단계 100% 완성 검증 (분석+참조+합성 전부) ──
    from app.core.pipeline_gate import check_scene_images_ready
    from app.services.dispatcher_preflight import check_bg_catalog_freshness

    # D6 §4.8 preflight — first signal (catalog freshness 가 다른 gate 보다
    # 먼저). 배치는 binding 강제 (custom_prompt 의미 없음 — operator 단건
    # path 에서만 binding-stale-skip 허용).
    check_bg_catalog_freshness(project_id, episode_id, enforce_shot_binding=True)
    check_scene_images_ready(db, project_id, episode_id)

    # Group 1 #2 (Codex review IMPORTANT): asset readiness preflight 를 동기적으로
    # API entry 에서 호출. background thread 안에서 raise 하면 FastAPI middleware
    # 가 못 잡아 사용자는 "started" 응답 받고 silent failure. 동기 raise 로 즉시 노출.
    from app.core.asset_readiness import assert_episode_asset_readiness
    assert_episode_asset_readiness(db, project_id, episode_id)

    started = submit_background_job(
        job_key=f"image_gen:{episode_id}",
        target=_run_image_gen_in_background,
        args=(project_id, current_user.id, episode_id, ip, mode),
        description=f"Image generation for episode {episode_id}",
    )
    if not started:
        raise AppError(code="image.generation_already_running", message="이미지 생성이 이미 진행 중입니다", status_code=409)

    return {"ok": True, "message": t("image.generation_started")}


@router.post("/episodes/{episode_id}/generate-variations")
def generate_variations(
    episode_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """원본 이미지 기반 A/B 변형 추천 + 생성. 백그라운드 스레드."""
    ip = request.client.host if request.client else None

    def _run_var_gen(pid, actor_id, ep_id):
        db2 = SessionLocal()
        try:
            svc = SceneImageService(db=db2, project_id=pid, actor_id=actor_id)
            svc.generate_variations_only(ep_id)
        finally:
            db2.close()

    started = submit_background_job(
        job_key=f"var_gen:{episode_id}",
        target=_run_var_gen,
        args=(project_id, current_user.id, episode_id),
        description=f"Variation generation for episode {episode_id}",
    )
    if not started:
        raise AppError(code="image.generation_already_running", message="변형 생성이 이미 진행 중입니다", status_code=409)
    return {"ok": True, "message": "변형 추천+생성이 시작되었습니다"}


@router.get("/episodes/{episode_id}/generation-status")
def get_generation_status(
    episode_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """에피소드의 이미지 생성 완료 상태 조회."""
    from app.models.project import EntityEpisodeLink, SceneStill, ImageAsset

    # 참조 이미지 — ★게이트와 **같은 물음**을 쓴다 (정본 helper).
    # 종전에는 대상을 `entity_type != "location"` 으로 잡아 outlook 까지 세고,
    # 완료는 primary 도 파일 존재도 안 봤다. 그래서 게이트가 막는데 화면은
    # 「다 됐다」로 보일 수 있었다 (2026-08-29).
    from app.core.pipeline_gate import (
        missing_reference_entities,
        reference_targets,
    )

    ref_total = len(reference_targets(db, project_id, episode_id))
    ref_done = ref_total - len(
        missing_reference_entities(db, project_id, episode_id))

    # 씬 이미지: 전체 씬 수 vs 씬 이미지 있는 씬 수
    # shot-more: is_selected=True + non-stale 만 "이미지 생성 대상"
    scene_total = (
        db.query(SceneStill)
        .filter(
            SceneStill.project_id == project_id,
            SceneStill.episode_id == episode_id,
            SceneStill.is_selected == True,     # noqa: E712
            SceneStill.still_index >= 0,
            SceneStill.status != "stale",
        )
        .count()
    )
    scene_done = 0
    if scene_total > 0:
        stills_with_images = (
            db.query(ImageAsset.still_id)
            .filter(
                ImageAsset.project_id == project_id,
                ImageAsset.episode_id == episode_id,
                ImageAsset.asset_type == "scene",
            )
            .distinct()
            .count()
        )
        scene_done = stills_with_images

    # 체크포인트에서 실패 정보 로드
    from app.modules.image_checkpoint import ImageCheckpointManager
    from app.core.config import settings as _settings
    cp_dir = Path(_settings.projects_dir) / project_id / "checkpoints" / "images" / episode_id
    ref_failed = 0
    scene_failed = 0
    try:
        ref_cp = ImageCheckpointManager(cp_dir, "reference")
        ref_failed = ref_cp.failed_count
    except Exception as exc:
        logger.warning("reference checkpoint failed_count load failed: %s — 0 유지", exc)
    try:
        scene_cp = ImageCheckpointManager(cp_dir, "scene")
        scene_failed = scene_cp.failed_count
    except Exception as exc:
        logger.warning("scene checkpoint failed_count load failed: %s — 0 유지", exc)

    # primary 이미지 수
    scene_primary = (
        db.query(ImageAsset)
        .filter(
            ImageAsset.project_id == project_id,
            ImageAsset.episode_id == episode_id,
            ImageAsset.asset_type == "scene",
            ImageAsset.is_primary == 1,
        )
        .count()
    )

    # fal.ai 앵글 이미지 수
    fal_count = (
        db.query(ImageAsset)
        .filter(
            ImageAsset.project_id == project_id,
            ImageAsset.episode_id == episode_id,
            ImageAsset.variant_type == "angle_fal",
        )
        .count()
    )

    # 저빈도 스킵 수 — v0.5.17부터 유틸 경유
    from app.core.low_freq_skip import load_low_freq_skip_ids
    low_freq_skipped = len(load_low_freq_skip_ids(project_id, episode_id))

    return {
        "ref_total": ref_total,
        "ref_done": ref_done,
        "ref_failed": ref_failed,
        "ref_low_freq_skipped": low_freq_skipped,
        "scene_total": scene_total,
        "scene_done": scene_done,
        "scene_failed": scene_failed,
        "scene_primary": scene_primary,
        "fal_count": fal_count,
    }


@router.get("/episodes/{episode_id}/pipeline-gate")
def get_pipeline_gate(
    episode_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """파이프라인 단계별 완성도 + 다음 단계 가능 여부."""
    from app.core.pipeline_gate import get_pipeline_status
    return get_pipeline_status(db, project_id, episode_id)


@router.post("/episodes/{episode_id}/regenerate-needs-fix")
def regenerate_needs_fix(
    episode_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Regenerate all needs_fix images for an episode."""
    ip = request.client.host if request.client else None
    service = _make_service(project_id, db, current_user)
    count = service.regenerate_needs_fix(episode_id, ip=ip)
    return {"ok": True, "count": count, "message": t("image.regeneration_started")}


# ------------------------------------------------------------------
# Variation pipeline endpoints
# ------------------------------------------------------------------


@router.post("/stills/{still_id}/recommend-variations")
def recommend_variations(
    still_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """LLM에게 A/B 변형 추천 받기."""
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    return service.recommend_variations(still_id, ip=ip)


@router.post("/stills/{still_id}/generate-with-variations")
def generate_with_variations(
    still_id: str,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """원본 + A변형 + B변형 이미지 생성."""
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    return service.generate_scene_with_variations(still_id, ip=ip)


@router.post("/images/{image_id}/edit-angle")
def edit_angle(
    image_id: str,
    body: EditAngleRequest,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """앵글 재편집 (i2i)."""
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    import json
    angle_json = json.dumps({
        "horizontal": body.horizontal,
        "vertical": body.vertical,
        "zoom": body.zoom,
    })
    return service.regenerate_variation(image_id, angle_json=angle_json, ip=ip)


@router.post("/images/{image_id}/edit-color")
def edit_color(
    image_id: str,
    body: EditColorRequest,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """색감 재편집 (i2i)."""
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    return service.regenerate_variation(image_id, color_prompt=body.color_prompt, ip=ip)


@router.patch("/stills/{still_id}/select-variant")
def select_variant(
    still_id: str,
    body: SelectVariantRequest,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """PDF 대표 변형 선택."""
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    return service.select_variant(still_id, body.variant, ip=ip)


@router.patch("/stills/{still_id}/select-original")
def select_original(
    still_id: str,
    body: SelectOriginalRequest,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """원본 이미지 선택."""
    ip = request.client.host if request.client else None
    service = _make_scene_service(project_id, db, current_user)
    return service.select_original(still_id, body.image_id, ip=ip)


@router.get("/stills/{still_id}/images")
def get_still_images(
    still_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """씬의 모든 이미지 (original/A/B 그룹)."""
    service = _make_service(project_id, db, current_user)
    return service.get_still_images_grouped(still_id)


# ------------------------------------------------------------------
# Generation trace endpoints
# ------------------------------------------------------------------

@router.get("/generation-traces", response_model=list[GenerationTraceResponse])
def list_generation_traces(
    offset: int = Query(default=0, ge=0),
    limit: int = Query(default=50, ge=1, le=200),
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """List all generation traces (paginated)."""
    service = _make_service(project_id, db, current_user)
    return service.get_generation_traces(offset=offset, limit=limit)


@router.get("/generation-traces/stats", response_model=RejectionStatsResponse)
def get_rejection_stats(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Get aggregated rejection statistics."""
    service = _make_service(project_id, db, current_user)
    return service.get_rejection_stats()


@router.get("/images/{image_id}/traces", response_model=list[GenerationTraceResponse])
def get_image_traces(
    image_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Get generation traces for a specific image."""
    service = _make_service(project_id, db, current_user)
    return service.get_image_traces(image_id)
