"""Git operations service for autowt."""

import logging
from collections.abc import Callable
from pathlib import Path

from autowt.models import BranchStatus, WorktreeInfo
from autowt.prompts import confirm_default_no
from autowt.utils import run_command, run_command_quiet_on_failure, run_command_visible

logger = logging.getLogger(__name__)


class GitCommands:
    """Low-level git command construction."""

    @staticmethod
    def worktree_add_existing(worktree_path: Path, branch: str) -> list[str]:
        """Build command to add worktree for existing branch."""
        return ["git", "worktree", "add", str(worktree_path), branch]

    @staticmethod
    def worktree_add_new_branch(
        worktree_path: Path, branch: str, start_point: str
    ) -> list[str]:
        """Build command to add worktree with new branch from start point."""
        return ["git", "worktree", "add", str(worktree_path), "-b", branch, start_point]

    @staticmethod
    def worktree_remove(worktree_path: Path, force: bool = False) -> list[str]:
        """Build command to remove worktree."""
        cmd = ["git", "worktree", "remove"]
        if force:
            cmd.append("--force")
        cmd.append(str(worktree_path))
        return cmd

    @staticmethod
    def branch_exists_locally(branch: str) -> list[str]:
        """Build command to check if branch exists locally."""
        return ["git", "show-ref", "--verify", f"refs/heads/{branch}"]

    @staticmethod
    def branch_exists_remotely(branch: str) -> list[str]:
        """Build command to check if branch exists on remote."""
        return ["git", "show-ref", "--verify", f"refs/remotes/origin/{branch}"]


class BranchResolver:
    """Resolves branch existence and determines worktree creation strategy."""

    def __init__(self, git_service):
        self.commands = GitCommands()
        self.git_service = git_service

    def resolve_worktree_source(
        self, repo_path: Path, branch: str, from_branch: str | None
    ) -> Callable[[Path], list[str]]:
        """Return function that builds git command for creating worktree."""
        if from_branch:
            return self._build_command_from_specific_branch(
                repo_path, branch, from_branch
            )
        return self._build_command_from_branch_hierarchy(repo_path, branch)

    def _build_command_from_specific_branch(
        self, repo_path: Path, branch: str, from_branch: str
    ) -> Callable[[Path], list[str]]:
        """Return command builder when user specified source branch."""
        if self._branch_exists_locally(repo_path, branch):
            return lambda path: self.commands.worktree_add_existing(path, branch)
        return lambda path: self.commands.worktree_add_new_branch(
            path, branch, from_branch
        )

    def _build_command_from_branch_hierarchy(
        self, repo_path: Path, branch: str
    ) -> Callable[[Path], list[str]]:
        """Return command builder using branch resolution hierarchy."""
        if self._branch_exists_locally(repo_path, branch):
            return lambda path: self.commands.worktree_add_existing(path, branch)

        if self._branch_exists_remotely(repo_path, branch):
            return lambda path: self.commands.worktree_add_new_branch(
                path, branch, f"origin/{branch}"
            )

        start_point = self._find_best_start_point(repo_path)
        return lambda path: self.commands.worktree_add_new_branch(
            path, branch, start_point
        )

    def _branch_exists_locally(self, repo_path: Path, branch: str) -> bool:
        """Check if branch exists locally."""
        result = run_command_quiet_on_failure(
            self.commands.branch_exists_locally(branch),
            cwd=repo_path,
            timeout=10,
            description=f"Check if branch {branch} exists locally",
        )
        return result.returncode == 0

    def _branch_exists_remotely(self, repo_path: Path, branch: str) -> bool:
        """Check if branch exists on remote."""
        result = run_command_quiet_on_failure(
            self.commands.branch_exists_remotely(branch),
            cwd=repo_path,
            timeout=10,
            description=f"Check if remote branch origin/{branch} exists",
        )
        return result.returncode == 0

    def _find_best_start_point(self, repo_path: Path) -> str:
        """Find best starting point for new branch."""
        default_branch = self.git_service._get_default_branch(repo_path)
        if not default_branch:
            return "HEAD"

        if self._branch_exists_remotely(repo_path, default_branch):
            return f"origin/{default_branch}"

        if self._branch_exists_locally(repo_path, default_branch):
            return default_branch

        return "HEAD"


class GitOutputParser:
    """Parses git command outputs into structured data."""

    @staticmethod
    def parse_worktree_list(porcelain_output: str) -> list[WorktreeInfo]:
        """Parse git worktree list --porcelain output into WorktreeInfo objects."""
        worktrees = []
        current_path = None
        current_branch = None
        is_first_worktree = True

        for line in porcelain_output.strip().split("\n"):
            if not line:
                if current_path and current_branch:
                    worktrees.append(
                        WorktreeInfo(
                            branch=current_branch,
                            path=Path(current_path),
                            is_current=False,  # We don't track this in porcelain output
                            is_primary=is_first_worktree,
                        )
                    )
                current_path = None
                current_branch = None
                is_first_worktree = False
            elif line.startswith("worktree "):
                current_path = line[9:]  # Remove 'worktree ' prefix
            elif line.startswith("branch refs/heads/"):
                current_branch = line[18:]  # Remove 'branch refs/heads/' prefix
            elif line in ["bare", "detached"] or line.startswith("HEAD "):
                continue

        # Process last entry
        if current_path and current_branch:
            worktrees.append(
                WorktreeInfo(
                    branch=current_branch,
                    path=Path(current_path),
                    is_current=False,
                    is_primary=is_first_worktree,
                )
            )

        return worktrees


class GitService:
    """Handles all git operations for worktree management."""

    def __init__(self):
        """Initialize git service."""
        self.commands = GitCommands()
        self.branch_resolver = BranchResolver(self)
        self.parser = GitOutputParser()
        logger.debug("Git service initialized")

    def find_repo_root(self, start_path: Path | None = None) -> Path | None:
        """Find the root of the git repository."""
        if start_path is None:
            start_path = Path.cwd()

        current = start_path.resolve()
        while current != current.parent:
            # Check for normal git repository (.git directory)
            if (current / ".git").exists():
                logger.debug(f"Found repo root: {current}")
                return current

            # Check if current directory is a bare repository
            if self._is_bare_repo(current):
                logger.debug(f"Found bare repo root: {current}")
                return current

            # Check for bare repositories in subdirectories (*.git pattern)
            bare_repo = self._find_bare_repo_in_dir(current)
            if bare_repo:
                logger.debug(f"Found bare repo in subdirectory: {bare_repo}")
                return bare_repo

            current = current.parent

        logger.debug("No git repository found")
        return None

    def is_git_repo(self, path: Path) -> bool:
        """Check if the given path is a git repository."""
        try:
            # Check for regular git repository
            result = run_command(
                ["git", "rev-parse", "--git-dir"],
                cwd=path,
                timeout=10,
                description="Check if directory is git repo",
            )
            if result.returncode == 0:
                logger.debug(f"Path {path} is regular git repo")
                return True

            # Check for bare repository
            is_bare = self._is_bare_repo(path)
            logger.debug(f"Path {path} is git repo (bare: {is_bare}): {is_bare}")
            return is_bare
        except Exception as e:
            logger.debug(f"Error checking if {path} is git repo: {e}")
            return False

    def get_current_branch(self, repo_path: Path) -> str | None:
        """Get the current branch name."""
        try:
            result = run_command(
                ["git", "branch", "--show-current"],
                cwd=repo_path,
                timeout=10,
                description="Get current branch",
            )
            if result.returncode == 0:
                branch = result.stdout.strip()
                logger.debug(f"Current branch: {branch}")
                return branch
        except Exception as e:
            logger.error(f"Failed to get current branch: {e}")

        return None

    def list_worktrees(self, repo_path: Path) -> list[WorktreeInfo]:
        """List all git worktrees."""
        try:
            result = self._execute_worktree_list_command(repo_path)
            if result.returncode != 0:
                logger.error(f"Git worktree list failed: {result.stderr}")
                return []

            worktrees = self.parser.parse_worktree_list(result.stdout)
            logger.debug(f"Found {len(worktrees)} worktrees")
            return worktrees

        except Exception as e:
            logger.error(f"Failed to list worktrees: {e}")
            return []

    def _execute_worktree_list_command(self, repo_path: Path):
        """Execute git worktree list command."""
        return run_command(
            ["git", "worktree", "list", "--porcelain"],
            cwd=repo_path,
            timeout=30,
            description="List git worktrees",
        )

    def fetch_branches(self, repo_path: Path) -> bool:
        """Fetch latest branches from remote."""
        logger.debug("Fetching branches from remote")
        try:
            result = run_command_visible(
                ["git", "fetch", "--prune"],
                cwd=repo_path,
                timeout=60,
            )

            success = result.returncode == 0
            if success:
                logger.debug("Fetch completed successfully")
            else:
                logger.error(f"Fetch failed: {result.stderr}")

            return success

        except Exception as e:
            logger.error(f"Failed to fetch branches: {e}")
            return False

    def create_worktree(
        self,
        repo_path: Path,
        branch: str,
        worktree_path: Path,
        from_branch: str | None = None,
    ) -> bool:
        """Create a new worktree for the given branch."""
        logger.debug(f"Creating worktree for {branch} at {worktree_path}")

        try:
            command_builder = self.branch_resolver.resolve_worktree_source(
                repo_path, branch, from_branch
            )
            cmd = command_builder(worktree_path)
            result = run_command_visible(cmd, cwd=repo_path, timeout=30)

            return self._evaluate_worktree_creation_result(result, worktree_path)

        except Exception as e:
            logger.error(f"Failed to create worktree: {e}")
            return False

    def _evaluate_worktree_creation_result(self, result, worktree_path: Path) -> bool:
        """Evaluate worktree creation command result and log appropriately."""
        success = result.returncode == 0
        if success:
            logger.debug(f"Worktree created successfully at {worktree_path}")
        else:
            logger.error(f"Failed to create worktree: {result.stderr}")
        return success

    def remove_worktree(
        self,
        repo_path: Path,
        worktree_path: Path,
        force: bool = False,
        interactive: bool = True,
    ) -> bool:
        """Remove a worktree."""
        logger.debug(f"Removing worktree at {worktree_path}")

        try:
            cmd = self.commands.worktree_remove(worktree_path, force)
            result = run_command_visible(cmd, cwd=repo_path, timeout=30)

            if result.returncode == 0:
                logger.debug("Worktree removed successfully")
                return True

            return self._retry_worktree_removal_if_needed(
                repo_path, worktree_path, force, interactive, result
            )

        except Exception as e:
            logger.error(f"Failed to remove worktree: {e}")
            return False

    def _retry_worktree_removal_if_needed(
        self,
        repo_path: Path,
        worktree_path: Path,
        force: bool,
        interactive: bool,
        result,
    ) -> bool:
        """Retry worktree removal with force if user confirms and conditions are met."""
        if (
            not force
            and interactive
            and result.stderr
            and "modified or untracked files" in result.stderr
        ):
            logger.error(f"Failed to remove worktree: {result.stderr}")
            print(f"Git error: {result.stderr.strip()}")

            if confirm_default_no(
                "Retry with --force to remove worktree with modified files?"
            ):
                return self.remove_worktree(
                    repo_path, worktree_path, force=True, interactive=False
                )
        else:
            logger.error(f"Failed to remove worktree: {result.stderr}")

        return False

    def analyze_branches_for_cleanup(
        self,
        repo_path: Path,
        worktrees: list[WorktreeInfo],
        preferred_remote: str | None = None,
    ) -> list[BranchStatus]:
        """Analyze branches to determine cleanup candidates.

        Args:
            repo_path: Repository path
            worktrees: List of worktrees to analyze
            preferred_remote: Preferred remote name (future --remote flag support)
        """
        logger.debug("Analyzing branches for cleanup")

        default_branch = self._prepare_default_branch_for_analysis(
            repo_path, preferred_remote
        )
        branch_statuses = [
            self._analyze_single_branch(repo_path, worktree, default_branch)
            for worktree in worktrees
        ]

        logger.debug(f"Analyzed {len(branch_statuses)} branches")
        return branch_statuses

    def _prepare_default_branch_for_analysis(
        self, repo_path: Path, preferred_remote: str | None = None
    ) -> str | None:
        """Prepare default branch reference for merge analysis.

        Args:
            repo_path: Repository path
            preferred_remote: Preferred remote name (future --remote flag support)

        Returns:
            Remote branch reference if remotes exist, otherwise local branch reference
        """
        default_branch = self._get_default_branch(repo_path)
        if not default_branch:
            logger.warning(
                "Could not determine default branch, skipping merge analysis"
            )
            return None

        # Try to find a remote branch reference first
        remote_ref = self._find_remote_branch_reference(
            repo_path, default_branch, preferred_remote
        )
        if remote_ref:
            logger.debug(f"Using remote branch reference: {remote_ref}")
            return remote_ref

        # Fall back to local branch for remoteless repos
        logger.debug(
            f"No remotes found, using local branch reference: {default_branch}"
        )
        return default_branch

    def _analyze_single_branch(
        self, repo_path: Path, worktree: WorktreeInfo, default_branch: str | None
    ) -> BranchStatus:
        """Analyze a single branch for cleanup eligibility."""
        branch = worktree.branch
        return BranchStatus(
            branch=branch,
            has_remote=self._branch_has_remote(repo_path, branch),
            is_merged=self._branch_is_merged_cached(repo_path, branch, default_branch),
            is_identical=self._branches_are_identical_cached(
                repo_path, branch, default_branch
            ),
            path=worktree.path,
            has_uncommitted_changes=self.has_uncommitted_changes(worktree.path),
        )

    def _get_default_branch(self, repo_path: Path) -> str | None:
        """Get the default branch name (main, master, etc.)."""
        try:
            branch_from_origin = self._extract_default_branch_from_origin(repo_path)
            if branch_from_origin:
                return branch_from_origin

            branch_from_common_names = self._find_common_default_branch(repo_path)
            if branch_from_common_names:
                return branch_from_common_names

            return self._get_current_branch_as_fallback(repo_path)
        except Exception:
            return None

    def _extract_default_branch_from_origin(self, repo_path: Path) -> str | None:
        """Extract default branch name from origin HEAD reference."""
        result = run_command_quiet_on_failure(
            ["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
            cwd=repo_path,
            timeout=10,
            description="Get default branch from origin",
        )
        if result.returncode == 0:
            branch_ref = result.stdout.strip()
            if branch_ref.startswith("refs/remotes/origin/"):
                return branch_ref[len("refs/remotes/origin/") :]
        return None

    def _find_common_default_branch(self, repo_path: Path) -> str | None:
        """Check for common default branch names (main, master)."""
        for branch in ["main", "master"]:
            result = run_command_quiet_on_failure(
                ["git", "show-ref", "--verify", f"refs/heads/{branch}"],
                cwd=repo_path,
                timeout=10,
                description=f"Check if {branch} exists",
            )
            if result.returncode == 0:
                return branch
        return None

    def _get_current_branch_as_fallback(self, repo_path: Path) -> str | None:
        """Get current branch as last resort fallback."""
        result = run_command(
            ["git", "branch", "--show-current"],
            cwd=repo_path,
            timeout=10,
            description="Get current branch as fallback",
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
        return None

    def _get_available_remotes(self, repo_path: Path) -> list[str]:
        """Get list of available remotes, with preferred remotes first."""
        try:
            result = run_command_quiet_on_failure(
                ["git", "remote"],
                cwd=repo_path,
                timeout=10,
                description="Get available remotes",
            )
            if result.returncode != 0:
                return []

            remotes = result.stdout.strip().split("\n") if result.stdout.strip() else []

            # Prioritize common remote names
            preferred_order = ["origin", "upstream"]
            sorted_remotes = []

            # Add preferred remotes first
            for preferred in preferred_order:
                if preferred in remotes:
                    sorted_remotes.append(preferred)

            # Add remaining remotes
            for remote in remotes:
                if remote not in sorted_remotes:
                    sorted_remotes.append(remote)

            return sorted_remotes
        except Exception:
            return []

    def _get_remote_default_branch(self, repo_path: Path, remote: str) -> str | None:
        """Get the default branch for a specific remote."""
        try:
            # Try to get from remote HEAD reference
            result = run_command_quiet_on_failure(
                ["git", "symbolic-ref", f"refs/remotes/{remote}/HEAD"],
                cwd=repo_path,
                timeout=10,
                description=f"Get default branch from {remote} HEAD",
            )
            if result.returncode == 0:
                branch_ref = result.stdout.strip()
                if branch_ref.startswith(f"refs/remotes/{remote}/"):
                    return branch_ref[len(f"refs/remotes/{remote}/") :]
            return None
        except Exception:
            return None

    def _find_remote_branch_reference(
        self, repo_path: Path, local_branch: str, preferred_remote: str | None = None
    ) -> str | None:
        """Find the best remote branch reference for comparison.

        Args:
            repo_path: Repository path
            local_branch: Local branch name (e.g., "main")
            preferred_remote: Preferred remote name (future --remote flag support)

        Returns:
            Remote branch reference (e.g., "origin/main") or None if no remotes exist
        """
        remotes = self._get_available_remotes(repo_path)
        if not remotes:
            return None

        # If a specific remote is preferred (future --remote flag), try it first
        if preferred_remote and preferred_remote in remotes:
            candidate = f"{preferred_remote}/{local_branch}"
            if self._remote_branch_exists(repo_path, candidate):
                return candidate

        # Try each available remote in priority order
        for remote in remotes:
            candidate = f"{remote}/{local_branch}"
            if self._remote_branch_exists(repo_path, candidate):
                return candidate

        return None

    def _remote_branch_exists(self, repo_path: Path, remote_branch: str) -> bool:
        """Check if a remote branch reference exists."""
        try:
            result = run_command_quiet_on_failure(
                ["git", "show-ref", "--verify", f"refs/remotes/{remote_branch}"],
                cwd=repo_path,
                timeout=10,
                description=f"Check if {remote_branch} exists",
            )
            return result.returncode == 0
        except Exception:
            return False

    def _branch_has_remote(self, repo_path: Path, branch: str) -> bool:
        """Check if branch has a remote tracking branch."""
        try:
            result = run_command(
                ["git", "config", f"branch.{branch}.remote"],
                cwd=repo_path,
                timeout=10,
                description=f"Check if branch {branch} has remote",
            )
            return result.returncode == 0
        except Exception:
            return False

    def _branches_are_identical_cached(
        self, repo_path: Path, branch: str, default_branch: str | None
    ) -> bool:
        """Check if branch points to the same commit as default branch."""
        try:
            if not default_branch:
                return False

            branch_hash = self._get_commit_hash(repo_path, branch)
            default_hash = self._get_commit_hash(repo_path, default_branch)

            return branch_hash and default_hash and branch_hash == default_hash
        except Exception:
            return False

    def _get_commit_hash(self, repo_path: Path, branch: str) -> str | None:
        """Get commit hash for a branch."""
        result = run_command_quiet_on_failure(
            ["git", "rev-parse", branch],
            cwd=repo_path,
            timeout=10,
            description=f"Get commit hash for {branch}",
        )
        return result.stdout.strip() if result.returncode == 0 else None

    def _branch_is_merged_cached(
        self, repo_path: Path, branch: str, default_branch: str | None
    ) -> bool:
        """Check if branch is merged into default branch (but not identical)."""
        try:
            if not default_branch:
                return False

            if self._branches_are_identical_cached(repo_path, branch, default_branch):
                return False

            return self._is_branch_ancestor_of_default(
                repo_path, branch, default_branch
            )
        except Exception:
            return False

    def _is_branch_ancestor_of_default(
        self, repo_path: Path, branch: str, default_branch: str
    ) -> bool:
        """Check if branch is an ancestor of default branch (was merged)."""
        result = run_command_quiet_on_failure(
            ["git", "merge-base", "--is-ancestor", branch, default_branch],
            cwd=repo_path,
            timeout=10,
            description=f"Check if {branch} is merged into {default_branch}",
        )
        return result.returncode == 0

    def has_uncommitted_changes(self, worktree_path: Path) -> bool:
        """Check if a worktree has uncommitted changes (staged or unstaged)."""
        try:
            # Check for staged and unstaged changes
            result = run_command(
                ["git", "status", "--porcelain"],
                cwd=worktree_path,
                timeout=10,
                description=f"Check uncommitted changes in {worktree_path}",
            )

            # If status command succeeds and has output, there are uncommitted changes
            if result.returncode == 0:
                has_changes = bool(result.stdout.strip())
                logger.debug(
                    f"Worktree {worktree_path} has uncommitted changes: {has_changes}"
                )
                return has_changes

            logger.debug(f"Failed to check status in {worktree_path}")
            return False

        except Exception as e:
            logger.debug(f"Error checking uncommitted changes in {worktree_path}: {e}")
            return False

    def delete_branch(self, repo_path: Path, branch: str, force: bool = False) -> bool:
        """Delete a local branch."""
        try:
            flag = "-D" if force else "-d"
            result = run_command(
                ["git", "branch", flag, branch],
                cwd=repo_path,
                timeout=10,
                description=f"Delete branch {branch}",
            )
            return result.returncode == 0
        except Exception:
            return False

    def _is_bare_repo(self, path: Path) -> bool:
        """Check if the given path is a bare git repository."""
        try:
            result = run_command(
                ["git", "rev-parse", "--is-bare-repository"],
                cwd=path,
                timeout=10,
                description=f"Check if {path} is bare repo",
            )
            return result.returncode == 0 and result.stdout.strip() == "true"
        except Exception:
            return False

    def _find_bare_repo_in_dir(self, path: Path) -> Path | None:
        """Find bare git repositories in subdirectories (*.git pattern)."""
        try:
            bare_repos = []
            # Look for directories ending in .git
            for item in path.iterdir():
                if item.is_dir() and item.name.endswith(".git"):
                    if self._is_bare_repo(item):
                        bare_repos.append(item)

            if len(bare_repos) == 0:
                return None
            elif len(bare_repos) == 1:
                return bare_repos[0]
            else:
                # Multiple bare repositories found - this is ambiguous
                repo_names = [repo.name for repo in bare_repos]
                raise ValueError(
                    f"Multiple bare git repositories found in {path}: {', '.join(repo_names)}. "
                    f"Please run autowt from within one of the specific repository directories."
                )
        except ValueError:
            # Re-raise ValueError to preserve the specific error message
            raise
        except Exception:
            return None
