"""
Git workflow automation for branch creation and atomic commits.

This module provides core git workflow functions for the sync engine, including
branch creation and atomic commits. PR-related functions are in git_pr.py.

Key Functions:
    - is_git_repository: Check if path is inside a git repository
    - get_current_branch: Get the current git branch name
    - get_default_branch: Get the default branch name (main/master)
    - is_on_default_branch: Check if on default branch
    - create_sync_branch: Create a new branch for sync commits
    - create_atomic_commits: Create atomic commits from categorized file groups

See Also:
    persuade.git_pr: PR-related functions (push_branch, create_pull_request)

Usage Example:
    >>> from pathlib import Path
    >>> from persuade.git_workflow import create_sync_branch, create_atomic_commits
    >>>
    >>> # Create a branch for sync commits
    >>> repo_path = Path("/path/to/project")
    >>> branch_result = create_sync_branch(repo_path)
    >>> if branch_result.success:
    ...     print(f"Created branch: {branch_result.branch_name}")
    >>>
    >>> # Create atomic commits
    >>> from persuade.commit_categorization import categorize_for_commits
    >>> groups = categorize_for_commits(synced_files)
    >>> commit_result = create_atomic_commits(repo_path, groups)

Security:
    - All paths are resolved to prevent directory traversal
    - Uses subprocess with explicit command lists (no shell=True)
    - Timeouts prevent hanging on slow git operations
"""

import subprocess
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

from persuade.constants import (
    GIT_LS_FILES_TIMEOUT,
    GIT_REV_PARSE_TIMEOUT,
)
from persuade.logging import get_logger

# Maximum attempts when finding an available branch name (e.g., persuade/sync-20260121-1, -2, etc.)
MAX_BRANCH_NAME_ATTEMPTS = 100

if TYPE_CHECKING:
    from persuade.commit_categorization import CommitGroup

logger = get_logger()


@dataclass
class GitWorkflowResult:
    """Result of a git workflow operation.

    Attributes:
        success: Whether the operation succeeded
        message: Human-readable status message
        branch_name: Name of created/used branch (if applicable)
        commits_created: Number of commits created
        pr_url: URL of created PR (if applicable)
        error: Error message if operation failed
    """

    success: bool
    message: str
    branch_name: str | None = None
    commits_created: int = 0
    pr_url: str | None = None
    error: str | None = None


def is_git_repository(path: Path) -> bool:
    """Check if path is inside a git repository.

    Args:
        path: Directory path to check

    Returns:
        True if path is in a git repository, False otherwise
    """
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--git-dir"],
            cwd=path,
            capture_output=True,
            timeout=GIT_REV_PARSE_TIMEOUT,
            check=False,
        )
        return result.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False


def get_current_branch(repo_path: Path) -> str | None:
    """Get the current git branch name.

    Args:
        repo_path: Path to git repository

    Returns:
        Current branch name, or None if detached HEAD or error
    """
    try:
        result = subprocess.run(
            ["git", "branch", "--show-current"],
            cwd=repo_path,
            capture_output=True,
            text=True,
            timeout=GIT_REV_PARSE_TIMEOUT,
            check=False,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
        return None
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return None


def get_default_branch(repo_path: Path) -> str:
    """Get the default branch name (main or master).

    Args:
        repo_path: Path to git repository

    Returns:
        Default branch name (main, master, or falls back to main)
    """
    try:
        # Try to get from remote origin
        result = subprocess.run(
            ["git", "remote", "show", "origin"],
            cwd=repo_path,
            capture_output=True,
            text=True,
            timeout=GIT_REV_PARSE_TIMEOUT,
            check=False,
        )
        if result.returncode == 0:
            for line in result.stdout.splitlines():
                if "HEAD branch:" in line:
                    return line.split(":")[-1].strip()

        # Fallback: check if main or master exists
        for branch in ["main", "master"]:
            result = subprocess.run(
                ["git", "rev-parse", "--verify", branch],
                cwd=repo_path,
                capture_output=True,
                text=True,
                timeout=GIT_REV_PARSE_TIMEOUT,
                check=False,
            )
            if result.returncode == 0:
                return branch

        # Default fallback
        return "main"
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return "main"


def is_on_default_branch(repo_path: Path) -> bool:
    """Check if currently on the default branch (main/master).

    Args:
        repo_path: Path to git repository

    Returns:
        True if on default branch, False otherwise
    """
    current = get_current_branch(repo_path)
    default = get_default_branch(repo_path)
    return current == default


def create_sync_branch(repo_path: Path, branch_name: str | None = None) -> GitWorkflowResult:
    """Create a new branch for sync commits.

    Args:
        repo_path: Path to git repository
        branch_name: Optional branch name. If None, generates persuade/sync-YYYY-MM-DD

    Returns:
        GitWorkflowResult with branch creation status
    """
    if branch_name is None:
        # Use YYYYMMDD format (no hyphens) for better git tooling compatibility
        date_str = datetime.now().strftime("%Y%m%d")
        branch_name = f"persuade/sync-{date_str}"

    logger.info("creating_sync_branch", branch=branch_name)

    try:
        # Check if branch already exists
        result = subprocess.run(
            ["git", "rev-parse", "--verify", branch_name],
            cwd=repo_path,
            capture_output=True,
            text=True,
            timeout=GIT_REV_PARSE_TIMEOUT,
            check=False,
        )

        if result.returncode == 0:
            # Branch exists, add suffix. Limit iterations to prevent excessive subprocess calls.
            counter = 1
            base_name = branch_name
            while result.returncode == 0 and counter <= MAX_BRANCH_NAME_ATTEMPTS:
                branch_name = f"{base_name}-{counter}"
                result = subprocess.run(
                    ["git", "rev-parse", "--verify", branch_name],
                    cwd=repo_path,
                    capture_output=True,
                    text=True,
                    timeout=GIT_REV_PARSE_TIMEOUT,
                    check=False,
                )
                counter += 1

            if counter > MAX_BRANCH_NAME_ATTEMPTS and result.returncode == 0:
                return GitWorkflowResult(
                    success=False,
                    message=(
                        f"Could not find available branch name "
                        f"after {MAX_BRANCH_NAME_ATTEMPTS} attempts"
                    ),
                    branch_name=None,
                )

        # Create and checkout the branch
        result = subprocess.run(
            ["git", "checkout", "-b", branch_name],
            cwd=repo_path,
            capture_output=True,
            text=True,
            timeout=GIT_REV_PARSE_TIMEOUT,
            check=False,
        )

        if result.returncode != 0:
            return GitWorkflowResult(
                success=False,
                message=f"Failed to create branch: {result.stderr}",
                error=result.stderr,
            )

        logger.info("sync_branch_created", branch=branch_name)
        return GitWorkflowResult(
            success=True,
            message=f"Created branch: {branch_name}",
            branch_name=branch_name,
        )

    except FileNotFoundError:
        return GitWorkflowResult(
            success=False,
            message="Git command not found",
            error="Git is not installed or not in PATH",
        )
    except subprocess.TimeoutExpired:
        return GitWorkflowResult(
            success=False,
            message="Git command timed out",
            error="Git operation timed out",
        )


def create_atomic_commits(
    repo_path: Path,
    commit_groups: list["CommitGroup"],
    force_add: bool = False,
) -> GitWorkflowResult:
    """Create atomic commits from categorized file groups.

    Each CommitGroup becomes a separate commit with semantic message.

    Args:
        repo_path: Path to git repository
        commit_groups: List of CommitGroup objects from categorization
        force_add: Whether to use git add -f (for gitignored files)

    Returns:
        GitWorkflowResult with commit creation status.
        - success=True only if at least one commit was created
        - If files need force_add but force_add=False, returns error
        - error field contains details about failures
    """
    if not commit_groups:
        return GitWorkflowResult(
            success=True,
            message="No files to commit",
            commits_created=0,
        )

    logger.info("creating_atomic_commits", groups=len(commit_groups), force_add=force_add)

    commits_created = 0
    add_failures: list[str] = []
    commit_failures: list[str] = []
    add_flag = ["-f"] if force_add else []

    for group in commit_groups:
        if not group.files:
            continue

        try:
            # Stage files
            add_cmd = ["git", "add", *add_flag, *group.files]
            result = subprocess.run(
                add_cmd,
                cwd=repo_path,
                capture_output=True,
                text=True,
                timeout=GIT_LS_FILES_TIMEOUT,
                check=False,
            )

            if result.returncode != 0:
                error_msg = result.stderr[:200] if result.stderr else "Unknown error"
                logger.warning(
                    "git_add_failed",
                    files=group.files[:3],
                    error=error_msg,
                )
                # Check if this is a gitignore issue (files are ignored)
                if "ignored" in error_msg.lower() and not force_add:
                    add_failures.append(
                        f"Files ignored by .gitignore (use force_add=True): {group.files[:3]}"
                    )
                else:
                    add_failures.append(f"git add failed for {group.files[:3]}: {error_msg}")
                continue

            # Create commit
            commit_msg = group.full_message
            result = subprocess.run(
                ["git", "commit", "-m", commit_msg],
                cwd=repo_path,
                capture_output=True,
                text=True,
                timeout=GIT_LS_FILES_TIMEOUT,
                check=False,
            )

            if result.returncode != 0:
                # Check if nothing to commit (not an error)
                if "nothing to commit" in result.stdout or "nothing to commit" in result.stderr:
                    logger.debug("nothing_to_commit", group=commit_msg)
                    continue

                error_msg = result.stderr[:200] if result.stderr else "Unknown error"
                logger.warning(
                    "git_commit_failed",
                    message=commit_msg,
                    error=error_msg,
                )
                commit_failures.append(f"Commit failed for '{commit_msg}': {error_msg}")
                continue

            commits_created += 1
            logger.info("commit_created", message=commit_msg, files=len(group.files))

        except subprocess.TimeoutExpired:
            logger.warning("git_operation_timeout", group=group.message)
            commit_failures.append(f"Timeout for commit group: {group.message}")
            continue

    # Build result message and error details
    all_failures = add_failures + commit_failures
    if commits_created > 0:
        message = f"Created {commits_created} atomic commit(s)"
        if all_failures:
            message += f" ({len(all_failures)} group(s) failed)"
        return GitWorkflowResult(
            success=True,
            message=message,
            commits_created=commits_created,
            error="; ".join(all_failures) if all_failures else None,
        )
    else:
        # No commits created - this is a failure
        error_detail = "; ".join(all_failures) if all_failures else "No commits created"
        return GitWorkflowResult(
            success=False,
            message=f"Failed to create commits: {error_detail}",
            commits_created=0,
            error=error_detail,
        )
