import os
import subprocess
import tempfile
from typing import Tuple, List, Optional, Dict, Any
import logging

logger = logging.getLogger(__name__)


def find_git_repo_root(start_path: str) -> Optional[str]:
    """Return the git repo root for start_path, or None if not found."""
    try:
        start_dir = start_path if os.path.isdir(start_path) else os.path.dirname(start_path)
        result = subprocess.run(
            ["git", "-C", start_dir, "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            check=True,
        )
        root = result.stdout.strip()
        return root or None
    except Exception:
        return None


def _to_repo_relative(file_path: str, repo_root: str) -> str:
    rel = os.path.relpath(os.path.abspath(file_path), repo_root)
    # Normalize to POSIX style for git diffs
    return rel.replace(os.sep, "/")


def rewrite_diff_headers(diff_text: str, rel_path: str) -> str:
    """Rewrite diff headers to use a/<rel_path> and b/<rel_path> consistently."""
    lines = diff_text.splitlines()
    out: List[str] = []
    for line in lines:
        if line.startswith("--- "):
            out.append(f"--- a/{rel_path}")
        elif line.startswith("+++ "):
            out.append(f"+++ b/{rel_path}")
        else:
            out.append(line)
    return "\n".join(out) + "\n"


def count_hunks(diff_text: str) -> int:
    return sum(1 for l in diff_text.splitlines() if l.startswith("@@"))


def git_validate_diff(repo_root: str, diff_text: str, reverse: bool = False) -> Tuple[bool, List[str], Optional[int], Optional[int]]:
    """
    Validate a unified diff against the working tree using git without applying it.

    Returns: (valid, reasons, added, removed)
    - added/removed are totals from --numstat when valid; otherwise None
    """
    # Write patch to a temp file
    with tempfile.NamedTemporaryFile("w", delete=False, suffix=".patch") as tf:
        tf.write(diff_text)
        patch_path = tf.name
    try:
        # --check validates whether the patch can be applied
        cmd_check = ["git", "-C", repo_root, "apply", "--check"]
        if reverse:
            cmd_check.append("-R")
        cmd_check.append(patch_path)
        check = subprocess.run(
            cmd_check,
            capture_output=True,
            text=True,
        )
        if check.returncode != 0:
            # Collect stderr lines as reasons
            stderr = (check.stderr or "").strip()
            reasons = [l for l in stderr.splitlines() if l]
            return False, reasons or ["git_apply_check_failed"], None, None

        # Get added/removed counts
        cmd_num = ["git", "-C", repo_root, "apply", "--numstat"]
        if reverse:
            cmd_num.append("-R")
        cmd_num.append(patch_path)
        numstat = subprocess.run(
            cmd_num,
            capture_output=True,
            text=True,
        )
        added_total = 0
        removed_total = 0
        for line in (numstat.stdout or "").splitlines():
            parts = line.split()
            if len(parts) >= 3 and parts[0].isdigit() and parts[1].isdigit():
                added_total += int(parts[0])
                removed_total += int(parts[1])
        return True, [], added_total, removed_total
    finally:
        try:
            os.unlink(patch_path)
        except Exception:
            pass


def findings_to_patch(
    file_path: str,
    start_line: int,
    end_line: int,
    original_line: str,
    fixed_lines: List[str],
    context_lines: int = 3,
) -> Optional[str]:
    """
    Convert scanner fix data to a Git unified diff patch.

    Args:
        file_path: Absolute path to the file being fixed
        start_line: Starting line number of the finding (1-indexed)
        end_line: Ending line number of the finding (1-indexed)
        original_line: Original code line(s) from the scanner
        fixed_lines: List of fixed code lines from the scanner
        context_lines: Number of context lines before/after change (default: 3)

    Returns:
        Git unified diff string, or None if patch generation fails

    Example output:
        --- a/path/to/file.py
        +++ b/path/to/file.py
        @@ -20,7 +20,7 @@ def generate_token_sha1():
             '''Generate token using SHA-1 - quantum vulnerable'''
             data = str(os.urandom(16))
        -    return hashlib.sha1(data.encode()).hexdigest()
        +    return hashlib.sha256(data.encode()).hexdigest()
    """
    try:
        # Validate inputs
        if not file_path or not os.path.isfile(file_path):
            logger.error(f"Invalid file path: {file_path}")
            return None

        if start_line < 1 or end_line < 1 or start_line > end_line:
            logger.error(f"Invalid line range: {start_line}-{end_line}")
            return None

        if not original_line or not fixed_lines:
            logger.error("Missing original_line or fixed_lines")
            return None

        # Read the file content
        with open(file_path, "r", encoding="utf-8") as f:
            file_lines = f.readlines()

        total_lines = len(file_lines)

        # Adjust line numbers (OpenGrep uses 1-indexed)
        start_idx = start_line - 1
        end_idx = end_line - 1

        if start_idx < 0 or end_idx >= total_lines:
            logger.error(f"Line range {start_line}-{end_line} out of bounds for file with {total_lines} lines")
            return None

        # Calculate context window
        context_start = max(0, start_idx - context_lines)
        context_end = min(total_lines - 1, end_idx + context_lines)

        # Get the repository-relative path
        repo_root = find_git_repo_root(file_path)
        if repo_root:
            rel_path = _to_repo_relative(file_path, repo_root)
        else:
            # Fallback to basename if not in a git repo
            rel_path = os.path.basename(file_path)

        # Build the diff header
        diff_lines = [
            f"--- a/{rel_path}",
            f"+++ b/{rel_path}",
        ]

        # Calculate hunk header (old and new line counts)
        old_start = context_start + 1  # Git uses 1-indexed line numbers
        old_count = end_idx - start_idx + 1 + (start_idx - context_start) + (context_end - end_idx)
        new_count = len(fixed_lines) + (start_idx - context_start) + (context_end - end_idx)

        # Add hunk header
        # Format: @@ -old_start,old_count +new_start,new_count @@
        diff_lines.append(f"@@ -{old_start},{old_count} +{old_start},{new_count} @@")

        # Add context before change
        for i in range(context_start, start_idx):
            diff_lines.append(" " + file_lines[i].rstrip("\n"))

        # Add removed lines (original code)
        for i in range(start_idx, end_idx + 1):
            diff_lines.append("-" + file_lines[i].rstrip("\n"))

        # Add added lines (fixed code)
        for fixed_line in fixed_lines:
            diff_lines.append("+" + fixed_line.rstrip("\n"))

        # Add context after change
        for i in range(end_idx + 1, context_end + 1):
            diff_lines.append(" " + file_lines[i].rstrip("\n"))

        # Join with newlines and add final newline
        patch = "\n".join(diff_lines) + "\n"
        return patch

    except Exception as e:
        logger.error(f"Error generating patch for {file_path}: {e}")
        return None
