"""
Internal MCP tools for Gobby Worktree Management.

Exposes functionality for:
- Creating git worktrees for isolated development
- Managing worktree lifecycle (claim, release, cleanup)
- Syncing worktrees with main branch
- Spawning agents in worktrees

These tools are registered with the InternalToolRegistry and accessed
via the downstream proxy pattern (call_tool, list_tools, get_tool_schema).
"""

from __future__ import annotations

import json
import logging
import platform
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, cast

from gobby.mcp_proxy.tools.internal import InternalToolRegistry
from gobby.utils.project_context import get_project_context
from gobby.worktrees.git import WorktreeGitManager

if TYPE_CHECKING:
    from gobby.storage.worktrees import LocalWorktreeManager
    from gobby.worktrees.git import WorktreeGitManager

logger = logging.getLogger(__name__)

# Cache for WorktreeGitManager instances per repo path
_git_manager_cache: dict[str, WorktreeGitManager] = {}


def _get_worktree_base_dir() -> Path:
    """
    Get the base directory for worktrees.

    Uses the system temp directory:
    - macOS/Linux: /tmp/gobby-worktrees/
    - Windows: %TEMP%/gobby-worktrees/

    Returns:
        Path to worktree base directory (creates if needed)
    """
    if platform.system() == "Windows":
        # Windows: use %TEMP% (typically C:\\Users\\<user>\\AppData\\Local\\Temp)
        base = Path(tempfile.gettempdir()) / "gobby-worktrees"
    else:
        # macOS/Linux: use /tmp for better isolation (tmpfs, cleared on reboot)
        # Resolve symlink on macOS (/tmp -> /private/tmp) for consistent paths
        # nosec B108: /tmp is intentional for worktrees - they're temporary by design
        base = Path("/tmp").resolve() / "gobby-worktrees"  # nosec B108

    base.mkdir(parents=True, exist_ok=True)
    return base


def _generate_worktree_path(branch_name: str, project_name: str | None = None) -> str:
    """
    Generate a worktree path in the temp directory.

    Args:
        branch_name: Branch name (used as directory name)
        project_name: Optional project name for namespacing

    Returns:
        Full path for the worktree
    """
    base = _get_worktree_base_dir()

    # Sanitize branch name for filesystem (replace / with -)
    safe_branch = branch_name.replace("/", "-")

    if project_name:
        # Namespace by project: /tmp/gobby-worktrees/project-name/branch-name
        return str(base / project_name / safe_branch)
    else:
        # No project namespace: /tmp/gobby-worktrees/branch-name
        return str(base / safe_branch)


def _resolve_project_context(
    project_path: str | None,
    default_git_manager: WorktreeGitManager | None,
    default_project_id: str | None,
) -> tuple[WorktreeGitManager | None, str | None, str | None]:
    """
    Resolve project context from project_path or fall back to defaults.

    Args:
        project_path: Path to project directory (cwd from caller).
        default_git_manager: Registry-level git manager (may be None).
        default_project_id: Registry-level project ID (may be None).

    Returns:
        Tuple of (git_manager, project_id, error_message).
        If error_message is not None, the other values should not be used.
    """
    if project_path:
        # Resolve from provided path
        path = Path(project_path)
        if not path.exists():
            return None, None, f"Project path does not exist: {project_path}"

        project_ctx = get_project_context(path)
        if not project_ctx:
            return None, None, f"No .gobby/project.json found in {project_path}"

        resolved_project_id = project_ctx.get("id")
        resolved_path = project_ctx.get("project_path", str(path))

        # Get or create git manager for this path
        if resolved_path not in _git_manager_cache:
            try:
                _git_manager_cache[resolved_path] = WorktreeGitManager(resolved_path)
            except ValueError as e:
                return None, None, f"Invalid git repository: {e}"

        return _git_manager_cache[resolved_path], resolved_project_id, None

    # Fall back to defaults
    if default_git_manager is None:
        return None, None, "No project_path provided and no default git manager configured."
    if default_project_id is None:
        return None, None, "No project_path provided and no default project ID configured."

    return default_git_manager, default_project_id, None


def _copy_project_json_to_worktree(
    repo_path: str | Path,
    worktree_path: str | Path,
) -> None:
    """
    Copy .gobby/project.json from main repo to worktree, adding parent reference.

    This ensures worktree sessions:
    - Use the same project_id as the parent repo
    - Can discover the parent project path for workflow lookup

    Args:
        repo_path: Path to main repository
        worktree_path: Path to worktree directory
    """
    main_gobby_dir = Path(repo_path) / ".gobby"
    main_project_json = main_gobby_dir / "project.json"
    worktree_gobby_dir = Path(worktree_path) / ".gobby"

    if main_project_json.exists():
        try:
            worktree_gobby_dir.mkdir(parents=True, exist_ok=True)
            worktree_project_json = worktree_gobby_dir / "project.json"
            if not worktree_project_json.exists():
                # Read, add parent reference, write
                with open(main_project_json) as f:
                    data = json.load(f)

                data["parent_project_path"] = str(Path(repo_path).resolve())

                with open(worktree_project_json, "w") as f:
                    json.dump(data, f, indent=2)

                logger.info("Created project.json in worktree with parent reference")
        except Exception as e:
            logger.warning(f"Failed to create project.json in worktree: {e}")


def _install_provider_hooks(
    provider: Literal["claude", "gemini", "codex", "antigravity", "cursor", "windsurf", "copilot"]
    | None,
    worktree_path: str | Path,
) -> bool:
    """
    Install CLI hooks for the specified provider in the worktree.

    Args:
        provider: Provider name ('claude', 'gemini', 'antigravity', 'cursor', 'windsurf', 'copilot', or None)
        worktree_path: Path to worktree directory

    Returns:
        True if hooks were successfully installed, False otherwise
    """
    if not provider:
        return False

    worktree_path_obj = Path(worktree_path)
    try:
        if provider == "claude":
            from gobby.cli.installers.claude import install_claude

            result = install_claude(worktree_path_obj)
            if result["success"]:
                logger.info(f"Installed Claude hooks in worktree: {worktree_path}")
                return True
            else:
                logger.warning(f"Failed to install Claude hooks: {result.get('error')}")
        elif provider in ("cursor", "windsurf", "copilot"):
            # These editors use Claude hooks format
            from gobby.cli.installers.claude import install_claude

            result = install_claude(worktree_path_obj)
            if result["success"]:
                logger.info(f"Installed {provider} hooks in worktree: {worktree_path}")
                return True
            else:
                logger.warning(f"Failed to install {provider} hooks: {result.get('error')}")
        elif provider == "gemini":
            from gobby.cli.installers.gemini import install_gemini

            result = install_gemini(worktree_path_obj)
            if result["success"]:
                logger.info(f"Installed Gemini hooks in worktree: {worktree_path}")
                return True
            else:
                logger.warning(f"Failed to install Gemini hooks: {result.get('error')}")
        elif provider == "antigravity":
            from gobby.cli.installers.antigravity import install_antigravity

            result = install_antigravity(worktree_path_obj)
            if result["success"]:
                logger.info(f"Installed Antigravity hooks in worktree: {worktree_path}")
                return True
            else:
                logger.warning(f"Failed to install Antigravity hooks: {result.get('error')}")
        # Note: codex uses CODEX_NOTIFY_SCRIPT env var, not project-level hooks
    except Exception as e:
        logger.warning(f"Failed to install {provider} hooks in worktree: {e}")
    return False


def _build_worktree_context_prompt(
    original_prompt: str,
    worktree_path: str,
    branch_name: str,
    task_id: str | None,
    main_repo_path: str | None = None,
) -> str:
    """
    Build an enhanced prompt with worktree context injected.

    This helps the spawned agent understand it's working in an isolated worktree,
    not the main repository. Critical for preventing the agent from accessing
    wrong files or working in the wrong directory.

    Args:
        original_prompt: The original task prompt
        worktree_path: Path to the worktree
        branch_name: Name of the worktree branch
        task_id: Task ID being worked on (if any)
        main_repo_path: Path to the main repo (to explicitly warn against accessing it)

    Returns:
        Enhanced prompt with worktree context prepended
    """
    context_lines = [
        "## CRITICAL: Worktree Context",
        "You are working in an ISOLATED git worktree, NOT the main repository.",
        "",
        f"**Your workspace:** {worktree_path}",
        f"**Your branch:** {branch_name}",
    ]

    if task_id:
        context_lines.append(f"**Your task:** {task_id}")

    context_lines.extend(
        [
            "",
            "**IMPORTANT RULES:**",
            f"1. ALL file operations must be within {worktree_path}",
        ]
    )

    if main_repo_path:
        context_lines.append(f"2. Do NOT access {main_repo_path} (main repo)")
    else:
        context_lines.append("2. Do NOT access the main repository")

    context_lines.extend(
        [
            "3. Run `pwd` to verify your location before any file operations",
            f"4. Commit to YOUR branch ({branch_name}), not main/dev",
            "5. When your assigned task is complete, STOP - do not claim other tasks",
            "",
            "---",
            "",
        ]
    )

    worktree_context = "\n".join(context_lines)
    return f"{worktree_context}{original_prompt}"


def create_worktrees_registry(
    worktree_storage: LocalWorktreeManager,
    git_manager: WorktreeGitManager | None = None,
    project_id: str | None = None,
    session_manager: Any | None = None,
) -> InternalToolRegistry:
    """
    Create a worktree tool registry with all worktree-related tools.

    Args:
        worktree_storage: LocalWorktreeManager for database operations.
        git_manager: WorktreeGitManager for git operations.
        project_id: Default project ID for operations.
        session_manager: Session manager for resolving session references.

    Returns:
        InternalToolRegistry with all worktree tools registered.
    """

    def _resolve_session_id(ref: str) -> str:
        """Resolve session reference (#N, N, UUID, or prefix) to UUID."""
        if session_manager is None:
            return ref  # No resolution available, return as-is
        ctx = get_project_context()
        proj_id = ctx.get("id") if ctx else project_id
        return str(session_manager.resolve_session_reference(ref, proj_id))

    registry = InternalToolRegistry(
        name="gobby-worktrees",
        description="Git worktree management - create, manage, and cleanup isolated development directories",
    )

    @registry.tool(
        name="create_worktree",
        description="Create a new git worktree for isolated development.",
    )
    async def create_worktree(
        branch_name: str,
        base_branch: str = "main",
        task_id: str | None = None,
        worktree_path: str | None = None,
        create_branch: bool = True,
        project_path: str | None = None,
        provider: Literal[
            "claude", "gemini", "codex", "antigravity", "cursor", "windsurf", "copilot"
        ]
        | None = None,
    ) -> dict[str, Any]:
        """
        Create a new git worktree.

        Args:
            branch_name: Name for the new branch.
            base_branch: Branch to base the worktree on (default: main).
            task_id: Optional task ID to link to this worktree.
            worktree_path: Optional custom path (defaults to ../{branch_name}).
            create_branch: Whether to create a new branch (default: True).
            project_path: Path to project directory (pass cwd from CLI).
            provider: CLI provider to install hooks for (claude, gemini, codex, antigravity, cursor, windsurf, copilot).
                     If specified, installs hooks so agents can communicate with daemon.

        Returns:
            Dict with worktree ID, path, and branch info.
        """
        # Resolve project context
        resolved_git_mgr, resolved_project_id, error = _resolve_project_context(
            project_path, git_manager, project_id
        )
        if error:
            return {"success": False, "error": error}

        # Type narrowing: if no error, these are guaranteed non-None
        if resolved_git_mgr is None or resolved_project_id is None:
            raise RuntimeError("Git manager or project ID unexpectedly None")

        # Check if branch already exists as a worktree
        existing = worktree_storage.get_by_branch(resolved_project_id, branch_name)
        if existing:
            return {
                "success": False,
                "error": f"Worktree already exists for branch '{branch_name}'",
                "existing_worktree_id": existing.id,
                "existing_path": existing.worktree_path,
            }

        # Generate default worktree path if not provided
        if worktree_path is None:
            # Use temp directory (e.g., /tmp/gobby-worktrees/project-name/branch-name)
            project_name = Path(resolved_git_mgr.repo_path).name
            worktree_path = _generate_worktree_path(branch_name, project_name)

        # Create git worktree
        result = resolved_git_mgr.create_worktree(
            worktree_path=worktree_path,
            branch_name=branch_name,
            base_branch=base_branch,
            create_branch=create_branch,
        )

        if not result.success:
            return {
                "success": False,
                "error": result.error or "Failed to create git worktree",
            }

        # Record in database
        worktree = worktree_storage.create(
            project_id=resolved_project_id,
            branch_name=branch_name,
            worktree_path=worktree_path,
            base_branch=base_branch,
            task_id=task_id,
        )

        # Copy project.json and install provider hooks
        _copy_project_json_to_worktree(resolved_git_mgr.repo_path, worktree.worktree_path)
        hooks_installed = _install_provider_hooks(provider, worktree.worktree_path)

        return {
            "success": True,
            "worktree_id": worktree.id,
            "worktree_path": worktree.worktree_path,
            "hooks_installed": hooks_installed,
        }

    @registry.tool(
        name="get_worktree",
        description="Get details of a specific worktree.",
    )
    async def get_worktree(worktree_id: str) -> dict[str, Any]:
        """
        Get worktree details by ID.

        Args:
            worktree_id: The worktree ID.

        Returns:
            Dict with full worktree details.
        """
        worktree = worktree_storage.get(worktree_id)
        if not worktree:
            return {
                "success": False,
                "error": f"Worktree '{worktree_id}' not found",
            }

        # Get git status if manager available
        git_status = None
        if git_manager and Path(worktree.worktree_path).exists():
            status = git_manager.get_worktree_status(worktree.worktree_path)
            if status:
                git_status = {
                    "has_uncommitted_changes": status.has_uncommitted_changes,
                    "commits_ahead": status.ahead,
                    "commits_behind": status.behind,
                    "current_branch": status.branch,
                }

        return {
            "success": True,
            "worktree": worktree.to_dict(),
            "git_status": git_status,
        }

    @registry.tool(
        name="list_worktrees",
        description="List worktrees with optional filters. Accepts #N, N, UUID, or prefix for agent_session_id.",
    )
    async def list_worktrees(
        status: str | None = None,
        agent_session_id: str | None = None,
        limit: int | str = 50,
    ) -> dict[str, Any]:
        """
        List worktrees with optional filters.

        Args:
            status: Filter by status (active, stale, merged, abandoned).
            agent_session_id: Session reference (accepts #N, N, UUID, or prefix) to filter by owning session.
            limit: Maximum results (default: 50).

        Returns:
            Dict with list of worktrees.
        """
        # Handle string inputs from MCP
        limit = int(limit) if isinstance(limit, str) else limit

        # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
        resolved_session_id = agent_session_id
        if agent_session_id:
            try:
                resolved_session_id = _resolve_session_id(agent_session_id)
            except ValueError as e:
                return {"success": False, "error": str(e)}

        worktrees = worktree_storage.list_worktrees(
            project_id=project_id,
            status=status,
            agent_session_id=resolved_session_id,
            limit=limit,
        )

        return {
            "success": True,
            "worktrees": [
                {
                    "id": wt.id,
                    "branch_name": wt.branch_name,
                    "worktree_path": wt.worktree_path,
                    "status": wt.status,
                    "task_id": wt.task_id,
                    "agent_session_id": wt.agent_session_id,
                    "created_at": wt.created_at,
                }
                for wt in worktrees
            ],
            "count": len(worktrees),
        }

    @registry.tool(
        name="claim_worktree",
        description="Claim ownership of a worktree for an agent session. Accepts #N, N, UUID, or prefix for session_id.",
    )
    async def claim_worktree(
        worktree_id: str,
        session_id: str,
    ) -> dict[str, Any]:
        """
        Claim a worktree for an agent session.

        Args:
            worktree_id: The worktree ID to claim.
            session_id: Session reference (accepts #N, N, UUID, or prefix) claiming ownership.

        Returns:
            Dict with success status.
        """
        # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
        try:
            resolved_session_id = _resolve_session_id(session_id)
        except ValueError as e:
            return {"success": False, "error": str(e)}

        worktree = worktree_storage.get(worktree_id)
        if not worktree:
            return {
                "success": False,
                "error": f"Worktree '{worktree_id}' not found",
            }

        if worktree.agent_session_id and worktree.agent_session_id != resolved_session_id:
            return {
                "success": False,
                "error": f"Worktree already claimed by session '{worktree.agent_session_id}'",
            }

        updated = worktree_storage.claim(worktree_id, resolved_session_id)
        if not updated:
            return {"error": "Failed to claim worktree"}

        return {}

    @registry.tool(
        name="release_worktree",
        description="Release ownership of a worktree.",
    )
    async def release_worktree(worktree_id: str) -> dict[str, Any]:
        """
        Release a worktree from its current owner.

        Args:
            worktree_id: The worktree ID to release.

        Returns:
            Dict with success status.
        """
        worktree = worktree_storage.get(worktree_id)
        if not worktree:
            return {"error": f"Worktree '{worktree_id}' not found"}

        updated = worktree_storage.release(worktree_id)
        if not updated:
            return {"error": "Failed to release worktree"}

        return {}

    @registry.tool(
        name="delete_worktree",
        description="Delete a worktree (both git and database record).",
    )
    async def delete_worktree(
        worktree_id: str,
        force: bool | str = False,
        project_path: str | None = None,
    ) -> dict[str, Any]:
        """
        Delete a worktree completely (handles all cleanup).

        This is the proper way to remove a worktree. It handles:
        - Removes the worktree directory and all temporary files
        - Cleans up git's worktree tracking (.git/worktrees/)
        - Deletes the associated git branch
        - Removes the Gobby database record

        Do NOT manually run `git worktree remove` - use this tool instead.

        Args:
            worktree_id: The worktree ID to delete (e.g., "wt-abc123").
            force: Force deletion even if there are uncommitted changes.
            project_path: Optional path to project root to resolve git context.

        Returns:
            Dict with success status.
        """
        # Handle string inputs from MCP
        force = force in (True, "true", "True", "1") if isinstance(force, str) else force

        worktree = worktree_storage.get(worktree_id)

        if not worktree:
            return {
                "success": False,
                "error": f"Worktree '{worktree_id}' not found",
            }

        # Resolve git manager
        resolved_git_mgr = git_manager  # Start with the module-level git_manager
        if project_path:
            try:
                # _resolve_project_context is defined in this module scope
                mgr, _, _ = _resolve_project_context(project_path, resolved_git_mgr, None)
                if mgr:
                    resolved_git_mgr = mgr
            except Exception:
                # nosec B110 - if context resolution fails, continue without git manager
                pass

        # Check if worktree path exists
        worktree_exists = Path(worktree.worktree_path).exists()

        # Check for uncommitted changes if not forcing (only if path exists)
        if resolved_git_mgr and worktree_exists:
            status = resolved_git_mgr.get_worktree_status(worktree.worktree_path)
            if status and status.has_uncommitted_changes and not force:
                return {
                    "success": False,
                    "error": "Worktree has uncommitted changes. Use force=True to delete anyway.",
                    "uncommitted_changes": True,
                }

        # Delete git worktree (only if path exists - handles orphaned DB records)
        if resolved_git_mgr and worktree_exists:
            result = resolved_git_mgr.delete_worktree(
                worktree.worktree_path,
                force=force,
                delete_branch=True,
                branch_name=worktree.branch_name,
            )
            if not result.success:
                return {
                    "success": False,
                    "error": result.error or "Failed to delete git worktree",
                }
        elif not worktree_exists:
            # Worktree path gone (manually deleted) - just clean up DB record
            logger.info(
                f"Worktree path {worktree.worktree_path} doesn't exist, cleaning up DB record only"
            )

        # Delete database record
        deleted = worktree_storage.delete(worktree_id)
        if not deleted:
            return {"error": "Failed to delete worktree record"}

        return {}

    @registry.tool(
        name="sync_worktree",
        description="Sync a worktree with the main branch.",
    )
    async def sync_worktree(
        worktree_id: str,
        strategy: str = "merge",
        project_path: str | None = None,
    ) -> dict[str, Any]:
        """
        Sync a worktree with the main branch.

        Args:
            worktree_id: The worktree ID to sync.
            strategy: Sync strategy ('merge' or 'rebase').
            project_path: Path to project directory (pass cwd from CLI).

        Returns:
            Dict with sync result.
        """
        # Resolve git manager from project_path or fall back to default
        resolved_git_mgr, _, error = _resolve_project_context(project_path, git_manager, project_id)
        if error:
            return {"success": False, "error": error}

        if resolved_git_mgr is None:
            return {
                "success": False,
                "error": "Git manager not configured and no project_path provided.",
            }

        worktree = worktree_storage.get(worktree_id)
        if not worktree:
            return {
                "success": False,
                "error": f"Worktree '{worktree_id}' not found",
            }

        # Validate strategy
        if strategy not in ("rebase", "merge"):
            return {
                "success": False,
                "error": f"Invalid strategy '{strategy}'. Must be 'rebase' or 'merge'.",
            }

        strategy_literal = cast(Literal["rebase", "merge"], strategy)

        result = resolved_git_mgr.sync_from_main(
            worktree.worktree_path,
            base_branch=worktree.base_branch,
            strategy=strategy_literal,
        )

        if not result.success:
            return {
                "success": False,
                "error": result.error or "Sync failed",
            }

        # Update last activity
        worktree_storage.update(worktree_id)

        return {
            "success": True,
            "message": result.message,
            "output": result.output,
            "strategy": strategy,
        }

    @registry.tool(
        name="mark_worktree_merged",
        description="Mark a worktree as merged (ready for cleanup).",
    )
    async def mark_worktree_merged(worktree_id: str) -> dict[str, Any]:
        """
        Mark a worktree as merged.

        Args:
            worktree_id: The worktree ID to mark.

        Returns:
            Dict with success status.
        """
        worktree = worktree_storage.get(worktree_id)
        if not worktree:
            return {
                "success": False,
                "error": f"Worktree '{worktree_id}' not found",
            }

        updated = worktree_storage.mark_merged(worktree_id)
        if not updated:
            return {"error": "Failed to mark worktree as merged"}

        return {}

    @registry.tool(
        name="detect_stale_worktrees",
        description="Find worktrees with no activity for a period.",
    )
    async def detect_stale_worktrees(
        project_path: str | None = None,
        hours: int | str = 24,
        limit: int | str = 50,
    ) -> dict[str, Any]:
        """
        Find stale worktrees (no activity for N hours).

        Args:
            project_path: Path to project directory (pass cwd from CLI).
            hours: Hours of inactivity threshold (default: 24).
            limit: Maximum results (default: 50).

        Returns:
            Dict with list of stale worktrees.
        """
        # Handle string inputs from MCP (JSON params come as strings)
        hours = int(hours) if isinstance(hours, str) else hours
        limit = int(limit) if isinstance(limit, str) else limit

        _, resolved_project_id, error = _resolve_project_context(
            project_path, git_manager, project_id
        )
        if error:
            return {"success": False, "error": error}
        if resolved_project_id is None:
            return {"success": False, "error": "Could not resolve project ID"}

        stale = worktree_storage.find_stale(
            project_id=resolved_project_id,
            hours=hours,
            limit=limit,
        )

        return {
            "success": True,
            "stale_worktrees": [
                {
                    "id": wt.id,
                    "branch_name": wt.branch_name,
                    "worktree_path": wt.worktree_path,
                    "updated_at": wt.updated_at,
                    "task_id": wt.task_id,
                }
                for wt in stale
            ],
            "count": len(stale),
            "threshold_hours": hours,
        }

    @registry.tool(
        name="cleanup_stale_worktrees",
        description="Mark and optionally delete stale worktrees.",
    )
    async def cleanup_stale_worktrees(
        project_path: str | None = None,
        hours: int | str = 24,
        dry_run: bool | str = True,
        delete_git: bool | str = False,
    ) -> dict[str, Any]:
        """
        Cleanup stale worktrees.

        Args:
            project_path: Path to project directory (pass cwd from CLI).
            hours: Hours of inactivity threshold (default: 24).
            dry_run: If True, only report what would be cleaned (default: True).
            delete_git: If True, also delete git worktrees (default: False).

        Returns:
            Dict with cleanup results.
        """
        # Handle string inputs from MCP (JSON params come as strings)
        hours = int(hours) if isinstance(hours, str) else hours
        dry_run = dry_run in (True, "true", "True", "1") if isinstance(dry_run, str) else dry_run
        delete_git = (
            delete_git in (True, "true", "True", "1") if isinstance(delete_git, str) else delete_git
        )

        resolved_git_manager, resolved_project_id, error = _resolve_project_context(
            project_path, git_manager, project_id
        )
        if error:
            return {"success": False, "error": error}
        if resolved_project_id is None:
            return {"success": False, "error": "Could not resolve project ID"}

        # Find and mark stale worktrees
        stale = worktree_storage.cleanup_stale(
            project_id=resolved_project_id,
            hours=hours,
            dry_run=dry_run,
        )

        results = []
        for wt in stale:
            result = {
                "id": wt.id,
                "branch_name": wt.branch_name,
                "worktree_path": wt.worktree_path,
                "marked_abandoned": not dry_run,
                "git_deleted": False,
            }

            # Optionally delete git worktrees
            if delete_git and not dry_run and resolved_git_manager:
                git_result = resolved_git_manager.delete_worktree(
                    wt.worktree_path,
                    force=True,
                    delete_branch=True,
                    branch_name=wt.branch_name,
                )
                result["git_deleted"] = git_result.success
                if not git_result.success:
                    result["git_error"] = git_result.error or "Unknown error"

            results.append(result)

        return {
            "success": True,
            "dry_run": dry_run,
            "cleaned": results,
            "count": len(results),
            "threshold_hours": hours,
        }

    @registry.tool(
        name="get_worktree_stats",
        description="Get worktree statistics for the project.",
    )
    async def get_worktree_stats(project_path: str | None = None) -> dict[str, Any]:
        """
        Get worktree statistics.

        Args:
            project_path: Path to project directory (pass cwd from CLI).

        Returns:
            Dict with counts by status.
        """
        # Resolve project context (git_manager not needed for stats)
        _, resolved_project_id, error = _resolve_project_context(
            project_path, git_manager, project_id
        )
        if error:
            return {"success": False, "error": error}

        # Type narrowing: if no error, resolved_project_id is guaranteed non-None
        if resolved_project_id is None:
            raise RuntimeError("Project ID unexpectedly None")

        counts = worktree_storage.count_by_status(resolved_project_id)

        return {
            "success": True,
            "project_id": resolved_project_id,
            "counts": counts,
            "total": sum(counts.values()),
        }

    @registry.tool(
        name="get_worktree_by_task",
        description="Get worktree linked to a specific task.",
    )
    async def get_worktree_by_task(task_id: str) -> dict[str, Any]:
        """
        Get worktree linked to a task.

        Args:
            task_id: The task ID to look up.

        Returns:
            Dict with worktree details or not found.
        """
        worktree = worktree_storage.get_by_task(task_id)
        if not worktree:
            return {
                "success": False,
                "error": f"No worktree linked to task '{task_id}'",
            }

        return {
            "success": True,
            "worktree": worktree.to_dict(),
        }

    @registry.tool(
        name="link_task_to_worktree",
        description="Link a task to an existing worktree.",
    )
    async def link_task_to_worktree(
        worktree_id: str,
        task_id: str,
    ) -> dict[str, Any]:
        """
        Link a task to a worktree.

        Args:
            worktree_id: The worktree ID.
            task_id: The task ID to link.

        Returns:
            Dict with success status.
        """
        worktree = worktree_storage.get(worktree_id)
        if not worktree:
            return {
                "success": False,
                "error": f"Worktree '{worktree_id}' not found",
            }

        updated = worktree_storage.update(worktree_id, task_id=task_id)
        if not updated:
            return {"error": "Failed to link task to worktree"}

        return {}

    return registry
