"""
GitHub patch analysis module.

Provides functions to extract GitHub patch information from CVE references,
verify code types, and resolve pre-patch commits.
"""

import logging
import os
import re

from githubkit import GitHub, Response
from githubkit.exception import PrimaryRateLimitExceeded, RequestFailed
from githubkit.versions.latest.models import Commit, PullRequest

from .models import (
    CommitPatch,
    ComparePatch,
    CVERecord,
    FilePatch,
    GitHubPatch,
    PRPatch,
)

logger = logging.getLogger(__name__)

# GitHub URL patterns (compiled for performance)
COMMIT_PATTERN = re.compile(r"github\.com/([^/]+)/([^/]+)/commit/([a-f0-9]{40})")
FILE_PATTERN = re.compile(r"github\.com/([^/]+)/([^/]+)/blob/([a-f0-9]{40})/(.+)")
PR_PATTERN = re.compile(r"github\.com/([^/]+)/([^/]+)/pull/(\d+)")
COMPARE_PATTERN = re.compile(
    r"github\.com/([^/]+)/([^/]+)/compare/([a-f0-9]{40})\.\.\.([a-f0-9]{40})"
)

# File extension mapping for code type verification
LANGUAGE_EXTENSIONS = {
    "php": {".php", ".phtml", ".php3", ".php4", ".php5"},
    "python": {".py", ".pyw", ".pyi"},
    "javascript": {".js", ".jsx", ".mjs", ".cjs"},
    "typescript": {".ts", ".tsx"},
    "java": {".java"},
    "go": {".go"},
    "rust": {".rs"},
    "c": {".c", ".h"},
    "cpp": {".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"},
    "ruby": {".rb"},
}

# Module-level GitHub client (initialized lazily)
_github_client: GitHub | None = None


def _get_github_client() -> GitHub:
    """Get or create GitHub client with token from environment."""
    global _github_client
    if _github_client is None:
        token = os.environ.get("GITHUB_TOKEN")
        if not token:
            logger.warning("GITHUB_TOKEN not set; rate limits will be restrictive")
        _github_client = GitHub(token)
    return _github_client


def extract_github_patch(cve: CVERecord) -> GitHubPatch | None:
    """
    Extract single GitHub patch from CVE references with URL prioritization.

    Prioritizes URL types: commit > file > PR > compare.
    Returns first valid patch object found or None.

    Args:
        cve: CVE record to extract patch from

    Returns:
        GitHubPatch subclass instance or None if no valid GitHub URL found
    """
    if not cve.references:
        return None

    # Try commit URLs first (highest priority)
    for url in cve.references:
        if match := COMMIT_PATTERN.search(url):
            owner, repo, sha = match.groups()
            return CommitPatch(
                cve_id=cve.cve_id,
                repository=f"{owner}/{repo}",
                commit_sha=sha,
            )

    # Try file URLs (includes commit info)
    for url in cve.references:
        if match := FILE_PATTERN.search(url):
            owner, repo, sha, file_path = match.groups()
            return FilePatch(
                cve_id=cve.cve_id,
                repository=f"{owner}/{repo}",
                commit_sha=sha,
                file_path=file_path,
            )

    # Try PR URLs (requires API call to get merge commit)
    for url in cve.references:
        if match := PR_PATTERN.search(url):
            owner, repo, pr_number = match.groups()
            merge_sha = _resolve_pr_merge_commit(owner, repo, int(pr_number))
            return PRPatch(
                cve_id=cve.cve_id,
                repository=f"{owner}/{repo}",
                pr_number=int(pr_number),
                merge_commit_sha=merge_sha,
            )

    # Try compare URLs (lowest priority)
    for url in cve.references:
        if match := COMPARE_PATTERN.search(url):
            owner, repo, base_sha, head_sha = match.groups()
            return ComparePatch(
                cve_id=cve.cve_id,
                repository=f"{owner}/{repo}",
                base_sha=base_sha,
                head_sha=head_sha,
            )

    return None


def _resolve_pr_merge_commit(owner: str, repo: str, pr_number: int) -> str | None:
    """
    Query GitHub API for PR merge commit SHA.

    Args:
        owner: Repository owner
        repo: Repository name
        pr_number: Pull request number

    Returns:
        Merge commit SHA or None if PR not merged or API error
    """
    try:
        github = _get_github_client()
        response: Response[PullRequest] = github.rest.pulls.get(owner, repo, pr_number)
        pr = response.parsed_data
        return pr.merge_commit_sha
    except PrimaryRateLimitExceeded as e:
        logger.warning(
            "GitHub API rate limit exceeded for PR #%d in %s/%s (retry after %s)",
            pr_number, owner, repo, e.retry_after
        )
        return None
    except RequestFailed as e:
        logger.warning("Failed to fetch PR #%d for %s/%s: %s", pr_number, owner, repo, e)
        return None


def verify_patch_code_type(patch: GitHubPatch, language: str) -> bool:
    """
    Verify patch contains code files of specified language.

    Fetches commit files from GitHub API and checks file extensions.
    Returns False on errors (no raise).

    Args:
        patch: GitHub patch to verify
        language: Programming language (lowercase, e.g., "php", "python")

    Returns:
        True if patch modifies files of specified language, False otherwise
    """
    if language not in LANGUAGE_EXTENSIONS:
        logger.warning("Unknown language: %s", language)
        return False

    extensions = LANGUAGE_EXTENSIONS[language]

    try:
        commit_sha = patch.get_patch_commit_sha()
    except ValueError as e:
        logger.warning("Cannot extract commit SHA from patch: %s", e)
        return False

    owner, repo = patch.repository.split("/")

    try:
        github = _get_github_client()
        response: Response[Commit] = github.rest.repos.get_commit(owner, repo, commit_sha)
        commit = response.parsed_data

        if not commit.files:
            return False

        # Check if any file has matching extension
        for file in commit.files:
            if not file.filename:
                continue
            file_ext = "." + file.filename.split(".")[-1] if "." in file.filename else ""
            if file_ext.lower() in extensions:
                return True

        return False

    except PrimaryRateLimitExceeded as e:
        logger.warning(
            "GitHub API rate limit exceeded for commit %s in %s (retry after %s)",
            commit_sha, patch.repository, e.retry_after
        )
        return False
    except RequestFailed as e:
        logger.warning("Failed to fetch commit %s for %s: %s", commit_sha, patch.repository, e)
        return False


def resolve_pre_patch_commit(patch: GitHubPatch) -> str | None:
    """
    Resolve commit SHA immediately before the patch commit (parent).

    Queries GitHub API for commit parent. Returns None on failure.

    Args:
        patch: GitHub patch to resolve pre-patch commit for

    Returns:
        Parent commit SHA or None if unavailable or API error
    """
    try:
        commit_sha = patch.get_patch_commit_sha()
    except ValueError as e:
        logger.warning("Cannot extract commit SHA from patch: %s", e)
        return None

    owner, repo = patch.repository.split("/")

    try:
        github = _get_github_client()
        response: Response[Commit] = github.rest.repos.get_commit(owner, repo, commit_sha)
        commit = response.parsed_data

        if not commit.parents or len(commit.parents) == 0:
            logger.warning("Commit %s has no parents (initial commit)", commit_sha)
            return None

        # Use first parent (for merge commits, this is the main branch)
        parent_sha = commit.parents[0].sha
        return parent_sha

    except PrimaryRateLimitExceeded as e:
        logger.warning(
            "GitHub API rate limit exceeded for commit %s in %s (retry after %s)",
            commit_sha, patch.repository, e.retry_after
        )
        return None
    except RequestFailed as e:
        logger.warning(
            "Failed to resolve parent for commit %s in %s: %s", commit_sha, patch.repository, e
        )
        return None
