"""요소 관계 추출 StepRunner — entity_merge 이후 변형 관계 분석."""
import logging
from typing import Any, Dict

from app.core.step_runner import StepRunner
from app.core.steps.entity_steps import _EntityStepMixin

logger = logging.getLogger(__name__)


class EntityRelationStep(_EntityStepMixin, StepRunner):
    """Step 13.6: 요소 변형 관계 추출.

    entity_merge 결과에서 같은 대상의 변형 쌍을 식별하고
    시각적 유사성 여부를 판단하여 RelationFact에 기록.
    """

    def _chunk_projection(self) -> Dict[str, Any]:
        """C(c) 판 — `part_of` 를 **호출 0** 으로 투영한다.

        ★★producer 가 한 판독에서 `part_of` 를 이미 냈다. 그것을 관계 행으로
        옮기기만 한다 — 모델에게 다시 안 묻는다. `project_part_of` 는 sync 와
        **같은 짝 검사 함수**를 쓰므로 여기서 규칙이 갈리지 않는다.

        ★앞 판은 이 스텝에 C(c) 갈래가 없어서, `location_part` 의
        `part_of` RelationFact 가 **활성 경로에서 아예 안 생겼다**
        (Codex 재현 2026-09-01).
        """
        from app.core.errors import AppError
        from app.modules.pipeline import grounding_relation_projection as rp

        cp = self._load_prev_checkpoint("grounding_chunk")
        data = (cp or {}).get("data") or {}
        if not data:
            raise AppError(
                code="step.no_input",
                message=("grounding_chunk 산출 없음 — C(c) 판에서 관계는 "
                         "그 producer 가 낸 짝이 정본이다"),
                status_code=400)
        # ★★★producer 의 `part_of` 는 **한 벌인데 소비자가 둘**이다.
        #  아웃룩·장소부분을 주인에 붙이는 재료(`grounding_facet_binding`)와
        #  DB `relation_fact` 가 같은 목록을 본다. DB 계약이 받는 짝만 고르고
        #  나머지는 **까닭과 함께 남긴다** — 조용히 버리지 않는다.
        #  ★실측 2026-09-02 유료 재개: 모델이 `O01→C01`·`P04→P03` 을 냈고
        #   앞 판은 그것을 받고 주행을 세웠다.
        split = rp.select_db_part_of(list(data.get("grounding_part_of") or []))
        rows = rp.project_part_of(split["for_db"])
        return {
            "completed_count": len(rows), "applicable_count": len(rows),
            "failed_count": 0,
            "data": {"relations": rows,
                     "projected_from": "grounding_chunk",
                     # ★DB 가 안 받은 짝 — facet 결속이 쓰는 것들이다
                     "part_of_not_for_db": split["not_for_db"],
                     "projection_contract": rp.PROJECTION_CONTRACT_VERSION},
        }

    def _execute(self, mode="resume") -> Dict[str, Any]:
        from app.core.grounding_mode import (resolve_grounding_mode,
                                             uses_chunk_producer)

        # ★★★C(c) 판이면 **호출 0 투영**이다 — 관계를 다시 안 묻는다.
        if uses_chunk_producer(resolve_grounding_mode(
                getattr(self, "project_config", None))):
            return self._chunk_projection()

        # entity_merge 결과 로드
        merge_cp = self._load_prev_checkpoint("entity_merge")
        if merge_cp and merge_cp.get("data"):
            entities = {
                "characters": merge_cp["data"].get("characters", []),
                "locations": merge_cp["data"].get("locations", []),
                "props": merge_cp["data"].get("props", []),
            }
        else:
            char_cp = self._load_prev_checkpoint("entity_extract_character")
            loc_cp = self._load_prev_checkpoint("entity_extract_location")
            prop_cp = self._load_prev_checkpoint("entity_extract_prop")
            entities = {
                "characters": (char_cp or {}).get("data", {}).get("characters", []),
                "locations": (loc_cp or {}).get("data", {}).get("locations", []),
                "props": (prop_cp or {}).get("data", {}).get("props", []),
            }

        total_entities = sum(len(v) for v in entities.values())
        if total_entities == 0:
            return {
                "completed_count": 0,
                "applicable_count": 0,
                "failed_count": 0,
                "data": {"relations": [], "candidates_checked": 0, "relations_found": 0, "visual_similar_count": 0},
            }

        # beat + shot 컨텍스트 구성 (시나리오 전문 대신)
        beat_shot_context = self._build_beat_shot_context()

        from app.modules.pipeline.entity_relation import extract_entity_relations

        result = extract_entity_relations(
            entities=entities,
            beat_shot_context=beat_shot_context,
            project_config=self.project_config,
            opik_metadata=self.build_opik_metadata(),
        )

        return {
            "completed_count": 1,
            "applicable_count": 1,
            "failed_count": 0,
            "data": result,
        }

    def _build_beat_shot_context(self) -> str:
        """beat_extract + shot_extract 결과를 컨텍스트 문자열로 구성."""
        lines = []

        beat_cp = self._load_prev_checkpoint("beat_extract")
        shot_cp = self._load_prev_checkpoint("shot_validator")

        beats_by_scene = {}
        if beat_cp and beat_cp.get("data", {}).get("scenes"):
            for sc in beat_cp["data"]["scenes"]:
                beats_by_scene[sc["scene_index"]] = sc.get("beats", [])

        shots_by_scene = {}
        if shot_cp and shot_cp.get("data", {}).get("scenes"):
            for sc in shot_cp["data"]["scenes"]:
                shots_by_scene[sc["scene_index"]] = sc.get("shots", [])

        all_indices = sorted(set(list(beats_by_scene.keys()) + list(shots_by_scene.keys())))

        for si in all_indices:
            lines.append(f"[씬 {si}]")
            beats = beats_by_scene.get(si, [])
            for b in beats:
                lines.append(f"  Beat {b.get('beat_index', '?')}: [{b.get('change_type', '')}] {b.get('description', '')}")
            shots = shots_by_scene.get(si, [])
            for sh in shots:
                chars = ", ".join(sh.get("characters", []))
                lines.append(f"  Shot {sh.get('shot_index', '?')}: [{chars}] {sh.get('description', '')}")
            lines.append("")

        return "\n".join(lines) if lines else "(beat/shot 데이터 없음)"
