"""Git repository adapter using GitPython."""

from __future__ import annotations

import asyncio
from datetime import datetime
from pathlib import Path
from typing import Any

import git

from portal.core.domain.models import Worktree, WorktreeStatus


class GitCommandError(Exception):
    """Exception raised when git commands fail."""

    def __init__(self, message: str, stderr: str = "") -> None:
        super().__init__(message)
        self.stderr = stderr


class GitRepository:
    """Adapter for git operations using GitPython."""

    def __init__(self, repo_path: Path) -> None:
        """Initialize git repository adapter.

        Args:
            repo_path: Path to the git repository
        """
        self.repo_path = repo_path
        self._repo: git.Repo | None = None

    @property
    def repo(self) -> git.Repo:
        """Get the git repository instance."""
        if self._repo is None:
            self._repo = git.Repo(self.repo_path)
        return self._repo

    async def create_worktree(
        self, branch_name: str, worktree_path: Path, base_branch: str = "main"
    ) -> Worktree:
        """Create a new git worktree.

        Args:
            branch_name: Name of the new branch
            worktree_path: Path where the worktree should be created
            base_branch: Base branch to create from

        Returns:
            Worktree model representing the created worktree

        Raises:
            GitCommandError: If the git operation fails
        """
        await asyncio.to_thread(self._create_worktree_sync, branch_name, worktree_path, base_branch)

        # Get worktree info
        worktree_repo = git.Repo(worktree_path)
        head_sha = worktree_repo.head.commit.hexsha

        return Worktree(
            id=branch_name,
            name=branch_name,
            branch=branch_name,
            path=worktree_path,
            head_sha=head_sha,
            status=WorktreeStatus.ACTIVE,
            is_current=False,
            created_at=datetime.now(),
            last_accessed=None,
        )

    def _create_worktree_sync(
        self, branch_name: str, worktree_path: Path, base_branch: str
    ) -> None:
        """Synchronous worktree creation with validation."""
        # Validate inputs
        if not branch_name or not branch_name.strip():
            raise GitCommandError("Branch name cannot be empty")
        if not base_branch or not base_branch.strip():
            raise GitCommandError("Base branch cannot be empty")

        # Security check: ensure paths are safe
        try:
            worktree_path.resolve()
        except (OSError, RuntimeError) as e:
            raise GitCommandError("Invalid worktree path") from e

        # Check if base branch exists (local or remote)
        all_refs = [ref.name for ref in self.repo.references]
        if base_branch not in all_refs:
            # Try with origin/ prefix if not already prefixed
            if not base_branch.startswith("origin/") and f"origin/{base_branch}" in all_refs:
                base_branch = f"origin/{base_branch}"
            else:
                raise GitCommandError(f"Base branch '{base_branch}' does not exist")

        # Create the worktree directory with proper error handling
        try:
            worktree_path.parent.mkdir(parents=True, exist_ok=True)
        except (PermissionError, OSError) as e:
            raise GitCommandError("Cannot create worktree directory") from e

        try:
            # Create the worktree
            # If base_branch is a remote branch, git will automatically track it
            self.repo.git.worktree("add", str(worktree_path), "-b", branch_name, base_branch)
        except git.GitCommandError as e:
            # Include actual git error in the message for debugging
            raise GitCommandError(f"Failed to create worktree: {e.stderr}") from e

    async def delete_worktree(self, worktree_path: Path, force: bool = False) -> bool:
        """Delete a git worktree.

        Args:
            worktree_path: Path to the worktree to delete
            force: Force deletion even if worktree has changes

        Returns:
            True if deletion was successful

        Raises:
            GitCommandError: If the git operation fails
        """
        return await asyncio.to_thread(self._delete_worktree_sync, worktree_path, force)

    def _delete_worktree_sync(self, worktree_path: Path, force: bool) -> bool:
        """Synchronous worktree deletion."""
        try:
            if force:
                self.repo.git.worktree("remove", "--force", str(worktree_path))
            else:
                self.repo.git.worktree("remove", str(worktree_path))
            return True
        except git.GitCommandError as e:
            if force:
                # Try pruning and retry
                try:
                    self.repo.git.worktree("prune")
                    self.repo.git.worktree("remove", "--force", str(worktree_path))
                    return True
                except git.GitCommandError:
                    pass

            # Parse the git error message for common issues
            error_msg = str(e)
            if "is not a working tree" in error_msg:
                raise GitCommandError(
                    f"The directory '{worktree_path.name}' is not a Git worktree"
                ) from e
            elif "contains modified or untracked files" in error_msg:
                raise GitCommandError(
                    f"Worktree '{worktree_path.name}' contains uncommitted changes. Use --force to delete anyway"
                ) from e
            elif "Directory not empty" in error_msg:
                raise GitCommandError(
                    f"Worktree '{worktree_path.name}' directory is not empty. Use --force to delete anyway"
                ) from e
            elif "is locked" in error_msg:
                raise GitCommandError(
                    f"Worktree '{worktree_path.name}' is locked. Remove the lock file or use --force"
                ) from e
            elif "No such file or directory" in error_msg:
                raise GitCommandError(f"Worktree path '{worktree_path.name}' does not exist") from e
            else:
                # For unknown errors, include git's message but sanitize the path
                sanitized_error = error_msg.replace(str(worktree_path), worktree_path.name)
                raise GitCommandError(f"Failed to delete worktree: {sanitized_error}") from e

    async def delete_branch(self, branch_name: str, force: bool = False) -> bool:
        """Delete a git branch.

        Args:
            branch_name: Name of the branch to delete
            force: Force deletion even if branch is not merged

        Returns:
            True if deletion was successful

        Raises:
            GitCommandError: If the git operation fails
        """
        return await asyncio.to_thread(self._delete_branch_sync, branch_name, force)

    def _delete_branch_sync(self, branch_name: str, force: bool) -> bool:
        """Synchronous branch deletion."""
        try:
            if force:
                self.repo.git.branch("-D", branch_name)
            else:
                self.repo.git.branch("-d", branch_name)
            return True
        except git.GitCommandError as e:
            raise GitCommandError(f"Failed to delete branch '{branch_name}': {e}") from e

    async def list_worktrees(self) -> list[Worktree]:
        """List all git worktrees.

        Returns:
            List of Worktree models

        Raises:
            GitCommandError: If the git operation fails
        """
        return await asyncio.to_thread(self._list_worktrees_sync)

    def _list_worktrees_sync(self) -> list[Worktree]:
        """Synchronous worktree listing."""
        worktrees = []

        try:
            # Parse git worktree list output
            output = self.repo.git.worktree("list", "--porcelain")
        except git.GitCommandError as e:
            raise GitCommandError(f"Failed to list worktrees: {e}") from e

        current_worktree: dict[str, str | Path | bool] = {}
        is_first_worktree = True

        for line in output.splitlines():
            if line.startswith("worktree "):
                path = Path(line[9:])
                current_worktree = {"path": path, "is_base": is_first_worktree}
                is_first_worktree = False  # Only the first worktree is the base
            elif line.startswith("HEAD ") and current_worktree:
                current_worktree["head"] = line[5:]
            elif line.startswith("branch ") and current_worktree:
                current_worktree["branch"] = line[7:]
            elif line.startswith("locked") and current_worktree:
                current_worktree["locked"] = True
            elif line == "" and current_worktree:
                worktrees.append(self._create_worktree_from_info(current_worktree))
                current_worktree = {}

        if current_worktree:
            worktrees.append(self._create_worktree_from_info(current_worktree))

        return worktrees

    def _create_worktree_from_info(self, info: dict[str, str | Path | bool]) -> Worktree:
        """Create Worktree model from parsed git info."""
        path = info["path"]
        assert isinstance(path, Path)

        head_sha = str(info.get("head", "unknown"))
        branch = str(info.get("branch", "detached"))
        is_locked = info.get("locked", False)
        is_base = bool(info.get("is_base", False))

        # Extract branch name from refs/heads/ format
        if branch.startswith("refs/heads/"):
            branch = branch[11:]

        # Determine status
        if is_locked:
            status = WorktreeStatus.LOCKED
        elif not path.exists():
            status = WorktreeStatus.MISSING
        else:
            status = WorktreeStatus.ACTIVE

        # Use branch name as ID and name
        worktree_id = branch if branch != "detached" else path.name
        worktree_name = branch if branch != "detached" else path.name

        # Estimate creation time from directory stats
        try:
            created_at = datetime.fromtimestamp(path.stat().st_ctime)
        except (OSError, ValueError):
            created_at = datetime.now()

        return Worktree(
            id=worktree_id,
            name=worktree_name,
            branch=branch,
            path=path,
            head_sha=head_sha,
            status=status,
            is_current=path == self.repo_path,
            is_base=is_base,
            created_at=created_at,
            last_accessed=None,
        )

    async def switch_worktree(self, worktree_path: Path) -> bool:
        """Switch to a different worktree by changing directory.

        Note: This is typically handled by shell integration.
        The service layer will emit an event for the shell to handle.

        Args:
            worktree_path: Path to the worktree to switch to

        Returns:
            True if the worktree exists and is valid
        """
        return await asyncio.to_thread(self._switch_worktree_sync, worktree_path)

    def _switch_worktree_sync(self, worktree_path: Path) -> bool:
        """Synchronous worktree switch validation."""
        return worktree_path.exists() and worktree_path.is_dir()

    async def has_uncommitted_changes(self, worktree_path: Path) -> bool:
        """Check if worktree has uncommitted changes.

        Args:
            worktree_path: Path to the worktree to check

        Returns:
            True if there are uncommitted changes
        """
        return await asyncio.to_thread(self._has_uncommitted_changes_sync, worktree_path)

    def _has_uncommitted_changes_sync(self, worktree_path: Path) -> bool:
        """Synchronous uncommitted changes check."""
        try:
            worktree_repo = git.Repo(worktree_path)
            return worktree_repo.is_dirty()
        except git.InvalidGitRepositoryError:
            return False

    async def has_untracked_files(self, worktree_path: Path) -> bool:
        """Check if worktree has untracked files.

        Args:
            worktree_path: Path to the worktree to check

        Returns:
            True if there are untracked files
        """
        return await asyncio.to_thread(self._has_untracked_files_sync, worktree_path)

    def _has_untracked_files_sync(self, worktree_path: Path) -> bool:
        """Synchronous untracked files check."""
        try:
            worktree_repo = git.Repo(worktree_path)
            return len(worktree_repo.untracked_files) > 0
        except git.InvalidGitRepositoryError:
            return False

    async def has_unpushed_commits(self, worktree_path: Path) -> bool:
        """Check if worktree has unpushed commits.

        Args:
            worktree_path: Path to the worktree to check

        Returns:
            True if there are unpushed commits
        """
        return await asyncio.to_thread(self._has_unpushed_commits_sync, worktree_path)

    def _get_base_branches(self) -> list[str]:
        """Get list of common base branches to check against."""
        return ["origin/main", "origin/master", "origin/develop", "main", "master", "develop"]

    async def fetch_from_remote(self, remote: str = "origin") -> bool:
        """Fetch latest changes from remote repository.

        Args:
            remote: Remote name to fetch from (default: 'origin')

        Returns:
            True if fetch was successful
        """
        return await asyncio.to_thread(self._fetch_from_remote_sync, remote)

    def _fetch_from_remote_sync(self, remote: str) -> bool:
        """Synchronous fetch from remote."""
        try:
            self.repo.remotes[remote].fetch()
            return True
        except (git.GitCommandError, IndexError):
            return False

    async def list_all_branches(self, include_remotes: bool = True) -> dict[str, list[str]]:
        """List all available branches (local and remote).

        Args:
            include_remotes: Whether to include remote branches

        Returns:
            Dictionary with 'local' and 'remote' branch lists
        """
        return await asyncio.to_thread(self._list_all_branches_sync, include_remotes)

    def _list_all_branches_sync(self, include_remotes: bool) -> dict[str, list[str]]:
        """Synchronous branch listing."""
        result: dict[str, list[str]] = {"local": [], "remote": []}

        try:
            # Get local branches
            for ref in self.repo.references:
                ref_name = ref.name
                if not ref_name.startswith("origin/") and "HEAD" not in ref_name:
                    result["local"].append(ref_name)

            # Get remote branches if requested
            if include_remotes:
                for ref in self.repo.references:
                    ref_name = ref.name
                    if ref_name.startswith("origin/") and "HEAD" not in ref_name:
                        # Store without 'origin/' prefix for cleaner display
                        result["remote"].append(ref_name[7:])

            # Sort both lists
            result["local"].sort()
            result["remote"].sort()

        except Exception:
            # Return empty lists on error
            pass

        return result

    def _has_unpushed_commits_sync(self, worktree_path: Path) -> bool:
        """Synchronous unpushed commits check."""
        try:
            worktree_repo = git.Repo(worktree_path)

            if not worktree_repo.head.is_valid():
                return False

            tracking = getattr(worktree_repo.head.ref, "tracking_branch", lambda: None)()
            if not tracking:
                try:
                    current_branch = worktree_repo.active_branch.name

                    # Check reflog to find where this branch was created from
                    try:
                        reflog_output = worktree_repo.git.reflog("show", current_branch, "-1")
                        if "branch: Created from" in reflog_output:
                            base = reflog_output.split("branch: Created from")[-1].strip()
                            return len(list(worktree_repo.iter_commits(f"{base}..HEAD"))) > 0
                    except git.GitCommandError:
                        pass

                    # Fallback: compare against common base branches
                    for base in self._get_base_branches():
                        try:
                            if base in [ref.name for ref in worktree_repo.references]:
                                merge_base = worktree_repo.git.merge_base(base, "HEAD")
                                if worktree_repo.head.commit.hexsha == merge_base:
                                    return False
                                return (
                                    len(list(worktree_repo.iter_commits(f"{merge_base}..HEAD"))) > 0
                                )
                        except git.GitCommandError:
                            continue

                    return False  # Conservative: no remote and no determinable base is safe

                except (AttributeError, git.GitCommandError):
                    return False  # Can't determine status, assume safe

            # Has tracking branch - check for commits ahead
            return len(list(worktree_repo.iter_commits(f"{tracking}..HEAD"))) > 0
        except (git.InvalidGitRepositoryError, AttributeError, git.GitCommandError):
            return False

    async def get_uncommitted_changes(self, worktree_path: Path) -> str:
        """Get details of uncommitted changes in the worktree.

        Args:
            worktree_path: Path to the worktree to check

        Returns:
            String with git status output showing uncommitted changes
        """
        return await asyncio.to_thread(self._get_uncommitted_changes_sync, worktree_path)

    def _get_uncommitted_changes_sync(self, worktree_path: Path) -> str:
        """Synchronous get uncommitted changes."""
        try:
            worktree_repo = git.Repo(worktree_path)
            # Get short status
            status = worktree_repo.git.status("--short")
            if status:
                return str(status)
            return ""
        except (git.InvalidGitRepositoryError, git.GitCommandError):
            return ""

    async def get_unpushed_commits(self, worktree_path: Path) -> list[str]:
        """Get list of unpushed commits in the worktree.

        Args:
            worktree_path: Path to the worktree to check

        Returns:
            List of commit messages that haven't been pushed
        """
        return await asyncio.to_thread(self._get_unpushed_commits_sync, worktree_path)

    def _format_commit(self, commit: Any) -> str:
        """Format a commit for display."""
        summary = (
            commit.summary
            if isinstance(commit.summary, str)
            else commit.summary.decode("utf-8", errors="replace")
        )
        return f"{str(commit.hexsha)[:8]} {summary}"

    def _get_unpushed_commits_sync(self, worktree_path: Path) -> list[str]:
        """Synchronous get unpushed commits."""
        try:
            worktree_repo = git.Repo(worktree_path)

            if not worktree_repo.head.is_valid():
                return []

            commits = []
            tracking = getattr(worktree_repo.head.ref, "tracking_branch", lambda: None)()

            if not tracking:
                try:
                    current_branch = worktree_repo.active_branch.name

                    # Check reflog for base branch
                    try:
                        reflog_output = worktree_repo.git.reflog("show", current_branch, "-1")
                        if "branch: Created from" in reflog_output:
                            base = reflog_output.split("branch: Created from")[-1].strip()
                            return [
                                self._format_commit(c)
                                for c in worktree_repo.iter_commits(f"{base}..HEAD")
                            ]
                    except git.GitCommandError:
                        pass

                    # Fallback: compare against common base branches
                    for base in self._get_base_branches():
                        try:
                            if base in [ref.name for ref in worktree_repo.references]:
                                merge_base = worktree_repo.git.merge_base(base, "HEAD")
                                return [
                                    self._format_commit(c)
                                    for c in worktree_repo.iter_commits(f"{merge_base}..HEAD")
                                ]
                        except git.GitCommandError:
                            continue

                except (AttributeError, git.GitCommandError):
                    pass
            else:
                commits = [
                    self._format_commit(c) for c in worktree_repo.iter_commits(f"{tracking}..HEAD")
                ]

            return commits
        except (git.InvalidGitRepositoryError, AttributeError, git.GitCommandError):
            return []
