"""ReflectLoop — dream 后提案 + 三思 Panel + SL4 apply。"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any

import yaml

from niannian.soul.evolution_log import append_evolution
from niannian.soul.firewall import check_f9_sl_adopt, firewall_trace_line, f9_pending_verification, is_subject_summary
from niannian.soul.reflection import ReflectionPanel, TriggerKind
from niannian.soul.reflection_scorer import TriReviewScorer
from niannian.soul.subject_store import SubjectStateStore


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


class ReflectLoop:
    """异步进化门禁 — 由 dream / cultural_ingest / core_correct 触发。"""

    def __init__(
        self,
        store: SubjectStateStore,
        *,
        scorer: TriReviewScorer | None = None,
        config: dict[str, Any] | None = None,
    ) -> None:
        self._store = store
        self._panel = ReflectionPanel(store)
        self._scorer = scorer or TriReviewScorer(config=config)
        soul_cfg = (config or {}).get("soul", {}).get("reflection", {})
        self._max_proposals_per_dream = int(soul_cfg.get("max_proposals_per_dream", 3))

    def after_dream(
        self,
        summaries: dict[str, Any],
        *,
        subject_atom_ids: list[str] | None = None,
    ) -> dict[str, Any]:
        """dream 后：从 tag 摘要生成 proposal + 跑一轮 Panel。"""
        trace: list[str] = []
        proposed = 0
        ids: list[str] = list(subject_atom_ids or [])
        for key, item in list(summaries.items())[: self._max_proposals_per_dream]:
            if not isinstance(item, dict):
                continue
            if not is_subject_summary(str(key), item):
                continue
            if item.get("f9_sl_adopt_ok") is False:
                trace.append(firewall_trace_line(f"skip proposal {key}: f9_blocked"))
                continue
            text = str(item.get("text") or "").strip()
            if len(text) < 10:
                continue
            tid = self._panel.submit_proposal(
                f"Dream 归纳（{key}）：{text[:240]}",
                kind="narrative_line",
                atom_ids=ids[:5],
            )
            proposed += 1
            trace.append(TriReviewScorer.trace_line(f"proposal {tid} from {key}"))

        review = self.process_pending("dream")
        trace.extend(review.get("trace", []))
        applied = self.apply_adopted()
        if applied:
            trace.append(TriReviewScorer.trace_line(f"SL4 applied {applied} lines"))
        return {"proposed": proposed, "review": review, "sl4_applied": applied, "trace": trace}

    def process_pending(self, trigger: TriggerKind) -> dict[str, Any]:
        data = self._panel._load()
        values = self._store.load_values()
        trace: list[str] = []
        results: list[dict[str, Any]] = []
        for thought in data.get("pending_thoughts") or []:
            if not isinstance(thought, dict) or thought.get("status") != "pending":
                continue
            tid = str(thought.get("id"))
            scored = self._scorer.score(
                str(thought.get("text") or ""),
                kind=str(thought.get("kind") or "narrative_line"),
                values=values,
            )
            round_result = self._panel.run_round(
                trigger,
                tid,
                seat_a_pass=scored.seat_a_pass,
                seat_b_score=scored.seat_b_score,
                seat_c_score=scored.seat_c_score,
            )
            trace.append(
                TriReviewScorer.trace_line(
                    f"{tid} r={round_result.round} score={round_result.composite_score} "
                    f"status={round_result.status} engine={scored.engine}"
                )
            )
            results.append(
                {
                    "thought_id": tid,
                    "status": round_result.status,
                    "round": round_result.round,
                    "message": round_result.message,
                    "engine": scored.engine,
                }
            )
        return {"trigger": trigger, "results": results, "trace": trace}

    def apply_adopted(self, *, atom_metas: dict[str, dict[str, Any]] | None = None) -> int:
        """adopted proposal → narrative.yaml lines。"""
        data = self._panel._load()
        narrative = self._store.load_narrative()
        lines: list = list(narrative.get("lines") or [])
        applied = 0
        panel_dirty = False
        for thought in data.get("pending_thoughts") or []:
            if not isinstance(thought, dict) or thought.get("status") != "adopted":
                continue
            atom_ids = [str(aid) for aid in (thought.get("atom_ids") or []) if aid]
            resolved = self._resolve_atom_metas(atom_ids, atom_metas or {})
            if atom_ids:
                if any(aid not in resolved for aid in atom_ids):
                    thought["status"] = "abandoned"
                    thought["f9_blocked"] = True
                    panel_dirty = True
                    continue
                blocked = any(not check_f9_sl_adopt(resolved[aid]) for aid in atom_ids)
                if blocked:
                    thought["status"] = "abandoned"
                    thought["f9_blocked"] = True
                    panel_dirty = True
                    continue
            text = str(thought.get("text") or "").strip()
            if not text:
                continue
            entry = {
                "text": text,
                "thought_id": thought.get("id"),
                "atom_ids": thought.get("atom_ids") or [],
                "applied_at": _now_iso(),
                "source": "xiao_yi_adopt",
            }
            if atom_ids and any(f9_pending_verification(resolved[aid]) for aid in atom_ids):
                entry["verification_status"] = "pending"
            lines.append(entry)
            applied += 1
            thought["status"] = "merged"

        if applied:
            narrative["lines"] = lines
            path = self._store.subject_root / "narrative.yaml"
            path.write_text(yaml.safe_dump(narrative, allow_unicode=True, sort_keys=False), encoding="utf-8")
            self._append_evolution(f"sl4_apply count={applied}")
        if applied or panel_dirty:
            self._panel._save(data)
        return applied

    def _resolve_atom_metas(
        self,
        atom_ids: list[str],
        metas: dict[str, dict[str, Any]],
    ) -> dict[str, dict[str, Any]]:
        missing = [aid for aid in atom_ids if aid not in metas]
        if not missing:
            return metas
        from niannian.storage.local import LocalMemoryStore

        store = LocalMemoryStore(self._store._home)
        by_id = {a.id: dict(a.meta or {}) for a in store.read_atoms()}
        out = dict(metas)
        for aid in missing:
            if aid in by_id:
                out[aid] = by_id[aid]
        return out

    def _append_evolution(self, detail: str) -> None:
        append_evolution(
            self._store.subject_root / "evolution.jsonl",
            action="reflect_adopt",
            actor="system",
            target={"kind": "sl", "id": "narrative"},
            detail={"summary": detail},
            reviewer="xiao_yi",
        )
