"""
template-registry 库模块 — DSL → TemplateBinding 的纯函数实现。

仅作为 Python library 被 render-video 的 render_video.py / gen-script 的
gen_script.py 通过 `sys.path.insert` 后 `import match_template` 引入；
对外不暴露 binding CLI。template-registry skill 的 `--list-templates` 入口由
sibling 的 `list_templates.py` 实现（它复用 registry_loader 的统一加载 +
状态门控），两者共享同一份 registry 读取逻辑，无 TS 侧重复实现。

提供的公共接口：
  load_registry()                 → list[dict]   模板列表
  load_full_template(template)    → dict         模板完整定义（registry 自带，原样返回）
  build_binding(template, dsl)    → dict         DSL → TemplateBinding
"""

from registry_loader import load_registry_data
import sys


def load_registry() -> list:
    """Load template list via the unified registry loader."""
    return load_registry_data().get("templates", [])


def load_full_template(template: dict) -> dict:
    """Load the full template definition.

    Since the metadata registry.json now contains the complete template
    definition (including slotMapping, compositions, etc.), this simply
    returns the template as-is.
    """
    return template


def _resolve_slot_def(slot_mapping: dict, purpose: str) -> dict:
    """Resolve a slotMapping entry, following $ref chains (max depth 10)."""
    slot_def = slot_mapping.get(purpose) or slot_mapping.get("default") or {}
    visited: set = set()
    while "$ref" in slot_def:
        ref = slot_def["$ref"]
        if ref in visited:
            break
        visited.add(ref)
        slot_def = slot_mapping.get(ref) or {}
    return slot_def


def _extract_prop(scene: dict, extractor: dict):
    """Extract a single prop value from a scene using an extractor definition.

    Supports two extractor patterns:
      { "from": "textLayers", "role": "headline" }   – search textLayers by role
      { "from": "visuals.background.assetRef" }      – dotted-path traversal
    """
    source = extractor.get("from", "")

    # textLayers extractor
    if source == "textLayers":
        role = extractor.get("role", "")
        for tl in scene.get("textLayers") or []:
            if tl.get("role") == role:
                return tl.get("content")
        return None

    # Dotted-path extractor
    val = scene
    for part in source.split("."):
        if not isinstance(val, dict):
            return None
        val = val.get(part)
    return val


def build_binding(template: dict, dsl: dict) -> dict:
    """Build a TemplateBinding from template and DSL."""
    bindings = []
    slot_mapping = template.get("slotMapping", {})

    for idx, scene in enumerate(dsl.get("scenes", [])):
        scene_id = scene.get("id") or scene.get("sceneId") or f"scene-{idx:03d}"
        purpose = scene.get("purpose", "point")

        slot_def = _resolve_slot_def(slot_mapping, purpose)
        comp_id = slot_def.get("compositionId", "GenericScene")
        slot_id = slot_def.get("slotId", f"{purpose}-slot")

        # 用模板的 propExtractors 提取 props
        props = {}
        for prop_name, extractor in slot_def.get("propExtractors", {}).items():
            val = _extract_prop(scene, extractor)
            if val is not None:
                props[prop_name] = val

        # P1.3: props 提取仅保留两条路径——
        #   (1) 显式 propExtractors（上面的 _extract_prop 调用）
        #   (4) customPayload 全字段透传（顶层 + templateData）— 模板内部 schema
        # 去掉了原来的"白名单同名隐式透传"两层 fallback，因为它们让"字段写在哪"
        # 这个问题变得不可预测：同一字段写在 optionalProps + propExtractors
        # 里时优先级会冲突，让 AI 反复出错。
        #
        # 顶层透传是必要的：DSL 示例（hermes-agent-purple / vibe-coding 等）以
        # `customPayload.slideId` 顶层声明 slide id，而 html-slide 模板的
        # `propExtractors.slideId` 只声明从 templateData.slideId 取值。如果不
        # 同时透传顶层，slideId 就会缺失，html-slide 的 `point` slot 把
        # slideId 设为 requiredProps，缺失会导致 React 端回退到通用「标题 +
        # 字幕」兜底样式，所有场景看起来都是同一个布局。
        custom = scene.get("customPayload") or {}
        template_data = custom.get("templateData") or scene.get("templateData") or {}

        # 脆弱点 3：双通道冲突检测。同一字段名既声明在 slot 的 propExtractors，
        # 又被 customPayload 的全字段透传命中时，来源二义：propExtractors 永远赢
        # （下面 `k not in props` 透传），但如果 extractor 返回 None、值又恰好在
        # payload 里，作者会误以为"是 payload 生效"，反之亦然。html-slide 的
        # slideId 事故正是此类——extractor 读 `templateData.slideId`，DSL 却把值
        # 写在 `customPayload.slideId` 顶层。
        #
        # 仅标记**真正的位置冲突**，避开误报：当 extractor 的 `from` 恰好指向
        # `templateData.<同名>` 时，templateData 里出现该字段是它的**法定来源**，
        # 不算"写错位置"，跳过。其余命中（顶层透传、或 templateData 透传但 extractor
        # 从别处取值）才算冲突。只记录 + warning，不改透传行为；CI 的
        # test-template-pipeline.py 据此浮出去重提示。
        extractors = slot_def.get("propExtractors", {}) or {}
        top_keys = {k for k in custom.keys() if k != "templateData"}
        dual_channel = []
        for k in extractors:
            collides_top = k in top_keys
            extractor_from = (extractors.get(k) or {}).get("from", "")
            collides_td = k in template_data and extractor_from != f"templateData.{k}"
            if collides_top or collides_td:
                dual_channel.append(k)
        dual_channel = sorted(dual_channel)

        # 先注入 templateData.* 再注入顶层 customPayload.*，让顶层覆盖同名值
        for k, v in template_data.items():
            if k not in props:
                props[k] = v
        for k, v in custom.items():
            if k == "templateData":
                continue
            if k not in props:
                props[k] = v

        # animationHints 可能为 null
        anim = scene.get("animationHints") or {}
        entrance = anim.get("entrance", "fade")
        transition_type = entrance if entrance != "none" else "cut"

        # requiredProps 完整性：slotMapping 声明的 requiredProps 必须都已进 props。
        # 这里只**记录**缺失（不抛错，保持渲染行为不变）；CI 的
        # test-template-pipeline.py 据此断言「requiredProps 齐全」并 fail loud，
        # 避免历史上「声明了 requiredProps 却静默缺失 → React 端回退通用布局」的事故
        # （见 template-json-design.md 脆弱点 2）。是否升级为渲染前硬失败属后续决策。
        required_props = slot_def.get("requiredProps") or []
        missing_required = [p for p in required_props if p not in props]

        binding_row = {
            "sceneId": scene_id,
            "slotId": slot_id,
            "compositionId": comp_id,
            "layoutVariant": scene.get("layout", "full-visual"),
            "props": props,
            "transitionIn": {"type": transition_type, "durationFrames": 15},
            "transitionOut": {"type": "fade", "durationFrames": 15},
        }
        if missing_required:
            binding_row["missingRequiredProps"] = missing_required
        if dual_channel:
            binding_row["dualChannelProps"] = dual_channel
            print(
                f"⚠️  双通道字段：scene={scene_id!r} slot={slot_id!r} 的 "
                f"{dual_channel} 同时声明在 propExtractors 与 customPayload，"
                f"propExtractors 优先。建议二选一，避免写错位置时静默取错值。",
                file=sys.stderr,
            )
        bindings.append(binding_row)

    # P1.2: 不再做 screen-walkthrough 专属的截断 / 兜底。模板的 propExtractors
    # 已声明 titleText ← textLayers[role=headline]、projectName ← textLayers[role=subheadline]，
    # gen_script 在每个场景都填入了 headline / subheadline，因此 props 由提取器
    # 直接产出，不再需要后处理。

    typography = template.get("defaultTypography", {})
    motion = template.get("defaultMotionPreset", "smooth")
    colors = template.get("defaultColorScheme", [])

    # 用户选中的字体（DSL 的 global.font）。这里只是把不透明 key 折进 typography，
    # 真正的解析（uniqId → 排印家族名 + 字重文件）在 ab-render 侧完成。
    #
    # 优先级见 ab-platform/docs/font-selection-design.md §13.3：
    #   用户选择 > variant.defaultFontId（预留档位，当前恒为空）
    #     > template.defaultTypography.bodyFont（现状裸 family 名）> 模板 theme 字栈
    # 下面的 variant 合并在此之后执行，所以 fontId 单独在合并后再写回，
    # 避免被 variant 的 defaultTypography 覆盖掉。
    user_font_id = ((dsl.get("global", {}) or {}).get("font", {}) or {}).get("fontId")

    # --- variant selection（确定性）---
    # 优先级：
    #   1) DSL 显式声明 — meta.templateVariant / meta.variant / renderHints.templateVariant
    #   2) meta.style 与 variant.styleTags 的**分级**匹配（精确 > 整段包含 > token 子串）
    # 旧实现按 dict 遍历顺序「命中第一个」，多义时结果不稳定；现改为：
    #   - 按 variant id 排序遍历（消除顺序依赖）；
    #   - 取匹配等级最高（tier 数最小）的 variant；
    #   - 同一最高等级有多个候选 → 记歧义、取排序首个，并把理由写进 variantReason。
    variant_id = None
    variant_reason = None
    meta = dsl.get("meta", {}) or {}
    render_hints = dsl.get("renderHints", {}) or {}
    explicit_variant = (
        meta.get("templateVariant")
        or meta.get("variant")
        or render_hints.get("templateVariant")
    )
    variants = template.get("variants", {}) or {}

    if explicit_variant and explicit_variant in variants:
        variant_id = explicit_variant
        variant_reason = "explicit"
    elif variants:
        raw_style = (meta.get("style") or "").strip().lower()
        if raw_style:
            # token 化：整段 + 分隔符拆分 + 单中文字符级
            s = raw_style
            for sep in [",", "，", ";", "；", " ", "/", "·", "、"]:
                s = s.replace(sep, ",")
            tokens = set()
            for tok in s.split(","):
                tok = tok.strip()
                if not tok:
                    continue
                tokens.add(tok)
                for ch in tok:
                    if "\u4e00" <= ch <= "\u9fff":
                        tokens.add(ch)

            # 为每个 variant 算最佳匹配等级（1 最强）
            #   tier 1: 某 style token 与某 tag 完全相等
            #   tier 2: 整段 raw_style 与某 tag 互为子串（整体短语级）
            #   tier 3: 某 token 与某 tag 互为子串（细粒度，最宽松）
            by_tier: dict = {}
            for vid in sorted(variants.keys()):
                vtags = [str(t).strip().lower() for t in (variants[vid].get("styleTags") or []) if str(t).strip()]
                tier = None
                if any(tok == tag for tok in tokens for tag in vtags):
                    tier = 1
                elif any((raw_style in tag) or (tag in raw_style) for tag in vtags):
                    tier = 2
                elif any((tok in tag) or (tag in tok) for tok in tokens for tag in vtags):
                    tier = 3
                if tier is not None:
                    by_tier.setdefault(tier, []).append(vid)

            if by_tier:
                best_tier = min(by_tier.keys())
                winners = by_tier[best_tier]  # 已按 vid 排序
                variant_id = winners[0]
                if len(winners) > 1:
                    variant_reason = (
                        f"style-match tier{best_tier} (ambiguous among {winners}; "
                        f"picked '{variant_id}' by id order — 建议作者改用显式 meta.templateVariant)"
                    )
                    print(
                        f"⚠️  variant 选择歧义：style={meta.get('style')!r} 在 tier{best_tier} "
                        f"命中多个变体 {winners}，确定性取 '{variant_id}'。建议显式声明 meta.templateVariant。",
                        file=sys.stderr,
                    )
                else:
                    variant_reason = f"style-match tier{best_tier}"

    if variant_id and variant_id in variants:
        variant = variants[variant_id]
        vtypo = variant.get("defaultTypography")
        if vtypo:
            typography = {**typography, **vtypo}
        vmotion = variant.get("defaultMotionPreset")
        if vmotion:
            motion = vmotion
        vcolors = variant.get("defaultColorScheme")
        if vcolors:
            colors = vcolors

    # 用户的字体选择压过 variant 默认值（§13.3 的优先级第一档），所以写在合并之后。
    if isinstance(user_font_id, str) and user_font_id.strip():
        typography = {**typography, "fontId": user_font_id.strip()}

    result = {
        "version": "v1alpha1",
        "templateId": template.get("templateId", ""),
        "templateName": template.get("name", ""),
        "globalOverrides": {
            "typography": typography,
            "motionPreset": motion,
            "colorScheme": colors,
        },
        "bindings": bindings,
    }
    if variant_id:
        result["variantId"] = variant_id
        if variant_reason:
            result["variantReason"] = variant_reason
    return result


