"""엔티티 및 씬 스틸 API 라우터."""

from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session as OrmSession
from sqlalchemy import func

from app.api.deps import get_db, get_current_user, verify_project_access
from app.core.errors import AppError
from app.i18n.loader import t
from app.logging.activity_logger import ActivityLogger
from app.models.catalog import UserAccount
from app.models.project import (
    EntityCanon,
    EntityAlias,
    EntityEpisodeLink,
    Episode,
    RelationFact,
    RelationParticipant,
    SceneStill,
)
from app.schemas.entity import (
    EntityResponse,
    EntityDetailResponse,
    EntityUpdate,
    SceneStillResponse,
    SceneStillUpdate,
)


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


def _entity_to_response(canon: EntityCanon, episode_count: int) -> dict:
    return {
        "id": canon.id,
        "entity_type": canon.entity_type,
        "name": canon.name,
        "description": canon.description,
        "stable_traits": canon.stable_traits or "{}",
        "t2i_prompt": canon.t2i_prompt,
        "status": canon.status,
        "episode_count": episode_count,
        "created_at": canon.created_at,
    }


@router.get("/entities", response_model=list[EntityResponse])
def list_entities(
    type: str | None = None,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    query = db.query(EntityCanon).filter(EntityCanon.project_id == project_id)
    if type:
        query = query.filter(EntityCanon.entity_type == type)
    canons = query.order_by(EntityCanon.name).all()

    results = []
    for canon in canons:
        ep_count = (
            db.query(func.count(EntityEpisodeLink.id))
            .filter(EntityEpisodeLink.canon_id == canon.id)
            .scalar()
        ) or 0
        results.append(_entity_to_response(canon, ep_count))
    return results


@router.get("/entities/{entity_id}", response_model=EntityDetailResponse)
def get_entity(
    entity_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    canon = (
        db.query(EntityCanon)
        .filter(EntityCanon.id == entity_id, EntityCanon.project_id == project_id)
        .first()
    )
    if not canon:
        raise AppError(
            code="entity.not_found",
            message=t("entity.not_found"),
            status_code=404,
        )

    ep_count = (
        db.query(func.count(EntityEpisodeLink.id))
        .filter(EntityEpisodeLink.canon_id == canon.id)
        .scalar()
    ) or 0

    aliases = [
        a.alias
        for a in db.query(EntityAlias)
        .filter(EntityAlias.canon_id == canon.id)
        .all()
    ]

    # Build relations list
    participations = (
        db.query(RelationParticipant)
        .filter(RelationParticipant.canon_id == canon.id)
        .all()
    )
    relations = []
    seen_relation_ids = set()
    for p in participations:
        if p.relation_id in seen_relation_ids:
            continue
        seen_relation_ids.add(p.relation_id)

        fact = db.query(RelationFact).filter(RelationFact.id == p.relation_id).first()
        if not fact:
            continue

        all_participants = (
            db.query(RelationParticipant)
            .filter(RelationParticipant.relation_id == fact.id)
            .order_by(RelationParticipant.participant_order)
            .all()
        )

        participant_list = []
        for rp in all_participants:
            participant_canon = (
                db.query(EntityCanon)
                .filter(EntityCanon.id == rp.canon_id)
                .first()
            )
            participant_list.append({
                "entity_name": participant_canon.name if participant_canon else rp.canon_id,
                "entity_id": rp.canon_id,
                "role": rp.participant_role,
                "order": rp.participant_order,
            })

        relations.append({
            "id": fact.id,
            "relation_family": fact.relation_family,
            "relation_type": fact.relation_type,
            "directionality": fact.directionality,
            "temporal_scope": fact.temporal_scope,
            "continuity_priority": fact.continuity_priority,
            "continuity_reason": fact.continuity_reason,
            "participants": participant_list,
        })

    # Build episodes list
    ep_links = (
        db.query(EntityEpisodeLink)
        .filter(EntityEpisodeLink.canon_id == canon.id)
        .all()
    )
    episodes = []
    for link in ep_links:
        ep = db.query(Episode).filter(Episode.id == link.episode_id).first()
        if ep:
            episodes.append({
                "episode_id": ep.id,
                "episode_number": ep.episode_number,
                "title": ep.title,
            })

    base = _entity_to_response(canon, ep_count)
    base["aliases"] = aliases
    base["relations"] = relations
    base["episodes"] = episodes
    return base


@router.patch("/entities/{entity_id}", response_model=EntityResponse)
def update_entity(
    entity_id: str,
    body: EntityUpdate,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    canon = (
        db.query(EntityCanon)
        .filter(EntityCanon.id == entity_id, EntityCanon.project_id == project_id)
        .first()
    )
    if not canon:
        raise AppError(
            code="entity.not_found",
            message=t("entity.not_found"),
            status_code=404,
        )

    ip = request.client.host if request.client else None
    before = {}
    after = {}

    if body.name is not None:
        before["name"] = canon.name
        canon.name = body.name
        after["name"] = body.name

    if body.description is not None:
        before["description"] = canon.description
        canon.description = body.description
        after["description"] = body.description

    if body.stable_traits is not None:
        before["stable_traits"] = canon.stable_traits
        canon.stable_traits = body.stable_traits
        after["stable_traits"] = body.stable_traits

    if body.t2i_prompt is not None:
        before["t2i_prompt"] = canon.t2i_prompt
        canon.t2i_prompt = body.t2i_prompt
        after["t2i_prompt"] = body.t2i_prompt

    from datetime import datetime, timezone
    canon.updated_at = datetime.now(timezone.utc).isoformat()
    db.commit()

    logger = ActivityLogger(db)
    logger.log(
        actor_id=current_user.id,
        action="entity.update",
        resource_type="entity",
        resource_id=entity_id,
        project_id=project_id,
        detail={"before": before, "after": after},
        ip_address=ip,
    )

    ep_count = (
        db.query(func.count(EntityEpisodeLink.id))
        .filter(EntityEpisodeLink.canon_id == canon.id)
        .scalar()
    ) or 0

    return _entity_to_response(canon, ep_count)


@router.get("/episodes/{episode_id}/stills")
def list_stills(
    episode_id: str,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    stills = (
        db.query(SceneStill)
        .filter(SceneStill.project_id == project_id, SceneStill.episode_id == episode_id)
        .order_by(SceneStill.still_index)
        .all()
    )

    # Resolve visible entities with reference image info
    from app.models.project import EntityCanon, ImageAsset
    import json
    import re as _re

    # ── N+1 제거: 전체 entity ID 수집 후 벌크 로드 ──
    all_entity_ids = set()
    for still in stills:
        try:
            vis = json.loads(still.visible_entities_json or "[]")
        except json.JSONDecodeError:
            vis = []
        for v in vis:
            eid = v.get("entity_id", "") if isinstance(v, dict) else ""
            if eid:
                all_entity_ids.add(eid)

    # Bulk load entities
    entity_map = {}
    if all_entity_ids:
        entities = db.query(EntityCanon).filter(EntityCanon.id.in_(all_entity_ids)).all()
        entity_map = {e.id: e for e in entities}

    # Bulk load primary reference images for non-character/non-outlook entities
    primary_image_map = {}
    if all_entity_ids:
        primary_images = db.query(ImageAsset).filter(
            ImageAsset.entity_id.in_(all_entity_ids),
            ImageAsset.asset_type == "reference",
            ImageAsset.is_primary == 1,
        ).all()
        primary_image_map = {img.entity_id: img for img in primary_images}

    # Bulk load character face reference images (no outlook_id in prompt)
    char_entity_ids = {eid for eid in all_entity_ids if eid in entity_map and entity_map[eid].entity_type == "character"}
    char_face_image_map = {}
    if char_entity_ids:
        # Get all reference images for characters, then filter in Python
        char_ref_images = db.query(ImageAsset).filter(
            ImageAsset.entity_id.in_(char_entity_ids),
            ImageAsset.asset_type == "reference",
        ).order_by(ImageAsset.created_at.desc()).all()
        for img in char_ref_images:
            if img.entity_id not in char_face_image_map:
                if not img.prompt_used or "outlook_id:" not in img.prompt_used:
                    char_face_image_map[img.entity_id] = img

    # Bulk load character name→id map for outlook pairing (all project characters,
    # not just those in stills — T2I [[char]+[outlook]] may reference any character)
    all_project_chars = db.query(EntityCanon).filter(
        EntityCanon.project_id == project_id,
        EntityCanon.entity_type == "character",
    ).all()
    char_name_map = {c.name: c for c in all_project_chars}

    # Bulk load outlook reference images (prompt_used LIKE %outlook_id:...%)
    outlook_entity_ids = {eid for eid in all_entity_ids if eid in entity_map and entity_map[eid].entity_type == "outlook"}
    outlook_image_map = {}  # outlook_entity_id -> {char_id: ImageAsset, None: fallback}
    if outlook_entity_ids:
        for oid in outlook_entity_ids:
            outlook_images = db.query(ImageAsset).filter(
                ImageAsset.asset_type == "reference",
                ImageAsset.prompt_used.like(f"%outlook_id:{oid}%"),
            ).order_by(ImageAsset.created_at.desc()).all()
            if outlook_images:
                by_char = {}
                for img in outlook_images:
                    if img.entity_id and img.entity_id not in by_char:
                        by_char[img.entity_id] = img
                    if None not in by_char:
                        by_char[None] = img  # fallback = most recent
                outlook_image_map[oid] = by_char

    result = []
    for still in stills:
        data = SceneStillResponse.model_validate(still).model_dump()

        # Parse visible_entities and enrich with entity info + ref image
        try:
            vis_list = json.loads(still.visible_entities_json or "[]")
        except json.JSONDecodeError:
            vis_list = []

        # T2I에서 [[char]+[outlook]] 파싱하여 캐릭터별 아웃룩 매핑
        t2i_text = still.t2i_prompt_cinematic or ""
        char_outlook_pairs = {}  # outlook_name -> character_name
        for _m in _re.finditer(r'\[\[([^\]]+)\]\+\[([^\]]+)\]\]', t2i_text):
            char_outlook_pairs[_m.group(2)] = _m.group(1)

        resolved_entities = []
        for v in vis_list:
            if isinstance(v, dict):
                eid = v.get("entity_id", "")
                etype = v.get("entity_type", "")
                ename = v.get("entity_name", "")
            elif isinstance(v, str):
                eid = v
                etype = ""
                ename = ""
            else:
                continue
            if not eid:
                continue
            entity = entity_map.get(eid)
            if not entity:
                continue

            ref_img = None
            if entity.entity_type == "outlook":
                # 아웃룩: 이 씬에서 어떤 캐릭터가 입었는지 찾아서 해당 캐릭터의 합성 이미지
                paired_char_name = char_outlook_pairs.get(entity.name)
                char_images = outlook_image_map.get(eid, {})
                if paired_char_name:
                    paired_char = char_name_map.get(paired_char_name)
                    if paired_char:
                        ref_img = char_images.get(paired_char.id)
                if not ref_img:
                    # fallback: 아무 합성이라도
                    ref_img = char_images.get(None)
            elif entity.entity_type == "character":
                # 캐릭터: 얼굴 참조 (outlook_id 없는 것만)
                ref_img = char_face_image_map.get(eid)
            else:
                # 배경/물체: 기본 primary
                ref_img = primary_image_map.get(eid)

            resolved_entities.append({
                "entity_id": eid,
                "entity_name": entity.name,
                "entity_type": entity.entity_type,
                "t2i_prompt": entity.t2i_prompt or "",
                "has_reference_image": ref_img is not None,
                "reference_image_id": ref_img.id if ref_img else None,
            })

        data["resolved_entities"] = resolved_entities

        # 같은 장소의 이전 씬 (의존성) — 가장 가까운 1개만, 최대 2개
        location_ids = [e["entity_id"] for e in resolved_entities if e["entity_type"] == "location"]
        dependent_scenes = []
        if location_ids:
            # 역순으로 탐색하여 가장 가까운 것부터
            for prev in reversed(result):
                if len(dependent_scenes) >= 2:
                    break
                prev_locs = [e["entity_id"] for e in prev.get("resolved_entities", []) if e["entity_type"] == "location"]
                if set(prev_locs) & set(location_ids):
                    prev_img = (
                        db.query(ImageAsset)
                        .filter(
                            ImageAsset.still_id == prev["id"],
                            ImageAsset.asset_type == "scene",
                            ImageAsset.variant_type == "original",
                        )
                        .first()
                    )
                    dependent_scenes.append({
                        "still_id": prev["id"],
                        "still_index": prev["still_index"],
                        "beat_title": prev.get("beat_title", ""),
                        "has_image": prev_img is not None,
                        "image_id": prev_img.id if prev_img else None,
                    })
            # 기본은 1개만, 2개는 역순 복원
            dependent_scenes.reverse()
            if len(dependent_scenes) > 1:
                # 가장 가까운 1개만 기본 유지 (2개는 특수한 경우)
                dependent_scenes = dependent_scenes[-1:]
        data["dependent_scenes"] = dependent_scenes

        # 변형 추천을 동적 배열로 반환 (A/B 하드코딩 → variations[])
        variations = []
        for label_suffix in ["a", "b"]:
            vtype = data.get(f"variation_{label_suffix}_type")
            if vtype and vtype != "none":
                variations.append({
                    "label": label_suffix.upper(),
                    "type": vtype,
                    "angle": data.get(f"variation_{label_suffix}_angle"),
                    "color": data.get(f"variation_{label_suffix}_color"),
                    "reason": data.get(f"variation_{label_suffix}_reason"),
                })
        data["variations"] = variations

        result.append(data)

    return result


@router.patch("/stills/{still_id}", response_model=SceneStillResponse)
def update_still(
    still_id: str,
    body: SceneStillUpdate,
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """Update a scene still's prompt, beat_title, camera, or lighting."""
    still = (
        db.query(SceneStill)
        .filter(SceneStill.id == still_id, SceneStill.project_id == project_id)
        .first()
    )
    if not still:
        raise AppError(
            code="still.not_found",
            message=t("still.not_found"),
            status_code=404,
        )

    ip = request.client.host if request.client else None
    before = {}
    after = {}

    if body.still_frame_prompt is not None:
        before["still_frame_prompt"] = still.still_frame_prompt
        still.still_frame_prompt = body.still_frame_prompt
        after["still_frame_prompt"] = body.still_frame_prompt

    if body.beat_title is not None:
        before["beat_title"] = still.beat_title
        still.beat_title = body.beat_title
        after["beat_title"] = body.beat_title

    if body.camera_json is not None:
        before["camera_json"] = still.camera_json
        still.camera_json = body.camera_json
        after["camera_json"] = body.camera_json

    if body.lighting_json is not None:
        before["lighting_json"] = still.lighting_json
        still.lighting_json = body.lighting_json
        after["lighting_json"] = body.lighting_json

    if body.t2i_prompt_cinematic is not None:
        before["t2i_prompt_cinematic"] = still.t2i_prompt_cinematic
        still.t2i_prompt_cinematic = body.t2i_prompt_cinematic
        after["t2i_prompt_cinematic"] = body.t2i_prompt_cinematic

    if body.t2i_prompt_closeup is not None:
        before["t2i_prompt_closeup"] = still.t2i_prompt_closeup
        still.t2i_prompt_closeup = body.t2i_prompt_closeup
        after["t2i_prompt_closeup"] = body.t2i_prompt_closeup

    if body.visible_entities_json is not None:
        before["visible_entities_json"] = still.visible_entities_json
        still.visible_entities_json = body.visible_entities_json
        after["visible_entities_json"] = body.visible_entities_json

    if "dependent_scene_id" in (body.model_dump(exclude_unset=True) or {}):
        before["dependent_scene_id"] = still.dependent_scene_id
        still.dependent_scene_id = body.dependent_scene_id
        after["dependent_scene_id"] = body.dependent_scene_id

    db.commit()

    logger = ActivityLogger(db)
    logger.log(
        actor_id=current_user.id,
        action="still.update",
        resource_type="still",
        resource_id=still_id,
        project_id=project_id,
        detail={"before": before, "after": after},
        ip_address=ip,
    )

    return still


# ── 프로젝트 스타일 규칙 API ──

@router.get("/style-rules")
def get_style_rules(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """프로젝트 단위 스타일 규칙 조회."""
    from app.models.project import ProjectSettings
    import json as _json
    ps = db.query(ProjectSettings).filter(ProjectSettings.project_id == project_id).first()
    if not ps or not ps.style_rules_json:
        return {"style_rules": None, "world_summary": None, "scene_split_threshold": 600}
    return {
        "style_rules": _json.loads(ps.style_rules_json) if ps.style_rules_json else None,
        "world_summary": ps.world_summary,
        "scene_split_threshold": ps.scene_split_threshold or 600,
    }


@router.patch("/style-rules")
async def update_style_rules(
    request: Request,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """프로젝트 스타일 규칙 수정."""
    from app.models.project import ProjectSettings
    import json as _json
    from datetime import datetime, timezone

    ps = db.query(ProjectSettings).filter(ProjectSettings.project_id == project_id).first()
    body = await request.json()

    if not ps:
        ps = ProjectSettings(
            id=str(__import__("uuid").uuid4()),
            project_id=project_id,
            updated_at=datetime.now(timezone.utc).isoformat(),
        )
        db.add(ps)

    if "style_rules" in body:
        ps.style_rules_json = _json.dumps(body["style_rules"], ensure_ascii=False)
    if "world_summary" in body:
        ps.world_summary = body["world_summary"]
    if "scene_split_threshold" in body:
        ps.scene_split_threshold = int(body["scene_split_threshold"])
    ps.updated_at = datetime.now(timezone.utc).isoformat()
    db.commit()

    return {
        "style_rules": _json.loads(ps.style_rules_json) if ps.style_rules_json else None,
        "world_summary": ps.world_summary,
        "scene_split_threshold": ps.scene_split_threshold or 600,
    }


@router.get("/character-outlooks")
def list_character_outlooks(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """인물+아웃룩 조합 목록 반환."""
    from app.models.project import CharacterOutlook, EntityCanon

    combos = (
        db.query(CharacterOutlook)
        .filter(CharacterOutlook.project_id == project_id)
        .all()
    )
    # Bulk load all referenced entities
    all_ids = {co.character_id for co in combos} | {co.outlook_id for co in combos}
    ent_map = {}
    if all_ids:
        ents = db.query(EntityCanon).filter(EntityCanon.id.in_(all_ids)).all()
        ent_map = {e.id: e for e in ents}

    result = []
    for co in combos:
        char_ent = ent_map.get(co.character_id)
        outlook_ent = ent_map.get(co.outlook_id)
        if char_ent and outlook_ent:
            result.append({
                "character_name": char_ent.name,
                "character_id": char_ent.id,
                "outlook_name": outlook_ent.name,
                "outlook_id": outlook_ent.id,
            })
    return result
