"""
Core git attribution stripping functionality.

This module provides the main functions for stripping Claude attribution
from git commit messages and history.

Key Functions:
    - strip_claude_attribution: Remove Claude attribution from a commit message
    - strip_attribution_from_commits: Rewrite commit history to remove attribution
    - preview_changes: Preview what changes would be made
"""

import os
import re
import subprocess
from pathlib import Path

from persuade.constants import GIT_CHECK_IGNORE_TIMEOUT
from persuade.logging import get_logger

from .backup import create_backup_ref
from .types import (
    CLAUDE_COAUTHOR_PATTERNS,
    GENERATED_WITH_PATTERNS,
    GIT_FILTER_BRANCH_TIMEOUT,
    GIT_REF_OPERATION_TIMEOUT,
    CommitInfo,
    StripResult,
)

logger = get_logger()


def strip_claude_attribution(
    message: str,
    preserve_human_coauthors: bool = True,
) -> str:
    """
    Remove Claude attribution from a commit message.

    This removes:
    - Co-Authored-By lines for Claude/Anthropic
    - Generated-with signatures

    Args:
        message: The commit message to process
        preserve_human_coauthors: If True, keep Co-Authored-By lines for human
            contributors. If False, remove all Co-Authored-By lines.

    Returns:
        The commit message with Claude attribution removed

    Example:
        >>> msg = '''feat: add feature

        Co-Authored-By: Claude <noreply@anthropic.com>
        Co-Authored-By: John Doe <john@example.com>

        Generated with [Claude Code](https://claude.ai/code)'''
        >>> strip_claude_attribution(msg)
        'feat: add feature\\n\\nCo-Authored-By: John Doe <john@example.com>'
    """
    lines = message.splitlines()
    result_lines: list[str] = []
    skip_blank_before_trailer = False

    for line in lines:
        should_keep = True

        # Check for Claude co-author patterns
        for pattern in CLAUDE_COAUTHOR_PATTERNS:
            if re.match(pattern, line.strip()):
                should_keep = False
                skip_blank_before_trailer = True
                break

        # Check for generated-with patterns
        if should_keep:
            for pattern in GENERATED_WITH_PATTERNS:
                if re.match(pattern, line.strip(), re.IGNORECASE):
                    should_keep = False
                    break

        # Optionally strip all co-author lines
        if (
            should_keep
            and not preserve_human_coauthors
            and re.match(r"Co-[Aa]uthored-[Bb]y:\s*", line.strip())
        ):
            should_keep = False

        if should_keep:
            # Skip blank lines immediately before removed trailers
            if skip_blank_before_trailer and line.strip() == "":
                skip_blank_before_trailer = False
                continue
            result_lines.append(line)
            skip_blank_before_trailer = False

    # Clean up trailing whitespace and extra blank lines
    result = "\n".join(result_lines)
    # Remove trailing blank lines
    result = result.rstrip()
    # Ensure single trailing newline
    if result:
        result += "\n"

    return result


def _get_commit_info(repo_path: Path, commit_sha: str) -> CommitInfo | None:
    """Get detailed information about a commit."""
    try:
        # Use %x00 as separator to handle messages with newlines
        format_str = "%H%x00%B%x00%an%x00%ae%x00%ai%x00%cn%x00%ce%x00%ci"
        result = subprocess.run(
            ["git", "log", "-1", f"--format={format_str}", commit_sha],
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=True,
            timeout=GIT_CHECK_IGNORE_TIMEOUT,
        )
        parts = result.stdout.split("\x00")
        if len(parts) >= 8:
            return CommitInfo(
                sha=parts[0],
                message=parts[1],
                author_name=parts[2],
                author_email=parts[3],
                author_date=parts[4],
                committer_name=parts[5],
                committer_email=parts[6],
                committer_date=parts[7].strip(),
            )
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
        logger.error("get_commit_info_failed", sha=commit_sha, error=str(e))
    return None


def _get_default_branch(repo_path: Path) -> str | None:
    """Get the default branch name (main or master)."""
    try:
        # Try to get default branch from remote
        result = subprocess.run(
            ["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=False,
            timeout=GIT_CHECK_IGNORE_TIMEOUT,
        )
        if result.returncode == 0:
            return result.stdout.strip().split("/")[-1]

        # Fall back to checking if main or master exists
        for branch in ["main", "master"]:
            verify_result = subprocess.run(
                ["git", "rev-parse", "--verify", branch],
                cwd=repo_path,
                capture_output=True,
                check=False,
                timeout=GIT_CHECK_IGNORE_TIMEOUT,
            )
            if verify_result.returncode == 0:
                return branch
    except subprocess.TimeoutExpired:
        pass
    return None


def _get_commits_to_rewrite(
    repo_path: Path,
    commit_count: int | None = None,
    commit_range: str | None = None,
    branch_only: bool = False,
) -> list[str]:
    """
    Get list of commit SHAs to rewrite.

    Args:
        repo_path: Path to the git repository
        commit_count: Number of recent commits to include
        commit_range: Git revision range (e.g., "HEAD~10..HEAD")
        branch_only: If True, include commits since divergence from main

    Returns:
        List of commit SHAs (oldest first)
    """
    try:
        if commit_range:
            cmd = ["git", "rev-list", "--reverse", commit_range]
        elif branch_only:
            # Get commits since diverging from main/master
            main_branch = _get_default_branch(repo_path)
            if main_branch:
                cmd = ["git", "rev-list", "--reverse", f"{main_branch}..HEAD"]
            else:
                return []
        elif commit_count:
            # Use -n flag instead of HEAD~N..HEAD to handle repos with few commits
            cmd = ["git", "rev-list", "--reverse", "-n", str(commit_count), "HEAD"]
        else:
            return []

        result = subprocess.run(
            cmd,
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=True,
            timeout=GIT_REF_OPERATION_TIMEOUT,
        )
        return [sha.strip() for sha in result.stdout.strip().splitlines() if sha.strip()]
    except subprocess.CalledProcessError as e:
        logger.error("get_commits_failed", error=str(e), stderr=e.stderr)
        return []
    except subprocess.TimeoutExpired:
        logger.error("get_commits_timeout")
        return []


def preview_changes(
    repo_path: Path,
    commit_count: int | None = None,
    commit_range: str | None = None,
    branch_only: bool = False,
    preserve_human_coauthors: bool = True,
) -> list[tuple[str, str, str]]:
    """
    Preview changes that would be made by strip_attribution_from_commits.

    Args:
        repo_path: Path to the git repository
        commit_count: Number of recent commits to preview
        commit_range: Git revision range
        branch_only: If True, preview commits since divergence from main
        preserve_human_coauthors: If True, keep human Co-Authored-By lines

    Returns:
        List of (sha, original_message, new_message) tuples
    """
    commits = _get_commits_to_rewrite(repo_path, commit_count, commit_range, branch_only)
    changes = []

    for sha in commits:
        info = _get_commit_info(repo_path, sha)
        if info:
            new_message = strip_claude_attribution(info.message, preserve_human_coauthors)
            if new_message.strip() != info.message.strip():
                changes.append((sha[:8], info.message, new_message))

    return changes


def _create_filter_script(preserve_human_coauthors: bool) -> str:
    """Create the sed/awk script for filtering commit messages.

    Patterns are designed to match CLAUDE_COAUTHOR_PATTERNS and
    GENERATED_WITH_PATTERNS to ensure preview_changes() and
    strip_attribution_from_commits() produce consistent results.
    """
    # Build patterns to remove
    patterns = []

    # Claude co-author patterns (match CLAUDE_COAUTHOR_PATTERNS)
    # Pattern 1: "Claude" with whitespace boundaries (not "NotClaude" or "Claudette")
    patterns.append(r"/Co-[Aa]uthored-[Bb]y:[ \t]*Claude[ \t]*</d")
    # Pattern 2: "Claude Code" variant
    patterns.append(r"/Co-[Aa]uthored-[Bb]y:[ \t]*Claude[ \t]*Code[ \t]*</d")
    # Pattern 3: Anthropic domain emails
    patterns.append(r"/Co-[Aa]uthored-[Bb]y:.*<.*@anthropic\.com>/d")

    # Generated-with patterns (match GENERATED_WITH_PATTERNS)
    patterns.append(r"/Generated with.*[Cc]laude/d")

    if not preserve_human_coauthors:
        patterns.append(r"/Co-[Aa]uthored-[Bb]y:/d")

    # Chain sed commands
    sed_commands = " | ".join([f"sed '{p}'" for p in patterns])

    # Also remove empty lines at end and trailing generated lines
    return f"cat | {sed_commands} | sed '/^[[:space:]]*$/{{N;/^\\n$/d;}}'"


def strip_attribution_from_commits(
    repo_path: Path,
    commit_count: int | None = None,
    commit_range: str | None = None,
    branch_only: bool = False,
    preserve_human_coauthors: bool = True,
    dry_run: bool = False,
) -> StripResult:
    """
    Strip Claude attribution from commit history.

    This rewrites git history to remove Claude attribution from commit messages.
    A backup ref is created before any changes are made.

    Args:
        repo_path: Path to the git repository
        commit_count: Number of recent commits to process
        commit_range: Git revision range (e.g., "HEAD~10..HEAD")
        branch_only: If True, process commits since divergence from main
        preserve_human_coauthors: If True, keep Co-Authored-By for humans
        dry_run: If True, only preview changes without making them

    Returns:
        StripResult with details about the operation

    Raises:
        ValueError: If no commit selection method is specified
    """
    if not any([commit_count, commit_range, branch_only]):
        return StripResult(
            success=False,
            message="Must specify commit_count, commit_range, or branch_only",
            error="No commits specified",
        )

    # Get commits to rewrite
    commits = _get_commits_to_rewrite(repo_path, commit_count, commit_range, branch_only)
    if not commits:
        return StripResult(
            success=True,
            message="No commits found to process",
            commits_rewritten=0,
        )

    # Preview changes
    changes = preview_changes(
        repo_path, commit_count, commit_range, branch_only, preserve_human_coauthors
    )

    if not changes:
        return StripResult(
            success=True,
            message="No Claude attribution found in commits",
            commits_rewritten=0,
        )

    if dry_run:
        return StripResult(
            success=True,
            message=f"Dry run: {len(changes)} commits would be rewritten",
            commits_rewritten=len(changes),
        )

    # Create backup before rewriting
    backup_ref = create_backup_ref(repo_path)
    if not backup_ref:
        return StripResult(
            success=False,
            message="Failed to create backup ref",
            error="Could not create backup",
        )

    # Use git filter-branch with msg-filter to rewrite commits
    # This is safer than interactive rebase for automation
    try:
        # Find the oldest commit to rewrite
        oldest_commit = commits[0]

        # Get parent of oldest commit for the range
        result = subprocess.run(
            ["git", "rev-parse", f"{oldest_commit}^"],
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=False,
            timeout=GIT_CHECK_IGNORE_TIMEOUT,
        )

        if result.returncode == 0:
            parent = result.stdout.strip()
            revision_range = f"{parent}..HEAD"
        else:
            # No parent (root commit), rewrite from root
            revision_range = None

        # Create the filter script
        filter_script = _create_filter_script(preserve_human_coauthors)

        cmd = [
            "git",
            "filter-branch",
            "-f",
            "--msg-filter",
            filter_script,
        ]

        if revision_range:
            cmd.append(revision_range)
        else:
            # For root commits, use HEAD to only rewrite the current branch
            # (not --all which would rewrite our backup ref too)
            cmd.append("HEAD")

        # Build environment with preserve_human setting
        env = os.environ.copy()
        env["PERSUADE_PRESERVE_HUMAN"] = "1" if preserve_human_coauthors else "0"

        subprocess.run(
            cmd,
            cwd=repo_path,
            capture_output=True,
            text=True,
            check=True,
            timeout=GIT_FILTER_BRANCH_TIMEOUT,
            env=env,
        )

        logger.info(
            "strip_attribution_complete",
            commits_rewritten=len(changes),
            backup_ref=backup_ref,
        )

        return StripResult(
            success=True,
            message=f"Successfully rewrote {len(changes)} commits",
            commits_rewritten=len(changes),
            backup_ref=backup_ref,
        )

    except subprocess.CalledProcessError as e:
        logger.error("filter_branch_failed", error=str(e), stderr=e.stderr)
        return StripResult(
            success=False,
            message="Failed to rewrite commits",
            error=e.stderr if e.stderr else str(e),
            backup_ref=backup_ref,
        )
    except subprocess.TimeoutExpired:
        return StripResult(
            success=False,
            message="Operation timed out",
            error=f"Timeout after {GIT_FILTER_BRANCH_TIMEOUT} seconds",
            backup_ref=backup_ref,
        )
