"""PersonaPack 注册表 — 读/写 persona_registry.yaml · 构建 PersonaPack。"""

from __future__ import annotations

import shutil
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import yaml

from niannian.cognitive.planes import PlanePack, Regime
from niannian.users.paths import persona_registry_path as user_persona_registry_path


@dataclass(frozen=True)
class PlanesBias:
    active: frozenset[str] = frozenset()
    skip: frozenset[str] = frozenset()


@dataclass(frozen=True)
class PersonaPack:
    preset_id: str
    label: str
    prompt_block: str
    planes_bias: PlanesBias
    detached: bool = False


def _registry_path(config: dict[str, Any], home: Path | None = None) -> Path:
    cognitive = config.get("cognitive") or {}
    raw = cognitive.get("persona_registry") or "config/cognitive/persona_registry.yaml"
    path = Path(raw)
    if path.is_file():
        return path
    if home is not None:
        candidate = home / raw
        if candidate.is_file():
            return candidate
    return path


def _persona_seed_path(config: dict[str, Any], home: Path | None = None) -> Path:
    cognitive = config.get("cognitive") or {}
    raw = cognitive.get("persona_registry_seed") or "config/cognitive/persona_registry.seed.yaml"
    path = Path(raw)
    if path.is_file():
        return path
    if home is not None:
        candidate = home / raw
        if candidate.is_file():
            return candidate
    return _registry_path(config, home)


def _empty_bias() -> PlanesBias:
    return PlanesBias()


def _parse_bias(raw: Any) -> PlanesBias:
    if not isinstance(raw, dict):
        return _empty_bias()
    active_raw = raw.get("active") or []
    skip_raw = raw.get("skip") or []
    active = frozenset(str(x) for x in active_raw if isinstance(x, str))
    skip = frozenset(str(x) for x in skip_raw if isinstance(x, str))
    return PlanesBias(active=active, skip=skip)


def merge_planes_bias(base: PlanePack, bias: PlanesBias, *, required: frozenset[str] | None = None) -> PlanePack:
    """合并 persona planes_bias；required_planes 不可从 active 移除。"""
    req = required or frozenset({"P1", "P2", "P3"})
    active = set(base.active)
    skip = set(base.skip)
    if bias.active:
        active |= set(bias.active)
        skip -= set(bias.active)
    if bias.skip:
        skip |= set(bias.skip)
        active -= set(bias.skip)
    for pid in req:
        active.add(pid)
        skip.discard(pid)
    return PlanePack(regime=base.regime, active=frozenset(active), skip=frozenset(skip))


class PersonaRegistry:
    def __init__(
        self,
        config: dict[str, Any],
        *,
        home: Path | None = None,
        registry_path: Path | None = None,
    ) -> None:
        self._config = config
        self._home = home
        if registry_path is not None:
            self._path = Path(registry_path)
        else:
            self._path = _registry_path(config, home)

    @property
    def path(self) -> Path:
        return self._path

    def exists(self) -> bool:
        return self._path.is_file()

    def load(self) -> dict[str, Any]:
        if not self._path.is_file():
            return {"presets": {}, "modules": {}, "rules": {}}
        with self._path.open(encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
        return data if isinstance(data, dict) else {"presets": {}, "modules": {}, "rules": {}}

    def save(self, data: dict[str, Any]) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        backup_dir = self._path.parent.parent / "self" / "history"
        if self._path.is_file():
            backup_dir.mkdir(parents=True, exist_ok=True)
            stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
            shutil.copy2(self._path, backup_dir / f"persona_registry.{stamp}.yaml")
        self._path.write_text(yaml.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8")

    def _rules(self) -> dict[str, Any]:
        data = self.load()
        rules = data.get("rules")
        return rules if isinstance(rules, dict) else {}

    def detach_preset_id(self) -> str:
        return str(self._rules().get("detach_preset") or "plain")

    def required_planes(self) -> frozenset[str]:
        raw = self._rules().get("required_planes") or ["P1", "P2", "P3"]
        return frozenset(str(x) for x in raw if isinstance(x, str))

    def list_presets(self) -> dict[str, Any]:
        data = self.load()
        presets = data.get("presets")
        return presets if isinstance(presets, dict) else {}

    def list_modules(self) -> dict[str, Any]:
        data = self.load()
        modules = data.get("modules")
        return modules if isinstance(modules, dict) else {}

    def get_preset(self, name: str) -> dict[str, Any] | None:
        preset = self.list_presets().get(name)
        return preset if isinstance(preset, dict) else None

    def build_persona_pack(self, preset_id: str | None) -> PersonaPack:
        detach = self.detach_preset_id()
        pid = (preset_id or detach).strip() or detach
        if pid == detach:
            preset = self.get_preset(detach) or {}
            label = str(preset.get("label") or "无 overlay")
            return PersonaPack(
                preset_id=detach,
                label=label,
                prompt_block="",
                planes_bias=_empty_bias(),
                detached=True,
            )
        preset = self.get_preset(pid)
        if preset is None:
            return self.build_persona_pack(detach)
        modules = self.list_modules()
        module_ids = preset.get("modules") or []
        blocks: list[str] = []
        if isinstance(module_ids, list):
            for mid in module_ids:
                mod = modules.get(str(mid)) if isinstance(modules, dict) else None
                if isinstance(mod, dict):
                    block = mod.get("prompt_block")
                    if isinstance(block, str) and block.strip():
                        blocks.append(block.strip())
        bias = _parse_bias(preset.get("planes_bias"))
        return PersonaPack(
            preset_id=pid,
            label=str(preset.get("label") or pid),
            prompt_block="\n".join(blocks),
            planes_bias=bias,
            detached=False,
        )

    def preview(self, *, preset: str, regime: Regime = "companion") -> dict[str, Any]:
        from niannian.soul.assembly import build_persona_assembly_block

        pack = self.build_persona_pack(preset)
        block = build_persona_assembly_block(pack) if not pack.detached else ""
        return {
            "preset": pack.preset_id,
            "label": pack.label,
            "detached": pack.detached,
            "prompt_block": pack.prompt_block,
            "planes_bias": {
                "active": sorted(pack.planes_bias.active),
                "skip": sorted(pack.planes_bias.skip),
            },
            "assembly_block": block,
            "regime": regime,
        }

    def to_api_dict(self) -> dict[str, Any]:
        data = self.load()
        return {
            "presets": self.list_presets(),
            "modules": self.list_modules(),
            "rules": data.get("rules") if isinstance(data.get("rules"), dict) else {},
            "registry_path": str(self._path),
        }


def ensure_user_registry(
    home: Path,
    user_id: str,
    config: dict[str, Any],
    *,
    seed_path: Path | None = None,
) -> Path:
    """确保 users/{user_id}/persona/persona_registry.yaml 存在；缺失时从 seed 复制。"""
    dest = user_persona_registry_path(home, user_id)
    if dest.is_file():
        return dest
    seed = seed_path or _persona_seed_path(config, home)
    if not seed.is_file():
        raise FileNotFoundError(f"persona registry seed not found: {seed}")
    dest.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(seed, dest)
    return dest


def persona_registry_for_user(
    home: Path,
    user_id: str,
    config: dict[str, Any],
    *,
    seed_if_missing: bool = True,
) -> PersonaRegistry:
    path = user_persona_registry_path(home, user_id)
    if seed_if_missing and not path.is_file():
        ensure_user_registry(home, user_id, config)
    return PersonaRegistry(config, home=home, registry_path=path)


def resolve_persona_pack(preset_id: str | None, config: dict[str, Any] | None, *, home: Path | None = None) -> PersonaPack:
    if not config:
        return PersonaPack(
            preset_id="plain",
            label="无 overlay",
            prompt_block="",
            planes_bias=_empty_bias(),
            detached=True,
        )
    reg = PersonaRegistry(config, home=home)
    if reg.exists():
        return reg.build_persona_pack(preset_id)
    return PersonaPack(
        preset_id="plain",
        label="无 overlay",
        prompt_block="",
        planes_bias=_empty_bias(),
        detached=True,
    )
