"""联网自学批次 — 搜索 · allowlist · 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.reflection_rules import rules_precheck
from niannian.soul.reflect_loop import ReflectLoop
from niannian.soul.search_providers import (
    SearchProvider,
    filter_hits_by_allowlist,
)
from niannian.soul.web_learning import WebLearningService


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


class WebLearningRunner:
    def __init__(
        self,
        service: WebLearningService,
        *,
        reflect_loop: ReflectLoop,
        ingest_fn,
        config: dict[str, Any] | None = None,
        vault_store=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

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

        task = self._service.pick_search_task(topic_id=topic_id, query=query)
        yaml_cfg = self._service.load_yaml_config()
        search_cfg = yaml_cfg.get("search") or {}
        limit = int(search_cfg.get("max_results") or 5)
        min_hits = int(search_cfg.get("min_hits") or 2)
        allowlist = self._service.allowlist_patterns()

        raw_hits = search.search(str(task["query"]), limit=limit)
        hits = filter_hits_by_allowlist(raw_hits, allowlist)
        trace = [
            f"[联网] query={task['query']} raw={len(raw_hits)} allowed={len(hits)}",
        ]
        if len(raw_hits) > len(hits):
            trace.append(f"[联网] allowlist filtered {len(raw_hits) - len(hits)}")

        batch_id = f"wb_{uuid4().hex[:8]}"
        ingest_cfg = yaml_cfg.get("ingest") or {}

        if len(hits) < min_hits:
            batch = {
                "id": batch_id,
                "at": _now_iso(),
                "topic": task.get("topic_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]] = []
        vault_md = bool(ingest_cfg.get("vault_md", False))
        prov_type = ingest_cfg.get("provenance_type", "external_web")
        for hit in hits:
            meta = {
                "memory_scope": ingest_cfg.get("memory_scope", "subject"),
                "subject_eligible": bool(ingest_cfg.get("subject_eligible", True)),
                "provenance": {
                    "type": prov_type,
                    "trust_tier": ingest_cfg.get("trust_tier", "web"),
                    "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=["web", str(task.get("topic_id") or "web")],
                source="web_learning",
                project=project,
                meta=meta,
            )
            atom_ids.append(atom.id)
            if vault_md and self._vault_store is not None:
                from niannian.vault.web_md import write_niannian_web_md

                doc = write_niannian_web_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, yaml_cfg)
        values = self._loop._store.load_values()
        seat_a, doubts = rules_precheck(synthesis, values=values)
        if not seat_a:
            batch = {
                "id": batch_id,
                "at": _now_iso(),
                "topic": task.get("topic_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 ("web_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, 80.0),
                seat_c_score=max(scored.seat_c_score, 80.0),
            )
        applied = self._loop.apply_adopted()
        status = "adopted" if applied else "hold"
        trace.append(f"[{REFLECTION}] proposal {tid} status={status} applied={applied}")

        batch = {
            "id": batch_id,
            "at": _now_iso(),
            "topic": task.get("topic_id"),
            "title": task.get("query"),
            "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, yaml_cfg: dict[str, Any]) -> str:
        synth = yaml_cfg.get("synthesize") or {}
        label = synth.get("label") or "联网自学"
        topic_label = task.get("topic_label") or task.get("topic_id") or "通识"
        parts = [h.snippet.strip() for h in hits if h.snippet.strip()]
        body = "；".join(parts[:3])[:360]
        urls = ", ".join(h.url for h in hits[:3])
        return (
            f"【{label}·{topic_label}】综合多源检索归纳：{body}。"
            f"（来源：{urls}）"
        )

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