"""
llm_scorer.py — AI 评委打分入口（让模型真读文案，替代机械找词）
合规第7维仍走机械硬拦(score_compliance)；AI 读前6维给分+理由。
任何异常/解析失败 → 回退旧机械 score_copy，绝不卡链路。
"""
from __future__ import annotations
import asyncio
import json
import logging
import re
from typing import Any

from .constants import BANNED_WORDS_DEFAULT, BANNED_VISUAL_WORDS, QUALITY_PASS_SCORE
from ._scoring_dims import score_compliance
from .text_scoring import score_copy
from ._score_prompt import SCORER_PERSONA, build_score_prompt, COMPLIANCE_MAX

logger = logging.getLogger(__name__)

# 6 个 AI 维度满分（倩倩姐2026-06-15拍板·与 constants.AI_DIM_WEIGHTS/_score_prompt 三处同步）
# 痛点18+情绪18+买点18+钩子15+标题13+真实感13=95，+合规5=100
# "真实感"替换旧"产品聚焦一件事"，对齐富贵"很少提产品/前70%干货后30%植入"原则
_DIM_MAX = {
    "痛点人群精准": 18, "情绪张力": 18, "买点转化": 18,
    "开头钩子": 15,    "标题点击力": 13, "真实感": 13,
}
# 评委合规相关默认权重（仅供 score_compliance 复用其内部硬拦逻辑）
_COMPLIANCE_W = {"compliance": COMPLIANCE_MAX}


def _parse_verdict(raw: str) -> dict | None:
    """从模型输出里抠出 JSON 对象，失败返 None。"""
    s = raw.strip()
    m = re.search(r"\{.*\}", s, re.DOTALL)
    if not m:
        return None
    try:
        obj = json.loads(m.group(0))
        return obj if isinstance(obj, dict) and isinstance(obj.get("dims"), list) else None
    except (json.JSONDecodeError, ValueError):
        return None


async def llm_score_copy(
    client: Any,
    copy: dict[str, Any],
    source: dict[str, Any],
    banned_words: list[str] | None = None,
    pass_score: int = QUALITY_PASS_SCORE,
) -> dict[str, Any]:
    """AI 评委读 1 条文案 → 6维分+理由，合规机械硬拦。返回与 score_copy 同结构。"""
    bwords = list(set((banned_words or []) + BANNED_WORDS_DEFAULT + BANNED_VISUAL_WORDS))
    full = f"{copy.get('title','')}\n{copy.get('content','')}\n{' '.join(str(t) for t in copy.get('tags',[]))}"
    dim_comp, found_all = score_compliance(full, bwords, _COMPLIANCE_W)

    prompt = build_score_prompt(copy, source)
    raw = ""
    backoff = [5, 10, 20]
    for attempt in range(4):
        try:
            raw = await client.chat_complete(
                messages=[{"role": "system", "content": SCORER_PERSONA},
                          {"role": "user", "content": prompt}],
                model=client._model, max_tokens=1500, temperature=0.3,
            )
            break
        except Exception as exc:  # noqa: BLE001 — 含 httpx.HTTPStatusError 503/429
            status = getattr(getattr(exc, "response", None), "status_code", 0)
            if status in (503, 429) and attempt < 3:
                await asyncio.sleep(backoff[min(attempt, 2)])
                continue
            logger.warning("AI评委调用失败，回退机械打分: %s", exc)
            return score_copy(copy, source, banned_words, pass_score=pass_score)

    verdict = _parse_verdict(raw)
    if not verdict:
        logger.warning("AI评委输出解析失败，回退机械打分。raw[:120]=%s", raw[:120])
        return score_copy(copy, source, banned_words, pass_score=pass_score)

    details: list[dict] = []
    for d in verdict["dims"]:
        item = str(d.get("item", "")).strip()
        if item not in _DIM_MAX:          # 只收白名单6维，模型偶尔多吐"总分"等噪声项，丢弃
            continue
        mx = _DIM_MAX[item]
        sc = max(0, min(mx, int(round(float(d.get("score", 0))))))
        details.append({"item": item, "score": sc, "max": mx, "note": str(d.get("reason", ""))[:60]})
    details.append(dim_comp)

    total = max(0, min(100, sum(d["score"] for d in details)))
    passed = (total >= pass_score) and not found_all
    return {
        "score": total, "score_detail": details, "passed": passed,
        "banned_words_found": found_all,
        "verdict": verdict.get("verdict", ""), "summary": str(verdict.get("summary", ""))[:120],
    }
