"""아웃룩 중복 제거 — 씬 내 마커 중복 + 유사 아웃룩 병합.

1. 씬 내 동일 [[인물]+[아웃룩]] 마커 중복 제거
2. GPT로 유사 아웃룩 판별 → 병합 (완전히 같은 것만)
"""

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

from app.modules.llm.llm_client import call_structured

logger = logging.getLogger(__name__)

# ── 1. 씬 내 마커 중복 제거 (LLM 불필요) ──

def dedup_scene_markers(scenes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """각 씬의 visible_entities와 T2I 프롬프트에서 동일한 [인물]+[아웃룩] 중복 제거.

    LLM 호출 없음 — 단순 문자열 매칭.
    """
    fixed_count = 0
    for scene in scenes:
        # visible_entities 중복 제거 (short_id 또는 entity_name 기반)
        vis = scene.get("visible_entities", [])
        seen = set()
        deduped_vis = []
        for v in vis:
            key = v.get("short_id") or f"{v.get('entity_name', '')}|{v.get('entity_type', '')}"
            if key not in seen:
                seen.add(key)
                deduped_vis.append(v)
        if len(deduped_vis) < len(vis):
            removed = len(vis) - len(deduped_vis)
            logger.info("Scene %d: removed %d duplicate visible_entities", scene.get("scene_index", 0), removed)
            scene["visible_entities"] = deduped_vis
            fixed_count += removed

        # T2I 프롬프트 내 동일 마커 중복 제거
        for key in ["t2i_prompt"]:
            prompt = scene.get(key, "")
            if not prompt:
                continue
            scene[key] = _dedup_markers_in_text(prompt)

        variations = scene.get("t2i_variations", [])
        for var in variations:
            t2i = var.get("t2i_prompt", "")
            if t2i:
                var["t2i_prompt"] = _dedup_markers_in_text(t2i)

    logger.info("Marker dedup: fixed %d duplicates across %d scenes", fixed_count, len(scenes))
    return scenes


def _dedup_markers_in_text(text: str) -> str:
    """텍스트 내 동일 마커가 2번 이상 나오면 첫 번째만 유지.

    short_id(C01O02) + 레거시([[X]+[Y]]) 동시 지원.
    """
    seen: Set[str] = set()

    # 1) short_id 패턴: C01O02
    def _replace_sid(m):
        key = m.group(0)
        if key in seen:
            return ""
        seen.add(key)
        return key

    result = re.sub(r'C\d{2,3}O\d{2,3}', _replace_sid, text)

    # 2) 레거시 [[X]+[Y]]
    def _replace_legacy(m):
        key = f"{m.group(1)}+{m.group(2)}"
        if key in seen:
            return ""
        seen.add(key)
        return m.group(0)

    result = re.compile(r'\[\[([^\]]+)\]\+\[([^\]]+)\]\]').sub(_replace_legacy, result)
    return re.sub(r'\s{2,}', ' ', result).strip()


# ── 2. 유사 아웃룩 판별 + 병합 (GPT 사용) ──

MERGE_SCHEMA = {
    "type": "object",
    "properties": {
        "merge_groups": {
            "type": "array",
            "description": "병합해야 하는 아웃룩 그룹들. 각 그룹은 완전히 같은 의상을 가리키는 이름들.",
            "items": {
                "type": "object",
                "properties": {
                    "keep": {"type": "string", "description": "유지할 아웃룩 이름 (대표)"},
                    "remove": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "제거할 아웃룩 이름들 (keep과 완전히 같은 의상)",
                    },
                    "reason": {"type": "string", "description": "같다고 판단한 이유"},
                },
                "required": ["keep", "remove", "reason"],
                "additionalProperties": False,
            },
        },
    },
    "required": ["merge_groups"],
    "additionalProperties": False,
}


def find_duplicate_outlooks(
    outlooks: List[Dict[str, str]],
    project_config: Optional[Dict] = None,
) -> List[Dict[str, Any]]:
    """GPT로 유사 아웃룩 판별. 완전히 같은 의상만 병합 대상으로 반환.

    Args:
        outlooks: [{name, description}]

    Returns:
        [{keep, remove: [], reason}]
    """
    if len(outlooks) < 2:
        return []

    # Claude Review Minor #11: CLAUDE.md 절대 규칙 — LLM에 전달하는 데이터 자르지 않음.
    # 의상 설명이 길어도 병합 판별 정확도를 위해 전문 전달.
    outlook_list = "\n".join(
        f"- {o['name']}: {o['description']}" for o in outlooks
    )

    prompt = f"""아래 아웃룩(의상) 목록에서 **완전히 같은 의상**인데 이름만 다른 것을 찾으세요.

규칙:
- 이름이 다르지만 설명이 거의 동일한 것만 병합 대상
- 비슷하지만 다른 의상은 병합하지 마세요 (예: 이름이 비슷한 두 의상이라도 설명이 다르면 다른 의상)
- 확실한 경우만 병합 — 애매하면 병합하지 마세요
- merge_groups가 비어있으면 빈 배열 반환

[아웃룩 목록]
{outlook_list}"""

    try:
        result = call_structured(
            step="entity_review",  # GPT 사용
            system_prompt="의상 전문가. 같은 의상인데 이름만 다른 것을 정확히 식별한다. 애매하면 병합하지 않는다.",
            user_prompt=prompt,
            response_schema=MERGE_SCHEMA,
            project_config=project_config,
            schema_name="outlook_merge",
        )
        groups = result.get("merge_groups", [])
        if groups:
            logger.info("Found %d duplicate outlook groups", len(groups))
            for g in groups:
                logger.info("  Merge: keep=%s, remove=%s, reason=%s", g["keep"], g["remove"], g["reason"])
        return groups
    except Exception as exc:
        logger.warning("Outlook dedup failed: %s", exc)
        return []


def _resolve_outlook_id(db, project_id: str, name: str) -> Optional[str]:
    """project 범위 내 outlook 이름으로 EntityCanon.id 조회."""
    from sqlalchemy import text

    row = db.execute(
        text(
            "SELECT id FROM entity_canon "
            "WHERE project_id = :pid AND name = :name AND entity_type = 'outlook'"
        ),
        {"pid": project_id, "name": name},
    ).fetchone()
    return row[0] if row else None


def _reassign_character_outlook(db, project_id: str, keep_id: str, remove_id: str) -> None:
    """CharacterOutlook: remove_id 참조 → keep_id (character별 중복 시 skip-DELETE).

    (project_id, character_id, outlook_id) 조합의 논리적 unique를 유지.
    """
    from sqlalchemy import text

    existing = db.execute(
        text(
            "SELECT character_id FROM character_outlook "
            "WHERE outlook_id = :keep_id AND project_id = :pid"
        ),
        {"keep_id": keep_id, "pid": project_id},
    ).fetchall()
    existing_chars = {r[0] for r in existing}

    remove_combos = db.execute(
        text(
            "SELECT id, character_id FROM character_outlook "
            "WHERE outlook_id = :remove_id AND project_id = :pid"
        ),
        {"remove_id": remove_id, "pid": project_id},
    ).fetchall()

    for combo in remove_combos:
        if combo[1] in existing_chars:
            db.execute(
                text("DELETE FROM character_outlook WHERE id = :id"),
                {"id": combo[0]},
            )
        else:
            db.execute(
                text("UPDATE character_outlook SET outlook_id = :keep_id WHERE id = :id"),
                {"keep_id": keep_id, "id": combo[0]},
            )


def _reassign_image_assets(db, project_id: str, keep_id: str, remove_id: str) -> None:
    """ImageAsset: remove 참조를 keep으로 이관. keep에 이미 이미지 있으면 remove DELETE.

    asset_type='reference' 기준 — scene 이미지(asset_type='scene')는 still_id 기반이라 무관.
    """
    from sqlalchemy import text

    keep_has_images = db.execute(
        text(
            "SELECT COUNT(*) FROM image_asset "
            "WHERE entity_id = :eid AND project_id = :pid AND asset_type = 'reference'"
        ),
        {"eid": keep_id, "pid": project_id},
    ).scalar()

    if keep_has_images and keep_has_images > 0:
        db.execute(
            text(
                "DELETE FROM image_asset "
                "WHERE entity_id = :remove_id AND project_id = :pid AND asset_type = 'reference'"
            ),
            {"remove_id": remove_id, "pid": project_id},
        )
    else:
        db.execute(
            text(
                "UPDATE image_asset SET entity_id = :keep_id "
                "WHERE entity_id = :remove_id AND project_id = :pid AND asset_type = 'reference'"
            ),
            {"keep_id": keep_id, "remove_id": remove_id, "pid": project_id},
        )


def _rewrite_scene_still_references(
    db, project_id: str, keep_name: str, remove_name: str
) -> None:
    """SceneStill JSON 필드(visible_entities / t2i_variations / t2i_prompt_cinematic /
    t2i_prompt_closeup)에서 remove_name → keep_name으로 교체. 마커는 `[name]` 패턴만 치환.

    Codex Review Important #3: `t2i_prompt_closeup`도 rewrite 대상에 포함.
    image_service.py의 closeup 생성 경로가 이 필드를 읽기 때문.
    """
    from sqlalchemy import text

    stills = db.execute(
        text(
            "SELECT id, visible_entities_json, t2i_variations_json, "
            "       t2i_prompt_cinematic, t2i_prompt_closeup "
            "FROM scene_still WHERE project_id = :pid"
        ),
        {"pid": project_id},
    ).fetchall()

    remove_marker = f"[{remove_name}]"
    keep_marker = f"[{keep_name}]"

    for still in stills:
        changed = False

        vis_data = json.loads(still[1] or "[]")
        for item in vis_data:
            if item.get("entity_name") == remove_name:
                item["entity_name"] = keep_name
                changed = True
        vis_json = json.dumps(vis_data, ensure_ascii=False)

        vars_data = json.loads(still[2] or "[]")
        for var in vars_data:
            t2i_v = var.get("t2i_prompt", "")
            if remove_marker in t2i_v:
                var["t2i_prompt"] = t2i_v.replace(remove_marker, keep_marker)
                changed = True
        vars_json = json.dumps(vars_data, ensure_ascii=False)

        t2i_cin = still[3] or ""
        if remove_marker in t2i_cin:
            t2i_cin = t2i_cin.replace(remove_marker, keep_marker)
            changed = True

        t2i_close = still[4] or ""
        if remove_marker in t2i_close:
            t2i_close = t2i_close.replace(remove_marker, keep_marker)
            changed = True

        if changed:
            db.execute(
                text(
                    "UPDATE scene_still SET visible_entities_json = :vis, "
                    "t2i_variations_json = :vars, "
                    "t2i_prompt_cinematic = :t2i_cin, "
                    "t2i_prompt_closeup = :t2i_close "
                    "WHERE id = :id"
                ),
                {
                    "vis": vis_json,
                    "vars": vars_json,
                    "t2i_cin": t2i_cin,
                    "t2i_close": t2i_close,
                    "id": still[0],
                },
            )


def _migrate_entity_aliases(db, keep_id: str, remove_id: str) -> None:
    """EntityAlias: remove의 alias를 keep으로 이관 (UPSERT).

    Phase 4.3: 기존은 DELETE만 했음 → alias 유실 위험.
    unique (canon_id, alias) 기반: keep에 이미 있으면 remove의 중복 행만 DELETE,
    없으면 canon_id를 keep으로 UPDATE.

    Note: `entity_alias` 테이블에 `project_id` 컬럼 없음 — `canon_id` 기반 격리.
    호출자가 keep_id/remove_id를 project_id 스코프로 이미 검증함.
    """
    from sqlalchemy import text

    existing_rows = db.execute(
        text("SELECT alias FROM entity_alias WHERE canon_id = :kid"),
        {"kid": keep_id},
    ).fetchall()
    keep_aliases = {r[0] for r in existing_rows}

    remove_rows = db.execute(
        text("SELECT id, alias FROM entity_alias WHERE canon_id = :rid"),
        {"rid": remove_id},
    ).fetchall()

    for row in remove_rows:
        row_id, alias_value = row[0], row[1]
        if alias_value in keep_aliases:
            db.execute(
                text("DELETE FROM entity_alias WHERE id = :id"),
                {"id": row_id},
            )
        else:
            db.execute(
                text("UPDATE entity_alias SET canon_id = :kid WHERE id = :id"),
                {"kid": keep_id, "id": row_id},
            )
            keep_aliases.add(alias_value)


def _migrate_entity_episode_links(
    db, project_id: str, keep_id: str, remove_id: str
) -> None:
    """EntityEpisodeLink: remove의 episode별 link를 keep으로 이관.

    unique (canon_id, episode_id) 기반.
    keep에 이미 동일 episode link가 있으면 t2i_appearance_count를 합산 후 remove DELETE.
    없으면 canon_id를 keep으로 UPDATE.

    Note: remove_rows에 같은 episode_id가 2번 등장할 수는 없음 — `(canon_id, episode_id)`
    unique constraint가 remove_id에도 걸려 있기 때문. 따라서 keep_map[eid] 갱신 없이도
    합산 정확. (Claude Review Important #2)
    """
    from sqlalchemy import text

    keep_rows = db.execute(
        text(
            "SELECT episode_id, t2i_appearance_count FROM entity_episode_link "
            "WHERE canon_id = :kid AND project_id = :pid"
        ),
        {"kid": keep_id, "pid": project_id},
    ).fetchall()
    keep_map = {r[0]: r[1] or 0 for r in keep_rows}

    remove_rows = db.execute(
        text(
            "SELECT id, episode_id, t2i_appearance_count FROM entity_episode_link "
            "WHERE canon_id = :rid AND project_id = :pid"
        ),
        {"rid": remove_id, "pid": project_id},
    ).fetchall()

    for row in remove_rows:
        row_id, eid, count = row[0], row[1], row[2] or 0
        if eid in keep_map:
            merged = keep_map[eid] + count
            db.execute(
                text(
                    "UPDATE entity_episode_link SET t2i_appearance_count = :cnt "
                    "WHERE canon_id = :kid AND episode_id = :eid"
                ),
                {"cnt": merged, "kid": keep_id, "eid": eid},
            )
            db.execute(
                text("DELETE FROM entity_episode_link WHERE id = :id"),
                {"id": row_id},
            )
        else:
            db.execute(
                text("UPDATE entity_episode_link SET canon_id = :kid WHERE id = :id"),
                {"kid": keep_id, "id": row_id},
            )
            keep_map[eid] = count


def _migrate_relation_participants(db, keep_id: str, remove_id: str) -> None:
    """RelationParticipant: canon_id를 remove → keep으로 UPDATE.

    Phase 4.3: 기존은 DELETE만 했음 → visual_variant 등 outlook 관계 유실 위험.
    RelationParticipant에 (relation_id, canon_id) unique 없음.

    UPDATE 후 동일 `relation_id` 내에서 keep_id가 2번 등장하는 경우가 생길 수 있음.
    dedup는 `(relation_id, canon_id, participant_role)` 기준 — role이 다르면 각각
    유지하여 base/variant 양쪽이 keep인 self-relation은 보존 후 별도 처리 필요.
    (Codex Review Important #2: role 무시 dedup 시 relation 의미 파괴)

    role이 같은 동일 canon_id 중복(예: 두 행 모두 `base`)만 최소 id 보존, 나머지 제거.
    role이 달라 `base`/`variant`가 모두 keep으로 수렴한 self-collapsed relation은
    사후 cleanup에서 별도 정책으로 처리 예정.

    Note: 호출자 `_merge_single_outlook`에서 keep_id/remove_id가 이미 `project_id`
    스코프로 resolve되므로 이 헬퍼는 project_id 파라미터 없이도 정확히 동작.
    (Claude Review Critical #1)
    """
    from sqlalchemy import text

    db.execute(
        text("UPDATE relation_participant SET canon_id = :kid WHERE canon_id = :rid"),
        {"kid": keep_id, "rid": remove_id},
    )

    # 동일 (relation_id, canon_id, participant_role) 중복만 제거 — role이 다르면
    # relation 의미가 살아있으므로 유지 (Codex Important #2)
    db.execute(
        text(
            "DELETE FROM relation_participant WHERE id NOT IN ("
            "  SELECT min_id FROM ("
            "    SELECT MIN(id) AS min_id FROM relation_participant "
            "    WHERE canon_id = :kid "
            "    GROUP BY relation_id, canon_id, participant_role"
            "  ) AS survivors"
            ") AND canon_id = :kid"
        ),
        {"kid": keep_id},
    )


def _merge_single_outlook(
    db, project_id: str, keep_name: str, remove_name: str
) -> bool:
    """한 쌍의 outlook 병합 (keep ← remove). remove는 완전히 제거.

    단일 트랜잭션 내에서 호출된다고 가정 (호출자가 commit/rollback 관리).
    """
    from sqlalchemy import text

    keep_id = _resolve_outlook_id(db, project_id, keep_name)
    remove_id = _resolve_outlook_id(db, project_id, remove_name)

    if not keep_id or not remove_id:
        logger.warning(
            "Merge skip: keep=%s(%s) remove=%s(%s)",
            keep_name,
            keep_id,
            remove_name,
            remove_id,
        )
        return False

    # Codex Review Critical #1: self-merge 가드. keep_id == remove_id면 remove 측
    # cleanup이 keep 자신을 삭제하여 alias/outlook/image 참조 유실.
    # name 동일이거나 동명이인 canon(project 내 unique 없음)에서 fetchone이 같은 행을
    # 두 번 반환하는 경우 발생 가능.
    if keep_id == remove_id:
        logger.warning(
            "Self-merge skip: keep과 remove가 동일 entity_canon.id=%s (keep=%s, remove=%s)",
            keep_id,
            keep_name,
            remove_name,
        )
        return False

    _reassign_character_outlook(db, project_id, keep_id, remove_id)
    _reassign_image_assets(db, project_id, keep_id, remove_id)
    _rewrite_scene_still_references(db, project_id, keep_name, remove_name)
    _migrate_entity_episode_links(db, project_id, keep_id, remove_id)
    _migrate_entity_aliases(db, keep_id, remove_id)
    _migrate_relation_participants(db, keep_id, remove_id)

    # 마지막으로 EntityCanon 제거 (모든 FK 참조가 선행 migration에서 해소됨)
    db.execute(
        text("DELETE FROM entity_canon WHERE id = :id AND project_id = :pid"),
        {"id": remove_id, "pid": project_id},
    )

    logger.info("Merged outlook: %s → %s", remove_name, keep_name)
    return True


def apply_outlook_merge(
    db,
    project_id: str,
    merge_groups: List[Dict[str, Any]],
) -> int:
    """병합 그룹 적용 — DB에서 중복 아웃룩을 keep으로 수렴 후 remove 완전 제거.

    Phase 4.3: EntityAlias / RelationParticipant / EntityEpisodeLink가 기존에
    DELETE-only 이던 것을 UPSERT 이관으로 전환 (alias / outlook 관계 / appearance_count
    유실 방지). EntityCanon 본체만 최종 DELETE.

    Returns: 제거된 아웃룩 수
    """
    removed_total = 0
    try:
        for group in merge_groups:
            keep_name = group["keep"]
            remove_names = group.get("remove", [])
            for remove_name in remove_names:
                if _merge_single_outlook(db, project_id, keep_name, remove_name):
                    removed_total += 1
        db.commit()
    except Exception as exc:
        db.rollback()
        logger.error("Outlook merge failed, rolled back: %s", exc)
        raise
    return removed_total
