"""GitHub API client for PR status checks and comments.

This module provides an async HTTP client for interacting with the GitHub REST API.
It handles authentication via GitHub App installation tokens and provides methods
for common PR-related operations:

- Posting and updating commit status checks
- Creating and updating PR comments
- Fetching PR diffs and file lists

The client manages HTTP connection lifecycle and supports both owned and shared
httpx.AsyncClient instances for connection pooling in Lambda environments.

Example usage:
    >>> from mergeguide.github.api import GitHubAPI
    >>> from mergeguide.github.auth import GitHubAuth
    >>>
    >>> auth = GitHubAuth(app_id=123, private_key="...")
    >>> api = GitHubAPI(auth)
    >>>
    >>> # Post a status check
    >>> await api.post_status(
    ...     installation_id=456,
    ...     owner="acme",
    ...     repo="my-app",
    ...     sha="abc123",
    ...     state="success",
    ...     description="All checks passed",
    ... )
    >>>
    >>> await api.close()  # Clean up when done

Note:
    All methods require an installation_id to authenticate requests.
    The GitHubAuth instance handles JWT generation and token caching.
"""

from __future__ import annotations

import logging
from typing import Any, Literal

import httpx

from .auth import GitHubAuth

logger = logging.getLogger(__name__)

# Base URL for GitHub REST API v3
GITHUB_API_BASE = "https://api.github.com"

# Valid states for commit status checks
StatusState = Literal["pending", "success", "failure", "error"]


class GitHubAPI:
    """Async GitHub API client for pull request operations.

    Provides methods for interacting with the GitHub API for PR-related
    workflows: status checks, comments, and diff retrieval. All operations
    use GitHub App installation tokens for authentication.

    The client supports two HTTP client modes:
    1. Owned client: Creates and manages its own httpx.AsyncClient
    2. Shared client: Uses a provided client for connection pooling

    Attributes:
        auth: GitHubAuth instance for generating installation tokens.

    Example:
        >>> auth = GitHubAuth(app_id=123, private_key=key)
        >>> api = GitHubAPI(auth)
        >>>
        >>> try:
        ...     await api.post_status(installation_id, owner, repo, sha, "success", "OK")
        ...     diff = await api.fetch_pr_diff(installation_id, owner, repo, 42)
        ... finally:
        ...     await api.close()
    """

    def __init__(
        self,
        auth: GitHubAuth,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """Initialize the GitHub API client.

        Args:
            auth: GitHubAuth instance for obtaining installation access tokens.
            http_client: Optional pre-configured httpx.AsyncClient for connection
                pooling. If None, the client creates its own on first use.
                When provided, the caller is responsible for closing it.
        """
        self.auth = auth
        self._http_client = http_client
        self._owns_client = http_client is None

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or lazily create the HTTP client.

        Creates a new httpx.AsyncClient on first call if none was provided
        during initialization. The client uses a 30-second timeout.

        Returns:
            The httpx.AsyncClient instance for making HTTP requests.
        """
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(timeout=30.0)
            self._owns_client = True
        return self._http_client

    async def _get_headers(self, installation_id: int) -> dict[str, str]:
        """Build request headers with installation token authentication.

        Obtains a fresh installation access token and constructs the standard
        GitHub API headers including authentication and API version.

        Args:
            installation_id: GitHub App installation ID to authenticate as.

        Returns:
            Dict of HTTP headers for GitHub API requests.
        """
        token = await self.auth.get_installation_token(installation_id)
        return {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
        }

    async def close(self) -> None:
        """Close the HTTP client if this instance owns it.

        Only closes the client if it was created by this instance (not provided
        during initialization). Safe to call multiple times.
        """
        if self._owns_client and self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None

    async def post_status(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        sha: str,
        state: StatusState,
        description: str,
        context: str = "MergeGuide Policy Check",
        target_url: str | None = None,
    ) -> None:
        """Post a commit status check to GitHub.

        Creates a status check that appears in the PR's checks section and
        can be configured as a required status check for branch protection.

        The status states have specific meanings:
        - pending: Check is in progress
        - success: Check passed, PR can be merged
        - failure: Check failed, PR should not be merged
        - error: Check itself encountered an error (infrastructure issue)

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            sha: Full commit SHA to associate the status with.
            state: One of 'pending', 'success', 'failure', or 'error'.
            description: Short status description. Automatically truncated
                to 140 characters (GitHub's limit).
            context: Status context/name shown in the PR UI.
                Defaults to "MergeGuide Policy Check".
            target_url: Optional URL for the "Details" link in the PR UI.
                Typically links to a detailed report or dashboard.

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response.
        """
        client = await self._get_client()
        headers = await self._get_headers(installation_id)
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/statuses/{sha}"

        # Truncate description to GitHub's 140-character limit
        if len(description) > 140:
            description = description[:137] + "..."

        payload: dict[str, Any] = {
            "state": state,
            "description": description,
            "context": context,
        }
        if target_url:
            payload["target_url"] = target_url

        response = await client.post(url, headers=headers, json=payload)
        response.raise_for_status()
        logger.info(f"Posted status '{state}' for {owner}/{repo}@{sha[:7]}")

    async def post_comment(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        pr_number: int,
        body: str,
    ) -> int:
        """Post a new comment on a pull request.

        Creates an issue comment (not a review comment) on the PR. Issue
        comments appear in the main conversation thread, not inline on code.

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            pr_number: Pull request number to comment on.
            body: Comment body in GitHub-flavored Markdown.

        Returns:
            The GitHub comment ID for the newly created comment.

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response.
        """
        client = await self._get_client()
        headers = await self._get_headers(installation_id)
        # PRs are issues in GitHub's data model, so we use the issues endpoint
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues/{pr_number}/comments"

        payload = {"body": body}
        response = await client.post(url, headers=headers, json=payload)
        response.raise_for_status()

        data = response.json()
        comment_id = data.get("id", 0)
        logger.info(f"Posted comment #{comment_id} on {owner}/{repo}#{pr_number}")
        return comment_id

    async def update_comment(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        comment_id: int,
        body: str,
    ) -> None:
        """Update the body of an existing PR comment.

        Replaces the entire comment body with the new content. Used for
        updating bot comments with new evaluation results on force-push.

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            comment_id: GitHub comment ID to update.
            body: New comment body in GitHub-flavored Markdown.

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response
                (e.g., comment not found, permission denied).
        """
        client = await self._get_client()
        headers = await self._get_headers(installation_id)
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues/comments/{comment_id}"

        payload = {"body": body}
        response = await client.patch(url, headers=headers, json=payload)
        response.raise_for_status()
        logger.info(f"Updated comment #{comment_id} on {owner}/{repo}")

    async def find_bot_comment(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        pr_number: int,
        marker: str = "<!-- mergeguide-policy-check -->",
    ) -> int | None:
        """Find an existing bot comment by searching for a marker string.

        Searches through all comments on a PR looking for one containing
        the specified marker. This enables idempotent commenting where
        the bot updates its existing comment instead of creating duplicates.

        The marker should be an HTML comment that's invisible when rendered
        but can be reliably detected in the comment body.

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            pr_number: Pull request number to search comments on.
            marker: String to search for in comment bodies.
                Defaults to "<!-- mergeguide-policy-check -->".

        Returns:
            The comment ID if a matching comment is found, None otherwise.

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response.
        """
        client = await self._get_client()
        headers = await self._get_headers(installation_id)
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues/{pr_number}/comments"

        response = await client.get(url, headers=headers)
        response.raise_for_status()

        # Search through all comments for the marker
        comments = response.json()
        for comment in comments:
            if marker in comment.get("body", ""):
                return comment.get("id")

        return None

    async def fetch_diff(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        base: str,
        head: str,
    ) -> str:
        """Fetch the unified diff between two Git refs.

        Uses GitHub's compare endpoint with the diff media type to get
        a standard unified diff format. Useful for comparing branches
        or arbitrary commits.

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            base: Base ref for comparison (branch name or commit SHA).
            head: Head ref for comparison (branch name or commit SHA).

        Returns:
            Unified diff string in standard patch format, suitable for
            parsing by the mergeguide.engine.parser module.

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response
                (e.g., refs not found, no common ancestor).
        """
        client = await self._get_client()
        token = await self.auth.get_installation_token(installation_id)

        # Use compare endpoint with diff media type for raw diff output
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/compare/{base}...{head}"
        headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github.v3.diff",
            "X-GitHub-Api-Version": "2022-11-28",
        }

        response = await client.get(url, headers=headers)
        response.raise_for_status()

        # Response body is raw unified diff text (not JSON)
        return response.text

    async def fetch_pr_diff(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        pr_number: int,
    ) -> str:
        """Fetch the unified diff for a pull request.

        Retrieves the complete diff for all files changed in a PR using
        GitHub's diff media type. This is the primary method for getting
        PR content for policy evaluation.

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            pr_number: Pull request number.

        Returns:
            Unified diff string in standard patch format, containing all
            file changes in the PR.

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response
                (e.g., PR not found, permission denied).
        """
        client = await self._get_client()
        token = await self.auth.get_installation_token(installation_id)

        # Request PR with diff media type for raw diff output
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/pulls/{pr_number}"
        headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github.v3.diff",
            "X-GitHub-Api-Version": "2022-11-28",
        }

        response = await client.get(url, headers=headers)
        response.raise_for_status()

        return response.text

    async def get_pr_files(
        self,
        installation_id: int,
        owner: str,
        repo: str,
        pr_number: int,
    ) -> list[dict[str, Any]]:
        """Get metadata about files changed in a pull request.

        Retrieves structured information about each file in the PR including
        filename, change status, and line counts. Handles pagination
        automatically for PRs with many files.

        Note: This returns file metadata, not full diffs. For the actual
        diff content, use fetch_pr_diff() instead.

        Args:
            installation_id: GitHub App installation ID for authentication.
            owner: Repository owner (organization or user).
            repo: Repository name.
            pr_number: Pull request number.

        Returns:
            List of file info dicts, each containing:
            - filename: Path to the file
            - status: One of 'added', 'removed', 'modified', 'renamed', 'copied'
            - additions: Number of lines added
            - deletions: Number of lines deleted
            - patch: The patch/diff for this file (may be truncated for large files)

        Raises:
            httpx.HTTPStatusError: If the GitHub API returns an error response.
        """
        client = await self._get_client()
        headers = await self._get_headers(installation_id)
        url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/pulls/{pr_number}/files"

        files = []
        page = 1

        # Paginate through all files in the PR
        while True:
            response = await client.get(
                url,
                headers=headers,
                params={"page": page, "per_page": 100},
            )
            response.raise_for_status()
            page_files = response.json()

            if not page_files:
                break

            files.extend(page_files)
            page += 1

            # Safety limit to prevent runaway pagination on massive PRs
            if page > 50:
                logger.warning(f"PR {owner}/{repo}#{pr_number} has >5000 files, truncating")
                break

        return files
