"""Git operations — snapshot and diff via subprocess."""

from __future__ import annotations

import logging
import subprocess
from pathlib import Path

from nestdx.session.models import FileChange, GitSnapshot

logger = logging.getLogger(__name__)


def _run_git(args: list[str], cwd: Path, *, timeout: int = 10) -> subprocess.CompletedProcess:
    """Run a git command and return the result."""
    return subprocess.run(
        ["git", *args],
        cwd=cwd,
        capture_output=True,
        text=True,
        timeout=timeout,
    )


def is_git_repo(path: Path) -> bool:
    """Check if path is inside a git repository."""
    try:
        result = _run_git(["rev-parse", "--is-inside-work-tree"], cwd=path)
        return result.returncode == 0
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return False


def snapshot(project_root: Path) -> GitSnapshot | None:
    """Capture current git state. Returns None if not a git repo."""
    if not is_git_repo(project_root):
        return None

    try:
        # Get HEAD sha
        head_result = _run_git(["rev-parse", "HEAD"], cwd=project_root)
        if head_result.returncode != 0:
            # No commits yet (empty repo)
            head_sha = "0" * 40
        else:
            head_sha = head_result.stdout.strip()

        # Get branch name
        branch_result = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=project_root)
        branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "HEAD"

        # Get dirty files (modified + untracked)
        status_result = _run_git(["status", "--porcelain"], cwd=project_root)
        dirty_files = []
        if status_result.returncode == 0 and status_result.stdout.strip():
            for line in status_result.stdout.strip().splitlines():
                # Format: "XY filename" or "XY filename -> newname"
                filepath = line[3:].split(" -> ")[-1]
                dirty_files.append(filepath)

        return GitSnapshot(
            head_sha=head_sha,
            branch=branch,
            dirty_files=dirty_files,
        )
    except (subprocess.TimeoutExpired, FileNotFoundError) as e:
        logger.warning("Git snapshot failed: %s", e)
        return None


def diff_since(project_root: Path, snap: GitSnapshot) -> list[FileChange]:
    """Compute file changes since snapshot.

    Uses git diff --numstat against the snapshot SHA. Also picks up
    new untracked files that appeared since the snapshot.
    """
    changes: list[FileChange] = []

    if not is_git_repo(project_root):
        return changes

    try:
        # Diff committed + staged + unstaged changes against snapshot SHA
        # Use diff against HEAD to capture everything in working tree
        diff_result = _run_git(
            ["diff", "--numstat", snap.head_sha, "--"],
            cwd=project_root,
        )

        if diff_result.returncode == 0 and diff_result.stdout.strip():
            for line in diff_result.stdout.strip().splitlines():
                parts = line.split("\t")
                if len(parts) < 3:
                    continue
                added, deleted, path = parts[0], parts[1], parts[2]
                # Binary files show "-" for additions/deletions
                additions = int(added) if added != "-" else 0
                deletions = int(deleted) if deleted != "-" else 0

                # Determine change type
                change_type = "modified"
                changes.append(
                    FileChange(
                        path=path,
                        change_type=change_type,
                        additions=additions,
                        deletions=deletions,
                    )
                )

        # Also get diff for staged but uncommitted changes
        staged_result = _run_git(
            ["diff", "--numstat", "--cached"],
            cwd=project_root,
        )
        staged_paths = {c.path for c in changes}
        if staged_result.returncode == 0 and staged_result.stdout.strip():
            for line in staged_result.stdout.strip().splitlines():
                parts = line.split("\t")
                if len(parts) < 3:
                    continue
                added, deleted, path = parts[0], parts[1], parts[2]
                if path in staged_paths:
                    continue
                additions = int(added) if added != "-" else 0
                deletions = int(deleted) if deleted != "-" else 0
                changes.append(
                    FileChange(
                        path=path,
                        change_type="modified",
                        additions=additions,
                        deletions=deletions,
                    )
                )
                staged_paths.add(path)

        # Check for new untracked files not in the original snapshot
        status_result = _run_git(["status", "--porcelain"], cwd=project_root)
        if status_result.returncode == 0 and status_result.stdout.strip():
            for line in status_result.stdout.strip().splitlines():
                status_code = line[:2]
                filepath = line[3:].split(" -> ")[-1]

                if filepath in staged_paths:
                    continue

                if status_code.strip() == "??":
                    changes.append(
                        FileChange(path=filepath, change_type="added", additions=0, deletions=0)
                    )
                elif status_code.strip() == "D" or status_code.strip() == "D":
                    changes.append(
                        FileChange(path=filepath, change_type="deleted", additions=0, deletions=0)
                    )
                elif status_code.strip() == "A":
                    changes.append(
                        FileChange(path=filepath, change_type="added", additions=0, deletions=0)
                    )

        # Refine change types using diff-filter
        diff_filter_result = _run_git(
            ["diff", "--diff-filter=ADMR", "--name-status", snap.head_sha, "--"],
            cwd=project_root,
        )
        if diff_filter_result.returncode == 0 and diff_filter_result.stdout.strip():
            type_map: dict[str, str] = {}
            for line in diff_filter_result.stdout.strip().splitlines():
                parts = line.split("\t")
                if len(parts) >= 2:
                    status_char = parts[0][0]  # A, D, M, or R
                    path = parts[-1]
                    type_map[path] = {
                        "A": "added",
                        "D": "deleted",
                        "M": "modified",
                        "R": "renamed",
                    }.get(status_char, "modified")

            for change in changes:
                if change.path in type_map:
                    change.change_type = type_map[change.path]

    except (subprocess.TimeoutExpired, FileNotFoundError) as e:
        logger.warning("Git diff failed: %s", e)

    return changes


def diff_between_refs(
    project_root: Path, base_ref: str, head_ref: str
) -> list[FileChange]:
    """Compute file changes between two git refs.

    Uses ``git diff --name-status`` to list changed files, then reads file
    content at *head_ref* for added/modified files so downstream scanners
    (e.g. secrets) can inspect the content.

    Args:
        project_root: Root directory of the git repository.
        base_ref: Base commit/ref (e.g. a merge-base SHA).
        head_ref: Head commit/ref to compare against.

    Returns:
        A list of :class:`FileChange` objects.

    Raises:
        ValueError: If *base_ref* or *head_ref* cannot be resolved.
    """
    if not is_git_repo(project_root):
        raise ValueError(f"Not a git repository: {project_root}")

    # Validate that both refs are resolvable
    for ref_name, ref_value in [("base_ref", base_ref), ("head_ref", head_ref)]:
        verify = _run_git(["rev-parse", "--verify", ref_value], cwd=project_root)
        if verify.returncode != 0:
            raise ValueError(
                f"Invalid {ref_name} '{ref_value}': "
                f"{verify.stderr.strip() or 'could not resolve ref'}"
            )

    # Get changed files with status letters
    diff_result = _run_git(
        ["diff", "--name-status", base_ref, head_ref],
        cwd=project_root,
    )

    if diff_result.returncode != 0:
        raise ValueError(
            f"git diff failed: {diff_result.stderr.strip() or 'unknown error'}"
        )

    status_map = {
        "A": "added",
        "M": "modified",
        "D": "deleted",
        "R": "renamed",
    }

    # Get line counts for all files in a single call (avoids N+1 subprocess problem)
    numstat_result = _run_git(
        ["diff", "--numstat", base_ref, head_ref],
        cwd=project_root,
    )
    numstat_map: dict[str, tuple[int, int]] = {}
    if numstat_result.returncode == 0 and numstat_result.stdout.strip():
        for nline in numstat_result.stdout.strip().splitlines():
            nparts = nline.split("\t")
            if len(nparts) >= 3:
                added = int(nparts[0]) if nparts[0] != "-" else 0
                deleted = int(nparts[1]) if nparts[1] != "-" else 0
                numstat_map[nparts[2]] = (added, deleted)

    changes: list[FileChange] = []

    for line in diff_result.stdout.strip().splitlines():
        if not line.strip():
            continue
        parts = line.split("\t")
        if len(parts) < 2:
            continue

        status_char = parts[0][0]  # First character: A, M, D, R (R may have score)
        # For renames, the path is the *destination* (last element)
        file_path = parts[-1]
        change_type = status_map.get(status_char, "modified")
        additions, deletions = numstat_map.get(file_path, (0, 0))

        changes.append(
            FileChange(
                path=file_path,
                change_type=change_type,
                additions=additions,
                deletions=deletions,
            )
        )

    return changes


def read_file_at_ref(project_root: Path, ref: str, file_path: str) -> str | None:
    """Read file content at a specific git ref.

    Returns the file content as a string, or None if the file does not exist
    at that ref.
    """
    result = _run_git(["show", f"{ref}:{file_path}"], cwd=project_root)
    if result.returncode != 0:
        return None
    return result.stdout
