"""节点 5.5/6：生成印花设计稿 + 导出简报包（compose）。

流程位置：prompt_build → compose → product（compose 在 product 之前）。
职责��
1. 生成纯印花设计稿：对前 N 个 safe 简报（N=config.compose.design_count，默认 1），
   用 image_prompt 调图像后端 generate()（白底、可直接打印），产物存 output/<country>/designs/，
   设计稿路径写回 brief.design_path，并汇总返回 designs 列表供 product 节点使用（图2）。
2. 导出简报包：design_briefs.json/md、composite_prompts.json/md、report.md。
"""
import json
import time
from pathlib import Path
from typing import Any, Dict, List

from graph.validate import with_fallback

RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}


def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
    lines = [
        "# POD 印花设计简报（LLM 合规筛选 + 生图提示词）",
        "",
        f"- 生成时间: {generated_at}",
        f"- 通过筛选: {len(briefs)} 条",
        "",
        "## 一、安全设计清单（按综合分排序）",
        "",
        "| 排名 | 国家 | 热点词 | 类别 | 风险 | 设计概念 |",
        "|---|---|---|---|---|---|",
    ]
    for i, r in enumerate(briefs, 1):
        flag = RISK_LABEL.get(r.get("risk_level"), r.get("risk_level"))
        lines.append(
            f"| {i} | {r.get('country','')} | {r.get('topic','')} | {r.get('design_category','')} | {flag} | {r.get('concept','')} |"
        )
    lines += ["", "## 二、设计要素 + 封装提示词", ""]
    lines.append("> 工作流：① `image_prompt` = 印花设计稿（白底，单独生图）；② 上传平铺衣服底图(图1)后，")
    lines.append("> 用 `composite_prompt` + 图1 经 img2img 把设计印到衣服；规则写死：保留衣服、胸前居中印花、真实丝网质感。")
    lines.append("")
    for i, r in enumerate(briefs, 1):
        flag = RISK_LABEL.get(r.get("risk_level"), r.get("risk_level"))
        lines.append(f"### {i}. [{r.get('country','')}] {r.get('topic','')}  （{flag}）")
        lines.append(f"- 类别: {r.get('design_category','')}")
        lines.append(f"- 设计要素: 主体=「{r.get('motif','')}」 | 风格=「{r.get('art_style','')}」 | 配色=「{r.get('color_palette','')}」 | 构图=「{r.get('composition','')}」")
        lines.append(f"- 概念: {r.get('concept','')}")
        if r.get("risk_reasons"):
            lines.append(f"- 风险提示: {'; '.join(r['risk_reasons'])}")
        if r.get("design_path"):
            lines.append(f"- **设计稿**: {r['design_path']}")
        lines.append(f"- **设计稿 Prompt (image_prompt)**: {r.get('image_prompt','')}")
        lines.append(f"- **印到底图 Prompt (composite_prompt)**: {r.get('composite_prompt','')}")
        lines.append(f"- **Composite Negative**: {r.get('composite_negative','')}")
        lines.append("")
    return "\n".join(lines)


def _build_composite_md(briefs: List[Dict[str, Any]]) -> str:
    lines = [
        "# 封装提示词包（印到平铺衣服底图 图1）",
        "",
        f"- 共 {len(briefs)} 条，每条含 `composite_prompt`（印图指令）+ `composite_negative`。",
        "- 用法：将你的平铺衣服参考图作为图1，连同 `composite_prompt` 送入任意 img2img / inpaint 模型。",
        "",
    ]
    for i, r in enumerate(briefs, 1):
        lines.append(f"### {i}. [{r.get('country','')}] {r.get('topic','')}")
        lines.append(f"- composite_prompt: {r.get('composite_prompt','')}")
        lines.append(f"- composite_negative: {r.get('composite_negative','')}")
        lines.append("")
    return "\n".join(lines)


def _build_report_md(state: Dict[str, Any]) -> str:
    country = state.get("country", "")
    stats = state.get("stats") or {}
    errors = state.get("errors") or []
    lines = [
        f"# POD 热点抓取报告 - {country}",
        "",
        f"- 生成时间: {time.strftime('%Y-%m-%dT%H:%M:%S')}",
        "",
        "## 各阶段统计",
        "",
        "| 阶段 | 指标 |",
        "|---|---|",
    ]
    for k, v in stats.items():
        lines.append(f"| {k} | {v} |")
    lines += ["", "## 兜底错误记录（节点级 fallback 捕获）", ""]
    if errors:
        for e in errors:
            lines.append(f"- [{e.get('node')}] {e.get('type')}: {e.get('message')}")
    else:
        lines.append("- 无（全部节点正常）")
    return "\n".join(lines)


@with_fallback("compose")
def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
    briefs: List[Dict[str, Any]] = state.get("briefs") or []
    output_dir = Path(state["output_dir"])            # 本次任务产物（时间戳文件夹）
    output_dir.mkdir(parents=True, exist_ok=True)
    cache_dir = Path(state.get("cache_dir") or output_dir)   # 缓存/去重（根目录）
    config = state["config"]
    country = state.get("country", "")

    generated_at = time.strftime("%Y-%m-%dT%H:%M:%S")

    # 1) design_briefs.json（缓存 → 根目录，不进时间戳任务文件夹）
    (cache_dir / "design_briefs.json").write_text(
        json.dumps({"generated_at": generated_at, "total": len(briefs), "design_briefs": briefs},
                   ensure_ascii=False, indent=2), encoding="utf-8")

    # 2) design_briefs.md
    (cache_dir / "design_briefs.md").write_text(
        _build_briefs_md(briefs, generated_at), encoding="utf-8")

    # 3) composite_prompts.json / .md
    (cache_dir / "composite_prompts.json").write_text(
        json.dumps({"generated_at": generated_at, "total": len(briefs), "composite_prompts": briefs},
                   ensure_ascii=False, indent=2), encoding="utf-8")
    (cache_dir / "composite_prompts.md").write_text(
        _build_composite_md(briefs), encoding="utf-8")

    # 4) report.md（本次任务报告 → 产物目录）
    (output_dir / "report.md").write_text(_build_report_md(state), encoding="utf-8")

    # 5) 生成纯印花设计稿（图2）：前 N 个 safe 简报用 image_prompt 文生图
    designs: List[Dict[str, Any]] = []
    compose_cfg = config.get("compose") or {}
    backend_name = compose_cfg.get("backend", "")
    ib = None
    if backend_name:
        try:
            from graph.backends import get_image_backend
            ib = get_image_backend(backend_name)
            if ib is not None:
                ib.bind_config(compose_cfg)
        except Exception as e:  # noqa: BLE001
            print(f"[compose] 图像后端 {backend_name} 不可用: {e}")
    if ib is None:
        print("[compose] 未配置 compose.backend（openai/mock），跳过印花设计稿生成。")
    else:
        # 设计稿覆盖所有简报（含 review）：每个热点一张设计，避免 review 热点无设计
        # 导致 product 回退生成重复占位图；风险由 assign 层（allow_review）控制是否分配
        safe_briefs = briefs
        design_count = int(compose_cfg.get("design_count", 1))
        # 联动总任务数：每个产品一张设计 → 生成 扩展后 spu_tasks 总数 张设计
        task_n = int(len((state.get("config") or {}).get("product", {}).get("spu_tasks") or []))
        if task_n > design_count:
            design_count = task_n
        design_dir = output_dir / "designs"
        design_dir.mkdir(exist_ok=True)

        from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
        from concurrent.futures import ThreadPoolExecutor, as_completed

        def _gen_one(i: int, b: Dict[str, Any]):
            """单张设计稿生成（并发线程内调用，每设计一线程）。"""
            try:
                img_prompt = sanitize_image_prompt(b.get("image_prompt", ""))
                img_prompt = ensure_rebrand_hint(b, img_prompt)   # review → 原创化魔改引导
                out_path = ib.generate(
                    img_prompt,
                    str(design_dir / f"{country}_{i:02d}_design.png"),
                    b.get("composite_negative", ""),
                    size="1024x1024")   # 印花设计统一 1024x1024
                return i, b, out_path, None
            except Exception as e:  # noqa: BLE001
                return i, b, None, e

        targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
        # 并发生成：每张设计一个线程（并行调图像网关），数量多时不串行等待
        workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5))))
        print(f"[compose] 并发生成 {len(targets)} 张设计稿（{workers} 线程）…")
        with ThreadPoolExecutor(max_workers=workers) as _ex:
            _futs = [_ex.submit(_gen_one, i, b) for i, b in targets]
            for _f in as_completed(_futs):
                i, b, out_path, err = _f.result()
                if err is not None:
                    print(f"[compose] 设计稿生成失败 {b.get('topic', '')}: {err}")
                    state.setdefault("errors", []).append({
                        "node": "compose", "type": type(err).__name__,
                        "message": f"设计稿生成失败 {b.get('topic','')}: {err}", "trace": ""})
                else:
                    b["design_path"] = out_path
                    designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
                    print(f"[compose] 印花设计稿已生成: {out_path}")

    stats = dict(state.get("stats") or {})
    stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}
    return {"composite": briefs, "designs": designs, "stats": stats,
            "errors": state.get("errors") or []}
