"""
storyboard 分镜引擎
扒自：worker/src/image.js
- getNarrativeRoles：按图数取分镜角色
- proof_strategy：按品类定证明页策略（品类不写死，走数据驱动）
- build_visual_system：成组视觉统一
- plan_image_set：组装最终分镜计划
"""
from __future__ import annotations
import re
from .constants import (
    PAGE_ROLE_MAP, IMAGE_NEGATIVE_CONSTRAINTS,
    STYLE_PROMPTS, STYLE_DEFAULT, NARRATIVE_BY_COUNT, NARRATIVE_BY_STRATEGY,
)
# sanitize_text 移至 templates（腾行数），此处 re-export 供 image_gen 沿用 import
from .storyboard_templates import role_template, proof_strategy, sanitize_text  # noqa: F401


def clamp_count(value: int, fallback: int = 6, lo: int = 1, hi: int = 8) -> int:
    try:
        return max(lo, min(hi, int(value)))
    except (TypeError, ValueError):
        return fallback


def short_selling_points(points, fallback: str = "") -> str:
    """3个短卖点拼成 a / b / c（扒 shortSellingPoints:112-120）"""
    src = points if isinstance(points, list) else str(points or "").split("、")
    clean = [sanitize_text(p, 18) for p in src if sanitize_text(p, 18)][:3]
    return " / ".join(clean) if clean else sanitize_text(fallback, 28)


def short_tags(tags, keywords=None) -> str:
    """标签去#截断拼成 #a #b（扒 shortTags:48-54）"""
    merged = list(tags or []) + list(keywords or [])
    out = []
    for t in merged:
        c = sanitize_text(str(t), 12).lstrip("#")
        if c:
            out.append(f"#{c}")
    return " ".join(out[:5])


def analyze_copy_for_image(note: dict, product: dict) -> dict:
    """
    从文案+产品提取生图锚点（扒 analyzeCopyForImage:129-148）
    给每张图填 audience/pain/scene/hook，让画面有真实代入而非空泛。
    """
    text = f"{note.get('title','')}。{note.get('coverTitle','')}。{note.get('content','')}"
    tags = [sanitize_text(str(t).lstrip('#'), 12) for t in (note.get("tags") or [])]
    audience = sanitize_text(
        product.get("target_audience")
        or next((t for t in tags if re.search(r"党|人|妈妈|女生|学生|通勤|上班|办公室", t)), "")
        or "目标用户", 18)
    scene = sanitize_text(
        next((t for t in tags if re.search(r"通勤|宿舍|上课|约会|出门|办公室|旅行|居家|工位", t)), "")
        or "日常自然光场景", 18)
    pain = sanitize_text(
        next((w for w in re.split(r"[、,，。；;！!？?\n]", text)
              if re.search(r"暗沉|没气色|假白|卡粉|搓泥|油|干|赶时间|预算|麻烦", w)), "")
        or "日常使用痛点", 18)
    hook = sanitize_text(note.get("coverTitle") or note.get("title") or f"{audience}{scene}", 18)
    return {"audience": audience, "scene": scene, "pain": pain, "hook": hook}


def get_narrative_roles(image_count: int = 6) -> list[dict]:
    """
    按图数返回分镜角色列表（扒 getNarrativeRoles，Q6对齐北哥6张套路）
    ≤3 张：极速链路  hook / applied_proof / closer
    ≤6 张：北哥标准链路  ①封面痛点大字 ②单品特写+品牌词 ③成分 ④质地 ⑤上脸对比 ⑥促单
    >6 张：沉浸链路  + pain_scene / scenario / social_proof
    """
    count = clamp_count(image_count)
    m = PAGE_ROLE_MAP
    if count <= 3:
        sequence = ["hook", "applied_proof", "closer"]
    elif count <= 6:
        # Q6：北哥6张标准顺序——品牌词在②(product_closeup)和⑥(closer)两次出现
        sequence = ["hook", "product_closeup", "ingredient", "texture", "applied_proof", "closer"]
    else:
        # 8张沉浸链路：在北哥6张基础上插入 pain_scene / social_proof
        sequence = ["hook", "pain_scene", "product_closeup", "ingredient", "texture", "applied_proof", "social_proof", "closer"]
    return [m[r] for r in sequence[:count] if r in m]


# ── proofStrategy 已移至 storyboard_templates.proof_strategy（腾行数，超200拆）


def build_visual_system(product: dict, analysis: dict | None = None) -> dict:
    """
    成组视觉统一（扒 buildVisualSystem）
    analysis 来自 product.js 分析结果（visualIdentity），可空
    """
    identity = (analysis or {}).get("visualIdentity", {})
    palette = (
        "、".join(identity["colorPalette"][:5])
        if isinstance(identity.get("colorPalette"), list) and identity["colorPalette"]
        else "提取产品包装主色，搭配浅色真实生活背景"
    )
    return {
        "palette": palette,
        "typography": identity.get("typographyStyle", "主标题清晰黑体或手写感标题，辅助文字便签/勾选标注，字重颜色保持同一体系"),
        "sticker": identity.get("stickerLanguage", "少量箭头、放大镜、勾选、小表情、便签，不使用促销按钮"),
        "layout": identity.get("layoutStyle", "同一组图片保持色调、光线、产品露出方式一致，每张图承担不同叙事角色"),
        "texture": identity.get("materialTexture", "产品包装、质地、手背/上脸肤感要真实自然"),
        "package_details": identity.get("packageDetails", "如果提供产品图，必须还原包装颜色、瓶身形状、标签方向和主视觉"),
        "xhs_style_preset": identity.get("xhsStylePreset", "真实测评风/手写安利风/清单便签风"),
        "symbol_system": identity.get("symbolSystem", "中等密度小红书种草符号：✅ ✨ 🌿 💧 🪞 🧴 📦 🔍 💛，每张最多2-4个"),
        "quality_rules": [
            "同组图片字体体系相对一致，但不要像固定模板",
            "每张压图文字必须服务当前叙事角色，不能重复封面标题",
            "护肤品优先出现手背涂抹、质地微距、自然上脸局部或真实生活场景",
            "人物真实自然，有轻微皮肤纹理和生活感，不要AI精修美女",
            "禁止乱码、错别字、App底栏、Like评论分享、硬广价格牌、虚假功效before/after",
        ],
    }


def plan_image_set(note: dict, product: dict, image_count: int = 3, analysis: dict | None = None, strategy: str | None = None) -> dict:
    """
    组装分镜计划（主入口）
    strategy: None=默认按图数叙事，'A'/'B'/'C'=三套正交叙事策略
    返回：{requested_count, storyboard, visual_system, base_prompt}
    storyboard 每项：{role, name, focus, overlay_text, prompt_for_item}
    """
    count = clamp_count(image_count)
    roles = get_narrative_roles(count)
    visual = build_visual_system(product, analysis)
    category = product.get("category", "通用好物")
    points = product.get("selling_points") or ["核心买点"]
    src = analyze_copy_for_image(note, product)  # 文案锚点：audience/pain/scene/hook

    storyboard = []
    brand_kw = sanitize_text(product.get("brand_keyword") or "", 12)
    brand_roles = {"product_closeup", "closer"}  # Q6：第2/6张带品牌词

    for i, role in enumerate(roles):
        point = sanitize_text(points[i % len(points)], 18)
        tpl = role_template(role["role"])
        proof = proof_strategy(category, point)  # 仅 applied_proof 用品类证明
        # 填模板占位：每角色画面/文字各不同（修缩水根因——不再全角色共用proof）
        fill = {
            "audience": src["audience"], "pain": src["pain"], "scene": src["scene"],
            "hook": src["hook"], "point": point, "brand": brand_kw or product.get("name", "产品"),
            "proof_overlay": proof.get("overlay", point),
            "proof_visual": proof.get("visual", ""),
            "proof_forbidden": proof.get("forbidden", ""),
        }
        item = {
            "role": role["role"],
            "name": role["name"],
            "focus": role["focus"],
            "goal": sanitize_text(tpl["goal"].format(**fill), 40),
            "overlay_text": sanitize_text(tpl["overlay"].format(**fill), 20),
            "visual_strategy": sanitize_text(tpl["visual"].format(**fill), 120),
            "source_basis": sanitize_text(tpl["basis"].format(**fill), 60),
            "selling_point": point,
            "asset_use": proof.get("asset_use", "产品图保证包装准确，参考图用于真实场景"),
            "forbidden": sanitize_text(tpl["forbidden"].format(**fill), 60),
        }
        if brand_kw and role["role"] in brand_roles:
            item["brand_keyword"] = brand_kw
            item["brand_keyword_rule"] = "品牌词自然融入画面文字或产品露出，不做广告牌感"
        storyboard.append(item)

    # base_prompt 全局规则（扒 planImageSet basePrompt:682-690）
    product_name = product.get("name", "产品")
    cover_title  = sanitize_text(note.get("coverTitle") or note.get("title") or "", 18)
    style_text   = STYLE_PROMPTS.get(product.get("style_tone") or STYLE_DEFAULT, STYLE_PROMPTS[STYLE_DEFAULT])
    # strategy非空时取对应正交叙事，否则按图数取默认链路
    narrative    = NARRATIVE_BY_STRATEGY.get(strategy) if strategy else None
    if not narrative:
        narrative = NARRATIVE_BY_COUNT.get(count, NARRATIVE_BY_COUNT[6])
    sp_text      = short_selling_points(points, cover_title)
    tag_text     = short_tags(note.get("tags") or [], product.get("keywords") or [])
    base_prompt = (
        f"生成一张小红书可上传的独立3:4图文海报/素材图，目标比例1024×1536，可直接上传的独立图片，不是提示词，不是App截图。"
        f"产品：{product_name}。短标题：{cover_title}。"
        f"短卖点：{sp_text}。短标签：{tag_text}。"
        f"叙事链路：{narrative}"
        f"视觉风格：{style_text}。"
        f"成组视觉：主色={visual['palette']}；字体={visual['typography']}；贴纸={visual['sticker']}；"
        f"符号系统={visual['symbol_system']}；产品还原={visual['package_details']}。"
        "重要限制：中文文字少而清晰，每张只允许一个主标题，同一句话禁止重复出现，正文点位最多3条；"
        "四周留安全边距，文字不贴边不被裁切；真实自然像实拍素材后排版，降低AI味；"
        "禁止生成小红书App界面截图、Like/评论/分享/底栏/头像等社交元素；"
        "禁止肤色变白、瑕疵消失、治疗前后等视觉暗示，允许安全的未推开/推开后质地状态对比；"
        "如果提供产品图，产品是不可修改的真实商品锚点，禁止改名、换包装、混入其他产品。"
        f"\n{IMAGE_NEGATIVE_CONSTRAINTS}"
    )

    return {
        "requested_count": count,
        "storyboard": storyboard,
        "visual_system": visual,
        "base_prompt": base_prompt,
    }
