"""
Crossref API client for querying academic paper metadata.
This module provides a client for interacting with the Crossref API
to retrieve detailed information about publications using DOIs.

Rate Limits:
    The Crossref API has different rate limits based on authentication:
    - Public: 5 requests per interval, 1 concurrent
    - Polite: 10 requests per interval, 3 concurrent (recommended for most uses)
    - Plus: 150 requests per interval, no concurrent limit (requires API key)

    Rate limit headers in responses:
    - X-RateLimit-Limit: Maximum requests allowed
    - X-RateLimit-Interval: Time interval for rate limit
    - X-Concurrency-Limit: Maximum concurrent requests

    If rate limit is exceeded (429 response), wait and retry with backoff.

Best Practices:
    - Use polite pool by including mailto parameter
    - Add delay between requests (recommend 0.5s for polite pool)
    - Cache responses to avoid duplicate requests
    - Handle 429 responses with exponential backoff
"""

import logging
import time
from typing import Any
from urllib.parse import quote

import requests


logger = logging.getLogger(__name__)


class CrossrefAPIError(Exception):
    """Exception raised when Crossref API request fails."""

    def __init__(self, message: str, status_code: int | None = None) -> None:
        """
        Initialize CrossrefAPIError.

        Args:
            message: Error message.
            status_code: HTTP status code from the API response.
        """
        super().__init__(message)
        self.status_code = status_code


class CrossrefRateLimitExceeded(CrossrefAPIError):
    """Exception raised when Crossref API rate limit is exceeded."""

    def __init__(
        self,
        message: str,
        retry_after: int | None = None,
        limit: int | None = None,
        interval: str | None = None,
    ) -> None:
        """
        Initialize CrossrefRateLimitExceeded.

        Args:
            message: Error message.
            retry_after: Seconds to wait before retrying.
            limit: Rate limit that was exceeded.
            interval: Time interval for the rate limit.
        """
        super().__init__(message, status_code=429)
        self.retry_after = retry_after
        self.limit = limit
        self.interval = interval


class CrossrefClient:
    """
    Client for querying the Crossref API.
    This class provides methods to retrieve metadata for academic publications
    using their DOIs (Digital Object Identifiers).

    Rate Limits:
        - Public pool: 5 requests/interval, 1 concurrent
        - Polite pool (default): 10 requests/interval, 3 concurrent
        - Plus pool: 150 requests/interval, unlimited concurrent

        The "polite" pool is recommended for most uses as it provides higher
        rate limits. To use the polite pool, include your email address in
        the mailto parameter or User-Agent header.

    Example:
        >>> client = CrossrefClient(mailto="user@example.com")
        >>> data = client.query_doi("10.1126/science.291.5503.447")
    """

    BASE_URL = "https://api.crossref.org"
    DEFAULT_TIMEOUT = 30
    DEFAULT_MAX_RETRIES = 3
    DEFAULT_INITIAL_BACKOFF = 1.0  # seconds
    DEFAULT_MAX_BACKOFF = 60.0  # seconds

    # Default rate limit delays (seconds between requests)
    PUBLIC_POOL_DELAY = 0.5  # 5 requests/second = 0.2s, use 0.5s for safety
    POLITE_POOL_DELAY = 0.3  # 10 requests/second = 0.1s, use 0.3s for safety
    PLUS_POOL_DELAY = 0.05  # 150 requests/second, very fast

    def __init__(
        self,
        timeout: int = DEFAULT_TIMEOUT,
        user_agent: str = "Information-Composer/0.2.1 (mailto:mcp@information-composer.org)",
        mailto: str | None = None,
        max_retries: int = DEFAULT_MAX_RETRIES,
        initial_backoff: float = DEFAULT_INITIAL_BACKOFF,
        max_backoff: float = DEFAULT_MAX_BACKOFF,
        request_delay: float | None = None,
    ) -> None:
        """
        Initialize the Crossref client.

        Args:
            timeout: Request timeout in seconds. Defaults to 30.
            user_agent: User-Agent string for API requests.
            mailto: Email address for polite pool access. Recommended format:
                    "mailto:your-email@example.com"
            max_retries: Maximum number of retries for rate-limited requests.
                        Defaults to 3.
            initial_backoff: Initial backoff time in seconds. Defaults to 1.
            max_backoff: Maximum backoff time in seconds. Defaults to 60.
            request_delay: Delay between requests in seconds. If None, auto-detect
                          based on mailto (polite pool uses 0.3s, public uses 0.5s).
        """
        self.timeout = timeout
        self.max_retries = max_retries
        self.initial_backoff = initial_backoff
        self.max_backoff = max_backoff

        # Extract email from mailto if provided in user_agent
        extracted_mailto = self._extract_mailto(user_agent)
        if mailto:
            mailto = f"mailto:{mailto}" if not mailto.startswith("mailto:") else mailto
        elif extracted_mailto:
            mailto = extracted_mailto
        else:
            mailto = "mailto:mcp@information-composer.org"

        self.user_agent = user_agent
        self.mailto = mailto

        self.session = requests.Session()
        self.session.headers.update({"User-Agent": self.user_agent})

        # Rate limit tracking
        self._rate_limit_limit: int | None = None
        self._rate_limit_interval: str | None = None
        self._concurrency_limit: int | None = None
        self._last_request_time: float = 0
        self._request_count: int = 0
        self._request_window_start: float = 0

        # Auto-detect request delay based on pool type
        self.request_delay = (
            request_delay if request_delay is not None else self._auto_detect_delay()
        )

    def _auto_detect_delay(self) -> float:
        """
        Auto-detect appropriate request delay based on mailto presence.
        Polite pool (with mailto) gets faster rate limits.

        Returns:
            Delay in seconds between requests.
        """
        # If mailto is provided and looks like a real email, use polite pool delay
        if self.mailto and self.mailto != "mailto:mcp@information-composer.org":
            return self.POLITE_POOL_DELAY
        return self.PUBLIC_POOL_DELAY

    def _extract_mailto(self, user_agent: str) -> str | None:
        """Extract mailto from User-Agent string."""
        import re

        match = re.search(r"mailto:[\w.-]+@[\w.-]+\.\w+", user_agent)
        return match.group(0) if match else None

    def _parse_rate_limit_headers(
        self, headers: requests.structures.CaseInsensitiveDict
    ) -> None:
        """Parse and store rate limit headers from response."""
        self._rate_limit_limit = headers.get("X-RateLimit-Limit")
        self._rate_limit_interval = headers.get("X-RateLimit-Interval")
        self._concurrency_limit = headers.get("X-Concurrency-Limit")

        if self._rate_limit_limit:
            logger.debug(
                f"Rate limit: {self._rate_limit_limit} requests per "
                f"{self._rate_limit_interval}"
            )

    def _wait_for_rate_limit(self) -> None:
        """Wait to respect rate limits between requests."""
        if self.request_delay > 0:
            elapsed = time.time() - self._last_request_time
            if elapsed < self.request_delay:
                sleep_time = self.request_delay - elapsed
                logger.debug(f"Rate limiting: sleeping {sleep_time:.2f}s")
                time.sleep(sleep_time)
            self._last_request_time = time.time()

    def query_doi(self, doi: str, include_references: bool = True) -> dict[str, Any]:
        """
        Query Crossref API for metadata associated with a DOI.

        This method automatically handles rate limiting by:
        1. Waiting between requests based on pool type
        2. Retrying with exponential backoff on 429 errors
        3. Tracking rate limit headers from responses

        Args:
            doi: The DOI to query (with or without doi: prefix).
            include_references: Whether to include reference data in response.

        Returns:
            Dictionary containing the API response.

        Raises:
            CrossrefAPIError: If the API request fails or DOI is not found.
            CrossrefRateLimitExceeded: If rate limit is exceeded after retries.
        """
        # Wait before making request to respect rate limits
        self._wait_for_rate_limit()

        # Clean up the DOI
        clean_doi = self._clean_doi(doi)

        # Build the URL with mailto parameter for polite pool
        encoded_doi = quote(clean_doi, safe="")
        url = f"{self.BASE_URL}/works/{encoded_doi}"

        params = {"mailto": self.mailto}

        logger.info(f"Querying Crossref API for DOI: {clean_doi}")

        # Implement retry logic with exponential backoff
        backoff_time = self.initial_backoff

        for attempt in range(self.max_retries + 1):
            try:
                response = self.session.get(url, params=params, timeout=self.timeout)

                # Parse rate limit headers
                self._parse_rate_limit_headers(response.headers)

                if response.status_code == 200:
                    return response.json()
                elif response.status_code == 404:
                    raise CrossrefAPIError(
                        f"DOI not found: {clean_doi}", status_code=404
                    )
                elif response.status_code == 429:
                    # Rate limited - parse Retry-After header
                    retry_after = response.headers.get("Retry-After")
                    retry_after_sec = int(retry_after) if retry_after else None

                    logger.warning(
                        f"Rate limit exceeded (attempt {attempt + 1}/"
                        f"{self.max_retries + 1})"
                    )

                    if attempt < self.max_retries:
                        wait_time = retry_after_sec or backoff_time
                        logger.info(f"Waiting {wait_time:.1f}s before retry...")
                        time.sleep(wait_time)
                        backoff_time = min(backoff_time * 2, self.max_backoff)
                        continue
                    else:
                        raise CrossrefRateLimitExceeded(
                            "Rate limit exceeded. Please reduce request frequency.",
                            retry_after=retry_after_sec,
                            limit=self._rate_limit_limit,
                            interval=self._rate_limit_interval,
                        )
                else:
                    response.raise_for_status()

            except requests.exceptions.HTTPError as e:
                raise CrossrefAPIError(
                    f"HTTP error querying Crossref API: {e!s}",
                    status_code=response.status_code if "response" in dir() else None,
                ) from e
            except requests.exceptions.RequestException as e:
                raise CrossrefAPIError(f"Error querying Crossref API: {e!s}") from e

        # This should not be reached, but just in case
        raise CrossrefAPIError("Unexpected error in query_doi")

    def query_batch(
        self, dois: list[str], include_references: bool = True
    ) -> list[dict[str, Any]]:
        """
        Query Crossref API for multiple DOIs with rate limiting.

        This method handles batching with proper rate limiting to avoid
        hitting API limits. Results are returned in the same order as input.

        Args:
            dois: List of DOIs to query.
            include_references: Whether to include reference data in responses.

        Returns:
            List of API response dictionaries in the same order as input DOIs.
            Failed queries return None for that entry.

        Raises:
            CrossrefAPIError: If all requests fail.
        """
        results: list[dict[str, Any] | None] = []

        for i, doi in enumerate(dois):
            try:
                data = self.query_doi(doi, include_references=include_references)
                results.append(data)
            except CrossrefAPIError as e:
                logger.error(f"Failed to query DOI {doi}: {e!s}")
                results.append(None)

            # Show progress for large batches
            if len(dois) > 10 and (i + 1) % 10 == 0:
                logger.info(f"Progress: {i + 1}/{len(dois)} DOIs processed")

        return results

    def query_doi_simple(self, doi: str) -> dict[str, Any] | None:
        """
        Query Crossref API and return basic information for a DOI.

        Args:
            doi: The DOI to query.

        Returns:
            Dictionary with basic DOI metadata, or None if not found.
        """
        try:
            data = self.query_doi(doi)
            message = data.get("message", {})
            return {
                "doi": message.get("DOI"),
                "title": message.get("title", []),
                "journal": message.get("container-title"),
                "publisher": message.get("publisher"),
                "published_date": message.get("published-print", {}).get(
                    "date-parts", [[None]]
                )[0],
                "type": message.get("type"),
                "is_referenced_by_count": message.get("is-referenced-by-count", 0),
                "url": f"https://doi.org/{message.get('DOI', '')}"
                if message.get("DOI")
                else None,
            }
        except CrossrefAPIError:
            return None

    def get_rate_limit_info(self) -> dict[str, Any]:
        """
        Get information about current rate limit status.

        Returns:
            Dictionary with rate limit information.
        """
        return {
            "limit": self._rate_limit_limit,
            "interval": self._rate_limit_interval,
            "concurrency_limit": self._concurrency_limit,
            "request_delay": self.request_delay,
        }

    @staticmethod
    def _clean_doi(doi: str) -> str:
        """
        Clean and normalize a DOI string.

        Args:
            doi: The DOI string to clean.

        Returns:
            Cleaned DOI string.
        """
        doi = doi.strip()
        # Remove common prefixes
        for prefix in ["doi:", "DOI:", "https://doi.org/", "http://doi.org/"]:
            if doi.lower().startswith(prefix.lower()):
                doi = doi[len(prefix) :]
                break
        return doi


def query_crossref(doi: str) -> dict[str, Any]:
    """
    Convenience function to query Crossref API for a DOI.

    This is a simple wrapper for single DOI queries. For batch operations
    or custom rate limiting, use CrossrefClient directly.

    Args:
        doi: The DOI to query.

    Returns:
        Dictionary containing the API response.

    Raises:
        CrossrefAPIError: If the API request fails or DOI is not found.
        CrossrefRateLimitExceeded: If rate limit is exceeded after retries.
    """
    client = CrossrefClient()
    return client.query_doi(doi)
