"""VirtualPersonaStore — users/{user_id}/virtual_personas/{vp_id}/。"""

from __future__ import annotations

import io
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import yaml

from niannian.users.avatar import AVATAR_SIZE, MAX_SIZE
from niannian.users.paths import virtual_personas_root
from niannian.virtual_persona.profile_schema import (
    REQUIRED_PROFILE_SECTIONS,
    VirtualPersonaValidationError,
    default_profile_md,
    validate_profile_sections,
)

_VP_ID_RE = re.compile(r"^[\w\-]{1,64}$")


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


def _sanitize_vp_id(vp_id: str) -> str:
    vid = str(vp_id or "").strip()
    if not vid or not _VP_ID_RE.match(vid):
        raise ValueError(f"invalid vp_id: {vp_id!r}")
    return vid


def vp_proactive_profile_path(home: Path, user_id: str, vp_id: str) -> Path:
    """users/{user}/virtual_personas/{vp_id}/memory/proactive_profile.json"""
    return (
        virtual_personas_root(home, user_id)
        / _sanitize_vp_id(vp_id)
        / "memory"
        / "proactive_profile.json"
    )


class VirtualPersonaStore:
    """users/{user_id}/virtual_personas/{vp_id}/ — profile.md + meta.yaml。"""

    def __init__(self, home: Path, user_id: str = "pinn") -> None:
        self._home = Path(home)
        self._user_id = str(user_id).strip() or "pinn"
        self._root = virtual_personas_root(self._home, self._user_id)

    @property
    def user_id(self) -> str:
        return self._user_id

    @property
    def root(self) -> Path:
        return self._root

    def vp_dir(self, vp_id: str) -> Path:
        return self._root / _sanitize_vp_id(vp_id)

    def memory_dir(self, vp_id: str) -> Path:
        return self.vp_dir(vp_id) / "memory"

    def proactive_profile_path(self, vp_id: str) -> Path:
        return vp_proactive_profile_path(self._home, self._user_id, vp_id)

    def avatar_path(self, vp_id: str) -> Path:
        return self.vp_dir(vp_id) / "avatar.png"

    def assembly_master_path(self, vp_id: str) -> Path:
        return self.vp_dir(vp_id) / "assembly_master.md"

    def has_avatar(self, vp_id: str) -> bool:
        return self.avatar_path(vp_id).is_file()

    def save_avatar(
        self,
        vp_id: str,
        data: bytes,
        *,
        kind: str = "upload",
        source_url: str | None = None,
    ) -> Path:
        if not data:
            raise ValueError("avatar empty")
        if len(data) > MAX_SIZE:
            raise ValueError(f"avatar exceeds {MAX_SIZE} bytes")

        try:
            from PIL import Image
        except ImportError as exc:
            raise RuntimeError("Pillow required for avatar resize") from exc

        try:
            image = Image.open(io.BytesIO(data))
            image.load()
        except OSError as exc:
            raise ValueError("invalid image format") from exc

        image = image.convert("RGBA")
        image = image.resize((AVATAR_SIZE, AVATAR_SIZE), Image.Resampling.LANCZOS)

        dest = self._require_vp_dir(vp_id)
        path = self.avatar_path(vp_id)
        path.parent.mkdir(parents=True, exist_ok=True)

        buf = io.BytesIO()
        image.save(buf, format="PNG", optimize=True)
        path.write_bytes(buf.getvalue())

        meta = self._load_meta(dest)
        now = _now_iso()
        avatar_meta: dict[str, Any] = {
            "kind": kind,
            "updated_at": now,
        }
        if source_url:
            avatar_meta["source_url"] = str(source_url)
        if kind == "web_search":
            avatar_meta["fetched_at"] = now
        meta["avatar"] = avatar_meta
        meta["updated_at"] = now
        self._write_meta(dest, meta)
        return path

    def delete_avatar(self, vp_id: str) -> None:
        dest = self._require_vp_dir(vp_id)
        path = self.avatar_path(vp_id)
        if path.is_file():
            path.unlink()
        meta = self._load_meta(dest)
        meta["avatar"] = {"kind": "none", "updated_at": _now_iso()}
        meta["updated_at"] = _now_iso()
        self._write_meta(dest, meta)

    def list_personas(self) -> list[dict[str, Any]]:
        if not self._root.is_dir():
            return []
        rows: list[dict[str, Any]] = []
        for child in sorted(self._root.iterdir()):
            if not child.is_dir():
                continue
            meta_path = child / "meta.yaml"
            if not meta_path.is_file():
                continue
            try:
                meta = self._load_meta_file(meta_path)
            except (OSError, yaml.YAMLError):
                continue
            rows.append(self._meta_summary(meta, child.name))
        return rows

    def create(self, vp_id: str, *, label: str) -> dict[str, Any]:
        vid = _sanitize_vp_id(vp_id)
        dest = self.vp_dir(vid)
        if dest.exists():
            raise FileExistsError(f"virtual persona already exists: {vid}")
        dest.mkdir(parents=True, exist_ok=False)
        (dest / "sources").mkdir(exist_ok=True)
        (dest / "memory").mkdir(exist_ok=True)
        from niannian.virtual_persona.pack import ensure_pack_on_create
        from niannian.virtual_persona.stickers import ensure_vp_sticker_dir

        ensure_vp_sticker_dir(self, vid)
        ensure_pack_on_create(self, vid)
        profile = default_profile_md(label)
        validate_profile_sections(profile)
        (dest / "profile.md").write_text(profile, encoding="utf-8")
        now = _now_iso()
        meta = {
            "id": vid,
            "label": str(label or vid).strip() or vid,
            "owner_user_id": self._user_id,
            "storage_tier": "local",
            "created_at": now,
            "updated_at": now,
            "last_enriched_at": None,
            "sources": [],
        }
        self._write_meta(dest, meta)
        return self.get(vid)

    def get(self, vp_id: str) -> dict[str, Any]:
        dest = self._require_vp_dir(vp_id)
        profile_md = (dest / "profile.md").read_text(encoding="utf-8")
        meta = self._load_meta(dest)
        return {
            "id": meta["id"],
            "label": meta.get("label") or meta["id"],
            "owner_user_id": meta.get("owner_user_id") or self._user_id,
            "storage_tier": meta.get("storage_tier") or "local",
            "created_at": meta.get("created_at"),
            "updated_at": meta.get("updated_at"),
            "last_enriched_at": meta.get("last_enriched_at"),
            "sources": list(meta.get("sources") or []),
            "profile_md": profile_md,
            "path": str(dest),
            "has_avatar": self.has_avatar(vp_id),
            "sticker_count": self._sticker_count(vp_id),
        }

    def update(
        self,
        vp_id: str,
        *,
        profile_md: str | None = None,
        label: str | None = None,
        storage_tier: str | None = None,
        stage: str | None = None,
    ) -> dict[str, Any]:
        dest = self._require_vp_dir(vp_id)
        meta = self._load_meta(dest)
        if profile_md is not None:
            validate_profile_sections(profile_md)
            (dest / "profile.md").write_text(profile_md, encoding="utf-8")
        if label is not None:
            meta["label"] = str(label).strip() or meta.get("label") or meta["id"]
        if storage_tier is not None:
            tier = str(storage_tier).strip().lower()
            if tier not in ("local", "cloud"):
                raise VirtualPersonaValidationError(f"invalid storage_tier: {storage_tier!r}")
            meta["storage_tier"] = tier
        if stage is not None:
            from niannian.virtual_persona.pack import read_pack, validate_stage, write_pack

            pack = read_pack(self, vp_id)
            pack["stage"] = validate_stage(stage)
            pack["updated_at"] = _now_iso()
            write_pack(self, vp_id, pack)
        meta["updated_at"] = _now_iso()
        self._write_meta(dest, meta)
        return self.get(vp_id)

    def delete(self, vp_id: str) -> None:
        dest = self._require_vp_dir(vp_id)
        shutil.rmtree(dest)

    def apply_enrich(
        self,
        vp_id: str,
        *,
        profile_md: str,
        sources: list[dict[str, Any]],
        source_files: list[dict[str, Any]] | None = None,
        query: str | None = None,
    ) -> dict[str, Any]:
        dest = self._require_vp_dir(vp_id)
        validate_profile_sections(profile_md)
        (dest / "profile.md").write_text(profile_md, encoding="utf-8")
        meta = self._load_meta(dest)
        meta["sources"] = list(sources)
        meta["last_enriched_at"] = _now_iso()
        meta["updated_at"] = _now_iso()
        if query:
            meta["last_enrich_query"] = str(query)
        if source_files:
            meta["source_files"] = list(source_files)
        self._write_meta(dest, meta)
        return self.get(vp_id)

    def export_bundle(self, vp_id: str) -> Path:
        """返回 VP 目录路径，供 cloud_sync 复制。"""
        return self._require_vp_dir(vp_id)

    def export_bundle_versions_dir(self, vp_id: str) -> Path:
        """virtual_personas/{vp_id}/export_bundle/ — perfect-distill 版本目录。"""
        return self.vp_dir(vp_id) / "export_bundle"

    def ensure_export_bundle_dir(self, vp_id: str) -> Path:
        """Ensure export_bundle/ exists for perfect-distill writes."""
        self._require_vp_dir(vp_id)
        root = self.export_bundle_versions_dir(vp_id)
        root.mkdir(parents=True, exist_ok=True)
        return root

    def set_storage_tier(self, vp_id: str, tier: str) -> dict[str, Any]:
        return self.update(vp_id, storage_tier=tier)

    def _require_vp_dir(self, vp_id: str) -> Path:
        dest = self.vp_dir(vp_id)
        if not dest.is_dir() or not (dest / "meta.yaml").is_file():
            raise FileNotFoundError(f"virtual persona not found: {vp_id}")
        return dest

    def _load_meta(self, dest: Path) -> dict[str, Any]:
        return self._load_meta_file(dest / "meta.yaml")

    def _load_meta_file(self, path: Path) -> dict[str, Any]:
        with path.open(encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
        if not isinstance(data, dict):
            raise VirtualPersonaValidationError("meta.yaml must be a mapping")
        if not data.get("id"):
            data["id"] = path.parent.name
        return data

    def _write_meta(self, dest: Path, meta: dict[str, Any]) -> None:
        dest.mkdir(parents=True, exist_ok=True)
        with (dest / "meta.yaml").open("w", encoding="utf-8") as f:
            yaml.safe_dump(meta, f, allow_unicode=True, sort_keys=False)

    def _meta_summary(self, meta: dict[str, Any], vp_id: str | None = None) -> dict[str, Any]:
        vid = vp_id or str(meta.get("id") or "")
        return {
            "id": meta.get("id"),
            "label": meta.get("label") or meta.get("id"),
            "owner_user_id": meta.get("owner_user_id"),
            "storage_tier": meta.get("storage_tier") or "local",
            "updated_at": meta.get("updated_at"),
            "last_enriched_at": meta.get("last_enriched_at"),
            "has_avatar": self.has_avatar(vid) if vid else False,
            "sticker_count": self._sticker_count(vid) if vid else 0,
        }

    def _sticker_count(self, vp_id: str) -> int:
        from niannian.virtual_persona.stickers import vp_sticker_count

        return vp_sticker_count(self, vp_id)
