"""ReferencePhase2Service — Phase 2 (standalone outfit reference) 단독 서비스.

F24.4.3 (2026-04-24): `ReferencePipelineOrchestrator.run()`의 Phase 2 블록
(character_outlook 기반 outfit 단독 이미지 병렬 생성)을 literal lift.

ctx.outlook_generated / ctx.outlook_skipped를 in-place 업데이트한다.
외부 예외는 원본과 동일하게 내부에서 log + swallow — run()은 None 반환.
"""
from __future__ import annotations

import json
import logging
import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict

from sqlalchemy.orm import Session as OrmSession

from app.core.config import settings
from app.core.file_paths import to_relative_image_path
from app.services.image_capture.annotate import annotate_generated_asset
from app.core.image_call_budget import bind_current_budget
from app.logging.activity_logger import ActivityLogger
from app.models.project import CharacterOutlook, EntityCanon, ImageAsset, ProjectSettings
from app.services.image_service_helpers import load_project_llm_config
from app.services.reference_pipeline_context import ReferencePipelineContext

logger = logging.getLogger(__name__)


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _new_id() -> str:
    return str(uuid.uuid4())


class ReferencePhase2Service:
    """Phase 2: character_outlook 기반 outfit 단독 reference 이미지 생성."""

    def __init__(
        self,
        db: OrmSession,
        project_id: str,
        actor_id: str,
        activity_logger: ActivityLogger,
    ) -> None:
        self._db = db
        self._project_id = project_id
        self._actor_id = actor_id
        self._logger = activity_logger

    def _get_style_context(self, episode_id: str = None) -> str:
        """프로젝트 세계관을 T2I 프롬프트 앞에 붙일 컨텍스트로 변환 (orchestrator와 동일 로직)."""
        ps = self._db.query(ProjectSettings).filter(
            ProjectSettings.project_id == self._project_id).first()
        if ps and ps.style_rules_json:
            sr = json.loads(ps.style_rules_json)
            return (
                f"Photorealistic cinematic still. "
                f"Setting: {sr.get('era','')}, {sr.get('region','')}. "
                f"Avoid: {sr.get('must_avoid','')}."
            )

        if episode_id:
            cp = (
                Path(settings.projects_dir) / self._project_id
                / "checkpoints" / "episodes" / episode_id
                / "visual_world_rules" / "manifest.json"
            )
            if cp.exists():
                vwr = json.loads(cp.read_text(encoding="utf-8")).get("data", {})
                era = vwr.get("era", "")
                region = vwr.get("region", "")
                if era or region:
                    return f"Photorealistic cinematic still. Setting: {era}, {region}."

        return "Photorealistic cinematic still."

    def run(self, ctx: ReferencePipelineContext) -> None:
        from app.modules.pipeline.ref_image_pipeline import generate_and_validate_reference

        # local capture (literal lift)
        episode_id = ctx.episode_id
        _low_freq_skip_ids = ctx.low_freq_skip_ids
        gemini_client = ctx.gemini_client
        reference_dir = ctx.reference_dir
        max_concurrent = ctx.max_concurrent

        try:
            char_outlooks = (
                self._db.query(CharacterOutlook)
                .filter(CharacterOutlook.project_id == self._project_id)
                .all()
            )

            outlook_tasks = []
            seen_outlook_ids = set()
            for co in char_outlooks:
                existing_composite = (
                    self._db.query(ImageAsset)
                    .filter(
                        ImageAsset.project_id == self._project_id,
                        ImageAsset.entity_id == co.outlook_id,
                        ImageAsset.asset_type == "reference",
                        ImageAsset.is_primary == 1,
                    )
                    .first()
                )
                if existing_composite and Path(existing_composite.file_path).exists():
                    ctx.outlook_skipped += 1
                    continue

                outlook_entity = self._db.query(EntityCanon).filter(EntityCanon.id == co.outlook_id).first()
                if not outlook_entity:
                    continue

                if outlook_entity.short_id == "O00":
                    ctx.outlook_skipped += 1
                    continue

                if co.character_id in _low_freq_skip_ids:
                    ctx.outlook_skipped += 1
                    continue

                outlook_desc = outlook_entity.description or outlook_entity.name

                if co.outlook_id in seen_outlook_ids:
                    ctx.outlook_skipped += 1
                    continue
                seen_outlook_ids.add(co.outlook_id)

                outlook_tasks.append({
                    "outlook_name": outlook_entity.name,
                    "outlook_desc": outlook_desc,
                    "outlook_id": co.outlook_id,
                })

            if outlook_tasks:
                logger.info("Generating %d standalone outfit images (parallel)", len(outlook_tasks))

                _outlook_t2i_cache: Dict[str, str] = {}
                _style_ctx = self._get_style_context(episode_id)
                _proj_config = load_project_llm_config(self._db, self._project_id)
                for otask in outlook_tasks:
                    oe = self._db.query(EntityCanon).filter(EntityCanon.id == otask["outlook_id"]).first()
                    if oe and oe.t2i_prompt:
                        _outlook_t2i_cache[otask["outlook_id"]] = oe.t2i_prompt
                    else:
                        from app.modules.llm.llm_client import call_text
                        try:
                            eng = call_text(
                                step="prompt_translation",
                                system_prompt="Translate the Korean costume description to English for image generation. Include the era/setting context. Output ONLY the English description.",
                                user_prompt=f"세계관: {_style_ctx}\n\n의상: {otask['outlook_desc']}",
                                project_config=_proj_config,
                            ).strip()
                        except Exception:
                            eng = otask["outlook_desc"]
                        _outlook_t2i_cache[otask["outlook_id"]] = eng
                        if oe and eng != otask["outlook_desc"]:
                            oe.t2i_prompt = eng
                self._db.commit()

                def _gen_outfit(task):
                    from app.modules.pipeline.ref_image_pipeline import _load_ref_image_prompt
                    eng_desc = _outlook_t2i_cache.get(task["outlook_id"], task["outlook_desc"])
                    ref_prompt = _load_ref_image_prompt("outlook",
                        outlook_description=eng_desc)
                    current_prompt = ref_prompt if ref_prompt else eng_desc
                    pipe_result = generate_and_validate_reference(
                        gemini_client=gemini_client,
                        entity_name=task["outlook_name"],
                        entity_description=task["outlook_desc"],
                        entity_type="outlook",
                        t2i_prompt=current_prompt,
                        output_dir=reference_dir,
                        extra_references=None,
                        style_context="",
                        # Phase 4 iter 7 B2 — observability: PID/EID 명시 forward.
                        trace_meta={
                            "project_id": self._project_id,
                            "episode_id": episode_id,
                            "operation_type": "ref_image_gen_phase2_outlook",
                            "entity_id": task["outlook_id"],
                        },
                    )
                    return task, pipe_result

                with ThreadPoolExecutor(max_workers=min(max_concurrent, len(outlook_tasks))) as executor:
                    # W20E5 Codex B2 — propagate parent-thread image-call
                    # budget (_gen_outfit → ReferencePipeline → Gemini image gen).
                    _submit_gen_outfit = bind_current_budget(_gen_outfit)
                    futures = {executor.submit(_submit_gen_outfit, t): t for t in outlook_tasks}
                    for future in as_completed(futures):
                        task = futures[future]
                        try:
                            _, pipe_result = future.result()
                            outfit_asset = ImageAsset(
                                id=_new_id(),
                                project_id=self._project_id,
                                asset_type="reference",
                                entity_id=task["outlook_id"],
                                episode_id=episode_id,
                                file_path=to_relative_image_path(pipe_result["file_path"]),
                                prompt_used=f"[outfit:{task['outlook_id']}] {task['outlook_desc'][:200]}",
                                generation_model=pipe_result["generation_model"],
                                status="generated",
                                review_notes=json.dumps(pipe_result.get("validation", {}), ensure_ascii=False),
                                validation_score=pipe_result.get("validation", {}).get("score"),
                                is_primary=1,
                                created_at=_now(),
                            )
                            self._db.add(outfit_asset)
                            # persist-all Wave 1 — 아웃룩=T2I 루트(extra_references=None).
                            annotate_generated_asset(
                                outfit_asset, pipeline_role="reference_outlook", input_image_ids=[]
                            )
                            self._db.commit()
                            ctx.outlook_generated += 1
                            logger.info("Standalone outfit generated: %s", task["outlook_name"])
                        except Exception as exc:
                            logger.warning("Outfit generation failed for %s: %s",
                                           task["outlook_name"], exc)

        except Exception as exc:
            logger.warning("Outfit standalone generation error: %s", exc)
