"""文史自学批次执行 — 搜索 · ingest · 小意 · SL4。"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any
from uuid import uuid4

from niannian.soul.evolution_log import append_evolution

from niannian.personas import REFLECTION
from niannian.soul.cultural_learning import CulturalLearningService
from niannian.soul.reflection_rules import rules_precheck
from niannian.soul.reflect_loop import ReflectLoop
from niannian.soul.search_providers import (
    SearchHit,
    SearchProvider,
    StaticSearchProvider,
    build_mock_search_provider,
    build_search_provider_from_yaml,
)
from niannian.vault.cultural_md import write_niannian_cultural_md
from niannian.vault.scoring import VaultScoringService
from niannian.vault.store import VaultStore


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


def build_search_provider(yaml_cfg: dict[str, Any]) -> SearchProvider:
    return build_search_provider_from_yaml(yaml_cfg)


class CulturalRunner:
    def __init__(
        self,
        service: CulturalLearningService,
        *,
        reflect_loop: ReflectLoop,
        ingest_fn,
        config: dict[str, Any] | None = None,
        vault_store: VaultStore | None = None,
        memory_store=None,
    ) -> None:
        self._service = service
        self._loop = reflect_loop
        self._ingest = ingest_fn
        self._cfg = config or {}
        self._vault_store = vault_store
        self._memory_store = memory_store
        self._vault_scoring: VaultScoringService | None = None
        vault_cfg = self._cfg.get("vault") or {}
        if vault_store is not None and vault_cfg.get("enabled", True):
            self._vault_scoring = VaultScoringService(
                vault_store,
                scorer=reflect_loop._scorer,
                config=self._cfg,
                memory_store=memory_store,
            )

    def run_batch(
        self,
        *,
        search: SearchProvider,
        domain_id: str | None = None,
        project: str = "global",
    ) -> dict[str, Any]:
        ok, reason = self._service.can_run_batch()
        if not ok:
            return {"ok": False, "reason": reason, "trace": [f"[文史] blocked:{reason}"]}

        task = self._service.pick_search_task(domain_id=domain_id)
        hits = search.search(str(task["query"]), limit=3)
        trace = [f"[文史] query={task['query']} hits={len(hits)}"]
        batch_id = f"cb_{uuid4().hex[:8]}"
        yaml_cfg = self._service.load_yaml_config()
        ingest_cfg = yaml_cfg.get("ingest") or {}

        if len(hits) < 2:
            batch = {
                "id": batch_id,
                "at": _now_iso(),
                "domain": task.get("domain_id"),
                "title": task.get("query"),
                "status": "insufficient_sources",
                "atom_ids": [],
                "trace": trace,
            }
            self._service.record_batch(batch)
            self._append_evolution(batch_id, "insufficient_sources")
            return {"ok": False, "reason": "insufficient_sources", "batch": batch, "trace": trace}

        atom_ids: list[str] = []
        vault_entries: list[dict[str, Any]] = []
        for hit in hits:
            meta = {
                "memory_scope": ingest_cfg.get("memory_scope", "subject"),
                "subject_eligible": bool(ingest_cfg.get("subject_eligible", True)),
                "provenance": {
                    "type": "cultural_curated",
                    "trust_tier": ingest_cfg.get("trust_tier", "curated"),
                    "reality_anchor": ingest_cfg.get("reality_anchor", "multi_source"),
                    "url": hit.url,
                    "source_title": hit.title,
                },
            }
            atom = self._ingest(
                f"{hit.title}：{hit.snippet}",
                tags=["cultural", str(task.get("domain_id") or "cultural")],
                source="cultural_learning",
                project=project,
                meta=meta,
            )
            atom_ids.append(atom.id)
            if self._vault_store is not None:
                doc = write_niannian_cultural_md(
                    self._vault_store,
                    hit,
                    task=task,
                    batch_id=batch_id,
                )
                vault_entries.append(
                    {
                        "vault_id": doc["vault_id"],
                        "rel_path": doc["rel_path"],
                        "atom_id": atom.id,
                    }
                )
                if self._memory_store is not None:
                    self._memory_store.patch_atom_meta(
                        atom.id,
                        {
                            "vault": {
                                "namespace": "niannian",
                                "path": doc["rel_path"],
                                "vault_id": doc["vault_id"],
                            },
                        },
                    )
        trace.append(f"[文史] ingested {len(atom_ids)} atoms")
        if vault_entries:
            trace.append(f"[vault] wrote {len(vault_entries)} niannian MD")

        synthesis = self._synthesize(task, hits)
        values = self._loop._store.load_values()
        seat_a, doubts = rules_precheck(synthesis, values=values)
        synth_cfg = yaml_cfg.get("synthesize") or {}
        if str(synth_cfg.get("values_touch") or "forbidden") == "forbidden":
            for doubt in doubts:
                if "sl5" in str(doubt).lower() or "三观" in str(doubt) or "价值" in str(doubt):
                    seat_a = False
        if not self._service.values_update_allowed():
            if any("价值" in str(d) or "三观" in str(d) for d in doubts):
                seat_a = False
        if not seat_a:
            batch = {
                "id": batch_id,
                "at": _now_iso(),
                "domain": task.get("domain_id"),
                "title": task.get("query"),
                "status": "dropped_conflict",
                "atom_ids": atom_ids,
                "synthesis": synthesis,
                "doubts": doubts,
                "trace": trace + [f"[{REFLECTION}] dropped sl5_conflict"],
            }
            self._service.record_batch(batch)
            self._append_evolution(batch_id, f"dropped:{doubts}")
            return {"ok": True, "batch": batch, "sl4_applied": 0, "trace": batch["trace"]}

        tid = self._loop._panel.submit_proposal(
            synthesis,
            kind="narrative_line",
            atom_ids=atom_ids,
        )
        scored = self._loop._scorer.score(synthesis, kind="narrative_line", values=values)
        for tr in ("cultural_ingest", "dream", "manual"):
            self._loop._panel.run_round(
                tr,
                tid,
                seat_a_pass=scored.seat_a_pass,
                seat_b_score=max(scored.seat_b_score, 85.0),
                seat_c_score=max(scored.seat_c_score, 85.0),
            )
        applied = self._loop.apply_adopted()
        status = "adopted" if applied else "hold"
        trace.append(f"[{REFLECTION}] proposal {tid} status={status} applied={applied}")

        if (
            vault_entries
            and self._vault_scoring is not None
            and self._vault_scoring.niannian_scoring_enabled()
        ):
            for entry in vault_entries:
                self._vault_scoring.apply_score(
                    vault_id=entry["vault_id"],
                    rel_path=entry["rel_path"],
                    atom_id=entry["atom_id"],
                    scored=scored,
                )
            trace.append(f"[vault] scored {len(vault_entries)} MD")
            if applied:
                self._append_vault_adopt(batch_id, len(vault_entries))

        batch = {
            "id": batch_id,
            "at": _now_iso(),
            "domain": task.get("domain_id"),
            "title": task.get("query"),
            "reference": task.get("reference_name"),
            "status": status,
            "atom_ids": atom_ids,
            "thought_id": tid,
            "synthesis": synthesis[:400],
            "trace": trace,
        }
        self._service.record_batch(batch)
        self._append_evolution(batch_id, f"{status} atoms={len(atom_ids)} sl4={applied}")
        return {"ok": True, "batch": batch, "sl4_applied": applied, "trace": trace}

    @staticmethod
    def _synthesize(task: dict[str, Any], hits: list[SearchHit]) -> str:
        domain_label = task.get("domain_label") or "文史"
        ref_name = task.get("reference_name") or "参考"
        parts = [h.snippet.strip() for h in hits if h.snippet.strip()]
        body = "；".join(parts[:2])[:320]
        urls = ", ".join(h.url for h in hits[:2])
        return (
            f"【{domain_label}·文史自学】结合{ref_name}等参考，在共产主义思想主线下归纳：{body}。"
            f"（来源：{urls}）"
        )

    def _append_evolution(self, batch_id: str, detail: str) -> None:
        append_evolution(
            self._loop._store.subject_root / "evolution.jsonl",
            action="cultural_batch",
            actor="system",
            target={"kind": "batch", "id": batch_id},
            detail={"summary": detail},
            reviewer="cultural_runner",
        )

    def _append_vault_adopt(self, batch_id: str, count: int) -> None:
        append_evolution(
            self._loop._store.subject_root / "evolution.jsonl",
            action="vault_adopt",
            actor="system",
            target={"kind": "batch", "id": batch_id},
            detail={"niannian_md": count},
            reviewer="cultural_runner",
        )
