"""Phase Q-2: 제약 기반 블록 선택 엔진.

relation_type → 블록 카테고리 결정론적 매핑 + 컨테이너 제약 필터링 + catalog 검증.
AI에게 불가능한 선택지를 주지 않는다 (Beautiful.ai 원칙).

주요 함수:
- select_block_candidates(): topic + 컨테이너 → 물리적으로 가능한 후보 2-4개
- load_catalog(): catalog.yaml 로딩 + 캐싱
"""
from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import yaml

from src.space_allocator import ContainerSpec, HEIGHT_COST_ORDER

logger = logging.getLogger(__name__)

CATALOG_PATH = Path("templates/catalog.yaml")
_catalog_cache: dict | None = None
_catalog_mtime: float = 0.0


# ──────────────────────────────────────
# relation_type → 블록 카테고리 매핑 (Napkin.ai 방식)
# ──────────────────────────────────────
RELATION_TO_CATEGORIES: dict[str, list[str]] = {
    "hierarchy":    ["visuals"],
    "inclusion":    ["visuals"],
    "comparison":   ["tables", "emphasis", "visuals"],
    "sequence":     ["visuals", "cards"],
    "cause_effect": ["emphasis"],
    "definition":   ["cards"],
    "none":         ["emphasis", "cards"],
}

# sidebar에 배치할 수 없는 카테고리
SIDEBAR_FORBIDDEN_CATEGORIES = {"visuals", "media"}

# 블록이 콘텐츠 형태 변환을 강제하는 경우 (원문 보존도 저하)
# key: block_id, value: 강제하는 형태
# 원문이 서술형인데 이 블록이 선택되면 재작성이 불가피
BLOCKS_FORCING_FORMAT_CHANGE = {
    "quote-question",   # question 슬롯 필수 → 서술형 원문을 질문으로 변환 강제
}

# zone: full-width-only 블록은 sidebar에 배치 불가
# (catalog.yaml의 zone 필드로도 관리)


# ──────────────────────────────────────
# catalog.yaml 로딩 (mtime 캐시)
# ──────────────────────────────────────
def load_catalog() -> dict:
    """catalog.yaml을 로딩한다. mtime 기반 캐싱."""
    global _catalog_cache, _catalog_mtime

    if not CATALOG_PATH.exists():
        logger.error(f"catalog.yaml 미발견: {CATALOG_PATH}")
        return {"blocks": []}

    current_mtime = CATALOG_PATH.stat().st_mtime
    if _catalog_cache is not None and current_mtime == _catalog_mtime:
        return _catalog_cache

    with open(CATALOG_PATH, encoding="utf-8") as f:
        _catalog_cache = yaml.safe_load(f)
    _catalog_mtime = current_mtime

    block_count = len(_catalog_cache.get("blocks", []))
    logger.info(f"[Q-2] catalog.yaml 로딩: {block_count}개 블록")
    return _catalog_cache


def _get_block_by_id(block_id: str, catalog: dict) -> dict | None:
    """catalog에서 블록 ID로 검색."""
    for block in catalog.get("blocks", []):
        if block.get("id") == block_id:
            return block
    return None


# ──────────────────────────────────────
# 핵심: 블록 후보 선택
# ──────────────────────────────────────
def select_block_candidates(
    topic: dict[str, Any],
    container_spec: ContainerSpec,
    used_blocks: set[str],
    catalog: dict | None = None,
) -> list[dict]:
    """topic + 컨테이너 → 물리적으로 가능한 블록 후보를 결정론적으로 필터링.

    AI 호출 없음. 결과는 보통 2-4개.

    Args:
        topic: {"id", "title", "purpose", "relation_type", ...}
        container_spec: 이 topic이 속한 컨테이너
        used_blocks: 슬라이드 내 이미 사용된 블록 ID 집합
        catalog: catalog.yaml 딕셔너리 (None이면 자동 로딩)

    Returns:
        [{"id": "venn-diagram", "category": "visuals", "min_height_px": 300, ...}, ...]
    """
    if catalog is None:
        catalog = load_catalog()

    relation_type = topic.get("relation_type", "none")
    categories = RELATION_TO_CATEGORIES.get(relation_type, ["emphasis", "cards"])

    # topic당 가용 높이
    topic_count_in_container = max(1, len(container_spec.topic_ids))
    per_topic_px = container_spec.height_px // topic_count_in_container

    candidates = []

    for block in catalog.get("blocks", []):
        block_id = block.get("id", "")
        block_category = block.get("category", "")

        # ── 필터 1: 카테고리 매칭 ──
        if block_category not in categories:
            continue

        # ── 필터 2: headers 제외 (headers는 슬라이드 제목용) ──
        if block_category == "headers":
            continue

        # ── 필터 3: 최소 생존 크기 (10% tolerance) ──
        # 7px 차이로 가장 적합한 블록이 탈락하는 것을 방지
        min_height = block.get("min_height_px", 0)
        min_height_with_tolerance = min_height * 0.9
        if min_height_with_tolerance > per_topic_px:
            continue

        # ── 필터 4: height_cost 범위 ──
        block_cost = block.get("height_cost", "medium")
        if HEIGHT_COST_ORDER.get(block_cost, 1) > HEIGHT_COST_ORDER.get(container_spec.max_height_cost, 3):
            continue

        # ── 필터 5: sidebar 제한 ──
        if container_spec.zone == "sidebar":
            if block_category in SIDEBAR_FORBIDDEN_CATEGORIES:
                continue
            if block.get("zone") == "full-width-only":
                continue

        # ── 필터 6: full-width-only 블록은 body/sidebar 나뉜 프리셋에서 body에만 ──
        if block.get("zone") == "full-width-only" and container_spec.zone == "sidebar":
            continue

        # ── 필터 7: 중복 사용 제한 ──
        if block_id in used_blocks:
            continue

        # ── 필터 8 (Phase Q fix): 형태 변환 강제 블록 제외 ──
        # 원문 보존이 중요하므로, 콘텐츠 형태를 강제로 바꾸는 블록은 제외
        if block_id in BLOCKS_FORCING_FORMAT_CHANGE:
            continue

        # ── 필터 9: relation_types 명시적 매칭 ──
        # relation_types가 명시되어 있고 현재 relation_type이 포함 안 되면 제외
        # (deprioritize가 아니라 exclude — 의미 왜곡 방지)
        # 예: process-horizontal(sequence)은 hierarchy 콘텐츠에서 제외
        block_relations = block.get("relation_types", [])
        if block_relations and relation_type not in block_relations:
            continue

        # Phase R: 블록에 available variants 정보 첨부
        variants = block.get("variants", [])
        if variants:
            block["_available_variants"] = variants
        else:
            block["_available_variants"] = [{"id": "default", "description": "기본"}]

        candidates.append(block)

    logger.info(
        f"[Q-2] topic {topic.get('id')} (relation={relation_type}, "
        f"container={per_topic_px}px): {len(candidates)}개 후보 "
        f"[{', '.join(c['id'] for c in candidates[:5])}]"
    )

    return candidates


# ──────────────────────────────────────
# 폴백: 카테고리 제한 없이 검색
# ──────────────────────────────────────
def select_fallback_candidates(
    container_spec: ContainerSpec,
    used_blocks: set[str],
    catalog: dict | None = None,
) -> list[dict]:
    """relation_type 매핑에서 후보가 없을 때, 물리적 제약만으로 검색.

    최소 크기 + height_cost + zone만 검사. 카테고리 무시.
    """
    if catalog is None:
        catalog = load_catalog()

    topic_count = max(1, len(container_spec.topic_ids))
    per_topic_px = container_spec.height_px // topic_count

    candidates = []
    for block in catalog.get("blocks", []):
        block_id = block.get("id", "")
        if block.get("category") == "headers":
            continue
        if block.get("min_height_px", 0) > per_topic_px:
            continue
        if HEIGHT_COST_ORDER.get(block.get("height_cost", "medium"), 1) > HEIGHT_COST_ORDER.get(container_spec.max_height_cost, 3):
            continue
        if container_spec.zone == "sidebar" and block.get("zone") == "full-width-only":
            continue
        if block_id in used_blocks:
            continue
        candidates.append(block)

    # compact 블록 우선 (작은 컨테이너에 적합)
    cost_order = {"compact": 0, "medium": 1, "large": 2, "xlarge": 3}
    candidates.sort(key=lambda c: cost_order.get(c.get("height_cost", "medium"), 1))

    logger.info(
        f"[Q-2 fallback] container={per_topic_px}px: {len(candidates)}개 후보"
    )
    return candidates


# ──────────────────────────────────────
# 후보 설명 텍스트 생성 (Kei 프롬프트용)
# ──────────────────────────────────────
def format_candidates_for_prompt(
    candidates: list[dict],
    budget: dict | None = None,
) -> str:
    """블록 후보 목록을 Kei 프롬프트에 포함할 텍스트로 포맷. Phase R: variant 정보 포함."""
    lines = []
    for i, c in enumerate(candidates[:5], 1):  # 최대 5개
        block_id = c.get("id", "")
        name = c.get("name", "")
        visual = c.get("visual", "")[:80]  # 80자로 축약
        height_cost = c.get("height_cost", "")
        when = c.get("when", "")[:60]

        budget_info = ""
        if budget and block_id in budget:
            b = budget[block_id]
            budget_info = f" | 예산: 최대 {b['max_items']}항목, 총 {b['total_chars']}자"

        # Phase R: variant 정보
        variants = c.get("_available_variants", [])
        variant_lines = ""
        if len(variants) > 1:  # default만 있으면 표시 안 함
            v_descs = [f"       - {v['id']}: {v.get('description', '')}" for v in variants]
            variant_lines = "\n     변형:\n" + "\n".join(v_descs)

        lines.append(
            f"  {i}. {block_id} ({name})\n"
            f"     시각: {visual}\n"
            f"     적합: {when}\n"
            f"     크기: {height_cost}{budget_info}{variant_lines}"
        )
    return "\n".join(lines)
