# tools/git_workspace.py - Git operations on the workspace via subprocess (no shell_execute).
# Uses --no-verify on commit to avoid running user hooks (reduces execution risk).
from __future__ import annotations

import os
import subprocess

from langchain_core.tools import tool
from tools._logging import _debug_log
from utils.path_safety import is_path_allowed, safe_resolve_path


def _get_git_repo_path() -> str:
    """Return the workspace directory (git operations run there)."""
    from core import get_workspace_path
    return get_workspace_path()


def _is_git_repo(cwd: str) -> bool:
    git_dir = os.path.join(cwd, ".git")
    return os.path.isdir(git_dir) or os.path.isfile(git_dir)


def _no_git_message() -> str:
    """Return helpful message when the project repo has no .git."""
    return "Project directory is not a git repository. No .git directory found."


@tool
def git_project_status() -> str:
    """Check git status in the workspace. Use this to see uncommitted changes, branch, etc.
    Returns output of `git status` or a message if not a git repository. No shell toggle required."""
    try:
        repo = _get_git_repo_path()
        if not _is_git_repo(repo):
            return _no_git_message()
        result = subprocess.run(
            ["git", "status"],
            cwd=repo,
            capture_output=True,
            text=True,
            timeout=30,
        )
        out = (result.stdout or "").strip()
        err = (result.stderr or "").strip()
        if result.returncode != 0:
            return f"git status failed:\n{err or out or 'unknown'}"
        return out or "(no output)"
    except subprocess.TimeoutExpired:
        return "git status timed out."
    except FileNotFoundError:
        return "git not found. Install Xcode Command Line Tools or Git."
    except Exception as e:
        _debug_log(f"[ERROR git_project_status] {e}", "ERROR")
        return f"Failed: {str(e)}"


def _validated_repo_relative_paths(repo: str, paths: list[str]) -> tuple[list[str] | None, str | None]:
    """Resolve user paths to paths relative to repo; return (rel_paths, error)."""
    repo_abs = os.path.abspath(os.path.expanduser(repo))
    rel_out: list[str] = []
    for p in paths:
        if not p or not isinstance(p, str):
            continue
        abs_path, err = safe_resolve_path(repo, p.strip(), allow_write=False, create_parents=False)
        if err or not abs_path:
            return None, f"Invalid path '{p}': {err or 'resolution failed'}"
        if not is_path_allowed(abs_path, base_dir=repo_abs, allow_write=False):
            return None, f"Path not allowed (must be under repo): {p}"
        rel_out.append(os.path.relpath(abs_path, repo_abs))
    if not rel_out:
        return None, "No valid paths."
    return rel_out, None


@tool
def git_project_restore(
    paths: list[str],
    discard_uncommitted: bool = True,
) -> str:
    """Restore workspace files from git. Use when the user wants to undo edits or match the last commit (e.g. "restore from git", "discard changes").

    Policy: **git_project_restore** on identity/personality filenames (e.g. IDENTITY.md) is blocked during background/heartbeat runs so automation cannot discard uncommitted user edits—only interactive chat may restore those paths.

    paths: Non-empty list of paths relative to the workspace repo (or absolute paths under the repo).
    discard_uncommitted: When True (default), reset both the index and working tree to match HEAD for those paths (`git restore --source=HEAD --staged --worktree`). When False, only refresh the working tree from the current index (`git restore -- path`), which does not undo staged changes.

    This is NOT git_project_commit — commit records new snapshots; restore throws away uncommitted changes. No shell toggle required."""
    try:
        repo = _get_git_repo_path()
        if not _is_git_repo(repo):
            return _no_git_message()
        rel_paths, err = _validated_repo_relative_paths(repo, paths)
        if err:
            return err
        assert rel_paths is not None
        if discard_uncommitted:
            cmd = ["git", "restore", "--source=HEAD", "--staged", "--worktree", "--"] + rel_paths
        else:
            cmd = ["git", "restore", "--"] + rel_paths
        result = subprocess.run(
            cmd,
            cwd=repo,
            capture_output=True,
            text=True,
            timeout=30,
        )
        if result.returncode != 0:
            combined = (result.stderr or "") + (result.stdout or "")
            return f"git restore failed: {combined.strip() or 'unknown'}"
        mode = "reset to HEAD (staged + working tree)" if discard_uncommitted else "working tree from index"
        return f"Restored ({mode}): " + ", ".join(rel_paths)
    except subprocess.TimeoutExpired:
        return "git restore timed out."
    except FileNotFoundError:
        return "git not found. Install Xcode Command Line Tools or Git."
    except Exception as e:
        _debug_log(f"[ERROR git_project_restore] {e}", "ERROR")
        return f"Restore failed: {str(e)}"


@tool
def git_project_commit(message: str, paths: list[str] | None = None) -> str:
    """Stage and commit changes in the workspace — creates a new commit from the current changes. Does NOT undo or revert files; for that use git_project_restore.

    message: Commit message (required). paths: Optional list of paths to add (relative to repo). If omitted, stages all changes (git add .). Uses --no-verify.
    For identity/soul/memory files, use commit_workspace_evolution (evolution mode) to commit only those. No shell toggle required."""
    try:
        repo = _get_git_repo_path()
        if not _is_git_repo(repo):
            return _no_git_message()
        msg = (message or "").strip()
        if not msg:
            return "Commit message is required."
        if paths:
            add_args, verr = _validated_repo_relative_paths(repo, paths)
            if verr:
                return verr
            assert add_args is not None
            result = subprocess.run(
                ["git", "add"] + add_args,
                cwd=repo,
                capture_output=True,
                text=True,
                timeout=30,
            )
        else:
            result = subprocess.run(
                ["git", "add", "."],
                cwd=repo,
                capture_output=True,
                text=True,
                timeout=30,
            )
        if result.returncode != 0:
            return f"git add failed: {result.stderr or result.stdout or 'unknown'}"
        result = subprocess.run(
            ["git", "commit", "-m", msg, "--no-verify"],
            cwd=repo,
            capture_output=True,
            text=True,
            timeout=30,
        )
        if result.returncode != 0:
            combined = (result.stderr or "") + (result.stdout or "")
            if "nothing to commit" in combined or "no changes added" in combined.lower():
                return "Nothing to commit — working tree clean or no changes staged."
            return f"git commit failed: {combined.strip() or 'unknown'}"
        return f"Committed: {msg}"
    except subprocess.TimeoutExpired:
        return "git command timed out."
    except FileNotFoundError:
        return "git not found. Install Xcode Command Line Tools or Git."
    except Exception as e:
        _debug_log(f"[ERROR git_project_commit] {e}", "ERROR")
        return f"Commit failed: {str(e)}"


@tool
def git_project_push(remote: str | None = None, branch: str | None = None) -> str:
    """Push committed changes from the workspace to the remote. remote: Optional (e.g. 'origin'). branch: Optional.
    No shell toggle required. Requires network."""
    try:
        repo = _get_git_repo_path()
        if not _is_git_repo(repo):
            return _no_git_message()
        cmd = ["git", "push"]
        if remote:
            cmd.append(remote.strip())
        if branch:
            cmd.append(branch.strip())
        result = subprocess.run(
            cmd,
            cwd=repo,
            capture_output=True,
            text=True,
            timeout=120,
        )
        out = (result.stdout or "").strip()
        err = (result.stderr or "").strip()
        if result.returncode != 0:
            return f"git push failed:\n{err or out or 'unknown'}"
        return out or "Push completed."
    except subprocess.TimeoutExpired:
        return "git push timed out."
    except FileNotFoundError:
        return "git not found. Install Xcode Command Line Tools or Git."
    except Exception as e:
        _debug_log(f"[ERROR git_project_push] {e}", "ERROR")
        return f"Push failed: {str(e)}"


__all__ = [
    "git_project_status",
    "git_project_restore",
    "git_project_commit",
    "git_project_push",
]
