"""小意 Tri-Review 三席打分 — 轻量 LLM + 规则兜底。"""

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any

from niannian.llm.types import LLMUnavailable, Message
from niannian.personas import REFLECTION
from niannian.soul.reflection_rules import rules_precheck, rules_seat_score
from niannian.soul.values import values_summary


@dataclass(frozen=True)
class ScoredRound:
    seat_a_pass: bool
    seat_b_score: float
    seat_c_score: float
    doubts: list[str]
    engine: str  # rules | llm+rules


class TriReviewScorer:
    """规则预检 + 可选 LLM 双 pass；失败则纯规则 hold。"""

    def __init__(self, *, llm=None, config: dict[str, Any] | None = None) -> None:
        self._llm = llm
        self._cfg = (config or {}).get("soul", {}).get("reflection", {})
        self._llm_enabled = bool(self._cfg.get("llm_enabled", True))

    @staticmethod
    def trace_line(msg: str) -> str:
        return f"[{REFLECTION}] {msg}"

    def score(
        self,
        text: str,
        *,
        kind: str = "narrative_line",
        values: dict[str, Any] | None = None,
        meta: dict[str, Any] | None = None,
    ) -> ScoredRound:
        seat_a_pass, doubts = rules_precheck(text, values=values, meta=meta)
        if not seat_a_pass:
            return ScoredRound(
                seat_a_pass=False,
                seat_b_score=0.0,
                seat_c_score=0.0,
                doubts=doubts,
                engine="rules",
            )

        if self._llm is not None and self._llm_enabled and self._llm.is_module_configured("left"):
            try:
                b = self._llm_seat(text, kind=kind, seat="skeptic", values=values)
                c = self._llm_seat(text, kind=kind, seat="integrator", values=values, blind_b=b)
                return ScoredRound(
                    seat_a_pass=True,
                    seat_b_score=b,
                    seat_c_score=c,
                    doubts=doubts,
                    engine="llm+rules",
                )
            except (LLMUnavailable, json.JSONDecodeError, KeyError, TypeError, ValueError):
                pass

        b = rules_seat_score(text, seat="skeptic", doubts=doubts, values=values)
        c = rules_seat_score(text, seat="integrator", doubts=doubts, values=values)
        # 规则-only 降档：不超过 hold 线，避免 silent adopt
        cap = float(self._cfg.get("rules_only_cap", 74))
        b = min(b, cap)
        c = min(c, cap)
        return ScoredRound(
            seat_a_pass=True,
            seat_b_score=b,
            seat_c_score=c,
            doubts=doubts + ["llm_unavailable"],
            engine="rules",
        )

    def _llm_seat(
        self,
        text: str,
        *,
        kind: str,
        seat: str,
        values: dict[str, Any] | None,
        blind_b: float | None = None,
    ) -> float:
        sl5 = values_summary(values or {})
        if seat == "skeptic":
            system = (
                "你是小意·疑。评估 thought_proposal 证据与隔离。输出 JSON："
                '{"score":0-100,"doubt":["…"]}'
            )
            user = f"kind={kind}\nSL5摘要：{sl5}\nproposal：{text}"
        else:
            system = (
                "你是小意·鉴。评估是否值得记入 SL4、与主体自洽。勿附和疑席。"
                '输出 JSON：{"score":0-100}'
            )
            user = f"kind={kind}\nSL5摘要：{sl5}\nproposal：{text}"
            if blind_b is not None:
                user += "\n（勿参考其他分数）"

        result = self._llm.complete(
            "left",
            [Message(role="system", content=system), Message(role="user", content=user)],
            json_mode=True,
            temperature=0.2,
        )
        parsed = json.loads(result.text)
        score = float(parsed.get("score", 0))
        return max(0.0, min(100.0, round(score, 2)))
