"""
Base Academic Content Downloader Abstract Class
"""

from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, NamedTuple
from enum import Enum
import requests
from urllib.parse import urlparse
from loguru import logger

# Import our adaptive rate limiting system
from ...web_search_engines.rate_limiting import (
    AdaptiveRateLimitTracker,
)
from ...security import SafeSession

# Import centralized User-Agent from constants
from ...constants import USER_AGENT  # noqa: F401 - re-exported for backward compatibility


class ContentType(Enum):
    """Supported content types for download."""

    PDF = "pdf"
    TEXT = "text"


class DownloadResult(NamedTuple):
    """Result of a download attempt."""

    content: Optional[bytes] = None
    skip_reason: Optional[str] = None
    is_success: bool = False
    status_code: Optional[int] = None


class BaseDownloader(ABC):
    """Abstract base class for academic content downloaders."""

    def __init__(self, timeout: int = 30):
        """
        Initialize the downloader.

        Args:
            timeout: Request timeout in seconds
        """
        self.timeout = timeout
        self.session = SafeSession()
        self.session.headers.update({"User-Agent": USER_AGENT})

        # Initialize rate limiter for PDF downloads
        # We'll use domain-specific rate limiting
        self.rate_tracker = AdaptiveRateLimitTracker(
            programmatic_mode=False  # We want to persist rate limit data
        )

    def close(self):
        """
        Close the HTTP session and clean up resources.

        Call this method when done using the downloader to prevent
        connection/file descriptor leaks.
        """
        if hasattr(self, "session") and self.session:
            try:
                self.session.close()
            except Exception:
                logger.exception("Error closing downloader session")
            finally:
                self.session = None  # type: ignore[assignment]

    def __del__(self):
        """Destructor to ensure session is closed."""
        self.close()

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - ensures session cleanup."""
        self.close()
        return False

    @abstractmethod
    def can_handle(self, url: str) -> bool:
        """
        Check if this downloader can handle the given URL.

        Args:
            url: The URL to check

        Returns:
            True if this downloader can handle the URL
        """
        pass

    @abstractmethod
    def download(
        self, url: str, content_type: ContentType = ContentType.PDF
    ) -> Optional[bytes]:
        """
        Download content from the given URL.

        Args:
            url: The URL to download from
            content_type: Type of content to download (PDF or TEXT)

        Returns:
            Content as bytes, or None if download failed
            For TEXT type, returns UTF-8 encoded text as bytes
        """
        pass

    def download_pdf(self, url: str) -> Optional[bytes]:
        """
        Convenience method to download PDF.

        Args:
            url: The URL to download from

        Returns:
            PDF content as bytes, or None if download failed
        """
        return self.download(url, ContentType.PDF)

    def download_with_result(
        self, url: str, content_type: ContentType = ContentType.PDF
    ) -> DownloadResult:
        """
        Download content and return detailed result with skip reason.

        Args:
            url: The URL to download from
            content_type: Type of content to download

        Returns:
            DownloadResult with content and/or skip reason
        """
        # Default implementation - derived classes should override for specific reasons
        content = self.download(url, content_type)
        if content:
            return DownloadResult(content=content, is_success=True)
        return DownloadResult(
            skip_reason="Download failed - content not available"
        )

    def download_text(self, url: str) -> Optional[str]:
        """
        Convenience method to download and return text content.

        Args:
            url: The URL to download from

        Returns:
            Text content as string, or None if download failed
        """
        content = self.download(url, ContentType.TEXT)
        if content:
            try:
                return content.decode("utf-8")
            except UnicodeDecodeError:
                logger.exception(f"Failed to decode text content from {url}")
        return None

    def _is_pdf_content(self, response: requests.Response) -> bool:
        """
        Check if response contains PDF content.

        Args:
            response: The response to check

        Returns:
            True if response appears to contain PDF content
        """
        content_type = response.headers.get("content-type", "").lower()

        # Check content type
        if "pdf" in content_type:
            return True

        # Check if content starts with PDF magic bytes
        if len(response.content) > 4:
            return response.content[:4] == b"%PDF"

        return False

    def _download_pdf(
        self, url: str, headers: Optional[Dict[str, str]] = None
    ) -> Optional[bytes]:
        """
        Helper method to download PDF with error handling and retry logic.
        Uses our optimized adaptive rate limiting/retry system.

        Args:
            url: The URL to download
            headers: Optional additional headers

        Returns:
            PDF content as bytes, or None if download failed
        """
        # Extract domain for rate limiting (each domain gets its own rate limit)
        domain = urlparse(url).netloc
        engine_type = f"pdf_download_{domain}"

        max_attempts = 3

        logger.debug(
            f"Downloading PDF from {url} with adaptive rate limiting (max {max_attempts} attempts)"
        )

        for attempt in range(1, max_attempts + 1):
            # Apply adaptive rate limiting before the request
            wait_time = self.rate_tracker.apply_rate_limit(engine_type)

            try:
                # Prepare headers
                if headers:
                    request_headers = dict(self.session.headers)
                    request_headers.update(headers)
                else:
                    request_headers = dict(self.session.headers)

                # Make the request
                response = self.session.get(
                    url,
                    headers=request_headers,
                    timeout=self.timeout,
                    allow_redirects=True,
                )

                # Check response
                if response.status_code == 200:
                    if self._is_pdf_content(response):
                        logger.debug(
                            f"Successfully downloaded PDF from {url} on attempt {attempt}"
                        )
                        # Record successful outcome
                        self.rate_tracker.record_outcome(
                            engine_type=engine_type,
                            wait_time=wait_time,
                            success=True,
                            retry_count=attempt,
                            search_result_count=1,  # We got the PDF
                        )
                        return response.content
                    logger.warning(
                        f"Response is not a PDF: {response.headers.get('content-type', 'unknown')}"
                    )
                    # Record failure but don't retry for wrong content type
                    self.rate_tracker.record_outcome(
                        engine_type=engine_type,
                        wait_time=wait_time,
                        success=False,
                        retry_count=attempt,
                        error_type="NotPDF",
                    )
                    return None
                if response.status_code in [
                    429,
                    503,
                ]:  # Rate limit or service unavailable
                    logger.warning(
                        f"Attempt {attempt}/{max_attempts} - HTTP {response.status_code} from {url}"
                    )
                    # Record rate limit failure
                    self.rate_tracker.record_outcome(
                        engine_type=engine_type,
                        wait_time=wait_time,
                        success=False,
                        retry_count=attempt,
                        error_type=f"HTTP_{response.status_code}",
                    )
                    if attempt == max_attempts:
                        logger.error(
                            f"Failed to download from {url}: HTTP {response.status_code} after {max_attempts} attempts"
                        )
                        return None
                    # Continue retry loop with adaptive wait
                    continue
                logger.warning(
                    f"Failed to download from {url}: HTTP {response.status_code}"
                )
                # Record failure but don't retry for other status codes
                self.rate_tracker.record_outcome(
                    engine_type=engine_type,
                    wait_time=wait_time,
                    success=False,
                    retry_count=attempt,
                    error_type=f"HTTP_{response.status_code}",
                )
                return None

            except (
                requests.exceptions.Timeout,
                requests.exceptions.ConnectionError,
            ) as e:
                # Record network failure
                self.rate_tracker.record_outcome(
                    engine_type=engine_type,
                    wait_time=wait_time,
                    success=False,
                    retry_count=attempt,
                    error_type=type(e).__name__,
                )
                if attempt == max_attempts:
                    logger.exception(
                        f"{type(e).__name__} downloading from {url} after {max_attempts} attempts"
                    )
                    return None
                logger.warning(
                    f"Attempt {attempt}/{max_attempts} - {type(e).__name__} downloading from {url}"
                )
                continue  # Retry with adaptive wait
            except requests.exceptions.RequestException as e:
                logger.exception(f"Request error downloading from {url}")
                # Record failure but don't retry
                self.rate_tracker.record_outcome(
                    engine_type=engine_type,
                    wait_time=wait_time,
                    success=False,
                    retry_count=attempt,
                    error_type=type(e).__name__,
                )
                return None
            except Exception:
                logger.exception(f"Unexpected error downloading from {url}")
                # Record failure but don't retry
                self.rate_tracker.record_outcome(
                    engine_type=engine_type,
                    wait_time=wait_time,
                    success=False,
                    retry_count=attempt,
                    error_type="UnexpectedError",
                )
                return None

        return None

    @staticmethod
    def extract_text_from_pdf(pdf_content: bytes) -> Optional[str]:
        """
        Extract text from PDF content using in-memory processing.

        This is part of the public API and can be used by other modules.

        Args:
            pdf_content: PDF file content as bytes

        Returns:
            Extracted text, or None if extraction failed
        """
        try:
            import io

            # Use pypdf for in-memory PDF text extraction (no disk writes)
            try:
                from pypdf import PdfReader as _PdfReader
            except ImportError:
                from PyPDF2 import PdfReader as _PdfReader  # type: ignore[no-redef]

            pdf_file = io.BytesIO(pdf_content)
            pdf_reader = _PdfReader(pdf_file)

            text_content = []
            for page in pdf_reader.pages:
                text = page.extract_text()
                if text:
                    text_content.append(text)

            full_text = "\n".join(text_content)
            return full_text if full_text.strip() else None

        except Exception:
            logger.exception("Failed to extract text from PDF")
            return None

    def _fetch_text_from_api(self, url: str) -> Optional[str]:
        """
        Fetch full text directly from API.

        This is a placeholder - derived classes should implement
        API-specific text fetching logic.

        Args:
            url: The URL or identifier

        Returns:
            Full text content, or None if not available
        """
        return None

    def get_metadata(self, url: str) -> Dict[str, Any]:
        """
        Get metadata about the resource (optional override).

        Args:
            url: The URL to get metadata for

        Returns:
            Dictionary with metadata
        """
        return {}
