"""缓存热点批量产品流程（用户新流程入口）。

流程：
1. 选定国家 → 直接加载最新缓存热点（output/<国家>/design_briefs.json，不重跑种子/抓取）；
   无缓存时回退跑一次完整流水线（run_country）生成缓存。
2. 按 SPU 数量（count）取 N 个「未用过」热点（safe 按分降序，跳过 used_designs.json 里已用的），
   一个款号分配一个热点（spu_tasks 每项绑定 topic）。
3. 调 product_node：每款号用自己热点的 image_prompt 生成设计稿 → 三图合成 → 多模态标题 → 模板导出；
   product_node 内部成功后把 (热点-风格-配色) 写入 used_designs.json 去重。
"""
import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional

from graph.paths import project_root, runtime_root

# 全项目 review 兜底：不适合 T 恤印花的类目关键词（美甲/食谱/彩票/赛果/天气/比分/日程等）
_UNSUITABLE = re.compile(
    r"\b(nails?|manicure|pedicure|recipes?|cooking|lottery|jackpot|results?|score|scores?|"
    r"fixtures?|forecast|weather|temperature|map|directions?|parking|opening hours?|"
    r"prices?|price|reviews?|jobs?|salary|mortgage|council tax|election|referendum|"
    r"stock market|exchange rate|gas prices?)\b",
    re.IGNORECASE,
)


def load_cached_briefs(output_dir: Path) -> List[Dict[str, Any]]:
    """读缓存简报（design_briefs.json，含 image_prompt/composite_prompt 四要素）。"""
    p = output_dir / "design_briefs.json"
    if not p.exists():
        return []
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
        return [b for b in (data.get("design_briefs") or [])
                if b.get("motif") and b.get("image_prompt")]
    except Exception:
        return []


def load_used(output_dir: Path) -> List[Dict[str, Any]]:
    p = output_dir / "used_designs.json"
    if not p.exists():
        return []
    try:
        return json.loads(p.read_text(encoding="utf-8")).get("used", []) or []
    except Exception:
        return []


def fingerprint(b: Dict[str, Any]) -> str:
    """(热点-风格) 指纹，用于去重（不按配色，配色不影响主题唯一性）。"""
    return "|".join(str(b.get(k, "")).strip().lower() for k in ("topic", "art_style"))


def assign_hotspots(briefs: List[Dict[str, Any]], used: List[Dict[str, Any]],
                    count: int, allow_review: bool = False,
                    exclude_topics: Optional[List[str]] = None) -> List[Dict[str, Any]]:
    """三级热点分配（严格 → 放宽 → 兜底）：

    ① 热点去重：只用「没用过」的热点（topic 不在 used_designs）
    ② 风格去重：热点池不够时放宽——同一热点允许换风格（(topic, art_style) 组合未用过）
    ③ 规则匹配：还不够时兜底——全部简报按分数/规则取（允许完全重复，mock 风格）

    allow_review=True（openai 模式，LLM 已安全改写）：review 且带 concept 的也进候选池。
    exclude_topics：不分配黑名单（如地名/美甲/版权剧名/真实人物等，来自国家配置 exclude_topics）。
    每级内部按 score 降序 + 高分池随机（不总取第一个）。"""
    import random
    used_topics = {str(u.get("topic", "")).strip().lower() for u in used}
    used_fp = {fingerprint(u) for u in used}
    safe = sorted([b for b in briefs if b.get("risk_level") == "safe"],
                  key=lambda b: -(b.get("score") or 0))
    pool = list(safe)
    if allow_review:
        reviewed = [b for b in briefs if b.get("risk_level") == "review" and (b.get("concept") or "").strip()]
        pool += sorted(reviewed, key=lambda b: -(b.get("score") or 0))
    # 黑名单过滤：不分配的热点（国家配置 exclude_topics，大小写不敏感）
    if exclude_topics:
        ban = {str(x).strip().lower() for x in exclude_topics if str(x).strip()}
        _before = len(pool)
        pool = [b for b in pool if str(b.get("topic", "")).strip().lower() not in ban]
        if len(pool) < _before:
            print(f"[batch] 黑名单过滤 {_before - len(pool)} 个不分配热点（exclude_topics）")
    # 全项目 review 兜底：剔除不适合 T 恤印花的类目（美甲/食谱/彩票/赛果/天气等通用识别）
    _before2 = len(pool)
    pool = [b for b in pool if not _UNSUITABLE.search(str(b.get("topic", "")))]
    if len(pool) < _before2:
        print(f"[batch] review 兜底剔除 {_before2 - len(pool)} 个不适合类目热点（美甲/食谱/彩票/赛果/天气等）")

    def _shuffle_top(stage: List[Dict[str, Any]], need: int) -> List[Dict[str, Any]]:
        k = max(need * 2, 4)
        top, rest = stage[:k], stage[k:]
        random.shuffle(top)
        return top + rest

    # ① 热点去重（topic 未用过）
    stage1 = [b for b in pool if str(b.get("topic", "")).strip().lower() not in used_topics]
    # ② 风格去重（topic 用过，但 热点-风格 指纹未用过）
    stage2 = [b for b in pool if str(b.get("topic", "")).strip().lower() in used_topics
              and fingerprint(b) not in used_fp]
    # ③ 规则匹配兜底（剩余未用指纹，允许低分热点；已用指纹一律不重复出）
    stage3 = [b for b in pool if fingerprint(b) not in used_fp]

    out: List[Dict[str, Any]] = []
    out_fp: set = set()   # 本批内 (topic, style) 指纹去重
    for si, stage in enumerate((stage1, stage2, stage3)):
        for b in _shuffle_top(stage, count - len(out)):
            if len(out) >= count:
                break
            # 去重策略（用户指定）：
            #  ① 先热点去重：本批内优先不同 topic（stage1 全未用热点）；
            #  ② 热点用完后自动切风格去重：同一热点换新风格（topic 可重复，style 不同，
            #     即 热点1-风格1 → 热点1-风格2 → 热点2-风格2）；
            #  fingerprint（topic+art_style）本批内绝不重复，保证同热点同风格只出一次。
            fp = fingerprint(b)
            if fp in out_fp:
                continue
            # 热点去重优先：本批已用过的 topic 只在「没有未用热点可挑」时放行（stage2/3）
            topic_used = any(str(x.get("topic", "")).strip().lower()
                             == str(b.get("topic", "")).strip().lower() for x in out)
            if topic_used and si == 0:
                continue
            out_fp.add(fp)
            out.append(b)
        if len(out) >= count:
            break
    return out[:count]


def _rebuild_briefs_from_cache(country: str, config: Dict[str, Any], project_root: Path,
                               cache_dir: Path, need: int) -> List[Dict[str, Any]]:
    """简报不足时：直接用采集缓存热点（collected_keywords）生成简报。
    跳过 seed/fetch/score（无需重新采集、无需种子词），screen（排除已用）+ prompt 组装即可。"""
    import json as _json
    import time as _tm
    cp = cache_dir / "collected_keywords.json"
    if not cp.exists():
        return []
    ck = _json.loads(cp.read_text(encoding="utf-8")).get("keywords") or []
    if not ck:
        return []
    from graph.loader import build_country_config
    from graph.nodes.screen_node import screen_node
    from graph.nodes.prompt_node import prompt_node
    cc = build_country_config(config, country, project_root)
    config.setdefault("product", {})["spu_count"] = need
    state: Dict[str, Any] = {
        "country": country, "config": config, "country_config": cc,
        "prompts_dir": str(project_root / "prompts" / country),
        "cache_dir": str(cache_dir), "output_dir": str(cache_dir),
        "scored_rows": [dict(r) for r in ck],
        "screened": [], "briefs": [], "errors": [], "stats": {},
    }
    try:
        r1 = screen_node(state)
        r2 = prompt_node({**state, "screened": r1.get("screened", [])})
        briefs = r2.get("briefs", []) or []
        if briefs:
            (cache_dir / "design_briefs.json").write_text(
                _json.dumps({"generated_at": _tm.strftime("%Y-%m-%dT%H:%M:%S"),
                             "total": len(briefs), "design_briefs": briefs},
                            ensure_ascii=False, indent=2), encoding="utf-8")
        return briefs
    except Exception as e:  # noqa: BLE001
        print(f"[batch] 采集缓存生成简报失败: {e}")
        return []


def run_product_batch(country: str, config: Dict[str, Any], project_root: Path,
                      output_root: Optional[Path], tasks: List[Dict[str, Any]],
                      count: int, log_q=None, task_timestamp: Optional[str] = None) -> Dict[str, Any]:
    """缓存模式入口：加载缓存热点 → 按量分配 → product_node 批量处理。

    task_timestamp: 任务开始时间戳（YYYYMMDDHHMMSS），作为 OSS 路径段；缺失取当前时间。
    """
    if log_q:
        log_q.put(("log", f"\n===== 缓存热点产品流程 {country}（SPU 数量 {count}）=====\n"))
    import time as _tm
    cache_dir = (output_root or project_root) / "output" / country          # 缓存/去重（根目录）
    cache_dir.mkdir(parents=True, exist_ok=True)
    ts = task_timestamp or _tm.strftime("%Y%m%d_%H%M%S")
    _base = ts
    _i = 1
    while (cache_dir / ts).exists():   # 时间戳文件夹唯一（同秒多任务防冲突/覆盖）
        ts = f"{_base}_{_i}"
        _i += 1
    output_dir = cache_dir / ts                                              # 本次任务产物（时间戳文件夹）
    output_dir.mkdir(parents=True, exist_ok=True)

    briefs = load_cached_briefs(cache_dir)
    # 任务扩展：每个「集合」（款-颜色集 + 独立数量 count）按自己数量复制 count 份；
    # 每份 skus 保留该款全部颜色集合（多色）→ 每个设计配全部颜色各出一张主图
    picked_tasks: List[Dict[str, Any]] = []
    if tasks:
        for t in tasks:
            n = int(t.get("count") or 0) or count or 1
            for _ in range(n):
                tt = dict(t)
                tt.pop("count", None)
                picked_tasks.append(tt)
    else:
        picked_tasks = [dict(t) for t in (tasks or [])]
    if not briefs or len(briefs) < len(picked_tasks):
        # 简报不足：直接用采集缓存热点生成简报（无需重新采集/种子词）
        print(f"[batch] 简报 {len(briefs)} 条 < 需要 {len(picked_tasks)} 条 → 直接用采集缓存热点生成简报（无需重新采集）…")
        briefs = _rebuild_briefs_from_cache(country, config, project_root, cache_dir, len(picked_tasks))
        if not briefs:
            print("[batch] 采集缓存无有效热点，无法生成简报")
            return {"product": [], "briefs": [], "errors": [{"node": "batch", "message": "无缓存热点"}]}

    used = load_used(cache_dir)
    used_topics = {str(u.get("topic", "")).strip().lower() for u in used}
    # 简报池未用数（design_briefs.json 旧简报里还没用过的）
    fresh_count = sum(1 for b in briefs
                      if str(b.get("topic", "")).strip().lower() not in used_topics)
    total_needed = len(picked_tasks) or count
    # 采集池未用数（collected_keywords 全量里的未用热点——佐证热点池是否真的充足）
    pool_fresh = fresh_count
    try:
        import json as _json
        cp = cache_dir / "collected_keywords.json"
        if cp.exists():
            ck = _json.loads(cp.read_text(encoding="utf-8")).get("keywords") or []
            pool_fresh = sum(1 for k in ck
                             if str(k.get("topic", "")).strip().lower() not in used_topics)
    except Exception:  # noqa: BLE001
        pass
    # 旧简报用完了（简报池未用 < 需要）→ 直接用采集缓存热点生成新简报（无需重新采集）
    if fresh_count < total_needed:
        print(f"[batch] 简报池未用 {fresh_count}/{len(briefs)} 条 < 需要 {total_needed} 条 → "
              f"直接用采集缓存热点生成新简报（采集池未用 {pool_fresh} 条充足，无需重新采集）…")
        briefs = _rebuild_briefs_from_cache(country, config, project_root, cache_dir, total_needed)

    # openai 模式：LLM 已安全改写，review（带 concept）也可用；mock 模式：review 留人工复核
    allow_review = str((config.get("llm_screen") or {}).get("provider", "")).strip() != "mock"
    from graph.loader import build_country_config
    _cc = build_country_config(config, country, project_root)
    exclude_topics = list((_cc.get("exclude_topics") or []) or [])   # 国家配置的黑名单热点
    assigned = assign_hotspots(briefs, used, total_needed, allow_review=allow_review,
                               exclude_topics=exclude_topics)
    if not assigned:
        print("[batch] 无可用热点分配")
        return {"product": [], "briefs": briefs, "errors": [{"node": "batch", "message": "无可用热点"}]}
    if len(assigned) < total_needed:
        print(f"[batch] ⚠ 热点不足：简报 {len(briefs)} 条，仅分配到 {len(assigned)}/{total_needed} 个热点（已用去重后剩余热点少，第 {len(assigned)+1} 个起无热点）")
    else:
        print(f"[batch] 缓存热点 {len(briefs)} 条 → 分配 {len(assigned)} 个热点")

    # 款号与热点一一绑定：第 i 个款号用第 i 个热点（缓存模式；完整流水线由 product 自行绑定）
    for i, t in enumerate(picked_tasks):
        if i < len(assigned):
            t["topic"] = assigned[i].get("topic", "")
    if log_q:
        for i, t in enumerate(picked_tasks):
            tp = t.get("topic", "")
            log_q.put(("log", f"[batch] 款号 {t.get('spu')} ← 热点「{tp}」\n"))

    # 直接构造 state 调 product_node（跳过 seed/fetch 等重跑）
    from graph.nodes.product_node import product_node
    from graph.loader import build_country_config
    import time as _time
    cc = build_country_config(config, country, project_root)
    config.setdefault("product", {})["spu_tasks"] = picked_tasks
    if count > 0:
        config.setdefault("product", {})["spu_count"] = count
    state: Dict[str, Any] = {
        "country": country,
        "config": config,
        "country_config": cc,
        "prompts_dir": str(project_root / "prompts" / country),
        "output_dir": str(output_dir),
        "briefs": briefs,
        "designs": [],
        "composite": [],
        "errors": [],
        "stats": {},
        "task_timestamp": task_timestamp or _time.strftime("%Y%m%d%H%M%S"),
        "oss_seq": 0,
    }
    out = product_node(state)
    # 缓存模式也走压缩+上传+种草图节点（与 graph 全流程一致）
    if out.get("product"):
        from graph.nodes.oss_upload_node import oss_upload_node
        out2 = oss_upload_node({**state, "product": out.get("product"),
                                "stats": out.get("stats") or {},
                                "errors": out.get("errors") or []})
        out["oss"] = out2.get("oss") or []
        out["product"] = out2.get("product") or out.get("product")
        out["stats"] = out2.get("stats") or out.get("stats") or {}
        if out2.get("oss_seq") is not None:
            state["oss_seq"] = out2["oss_seq"]

        from graph.nodes.seed_shot_node import seed_shot_node
        out3 = seed_shot_node({**state, "product": out.get("product"),
                               "stats": out.get("stats") or {},
                               "errors": out.get("errors") or []})
        out["seed_shots"] = out3.get("seed_shots") or []
        out["product"] = out3.get("product") or out.get("product")
        out["stats"] = out3.get("stats") or out.get("stats") or {}
        if out3.get("oss_seq") is not None:
            state["oss_seq"] = out3["oss_seq"]

        from graph.nodes.template_export_node import template_export_node
        out4 = template_export_node({**state, "product": out.get("product"),
                                     "stats": out.get("stats") or {},
                                     "errors": out.get("errors") or []})
        out["product"] = out4.get("product") or out.get("product")
        out["stats"] = out4.get("stats") or out.get("stats") or {}
    return out
