"""Git history extraction utilities for bare/mirror repositories."""

import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any


@dataclass
class Commit:
    """Represents a single git commit."""

    hash: str
    short_hash: str
    parents: list[str]
    author: str
    email: str
    date: str
    timestamp: int
    message: str
    branches: list[str]
    tags: list[str]


@dataclass
class FileChange:
    """Represents a changed file in a commit."""

    path: str
    change_type: str
    insertions: int
    deletions: int
    diff: str


@dataclass
class CommitDetail(Commit):
    """Extended commit info with diff."""

    body: str
    stats: dict[str, int]
    files: list[FileChange]


def _run_git_command(repo_path: str, args: list[str]) -> str:
    """Execute a git command in the repository directory."""
    result = subprocess.run(
        ["git", "-C", repo_path, *args],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode != 0:
        raise RuntimeError(f"Git command failed: {result.stderr}")
    return result.stdout


def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 100, offset: int = 0) -> dict[str, Any]:
    """Extract commit history from a git repository.

    Returns structured data including commits, branches, and graph information.
    """
    # Get list of branches
    branches_output = _run_git_command(repo_path, ["branch", "-a", "--format=%(refname:short)"])
    branches = [b.strip() for b in branches_output.strip().split("\n") if b.strip()]

    # Build git log command - use NULL bytes as separators to avoid parsing issues
    log_args = [
        "log",
        "--format=%H%x00%P%x00%an%x00%ae%x00%at%x00%s",
        f"--max-count={limit}",
        f"--skip={offset}",
    ]
    if branch:
        log_args.append(branch)
    else:
        log_args.append("--all")

    log_output = _run_git_command(repo_path, log_args)

    # Get branch info for each commit
    branch_map = _get_branch_map(repo_path)
    tag_map = _get_tag_map(repo_path)

    commits = []
    log_lines = log_output.strip().split("\n") if log_output.strip() else []

    for line in log_lines:
        line = line.strip()
        if not line:
            continue

        parts = line.split("\x00")
        if len(parts) < 6:
            continue

        commit_hash = parts[0]
        parents = parts[1].split() if parts[1] else []

        commits.append(
            Commit(
                hash=commit_hash,
                short_hash=commit_hash[:7],
                parents=parents,
                author=parts[2],
                email=parts[3],
                date=parts[4],
                timestamp=int(parts[4]),
                message=parts[5],
                branches=branch_map.get(commit_hash, []),
                tags=tag_map.get(commit_hash, []),
            )
        )

    # Get total commit count
    count_output = _run_git_command(repo_path, ["rev-list", "--all", "--count"])
    total_commits = int(count_output.strip()) if count_output.strip() else 0

    # Build graph data and generate graph symbols
    graph_data = _build_graph_data(commits)
    
    # Generate simple graph symbols based on parent count
    commit_dicts = []
    for i, commit in enumerate(commits):
        if len(commit.parents) == 0:
            graph_symbol = "○"  # Initial commit
        elif len(commit.parents) > 1:
            graph_symbol = "●"  # Merge commit
        else:
            graph_symbol = "○"  # Regular commit
        
        # Simple depth calculation based on merge status
        graph_depth = min(len(commit.parents), 3)
        
        commit_dicts.append(_commit_to_dict(commit, graph_symbol, graph_depth))

    return {
        "commits": commit_dicts,
        "branches": branches,
        "total_commits": total_commits,
        "graph_data": graph_data,
    }


def get_commit_detail(repo_path: str, commit_hash: str) -> dict[str, Any]:
    """Get detailed information about a specific commit."""
    # Get commit metadata
    format_str = "%H|%P|%an|%ae|%at|%s|%b"
    log_output = _run_git_command(
        repo_path, ["log", "-1", f"--format={format_str}", commit_hash]
    )

    parts = log_output.strip().split("|", 6)
    if len(parts) < 6:
        raise ValueError(f"Invalid commit: {commit_hash}")

    commit_hash = parts[0]
    parents = parts[1].split() if parts[1] else []
    author = parts[2]
    email = parts[3]
    timestamp = int(parts[4])
    message = parts[5]
    body = parts[6] if len(parts) > 6 else ""

    # Get stats
    stat_output = _run_git_command(
        repo_path, ["show", "--stat", "--format=", commit_hash]
    )
    stats = _parse_stats(stat_output)

    # Get diff
    diff_output = _run_git_command(
        repo_path, ["show", "--format=", commit_hash]
    )
    files = _parse_diff(diff_output)

    # Get branch/tag info
    branch_map = _get_branch_map(repo_path)
    tag_map = _get_tag_map(repo_path)

    return {
        "hash": commit_hash,
        "short_hash": commit_hash[:7],
        "parents": parents,
        "author_name": author,
        "author_email": email,
        "author_date": datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat(),
        "committer_name": author,  # TODO: extract committer separately
        "committer_email": email,  # TODO: extract committer separately
        "committer_date": datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat(),
        "message": message,
        "body": body,
        "branches": branch_map.get(commit_hash, []),
        "tags": tag_map.get(commit_hash, []),
        "stats": stats,
        "diff": diff_output,
        "files": [_file_change_to_dict(f) for f in files],
    }


def _get_branch_map(repo_path: str) -> dict[str, list[str]]:
    """Build a mapping of commit hash to branch names."""
    result = {}
    branch_output = _run_git_command(
        repo_path, ["for-each-ref", "--format=%(objectname) %(refname:short)", "refs/heads/"]
    )

    for line in branch_output.strip().split("\n"):
        if " " in line:
            commit_hash, branch_name = line.split(" ", 1)
            if commit_hash not in result:
                result[commit_hash] = []
            result[commit_hash].append(branch_name)

    return result


def _get_tag_map(repo_path: str) -> dict[str, list[str]]:
    """Build a mapping of commit hash to tag names."""
    result = {}
    tag_output = _run_git_command(
        repo_path, ["for-each-ref", "--format=%(objectname) %(refname:short)", "refs/tags/"]
    )

    for line in tag_output.strip().split("\n"):
        if " " in line:
            commit_hash, tag_name = line.split(" ", 1)
            if commit_hash not in result:
                result[commit_hash] = []
            result[commit_hash].append(tag_name)

    return result


def _build_graph_data(commits: list[Commit]) -> dict[str, Any]:
    """Build graph visualization data from commits."""
    if not commits:
        return {"nodes": [], "edges": []}

    # Create hash to index mapping
    hash_to_idx = {c.hash: i for i, c in enumerate(commits)}

    nodes = []
    edges = []

    for i, commit in enumerate(commits):
        # Calculate column based on branch
        column = 0
        if commit.branches:
            # Use first branch as column indicator
            column = hash(commit.branches[0]) % 5

        nodes.append(
            {
                "hash": commit.hash,
                "x": column * 60 + 30,
                "y": i * 50 + 25,
                "column": column,
            }
        )

        # Create edges to parents
        for parent_hash in commit.parents:
            if parent_hash in hash_to_idx:
                edges.append(
                    {
                        "from_hash": commit.hash,
                        "to_hash": parent_hash,
                        "type": "parent",
                    }
                )

    return {"nodes": nodes, "edges": edges}


def _parse_stats(stat_output: str) -> dict[str, int]:
    """Parse git show --stat output."""
    lines = stat_output.strip().split("\n")
    stats = {"files_changed": 0, "insertions": 0, "deletions": 0}

    for line in lines:
        line = line.strip()
        if "files changed" in line or "file changed" in line:
            # Parse summary line like "3 files changed, 45 insertions(+), 12 deletions(-)"
            parts = line.split(",")
            for part in parts:
                part = part.strip()
                if "file" in part:
                    try:
                        stats["files_changed"] = int(part.split()[0])
                    except (ValueError, IndexError):
                        pass
                elif "insertion" in part:
                    try:
                        stats["insertions"] = int(part.split()[0])
                    except (ValueError, IndexError):
                        pass
                elif "deletion" in part:
                    try:
                        stats["deletions"] = int(part.split()[0])
                    except (ValueError, IndexError):
                        pass

    return stats


def _parse_diff(diff_output: str) -> list[FileChange]:
    """Parse git diff output into file changes."""
    files = []
    current_file = None
    current_diff = []

    for line in diff_output.split("\n"):
        if line.startswith("diff --git"):
            # Save previous file
            if current_file:
                current_file.diff = "\n".join(current_diff)
                files.append(current_file)

            # Start new file
            current_diff = [line]
            current_file = FileChange(
                path="",
                change_type="modified",
                insertions=0,
                deletions=0,
                diff="",
            )
        elif line.startswith("--- ") or line.startswith("+++ "):
            current_diff.append(line)
            if line.startswith("+++ ") and not line.startswith("+++ /dev/null"):
                current_file.path = line[6:]
        elif line.startswith("@@ "):
            current_diff.append(line)
        elif line.startswith("+") and not line.startswith("+++"):
            current_diff.append(line)
            current_file.insertions += 1
        elif line.startswith("-") and not line.startswith("---"):
            current_diff.append(line)
            current_file.deletions += 1
        elif current_file:
            current_diff.append(line)

    # Save last file
    if current_file:
        current_file.diff = "\n".join(current_diff)
        files.append(current_file)

    return files


def _commit_to_dict(commit: Commit, graph_symbol: str = "", graph_depth: int = 0) -> dict[str, Any]:
    """Convert Commit dataclass to dictionary."""
    refs = []
    if commit.branches:
        refs.extend(commit.branches)
    if commit.tags:
        refs.extend(commit.tags)
    
    return {
        "hash": commit.hash,
        "short_hash": commit.short_hash,
        "parents": commit.parents,
        "author_name": commit.author,
        "author_email": commit.email,
        "author_date": datetime.fromtimestamp(commit.timestamp, tz=timezone.utc).isoformat(),
        "message": commit.message,
        "refs": refs,
        "graph_symbol": graph_symbol,
        "graph_depth": graph_depth,
    }


def _file_change_to_dict(file_change: FileChange) -> dict[str, Any]:
    """Convert FileChange dataclass to dictionary."""
    return {
        "path": file_change.path,
        "change_type": file_change.change_type,
        "insertions": file_change.insertions,
        "deletions": file_change.deletions,
        "diff": file_change.diff,
    }
