# platform/deskclaw/experiment/workspace.py
# ═══════════════════════════════════════════════════════════════
# WorkspaceBuilder — Isolated experiment workspaces with APFS COW
# ═══════════════════════════════════════════════════════════════

from __future__ import annotations

import logging
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any

from deskclaw.experiment.models import ExperimentError

logger = logging.getLogger("deskclaw.experiment.workspace")


class WorkspaceError(ExperimentError):
    """Workspace creation or cleanup failed."""


class WorkspaceBuilder:
    """Creates isolated experiment workspaces with APFS COW cloning and uv venv support."""

    def __init__(
        self,
        base_dir: str | Path | None = None,
        source_dir: str | Path | None = None,
        *,
        use_venv: bool = False,
        requirements: list[str] | None = None,
    ) -> None:
        self._base_dir = (
            Path(base_dir)
            if base_dir
            else Path(tempfile.mkdtemp(prefix="deskclaw-exp-"))
        )
        self._source_dir = Path(source_dir) if source_dir else None
        self._use_venv = use_venv
        self._requirements = requirements or []
        self._workspaces: list[Path] = []
        self._owns_base = base_dir is None

    def __enter__(self) -> WorkspaceBuilder:
        self._base_dir.mkdir(parents=True, exist_ok=True)
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        self.cleanup_all()

    def create_workspace(self, name: str) -> Path:
        workspace = self._base_dir / name
        if workspace.exists():
            raise WorkspaceError(f"Workspace already exists: {workspace}")

        if self._source_dir and self._source_dir.exists():
            if _is_apfs(self._base_dir):
                _cow_copy(self._source_dir, workspace)
            else:
                _fallback_copy(self._source_dir, workspace)
        else:
            workspace.mkdir(parents=True)

        if self._use_venv:
            self._setup_venv(workspace)

        self._workspaces.append(workspace)
        logger.info("Created workspace: %s", workspace)
        return workspace

    def cleanup(self, workspace: Path) -> None:
        if workspace.exists():
            shutil.rmtree(workspace)
            logger.info("Cleaned up workspace: %s", workspace)
        if workspace in self._workspaces:
            self._workspaces.remove(workspace)

    def cleanup_all(self) -> None:
        for ws in list(self._workspaces):
            self.cleanup(ws)
        if self._owns_base and self._base_dir.exists():
            shutil.rmtree(self._base_dir, ignore_errors=True)

    @property
    def workspaces(self) -> list[Path]:
        return list(self._workspaces)

    def _setup_venv(self, workspace: Path) -> None:
        venv_dir = workspace / ".venv"
        try:
            subprocess.run(
                ["uv", "venv", str(venv_dir), "--python", "3.12"],
                check=True,
                capture_output=True,
                text=True,
            )
            if self._requirements:
                subprocess.run(
                    [
                        "uv",
                        "pip",
                        "install",
                        "--python",
                        str(venv_dir / "bin" / "python"),
                    ]
                    + self._requirements,
                    check=True,
                    capture_output=True,
                    text=True,
                )
            logger.info("Created uv venv at %s", venv_dir)
        except FileNotFoundError:
            import venv as _venv

            _venv.create(str(venv_dir), with_pip=True)
            logger.warning("uv not found — fell back to stdlib venv at %s", venv_dir)


def _is_apfs(path: Path) -> bool:
    try:
        result = subprocess.run(
            ["diskutil", "info", str(path)],
            capture_output=True,
            text=True,
            timeout=5,
        )
        return "APFS" in result.stdout
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return False


def _cow_copy(src: Path, dst: Path) -> None:
    try:
        subprocess.run(
            ["cp", "-cr", str(src), str(dst)],
            check=True,
            capture_output=True,
            text=True,
        )
        logger.info("COW clone: %s → %s", src, dst)
    except subprocess.CalledProcessError as exc:
        logger.warning("COW clone failed, falling back to copytree: %s", exc)
        _fallback_copy(src, dst)


def _fallback_copy(src: Path, dst: Path) -> None:
    shutil.copytree(str(src), str(dst))
    logger.info("Fallback copy: %s → %s", src, dst)
