"""GitHub API client for interacting with the GitHub REST and GraphQL APIs.

Handles authentication via GitHub App installation tokens, including
automatic token refresh when tokens expire.
"""

from __future__ import annotations

import base64
import time
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any

import jwt
import requests
from django.conf import settings
from django.utils import timezone as dj_timezone

if TYPE_CHECKING:
    from integrations.models import GitHubInstallation


@dataclass
class GitHubRateLimitInfo:
    """Information about GitHub API rate limiting."""

    limit: int
    remaining: int
    reset_at: datetime
    used: int


class GitHubClientError(Exception):
    """Base exception for GitHub client errors."""

    pass


class GitHubAuthenticationError(GitHubClientError):
    """Error during GitHub authentication."""

    pass


class GitHubRateLimitError(GitHubClientError):
    """Rate limit exceeded error."""

    def __init__(self, message: str, reset_at: datetime):
        super().__init__(message)
        self.reset_at = reset_at


class GitHubAPIError(GitHubClientError):
    """General GitHub API error."""

    def __init__(self, message: str, status_code: int, response_body: dict | None = None):
        super().__init__(message)
        self.status_code = status_code
        self.response_body = response_body or {}


class GitHubClient:
    """Client for GitHub REST and GraphQL APIs.

    Uses GitHub App installation tokens for authentication, with automatic
    token refresh when tokens expire.
    """

    REST_API_BASE = "https://api.github.com"
    GRAPHQL_API_URL = "https://api.github.com/graphql"

    def __init__(self, installation: GitHubInstallation):
        """Initialize the client with a GitHub installation.

        Args:
            installation: The GitHubInstallation model instance
        """
        self.installation = installation
        self._session = requests.Session()
        self._session.headers.update(
            {
                "Accept": "application/vnd.github+json",
                "X-GitHub-Api-Version": "2022-11-28",
            }
        )

    def _get_private_key(self) -> str:
        """Get the decoded private key from settings."""
        key_base64 = settings.GITHUB_APP_PRIVATE_KEY_BASE64
        if not key_base64:
            raise GitHubAuthenticationError("GITHUB_APP_PRIVATE_KEY_BASE64 not configured")
        try:
            return base64.b64decode(key_base64).decode("utf-8")
        except Exception as e:
            raise GitHubAuthenticationError(f"Failed to decode private key: {e}") from e

    def _generate_jwt(self) -> str:
        """Generate a JWT for GitHub App authentication.

        The JWT is used to obtain an installation access token.
        """
        app_id = settings.GITHUB_APP_ID
        if not app_id:
            raise GitHubAuthenticationError("GITHUB_APP_ID not configured")

        private_key = self._get_private_key()
        now = int(time.time())

        payload = {
            "iat": now - 60,  # Allow for clock drift
            "exp": now + (10 * 60),  # 10 minutes max
            "iss": app_id,
        }

        return jwt.encode(payload, private_key, algorithm="RS256")

    def _refresh_installation_token(self) -> str:
        """Refresh the installation access token.

        Returns:
            The new access token
        """
        jwt_token = self._generate_jwt()
        url = f"{self.REST_API_BASE}/app/installations/{self.installation.installation_id}/access_tokens"

        response = self._session.post(
            url,
            headers={"Authorization": f"Bearer {jwt_token}"},
        )

        if response.status_code != 201:
            raise GitHubAuthenticationError(
                f"Failed to refresh installation token: {response.status_code} {response.text}"
            )

        data = response.json()
        token: str = data["token"]
        expires_at = datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00"))

        # Update the installation record
        self.installation.access_token = token
        self.installation.token_expires_at = expires_at
        self.installation.save(update_fields=["_access_token", "token_expires_at", "updated_at"])

        return token

    def _get_access_token(self) -> str:
        """Get a valid access token, refreshing if necessary.

        Returns:
            A valid access token
        """
        # Check if current token is valid (with 5-minute buffer)
        if (
            self.installation.access_token
            and self.installation.token_expires_at
            and self.installation.token_expires_at > dj_timezone.now() + timedelta(minutes=5)
        ):
            return self.installation.access_token

        # Refresh the token
        return self._refresh_installation_token()

    def _extract_rate_limit(self, response: requests.Response) -> GitHubRateLimitInfo | None:
        """Extract rate limit information from response headers."""
        try:
            return GitHubRateLimitInfo(
                limit=int(response.headers.get("X-RateLimit-Limit", 0)),
                remaining=int(response.headers.get("X-RateLimit-Remaining", 0)),
                reset_at=datetime.fromtimestamp(
                    int(response.headers.get("X-RateLimit-Reset", 0)),
                    tz=UTC,
                ),
                used=int(response.headers.get("X-RateLimit-Used", 0)),
            )
        except (ValueError, TypeError):
            return None

    def _handle_response(self, response: requests.Response) -> dict[str, Any]:
        """Handle API response, raising appropriate exceptions for errors.

        Args:
            response: The requests Response object

        Returns:
            The parsed JSON response body

        Raises:
            GitHubRateLimitError: If rate limit is exceeded
            GitHubAPIError: For other API errors
        """
        rate_limit = self._extract_rate_limit(response)

        if response.status_code == 403:
            # Check if it's a rate limit error
            if rate_limit and rate_limit.remaining == 0:
                raise GitHubRateLimitError(
                    f"GitHub API rate limit exceeded. Resets at {rate_limit.reset_at}",
                    reset_at=rate_limit.reset_at,
                )

        if response.status_code >= 400:
            try:
                body = response.json()
            except Exception:
                body = {}
            raise GitHubAPIError(
                f"GitHub API error: {response.status_code}",
                status_code=response.status_code,
                response_body=body,
            )

        if response.status_code == 204:
            return {}

        result: dict[str, Any] = response.json()
        return result

    def get(self, endpoint: str, params: dict | None = None) -> dict[str, Any]:
        """Make a GET request to the GitHub REST API.

        Args:
            endpoint: The API endpoint (e.g., "/repos/owner/repo/issues")
            params: Optional query parameters

        Returns:
            The parsed JSON response
        """
        token = self._get_access_token()
        url = f"{self.REST_API_BASE}{endpoint}"

        response = self._session.get(
            url,
            params=params,
            headers={"Authorization": f"Bearer {token}"},
        )

        return self._handle_response(response)

    def get_paginated(
        self,
        endpoint: str,
        params: dict | None = None,
        max_pages: int = 10,
    ) -> list[dict[str, Any]]:
        """Make paginated GET requests to the GitHub REST API.

        Args:
            endpoint: The API endpoint
            params: Optional query parameters
            max_pages: Maximum number of pages to fetch

        Returns:
            A list of all items from all pages
        """
        params = params or {}
        params.setdefault("per_page", 100)

        all_items = []
        page = 1

        while page <= max_pages:
            params["page"] = page
            response = self.get(endpoint, params)

            if isinstance(response, list):
                if not response:
                    break
                all_items.extend(response)
                if len(response) < params["per_page"]:
                    break
            else:
                # Single object response
                all_items.append(response)
                break

            page += 1

        return all_items

    def post(self, endpoint: str, data: dict | None = None) -> dict[str, Any]:
        """Make a POST request to the GitHub REST API.

        Args:
            endpoint: The API endpoint
            data: Optional request body

        Returns:
            The parsed JSON response
        """
        token = self._get_access_token()
        url = f"{self.REST_API_BASE}{endpoint}"

        response = self._session.post(
            url,
            json=data,
            headers={"Authorization": f"Bearer {token}"},
        )

        return self._handle_response(response)

    def graphql(self, query: str, variables: dict | None = None) -> dict[str, Any]:
        """Make a GraphQL query to the GitHub API.

        Args:
            query: The GraphQL query string
            variables: Optional query variables

        Returns:
            The parsed JSON response data
        """
        token = self._get_access_token()

        response = self._session.post(
            self.GRAPHQL_API_URL,
            json={"query": query, "variables": variables or {}},
            headers={"Authorization": f"Bearer {token}"},
        )

        result = self._handle_response(response)

        if "errors" in result:
            raise GitHubAPIError(
                f"GraphQL errors: {result['errors']}",
                status_code=200,
                response_body=result,
            )

        data: dict[str, Any] = result.get("data", {})
        return data

    def list_installation_repositories(self) -> list[dict[str, Any]]:
        """List all repositories accessible to this installation.

        Returns:
            A list of repository objects
        """
        all_repos = []
        page = 1

        while True:
            response = self.get(
                "/installation/repositories",
                params={"per_page": 100, "page": page},
            )

            repos = response.get("repositories", [])
            if not repos:
                break

            all_repos.extend(repos)

            if len(repos) < 100:
                break
            page += 1

        return all_repos

    def get_repository(self, owner: str, repo: str) -> dict[str, Any]:
        """Get a single repository.

        Args:
            owner: Repository owner
            repo: Repository name

        Returns:
            The repository object
        """
        return self.get(f"/repos/{owner}/{repo}")

    def list_issues(
        self,
        owner: str,
        repo: str,
        since: datetime | None = None,
        state: str = "all",
    ) -> list[dict[str, Any]]:
        """List issues for a repository.

        Args:
            owner: Repository owner
            repo: Repository name
            since: Only show issues updated after this time
            state: Filter by state (open, closed, all)

        Returns:
            A list of issue objects
        """
        params = {"state": state, "sort": "updated", "direction": "asc"}
        if since:
            params["since"] = since.isoformat()

        return self.get_paginated(f"/repos/{owner}/{repo}/issues", params)

    def list_pull_requests(
        self,
        owner: str,
        repo: str,
        since: datetime | None = None,
        state: str = "all",
    ) -> list[dict[str, Any]]:
        """List pull requests for a repository.

        Args:
            owner: Repository owner
            repo: Repository name
            since: Only show PRs updated after this time (applied client-side)
            state: Filter by state (open, closed, all)

        Returns:
            A list of pull request objects
        """
        params = {"state": state, "sort": "updated", "direction": "asc"}
        prs = self.get_paginated(f"/repos/{owner}/{repo}/pulls", params)

        # Filter by since (PR endpoint doesn't support since parameter)
        if since:
            prs = [pr for pr in prs if datetime.fromisoformat(pr["updated_at"].replace("Z", "+00:00")) >= since]

        return prs

    def list_issue_comments(
        self,
        owner: str,
        repo: str,
        issue_number: int,
        since: datetime | None = None,
    ) -> list[dict[str, Any]]:
        """List comments on an issue.

        Args:
            owner: Repository owner
            repo: Repository name
            issue_number: The issue number
            since: Only show comments created after this time

        Returns:
            A list of comment objects
        """
        params = {"sort": "created", "direction": "asc"}
        if since:
            params["since"] = since.isoformat()

        return self.get_paginated(
            f"/repos/{owner}/{repo}/issues/{issue_number}/comments",
            params,
        )

    def list_discussions(
        self,
        owner: str,
        repo: str,
        cursor: str | None = None,
    ) -> tuple[list[dict[str, Any]], str | None, bool]:
        """List discussions for a repository using GraphQL.

        Args:
            owner: Repository owner
            repo: Repository name
            cursor: Pagination cursor for fetching more results

        Returns:
            A tuple of (discussions, next_cursor, has_next_page)
        """
        query = """
        query($owner: String!, $repo: String!, $cursor: String) {
          repository(owner: $owner, name: $repo) {
            discussions(first: 100, after: $cursor, orderBy: {field: UPDATED_AT, direction: DESC}) {
              nodes {
                id
                number
                title
                body
                createdAt
                updatedAt
                author { login avatarUrl }
                category { name slug }
                answer { id }
                comments(first: 100) {
                  nodes {
                    id
                    body
                    createdAt
                    author { login avatarUrl }
                  }
                  pageInfo { hasNextPage endCursor }
                }
              }
              pageInfo { hasNextPage endCursor }
            }
          }
        }
        """

        data = self.graphql(query, {"owner": owner, "repo": repo, "cursor": cursor})

        discussions_data = data.get("repository", {}).get("discussions", {})
        discussions = discussions_data.get("nodes", [])
        page_info = discussions_data.get("pageInfo", {})

        return (
            discussions,
            page_info.get("endCursor"),
            page_info.get("hasNextPage", False),
        )
