"""
Git-related operations for sync validation and compliance.

This module provides git integration for the sync engine, focusing on gitignore
management and compliance validation.

Key Functions:
    - is_git_ignored: Check if file/directory is gitignored (checks file and parent directories)
    - validate_gitignore_compliance: Find files that should be gitignored but are tracked
    - update_gitignore: Ensure .gitignore has required entries for .claude/ files
    - check_ignored_synced_files: Detect synced files that are gitignored by target project

For git workflow automation (branches, commits, PRs), see persuade.git_workflow.

Usage Example:
    >>> from pathlib import Path
    >>> from persuade.git_ops import is_git_ignored, validate_gitignore_compliance, update_gitignore
    >>>
    >>> # Check if file is gitignored (useful for skipping in scans)
    >>> project_path = Path("/path/to/project")
    >>> file_path = project_path / ".claude" / "state" / "cache.json"
    >>> if is_git_ignored(file_path, project_path):
    ...     print("File is gitignored, skipping sync")
    >>>
    >>> # Validate gitignore compliance (find violations)
    >>> violations = validate_gitignore_compliance(project_path)
    >>> if violations:
    ...     print(f"Found {len(violations)} files that should be gitignored:")
    ...     for file in violations:
    ...         print(f"  - {file}")
    >>>
    >>> # Ensure .gitignore is up to date
    >>> added = update_gitignore(project_path)
    >>> if added:
    ...     print(f"Added {len(added)} entries to .gitignore")
    >>>
    >>> # Check for gitignored synced files
    >>> ignored = check_ignored_synced_files(project_path, [".claude/knowledge/git/README.md"])
    >>> if ignored:
    ...     print(f"Warning: {len(ignored)} synced files are gitignored")

Performance:
    - is_git_ignored: Moderate (checks file + parent directories, multiple subprocess calls)
    - validate_gitignore_compliance: Moderate (one git ls-files per pattern)
    - Uses timeouts to prevent hanging on slow git operations

Security:
    - All paths are resolved to prevent directory traversal
    - Only predefined patterns checked (no user-controlled commands)
    - Uses subprocess with explicit command lists (no shell=True)
"""

import subprocess
from pathlib import Path

from persuade.constants import (
    GIT_CHECK_IGNORE_TIMEOUT,
    GIT_LS_FILES_TIMEOUT,
    REQUIRED_GITIGNORE_ENTRIES,
)
from persuade.logging import get_logger

logger = get_logger()


def is_git_ignored(file_path: Path, repo_root: Path) -> bool:
    """
    Check if file is ignored by git.

    This function checks both the file directly AND all its parent directories
    up to the repo root. This is necessary because `git check-ignore` only
    checks the file path itself, not whether a parent directory is ignored.

    For example, if `.gitignore` contains `.claude` (without trailing slash),
    `git check-ignore .claude/settings.json` returns "not ignored", but
    `git add .claude/settings.json` will fail because the parent is ignored.

    Args:
        file_path: Path to the file to check
        repo_root: Repository root directory

    Returns:
        True if file or any parent directory is git-ignored, False otherwise
    """
    try:
        # First check the file itself
        result = subprocess.run(
            ["git", "check-ignore", "-q", str(file_path)],
            cwd=repo_root,
            capture_output=True,
            timeout=GIT_CHECK_IGNORE_TIMEOUT,
        )
        if result.returncode == 0:
            return True

        # Check all parent directories up to repo root
        # This handles the case where .gitignore contains ".claude" (without trailing slash)
        # which ignores the directory itself, but git check-ignore on files inside returns
        # a non-zero exit code (indicating "not ignored")
        file_path_resolved = Path(file_path)
        if file_path_resolved.is_absolute():
            # Make relative to repo_root for parent checking
            try:
                file_path_resolved = file_path_resolved.relative_to(repo_root.resolve())
            except ValueError:
                # file_path is not under repo_root, use as-is
                file_path_resolved = Path(file_path)

        # Check each parent directory
        for parent in file_path_resolved.parents:
            # Stop at repo root (empty path or ".")
            if str(parent) in ("", "."):
                break

            parent_result = subprocess.run(
                ["git", "check-ignore", "-q", str(parent)],
                cwd=repo_root,
                capture_output=True,
                timeout=GIT_CHECK_IGNORE_TIMEOUT,
            )
            if parent_result.returncode == 0:
                logger.debug(
                    "parent_directory_gitignored",
                    file=str(file_path),
                    parent=str(parent),
                )
                return True

        return False
    except subprocess.TimeoutExpired:
        # Git command timed out during file or parent directory check.
        # Returning False as fallback, but file may actually be ignored
        # (either directly or via an unchecked parent directory).
        # WARNING: This is a potentially incorrect result.
        logger.warning(
            "git_check_ignore_timeout",
            path=str(file_path),
            timeout=GIT_CHECK_IGNORE_TIMEOUT,
            note="returning False but file may be ignored",
        )
        return False
    except FileNotFoundError:
        # Git command not found - system configuration issue.
        # Returning False as fallback, but gitignore status is unknown.
        # WARNING: All gitignore checks will return False until git is available.
        logger.warning(
            "git_command_not_found",
            path=str(file_path),
            note="git not installed or not in PATH",
        )
        return False
    # Note: With check=False, CalledProcessError won't be raised.
    # TimeoutExpired and FileNotFoundError cover expected failure modes.


def validate_gitignore_compliance(target_path: Path) -> list[str]:
    """
    Check if any files that should be ignored are tracked in git.

    This function scans for files matching REQUIRED_GITIGNORE_ENTRIES
    that are currently tracked by git, which violates best practices.

    Args:
        target_path: Target project path (will be resolved to absolute path)

    Returns:
        List of tracked files that should be ignored (empty if compliant)

    Raises:
        This function does not raise exceptions. It gracefully handles:
        - Non-git repositories (returns empty list)
        - Missing git command (returns empty list)
        - Subprocess errors (skips problematic patterns)

    Security:
        - Resolves target_path to prevent directory traversal
        - Only checks against predefined REQUIRED_GITIGNORE_ENTRIES constants
        - Does not execute user-controlled commands (pat comes from constants)
        - Uses subprocess with explicit command list (no shell=True)

    Note:
        Safe to call on non-git repos - returns empty list.
        Requires git command to be available in PATH for git repos.

    Example:
        >>> from pathlib import Path
        >>> violations = validate_gitignore_compliance(Path("/path/to/project"))
        >>> if violations:
        ...     print(f"Found {len(violations)} files that should be ignored")
        ...     for file in violations:
        ...         print(f"  - {file}")
    """
    # Validate target_path is an absolute path to prevent directory traversal
    target_path = target_path.resolve()

    logger.info("validating_gitignore_compliance", target_path=str(target_path))

    tracked_but_should_ignore = []

    for pattern in REQUIRED_GITIGNORE_ENTRIES:
        patterns_to_check = []
        if pattern.endswith("/"):
            # Check both the directory itself and all files within it
            dir_no_slash = pattern.rstrip("/")
            patterns_to_check.append(dir_no_slash)
            patterns_to_check.append(f"{dir_no_slash}/*")
        else:
            patterns_to_check.append(pattern)

        for pat in patterns_to_check:
            try:
                result = subprocess.run(
                    ["git", "ls-files", pat],
                    cwd=target_path,
                    capture_output=True,
                    text=True,
                    check=False,
                    timeout=GIT_LS_FILES_TIMEOUT,
                )
                # Check explicit return code instead of catching broad exceptions
                if result.returncode != 0:
                    # Not a git repo or git command failed - skip this pattern
                    logger.debug(
                        "git_ls_files_failed",
                        pattern=pat,
                        returncode=result.returncode,
                        stderr=result.stderr[:200] if result.stderr else None,
                    )
                    continue

                if result.stdout.strip():
                    # Files found - they're tracked but shouldn't be
                    violations = result.stdout.strip().splitlines()
                    tracked_but_should_ignore.extend(violations)
                    logger.warning(
                        "gitignore_violations_found",
                        pattern=pat,
                        count=len(violations),
                        files=violations[:5],  # Log first 5 for context
                    )
            except FileNotFoundError:
                # git command not found - not installed
                logger.debug("git_command_not_found", pattern=pat)
                continue
            except subprocess.TimeoutExpired:
                # Git command timed out - skip this pattern
                logger.warning("git_command_timeout", pattern=pat, timeout=GIT_LS_FILES_TIMEOUT)
                continue
            # Note: CalledProcessError removed - not raised when check=False

    logger.info(
        "gitignore_validation_complete",
        violations_count=len(tracked_but_should_ignore),
    )
    return tracked_but_should_ignore


def update_gitignore(target_path: Path) -> list[str]:
    """
    Ensure .gitignore has required entries for .claude/.

    This function adds missing gitignore entries from REQUIRED_GITIGNORE_ENTRIES
    to the project's .gitignore file. Existing entries are preserved.

    Side effects:
        Modifies or creates .gitignore file if entries are missing

    Args:
        target_path: Target project path

    Returns:
        List of entries that were added (empty if none were added)

    Raises:
        OSError: If .gitignore file cannot be read or written
        PermissionError: If insufficient permissions to modify .gitignore

    Example:
        >>> from pathlib import Path
        >>> added = update_gitignore(Path("/path/to/project"))
        >>> if added:
        ...     print(f"Added {len(added)} entries to .gitignore:")
        ...     for entry in added:
        ...         print(f"  + {entry}")
        ... else:
        ...     print(".gitignore already up to date")
    """
    gitignore_path = target_path / ".gitignore"

    logger.info("updating_gitignore", target_path=str(target_path))

    # Read existing gitignore
    if gitignore_path.exists():
        with open(gitignore_path) as f:
            existing = f.read()
        logger.debug("gitignore_exists", size=len(existing))
    else:
        existing = ""
        logger.debug("gitignore_missing", action="will_create")

    # Check which entries are missing (line-by-line to avoid false matches)
    missing = []
    existing_lines = {line.strip() for line in existing.splitlines()}
    for entry in REQUIRED_GITIGNORE_ENTRIES:
        # Check if entry exists as a complete line (with or without trailing slash)
        entry_stripped = entry.rstrip("/")
        if entry not in existing_lines and entry_stripped not in existing_lines:
            missing.append(entry)

    # Append missing entries
    if missing:
        logger.info("adding_gitignore_entries", count=len(missing), entries=missing)
        with open(gitignore_path, "a") as f:
            if existing and not existing.endswith("\n"):
                f.write("\n")

            f.write("\n# Claude Code - runtime/temporary files\n")
            for entry in missing:
                f.write(f"{entry}\n")
    else:
        logger.debug("gitignore_up_to_date")

    return missing  # Return what was added


def check_ignored_synced_files(target_path: Path, synced_files: list[str]) -> list[str]:
    """Check which synced files are gitignored by the target project.

    This detects the "silent failure" where persuade syncs files but they
    don't appear in git status because they're gitignored.

    Args:
        target_path: Target project path
        synced_files: List of file paths that were synced (relative to target)

    Returns:
        List of files that are gitignored (need git add -f)

    Example:
        >>> ignored = check_ignored_synced_files(Path("."), [".claude/knowledge/git/README.md"])
        >>> if ignored:
        ...     print(f"{len(ignored)} files need force-add")
    """
    target_path = target_path.resolve()
    ignored_files: list[str] = []

    logger.info("checking_ignored_synced_files", count=len(synced_files))

    for file_path in synced_files:
        full_path = target_path / file_path

        # Only check files that actually exist
        if not full_path.exists():
            continue

        if is_git_ignored(full_path, target_path):
            ignored_files.append(file_path)
            logger.debug("synced_file_is_gitignored", file=file_path)

    logger.info("ignored_synced_files_found", count=len(ignored_files))
    return ignored_files
