"""
app/services/ai_engine/fission_prompt.py — 第11环裂变 prompt（对齐上线版 split.js）

裂变=1爆款→一次LLM出N套完整笔记包。重写对齐产品包上线版 split.js：
  - FISSION_SYSTEM 完整笔记包字段（含 dimension/audience/scene/painPoint/keywords/imagePlan）
  - 参考度连续 30-85（reference_strategy_from_level），低≤40/高≥80/中
  - 违禁词清洗 sanitize_image_plan_text
  - 品类兜底 build_fallback_notes / infer_category（LLM挂时不卡用户）

🔴 参考度入参保留 low/mid/high 三档（倩倩姐拍板），内部 _LEVEL_TO_INT 映射成数值。
🔴 三档真实业务措辞待北哥定义；本次接口按数值做对。
🔴 LLM 走 chat_complete(OpenAI兼容)，FISSION_SYSTEM 作 messages[0].role=system 传。
"""
from __future__ import annotations

# 三档→数值映射（保留枚举入参，内部走连续值逻辑，对齐上线版 referenceStrategyFromLevel）
_LEVEL_TO_INT = {"low": 35, "mid": 60, "high": 82}


def reference_strategy_from_level(level: str | int) -> dict:
    """参考度策略（对齐上线版 referenceStrategyFromLevel）。

    入参支持枚举 low/mid/high 或数值；钳到 30-85。
    返回 {level, level_label, prompt_rule, summary}。
    """
    if isinstance(level, str):
        value = _LEVEL_TO_INT.get(level, 60)
    else:
        try:
            value = int(level)
        except (TypeError, ValueError):
            value = 60
    value = max(30, min(85, value))
    if value <= 40:
        return {
            "level": value, "level_label": "低参考",
            "prompt_rule": "只参考爆款的内容结构和图文角色，不沿用原文表达、标题句式和具体画面。",
            "summary": "参考爆款结构，不贴近原文表达。",
        }
    if value >= 80:
        return {
            "level": value, "level_label": "高参考",
            "prompt_rule": "强参考爆款的标题节奏、痛点切入、图文递进和情绪强度，但必须替换人群、场景、表达和图片，不得抄袭。",
            "summary": "强参考爆点、标题节奏和图文递进，但替换表达避免相似。",
        }
    return {
        "level": value, "level_label": "中参考",
        "prompt_rule": "参考爆款的结构、痛点表达方式和标题节奏，同时重写正文、标签和每张图片画面。",
        "summary": "参考结构、痛点和标题节奏，输出新的完整笔记包。",
    }


def valid_level(level: str | None) -> str:
    """校验三档枚举，非法回落 mid（保留旧接口兼容）。"""
    return level if level in _LEVEL_TO_INT else "mid"


# 默认裂变维度（对齐上线版 dimensions）
DEFAULT_DIMENSIONS = ["换角度", "换痛点", "换人群"]

FISSION_SYSTEM = """你是小红书完整图文笔记裂变专家。

你必须基于爆款参考生成"完整小红书笔记包"，不是只生成文案，也不是只生成图片提示词。

完整笔记包必须包含：标题(可直接发布)、正文(180-260字种草口吻真实场景卖点转买点)、标签(5-8个)、点击钩子标题(第1张图大字)、imagePlan(每张图的图上文字+画面内容+排版)、dimension(裂变维度)、keywords、audience(适用人群)、scene(使用场景)、painPoint(切入痛点)。

裂变规则：
- 每套必须不重复，标题/正文/标签/图文结构都要变，图片重新配套不可一套图反复发
- 保持种草安利+情绪共鸣风格
- 正文自然出现2-5个小红书符号/emoji(✅✨🌿💧📦🔍🧡🥹‼️)，放在痛点/实测/选择理由/软性转化处，不堆砌不每句塞
- 标题可适度带符号，但不要所有标题同一种符号
- 图片=可上传的独立3:4图文海报，不是App截图/笔记详情页截图
- 图片禁止出现Like/评论/分享/底栏/头像/状态栏等社交App界面元素
- 对比页只做质地/场景/肤感说明对比，禁用前后、before/after、变白、瑕疵消失、治疗前后
- imagePlan只写短标题/短卖点/短画面要求，不塞长正文
- 禁用词：美白、祛斑、速效、医用、药妆、变白、before、after、使用前后
- 图文张数叙事：3张=点击→核心证明→软性转化；6张=点击→痛点→证明→质感→背书→软性转化；8张=点击→痛点→证明→质感→多场景→教程→背书→软性转化
- 最后一张是软性转化，不做淘宝式硬广；用囤货/省钱情报/搜索暗示/评论暗号等原生分享口吻

返回纯JSON数组，每个元素含：title/content/tags(数组)/coverTitle/dimension/audience/scene/painPoint/keywords(数组)/imagePlan(数组,每项{role,title,overlayText,text})。

硬性格式要求：
- 只输出JSON，不要markdown代码块
- 字符串内部不用英文双引号，引用词用中文书名号或中文引号
- content是客户可直接发布的正文，不能写"配图建议/图片方向/imagePlan/内页规划"等内部提示
- imagePlan数量必须等于用户要求的图片数量"""


import re

# 违禁词清洗替换表（对齐上线版 sanitizeImagePlanText，有序应用）
_SANITIZE_RULES = [
    (re.compile(r"before\s*&\s*after", re.I), "质地与肤感说明"),
    (re.compile(r"before\s*/?\s*after", re.I), "质地与肤感说明"),
    (re.compile(r"\bbefore\b", re.I), "质地状态"),
    (re.compile(r"\bafter\b", re.I), "上脸肤感"),
    (re.compile(r"使用前后|用前用后|用前后|前后对比|使用前|使用后"), "质地/场景/肤感说明"),
    (re.compile(r"功效对比|效果对比|改善对比"), "质地/场景说明对比"),
    (re.compile(r"肤色变白|皮肤变白|变白|美白"), "自然光泽感"),
    (re.compile(r"瑕疵消失|斑点消失|痘印消失|消除瑕疵|祛斑"), "妆感更服帖"),
    (re.compile(r"治疗前后|治疗后|医美前后|治愈|修复受损"), "日常使用场景说明"),
]


def sanitize_image_plan_text(value: str = "", max_length: int = 56) -> str:
    """清洗 imagePlan 文字里的违禁词（对齐上线版）。"""
    text = str(value or "")
    for pattern, repl in _SANITIZE_RULES:
        text = pattern.sub(repl, text)
    text = re.sub(r"\s+", " ", text).strip()
    return text[:max_length]


def normalize_tags(tags=None, keywords=None) -> list[str]:
    """标签归一化：补#前缀、去重、截8个（对齐上线版 normalizeTags）。"""
    tags = tags or []
    keywords = keywords or []
    if not isinstance(tags, list):
        tags = str(tags).split()
    from_tags = [
        t if str(t).strip().startswith("#") else f"#{str(t).strip()}"
        for t in tags if str(t).strip()
    ]
    from_kw = [
        k if str(k).startswith("#") else f"#{k}"
        for k in list(keywords)[:5] if str(k).strip()
    ]
    seen, out = set(), []
    for t in from_tags + from_kw:
        if t not in seen:
            seen.add(t)
            out.append(t)
    return out[:8]


def build_fission_prompt(
    source_note: dict, product: dict, reference_level: str,
    note_count: int, image_count: int, dimensions: list[str] | None = None,
) -> str:
    """组装裂变 user prompt（对齐上线版 handleContentSplit 的 prompt 变量拼装）。"""
    src = source_note or {}
    prod = product or {}
    dims = dimensions or DEFAULT_DIMENSIONS
    strategy = reference_strategy_from_level(reference_level)
    title = src.get("title", "")
    content = src.get("content") or src.get("text", "")
    tags = src.get("tags", []) or []
    name = prod.get("name", "") or "未提供"
    points = prod.get("selling_points", []) or []
    audience = prod.get("target_audience", "") or "未提供"
    keywords = prod.get("keywords", []) or []
    kw_line = "、".join(keywords) if keywords else "、".join(
        [x for x in [name, audience, "真实测评", "好物分享"] if x and x != "未提供"]
    )
    return f"""爆款参考：
标题：{title}
正文：{content}
标签：{' '.join(tags)}

产品：{name}
产品卖点：{'、'.join(points) or '未提供'}
目标人群：{audience}
关键词：{kw_line}
裂变维度：{'、'.join(dims)}
爆款参考度：{strategy['level_label']}。{strategy['prompt_rule']}
生成数量：{note_count}套
每套图片数量：{image_count}张

请生成{note_count}套完整小红书图文笔记包。每套都必须含标题、正文、标签、点击钩子标题、{image_count}张图的imagePlan。
imagePlan必须按叙事链路逐张递进。3张版第2张必须是按当前品类变化的核心证明页，不能和第1张重复构图，不得让第2张重复第1张标题。
正文必须像真实小红书种草笔记一样自然带2-5个emoji；不要把图片规划/配图建议/内部审核建议写进正文。"""


def notes_array_from_parsed(parsed) -> list[dict]:
    """从LLM解析结果里拎出笔记数组（对齐上线版 notesArrayFromParsed）。"""
    if isinstance(parsed, list):
        return parsed
    if not isinstance(parsed, dict):
        return []
    for key in ("notes", "variants", "data", "items", "result", "results"):
        if isinstance(parsed.get(key), list):
            return parsed[key]
    return [parsed] if (parsed.get("title") or parsed.get("content")) else []
