"""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.github 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.

    API base URLs are configurable via Django settings to support mock mode.
    """

    def __init__(self, installation: GitHubInstallation):
        # Use configurable URLs from settings (allows mock mode override)
        self.REST_API_BASE = getattr(settings, "GITHUB_API_BASE_URL", "https://api.github.com")
        self.GRAPHQL_API_URL = getattr(settings, "GITHUB_GRAPHQL_URL", "https://api.github.com/graphql")
        """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),
        )
