"""
Categorization of synced files for atomic git commits.

This module groups synced files into logical categories for creating
atomic commits that follow git best practices. Each category represents
a single logical concern that should be its own commit.

Key Features:
    - Group files by directory/pattern into commit categories
    - Generate semantic commit messages (docs:, chore:, ci:)
    - Support both new files and modified files

Usage Example:
    >>> from pathlib import Path
    >>> from persuade.commit_categorization import categorize_for_commits, CommitGroup
    >>>
    >>> # Categorize files for atomic commits
    >>> files = [
    ...     ".claude/knowledge/infrastructure/storage.md",
    ...     ".claude/knowledge/pr-feedback/README.md",
    ...     ".claude/standards/ENGINEERING_STANDARDS.md",
    ... ]
    >>> groups = categorize_for_commits(files)
    >>> for group in groups:
    ...     print(f"{group.commit_type}: {group.message} ({len(group.files)} files)")
"""

from dataclasses import dataclass, field

# Priority offset applied to modified files so they are committed after
# new files in the same category. This ensures "add X" commits come before
# "update X" commits within the same logical grouping.
MODIFIED_FILE_PRIORITY_OFFSET = 0.5


@dataclass
class CommitGroup:
    """A group of files that should be committed together.

    Attributes:
        commit_type: Semantic commit type (docs, chore, ci, feat)
        message: Commit message without type prefix
        files: List of file paths in this group
        priority: Lower priority commits first (for ordering)
    """

    commit_type: str
    message: str
    files: list[str] = field(default_factory=list)
    priority: float = 0.0

    @property
    def full_message(self) -> str:
        """Get full commit message with type prefix."""
        return f"{self.commit_type}: {self.message}"

    def add_file(self, file_path: str) -> None:
        """Add a file to this commit group."""
        if file_path not in self.files:
            self.files.append(file_path)


# Categorization rules: (path prefix, commit_type, message, priority)
# Priority determines commit order (lower = earlier)
# Files are matched by prefix - first match wins
COMMIT_CATEGORIES: list[tuple[str, str, str, int]] = [
    # Knowledge base categories
    (
        ".claude/knowledge/infrastructure/",
        "docs",
        "add infrastructure knowledge base",
        10,
    ),
    (
        ".claude/knowledge/pr-feedback/",
        "docs",
        "add PR feedback patterns",
        11,
    ),
    (
        ".claude/knowledge/testing/",
        "docs",
        "add testing knowledge base",
        12,
    ),
    (
        ".claude/knowledge/git/",
        "docs",
        "add git knowledge base",
        13,
    ),
    (
        ".claude/knowledge/claude-code/",
        "docs",
        "add claude-code knowledge base",
        14,
    ),
    (
        ".claude/knowledge/automation/",
        "docs",
        "add automation knowledge base",
        15,
    ),
    (
        ".claude/knowledge/python/",
        "docs",
        "add python knowledge base",
        16,
    ),
    (
        ".claude/knowledge/documentation/",
        "docs",
        "add documentation knowledge base",
        17,
    ),
    (
        ".claude/knowledge/",
        "docs",
        "add knowledge base files",
        19,
    ),
    # Standards and configuration
    (
        ".claude/standards/",
        "docs",
        "add engineering standards",
        20,
    ),
    (
        ".claude/commands/",
        "feat",
        "add slash commands",
        30,
    ),
    (
        ".claude/prompts/",
        "feat",
        "add prompts",
        31,
    ),
    (
        ".claude/scripts/",
        "feat",
        "add automation scripts",
        32,
    ),
    (
        ".claude/hooks/",
        "feat",
        "add hooks",
        33,
    ),
    (
        ".claude/settings.json",
        "chore",
        "update Claude Code settings",
        40,
    ),
    (
        ".claude/README.md",
        "docs",
        "add Claude Code documentation",
        41,
    ),
    (
        ".claude/CLAUDE.md",
        "docs",
        "add Claude Code instructions",
        42,
    ),
    (
        ".claude/",
        "chore",
        "add Claude Code configuration",
        49,
    ),
    # GitHub configuration
    (
        ".github/workflows/",
        "ci",
        "add GitHub workflow configurations",
        50,
    ),
    (
        ".github/ISSUE_TEMPLATE/",
        "chore",
        "add GitHub issue templates",
        51,
    ),
    (
        ".github/PULL_REQUEST_TEMPLATE",
        "chore",
        "add GitHub PR template",
        52,
    ),
    (
        ".github/SECURITY.md",
        "docs",
        "add security policy",
        53,
    ),
    (
        ".github/",
        "chore",
        "add GitHub configuration",
        59,
    ),
    # Git hooks
    (
        ".git/hooks/",
        "chore",
        "add git hooks",
        60,
    ),
    # Root configuration files
    (
        "lefthook.yml",
        "chore",
        "add lefthook configuration",
        70,
    ),
    (
        ".bandit",
        "chore",
        "add bandit security configuration",
        71,
    ),
    (
        ".envrc.example",
        "chore",
        "add environment configuration example",
        72,
    ),
    (
        "Justfile.example",
        "chore",
        "add Justfile example",
        73,
    ),
]


def _find_category(file_path: str) -> tuple[str, str, int]:
    """Find the commit category for a file path.

    Args:
        file_path: File path relative to project root

    Returns:
        Tuple of (commit_type, message, priority)
    """
    for prefix, commit_type, message, priority in COMMIT_CATEGORIES:
        if file_path.startswith(prefix) or file_path == prefix.rstrip("/"):
            return commit_type, message, priority

    # Default fallback for uncategorized files
    return "chore", "add configuration files", 99


def categorize_for_commits(
    files: list[str],
    existing_message_prefix: str = "add",
) -> list[CommitGroup]:
    """Categorize files into groups for atomic commits.

    Groups files by their directory/pattern and generates appropriate
    commit messages following semantic commit conventions.

    Args:
        files: List of file paths to categorize
        existing_message_prefix: Prefix for messages ("add" for new, "update" for modified)

    Returns:
        List of CommitGroup objects sorted by priority

    Example:
        >>> files = [
        ...     ".claude/knowledge/infrastructure/storage.md",
        ...     ".claude/knowledge/infrastructure/compute.md",
        ...     ".claude/standards/ENGINEERING.md",
        ... ]
        >>> groups = categorize_for_commits(files)
        >>> len(groups)
        2
        >>> groups[0].commit_type
        'docs'
    """
    # Map: (commit_type, message) -> CommitGroup
    groups: dict[tuple[str, str], CommitGroup] = {}

    for file_path in files:
        commit_type, base_message, priority = _find_category(file_path)

        # Adjust message based on prefix (add/update)
        if existing_message_prefix == "update" and base_message.startswith("add"):
            message = base_message.replace("add", "update", 1)
        else:
            message = base_message

        key = (commit_type, message)
        if key not in groups:
            groups[key] = CommitGroup(
                commit_type=commit_type,
                message=message,
                priority=priority,
            )
        groups[key].add_file(file_path)

    # Sort by priority and return
    return sorted(groups.values(), key=lambda g: (g.priority, g.message))


def categorize_added_and_modified(
    added_files: list[str],
    modified_files: list[str],
) -> list[CommitGroup]:
    """Categorize both added and modified files for commits.

    Creates separate commit groups for new files vs modifications,
    using appropriate semantic messages ("add" vs "update").

    Args:
        added_files: List of newly added file paths
        modified_files: List of modified file paths

    Returns:
        List of CommitGroup objects sorted by priority

    Example:
        >>> added = [".claude/knowledge/git/commits.md"]
        >>> modified = [".claude/settings.json"]
        >>> groups = categorize_added_and_modified(added, modified)
        >>> [g.full_message for g in groups]
        ['docs: add git knowledge base', 'chore: update Claude Code settings']
    """
    all_groups: list[CommitGroup] = []

    if added_files:
        all_groups.extend(categorize_for_commits(added_files, "add"))

    if modified_files:
        modified_groups = categorize_for_commits(modified_files, "update")
        # Adjust priority for modifications to come after additions in same category
        for group in modified_groups:
            group.priority += MODIFIED_FILE_PRIORITY_OFFSET
        all_groups.extend(modified_groups)

    # Re-sort by priority
    return sorted(all_groups, key=lambda g: (g.priority, g.message))


def format_commit_commands(
    groups: list[CommitGroup],
    force_add: bool = False,
) -> list[str]:
    """Generate git commands for creating atomic commits.

    Args:
        groups: List of CommitGroup objects
        force_add: Whether to use git add -f (for gitignored files)

    Returns:
        List of shell commands to execute

    Example:
        >>> group = CommitGroup("docs", "add git knowledge base", [".claude/knowledge/git/"])
        >>> commands = format_commit_commands([group])
        >>> print(commands[0])
        git add .claude/knowledge/git/ && git commit -m "docs: add git knowledge base"
    """
    commands: list[str] = []
    add_flag = "-f " if force_add else ""

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

        # Group files by directory where possible
        files_str = " ".join(sorted(group.files))
        commit_msg = group.full_message

        # Escape quotes in message
        commit_msg_escaped = commit_msg.replace('"', '\\"')

        command = f'git add {add_flag}{files_str} && git commit -m "{commit_msg_escaped}"'
        commands.append(command)

    return commands


def format_commit_summary(groups: list[CommitGroup]) -> str:
    """Format a summary of commit groups for display.

    Args:
        groups: List of CommitGroup objects

    Returns:
        Human-readable summary string

    Example:
        >>> groups = [
        ...     CommitGroup("docs", "add git knowledge base", ["file1.md", "file2.md"]),
        ...     CommitGroup("chore", "add config", ["config.yml"]),
        ... ]
        >>> print(format_commit_summary(groups))
        Atomic commits to create:
          [1] docs: add git knowledge base (2 files)
          [2] chore: add config (1 file)
    """
    if not groups:
        return "No commits to create."

    lines = ["Atomic commits to create:"]
    for i, group in enumerate(groups, 1):
        file_count = len(group.files)
        file_word = "file" if file_count == 1 else "files"
        lines.append(f"  [{i}] {group.full_message} ({file_count} {file_word})")

    return "\n".join(lines)
