"""Data-only deletion separates user state from the runtime that must survive."""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import stat

from pm.filesystem import is_junction


@dataclass(frozen=True)
class DataRemovalPlan:
    home: Path
    remove: tuple[Path, ...]
    keep: tuple[Path, ...]


def plan_data_removal(home: Path, project: Path, userdata: Path | None = None) -> DataRemovalPlan:
    from hermes_constants import get_default_hermes_root
    from pm.environments import base_venv, installs_root, store_root
    from hermes_cli.steward import is_bundled_payload
    from tools.checkpoint_pruning import store_lock_path

    home = home.resolve()
    if home == Path(home.anchor) or home == Path.home().resolve():
        raise ValueError(f"refusing to erase an entire filesystem or user home: {home}")
    machine = get_default_hermes_root(home=home).resolve()
    protected = {
        project.resolve(), base_venv(project).resolve(), store_root(project).resolve(),
        installs_root().resolve(), machine / "tools", machine / "bin", machine / "cache",
        machine / "spawn-ledger.json", home / "gateway.lock", home / ".backup.lock",
        home / "runtime" / "active_sessions.lock", home / "cron" / ".tick.lock",
        store_lock_path(home / "checkpoints"),
    }
    if home == machine:
        protected.add(machine / "profiles")
    if is_bundled_payload(project):
        from hermes_cli.bundled_app import NotBundledApp, resolve_bundle_layout

        try:
            protected.add(resolve_bundle_layout(project).app_root)
        except NotBundledApp as exc:
            raise ValueError(str(exc)) from exc
    # The canonical source installation may coexist with the invoking package.
    canonical = home / "hermes-agent"
    if (canonical / "hermes_cli" / "__init__.py").is_file():
        protected.add(canonical)
    remove: list[Path] = []
    keep: set[Path] = set()

    def visit(path: Path) -> None:
        try:
            mode = path.lstat().st_mode
        except FileNotFoundError:
            return
        resolved = path.resolve()
        if any(path == root or resolved == root or path.is_relative_to(root) for root in protected):
            keep.add(path)
        elif path.is_symlink() or is_junction(path):
            remove.append(path)  # Remove the directory entry, never its target.
        elif any(root.is_relative_to(path) for root in protected):
            if stat.S_ISDIR(mode):
                for child in sorted(path.iterdir()):
                    visit(child)
            else:
                keep.add(path)
        else:
            remove.append(path)

    if home.is_dir():
        for child in sorted(home.iterdir()):
            visit(child)
    if home != machine:
        userdata = None  # Desktop preferences belong to the app, not a named profile.
    if userdata is not None and not userdata.resolve().is_relative_to(home):
        if userdata.resolve() == Path.home().resolve() or home.is_relative_to(userdata.resolve()):
            raise ValueError(f"desktop data directory must not contain the Hermes or user home: {userdata}")
        visit(userdata.parent.resolve() / userdata.name)
    return DataRemovalPlan(home, tuple(remove), tuple(sorted(keep)))


def remove_data(plan: DataRemovalPlan) -> tuple[list[Path], list[tuple[Path, str]]]:
    import shutil

    removed: list[Path] = []
    failed: list[tuple[Path, str]] = []
    for path in plan.remove:
        try:
            if path.parent.resolve() != path.parent:
                raise OSError("parent directory changed after confirmation; refusing to follow a link")
            mode = path.lstat().st_mode
            if path.is_symlink() or not stat.S_ISDIR(mode):
                path.unlink()
            elif is_junction(path):
                path.rmdir()
            else:
                shutil.rmtree(path)
            removed.append(path)
        except FileNotFoundError:
            continue
        except OSError as exc:
            failed.append((path, str(exc)))
    return removed, failed
