"""Memory backup and wipe — v0.22 D22-M2."""

from __future__ import annotations

import json
import shutil
import tarfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Literal

from niannian.soul.subject_maturity import refresh_subject_matured

WipeScope = Literal["atoms_wiki", "atoms_wiki_vault", "full_except_subject"]

_BACKUP_DIR = "backups"
_ATOMS_WIKI_PATHS = (
    "left/atoms.jsonl",
    "cerebellum/pages.json",
    "cerebellum/graph.json",
    "cerebellum/compile_state.json",
    "hippocampus/summaries.json",
    "hippocampus/lifecycle.json",
    "sync/pending.jsonl",
)
_VAULT_PATHS = (
    "vault/_index.json",
    "vault/_embeddings.jsonl",
    "vault/_fts.sqlite",
)


class MemoryOpsError(Exception):
    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code
        self.message = message


def _now_ts() -> str:
    return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")


def _backup_dir(home: Path) -> Path:
    path = home / _BACKUP_DIR
    path.mkdir(parents=True, exist_ok=True)
    return path


def _paths_for_scope(home: Path, scope: WipeScope) -> list[Path]:
    rels = list(_ATOMS_WIKI_PATHS)
    if scope in ("atoms_wiki_vault", "full_except_subject"):
        rels.extend(_VAULT_PATHS)
        vault_user = home / "vault" / "user"
        if vault_user.is_dir():
            for p in vault_user.rglob("*"):
                if p.is_file():
                    rels.append(str(p.relative_to(home)))
    if scope == "full_except_subject":
        users = home / "users"
        if users.is_dir():
            for p in users.rglob("*"):
                if p.is_file():
                    rels.append(str(p.relative_to(home)))
    out: list[Path] = []
    seen: set[str] = set()
    for rel in rels:
        if rel in seen:
            continue
        seen.add(rel)
        path = home / rel
        if path.is_file():
            out.append(path)
    return out


def create_memory_backup(home: Path, *, scope: WipeScope = "full_except_subject") -> dict[str, Any]:
    backup_id = f"memory_{_now_ts()}"
    archive_path = _backup_dir(home) / f"{backup_id}.tar.gz"
    files = _paths_for_scope(home, scope)
    with tarfile.open(archive_path, "w:gz") as tar:
        for path in files:
            tar.add(path, arcname=str(path.relative_to(home)))
    return {
        "backup_id": backup_id,
        "path": str(archive_path.relative_to(home)),
        "files": len(files),
        "scope": scope,
    }


def _reset_file(path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    name = path.name
    if name.endswith(".jsonl"):
        path.write_text("", encoding="utf-8")
    elif name == "atoms.jsonl":
        path.write_text("", encoding="utf-8")
    elif name.endswith(".json"):
        if "summaries" in name:
            path.write_text("{}\n", encoding="utf-8")
        elif "lifecycle" in name:
            path.write_text("{}\n", encoding="utf-8")
        elif "_index" in name:
            path.write_text('{"version": 1, "entries": []}\n', encoding="utf-8")
        else:
            path.write_text("{}\n", encoding="utf-8")
    elif name.endswith(".sqlite"):
        path.unlink(missing_ok=True)
    else:
        path.unlink(missing_ok=True)


def _clear_vault_user(home: Path) -> int:
    vault_user = home / "vault" / "user"
    count = 0
    if vault_user.is_dir():
        for p in vault_user.rglob("*"):
            if p.is_file():
                p.unlink()
                count += 1
    idx = home / "vault" / "_index.json"
    if idx.is_file():
        idx.write_text('{"version": 1, "entries": []}\n', encoding="utf-8")
    return count


def _clear_users_ephemeral(home: Path) -> int:
    count = 0
    users = home / "users"
    if not users.is_dir():
        return 0
    for sub in ("chat_attachments", "stickers"):
        for p in users.rglob(f"*/{sub}/*"):
            if p.is_file():
                p.unlink()
                count += 1
            elif p.is_dir() and not any(p.iterdir()):
                p.rmdir()
    return count


def wipe_memory(
    home: Path,
    *,
    scope: WipeScope,
    confirm: str,
    username: str,
    backup_id: str | None = None,
) -> dict[str, Any]:
    maturity = refresh_subject_matured(home)
    if maturity.get("subject_matured"):
        raise MemoryOpsError("personality_locked", "subject matured — wipe forbidden")

    expected = f"WIPE-{username}"
    if confirm.strip() != expected:
        raise MemoryOpsError("confirm_mismatch", f"confirm must be {expected!r}")

    backup_dir = _backup_dir(home)
    if backup_id:
        archive = backup_dir / f"{backup_id}.tar.gz"
        if not archive.is_file():
            raise MemoryOpsError("backup_not_found", f"backup {backup_id!r} not found")
    else:
        latest = sorted(backup_dir.glob("memory_*.tar.gz"))
        if not latest:
            raise MemoryOpsError("backup_required", "create backup before wipe")
        archive = latest[-1]

    cleared: list[str] = []
    for rel in _ATOMS_WIKI_PATHS:
        path = home / rel
        if path.is_file():
            _reset_file(path)
            cleared.append(rel)

    vault_files = 0
    if scope in ("atoms_wiki_vault", "full_except_subject"):
        for rel in _VAULT_PATHS:
            path = home / rel
            if path.is_file():
                _reset_file(path)
                cleared.append(rel)
        vault_files = _clear_vault_user(home)

    user_files = 0
    if scope == "full_except_subject":
        user_files = _clear_users_ephemeral(home)

    manifest_path = home / "sync" / "manifest.json"
    if manifest_path.is_file():
        try:
            data = json.loads(manifest_path.read_text(encoding="utf-8"))
            if isinstance(data, dict):
                data["version"] = int(data.get("version") or 0) + 1
                data["updated_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
                manifest_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        except (OSError, json.JSONDecodeError, TypeError, ValueError):
            pass

    return {
        "scope": scope,
        "backup_id": archive.stem,
        "cleared_paths": cleared,
        "vault_user_files_removed": vault_files,
        "user_ephemeral_files_removed": user_files,
    }
