"""
Atomic commit creation with traceability to phase/plan/task IDs.

This module provides functionality for creating git commits that are
traceable to their originating phase, plan, and task. Each commit
follows the format: type(phase-plan-task): description

Requirements: INT-14 (atomic commits), INT-15 (traceability)
"""

import subprocess
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional


class CommitType(str, Enum):
    """Types of commits following conventional commits specification."""

    FEAT = "feat"  # New feature
    FIX = "fix"  # Bug fix
    TEST = "test"  # Test addition/modification
    DOCS = "docs"  # Documentation
    REFACTOR = "refactor"  # Code refactoring
    STYLE = "style"  # Code style changes
    CHORE = "chore"  # Maintenance tasks

    def __str__(self) -> str:
        return self.value


@dataclass
class CommitMetadata:
    """Metadata for creating an atomic commit with full traceability.

    This dataclass captures all the information needed to create a commit
    that can be traced back to its originating phase, plan, and task.

    Attributes:
        phase: Phase number (e.g., "05")
        plan: Plan number (e.g., "01")
        task: Task number (e.g., "03")
        type: Commit type (feat, fix, test, etc.)
        description: Brief description of the commit
        files: List of file paths to commit
        body: Optional extended commit message body
    """

    phase: str
    plan: str
    task: str
    type: CommitType
    description: str
    files: list[str] = field(default_factory=list)
    body: Optional[str] = None

    def to_message(self) -> str:
        """Generate commit message in traceability format.

        Format: type(phase-plan-task): description

        Example:
            feat(05-01-03): add checkpoint pause/resume logic

        Returns:
            Formatted commit message string
        """
        # Format: type(phase-plan-task): description
        header = (
            f"{self.type}({self.phase}-{self.plan}-{self.task}): {self.description}"
        )

        if self.body:
            return f"{header}\n\n{self.body}"

        return header

    def to_dict(self) -> dict:
        """Serialize metadata for logging or storage.

        Returns:
            Dictionary representation of the commit metadata
        """
        return {
            "phase": self.phase,
            "plan": self.plan,
            "task": self.task,
            "type": str(self.type),
            "description": self.description,
            "files": self.files,
            "body": self.body,
            "message": self.to_message(),
        }


class GitError(Exception):
    """Base exception for git operations."""

    pass


class NotAGitRepositoryError(GitError):
    """Raised when operations are attempted outside a git repository."""

    pass


class GitCommandError(GitError):
    """Raised when a git command fails."""

    def __init__(self, command: str, returncode: int, stderr: str):
        self.command = command
        self.returncode = returncode
        self.stderr = stderr
        super().__init__(f"Git command failed: {command}\n{stderr}")


def _run_git_command(
    args: list[str],
    working_dir: str = ".",
    check: bool = True,
) -> subprocess.CompletedProcess:
    """Run a git command and return the result.

    Args:
        args: Git command arguments (e.g., ["add", "file.txt"])
        working_dir: Directory to run the command in
        check: If True, raise exception on non-zero exit

    Returns:
        CompletedProcess with stdout, stderr, returncode

    Raises:
        GitCommandError: If command fails and check=True
    """
    cmd = ["git"] + args
    try:
        result = subprocess.run(
            cmd,
            cwd=working_dir,
            capture_output=True,
            text=True,
            check=False,
        )
        if check and result.returncode != 0:
            raise GitCommandError(
                command=" ".join(cmd),
                returncode=result.returncode,
                stderr=result.stderr.strip(),
            )
        return result
    except FileNotFoundError:
        raise GitError("Git executable not found. Is git installed?")


def is_git_repository(working_dir: str = ".") -> bool:
    """Check if the directory is inside a git repository.

    Args:
        working_dir: Directory to check

    Returns:
        True if inside a git repository, False otherwise
    """
    try:
        result = _run_git_command(
            ["rev-parse", "--is-inside-work-tree"],
            working_dir=working_dir,
            check=False,
        )
        return result.returncode == 0 and result.stdout.strip() == "true"
    except GitError:
        return False


def stage_files(
    files: list[str], working_dir: str = "."
) -> tuple[list[str], list[str]]:
    """Stage specified files for commit.

    Files that don't exist are skipped with a warning.

    Args:
        files: List of file paths to stage
        working_dir: Directory containing the files

    Returns:
        Tuple of (staged_files, skipped_files)
    """
    staged = []
    skipped = []

    for file_path in files:
        full_path = Path(working_dir) / file_path
        if full_path.exists():
            _run_git_command(["add", file_path], working_dir=working_dir)
            staged.append(file_path)
        else:
            # Skip non-existent files with warning
            skipped.append(file_path)

    return staged, skipped


def get_commit_hash(working_dir: str = ".", short: bool = True) -> str:
    """Get the commit hash for HEAD.

    Args:
        working_dir: Directory to run the command in
        short: If True, return short hash (7 chars), else full hash

    Returns:
        Commit hash string

    Raises:
        GitError: If unable to get commit hash
    """
    args = ["rev-parse"]
    if short:
        args.append("--short")
    args.append("HEAD")

    result = _run_git_command(args, working_dir=working_dir)
    return result.stdout.strip()


def create_atomic_commit(
    metadata: CommitMetadata,
    working_dir: str = ".",
) -> str:
    """Create an atomic commit with traceability metadata.

    This function:
    1. Validates that we're in a git repository
    2. Stages the specified files
    3. Creates a commit with the traceability-formatted message
    4. Returns the commit hash

    Args:
        metadata: CommitMetadata containing all commit information
        working_dir: Directory to run git commands in

    Returns:
        Short commit hash (7 characters)

    Raises:
        NotAGitRepositoryError: If not inside a git repository
        GitCommandError: If git operations fail
    """
    # Validate git repository
    if not is_git_repository(working_dir):
        raise NotAGitRepositoryError(
            f"Not a git repository: {Path(working_dir).resolve()}"
        )

    # Stage files
    staged, skipped = stage_files(metadata.files, working_dir)

    if not staged:
        raise GitError("No files to commit (all files were missing)")

    # Log skipped files as warnings (could use logging module)
    if skipped:
        pass  # In production, would log warning

    # Create commit with message
    message = metadata.to_message()
    _run_git_command(
        ["commit", "-m", message],
        working_dir=working_dir,
    )

    # Return commit hash
    return get_commit_hash(working_dir=working_dir, short=True)


def get_staged_files(working_dir: str = ".") -> list[str]:
    """Get list of currently staged files.

    Args:
        working_dir: Directory to run git commands in

    Returns:
        List of staged file paths
    """
    result = _run_git_command(
        ["diff", "--cached", "--name-only"],
        working_dir=working_dir,
        check=False,
    )
    if result.returncode != 0:
        return []
    return [f for f in result.stdout.strip().split("\n") if f]


def get_uncommitted_changes(working_dir: str = ".") -> dict[str, list[str]]:
    """Get summary of uncommitted changes.

    Args:
        working_dir: Directory to run git commands in

    Returns:
        Dict with 'staged', 'unstaged', and 'untracked' file lists
    """
    # Get staged files
    staged_result = _run_git_command(
        ["diff", "--cached", "--name-only"],
        working_dir=working_dir,
        check=False,
    )
    staged = [f for f in staged_result.stdout.strip().split("\n") if f]

    # Get unstaged modified files
    unstaged_result = _run_git_command(
        ["diff", "--name-only"],
        working_dir=working_dir,
        check=False,
    )
    unstaged = [f for f in unstaged_result.stdout.strip().split("\n") if f]

    # Get untracked files
    untracked_result = _run_git_command(
        ["ls-files", "--others", "--exclude-standard"],
        working_dir=working_dir,
        check=False,
    )
    untracked = [f for f in untracked_result.stdout.strip().split("\n") if f]

    return {
        "staged": staged,
        "unstaged": unstaged,
        "untracked": untracked,
    }
