"""节点级兜底校验工具。

设计目标：LangGraph 流水线里每个节点都必须"失败不影响整体"。
提供两类兜底：
1. with_fallback(node_name)：装饰器，节点函数抛异常时捕获，把错误写入 state['errors']，
   并返回最小更新（不破坏其它字段），整图继续往下走。
2. 数据校验函数：validate_rows / validate_brief，对节点产出的数据进行结构校验，
   剔除非法记录并记录原因，保证下游拿到的数据"形状正确"。
"""
import functools
import traceback
from typing import Any, Dict, List


def with_fallback(node_name: str):
    """装饰器：捕获节点异常，转为 state['errors'] 中的一条记录，返回空更新。

    节点内部仍建议自己做精细兜底（降级/默认），with_fallback 是最后一道保险：
    任何未预料的异常都不会让整张图中断。
    """

    def deco(fn):
        @functools.wraps(fn)
        def wrapper(state: Dict[str, Any]):
            try:
                return fn(state)
            except Exception as e:  # noqa: BLE001
                tb = traceback.format_exc(limit=3)
                err = {
                    "node": node_name,
                    "type": type(e).__name__,
                    "message": str(e)[:300],
                    "trace": tb[-400:],
                }
                errors = list(state.get("errors") or [])
                errors.append(err)
                # 只更新 errors，其它字段保持上一节点结果，下游继续
                return {"errors": errors}

        return wrapper

    return deco


def validate_rows(rows: List[Dict[str, Any]], node: str) -> List[Dict[str, Any]]:
    """校验抓取/过滤后的行结构，剔除缺 topic 或非法记录，返回干净列表。

    同时保证每个 row 至少含 country/topic/source/kind/raw_score，缺失时给默认。
    """
    clean: List[Dict[str, Any]] = []
    dropped = 0
    for r in rows or []:
        if not isinstance(r, dict):
            dropped += 1
            continue
        topic = (r.get("topic") or "").strip()
        if not topic:
            dropped += 1
            continue
        r.setdefault("country", "")
        r.setdefault("source", "unknown")
        r.setdefault("kind", "unknown")
        r.setdefault("raw_score", 0.0)
        if r.get("raw_score") is None:
            r["raw_score"] = 0.0
        clean.append(r)
    if dropped:
        # 简单记录到返回数据的副作用里（调用方会再汇总到 stats）
        pass
    return clean


def validate_brief(b: Dict[str, Any]) -> Dict[str, Any]:
    """校验单条设计简报结构，补齐缺失字段，保证下游 compose 不会因缺键崩溃。"""
    b = dict(b)
    b.setdefault("topic", "")
    b.setdefault("country", "")
    b.setdefault("design_category", "Niche")
    b.setdefault("risk_level", "safe")
    b.setdefault("motif", b.get("topic", ""))
    b.setdefault("art_style", "clean vector illustration")
    b.setdefault("color_palette", "balanced modern palette")
    b.setdefault("composition", "centered emblem with balanced negative space")
    b.setdefault("concept", b.get("topic", ""))
    b.setdefault("negative_prompt", "")
    b.setdefault("image_prompt", "")
    b.setdefault("wearable_prompt", "")
    b.setdefault("composite_prompt", "")
    b.setdefault("composite_negative", "")
    return b


def safe_get(state: Dict[str, Any], key: str, default=None):
    return state.get(key, default)
