import logging
import re
import ssl
from pathlib import Path
from urllib.parse import quote

import httpx
from bs4 import BeautifulSoup
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential

logger = logging.getLogger(__name__)


class SciHubClient:
    def __init__(self, http_client: httpx.AsyncClient, mirrors: list[str]):
        self._client = http_client
        self._mirrors = mirrors

    async def check_mirror(self, mirror: str) -> bool:
        """Check if a Sci-Hub mirror is reachable."""
        try:
            resp = await self._client.get(f"https://{mirror}", timeout=10)
            return resp.status_code < 500
        except (httpx.HTTPError, ssl.SSLError):
            return False

    async def get_mirror_status(self) -> dict[str, bool]:
        """Return reachability status for all configured mirrors."""
        result = {}
        for mirror in self._mirrors:
            result[mirror] = await self.check_mirror(mirror)
        return result

    async def find_pdf_url(self, doi: str) -> str | None:
        """Try each mirror to find a PDF URL for the given DOI."""
        for mirror in self._mirrors:
            url = await self._try_mirror(mirror, doi)
            if url:
                return url
        return None

    async def _try_mirror(self, mirror: str, doi: str) -> str | None:
        try:
            return await self._scrape_mirror(mirror, doi)
        except ssl.SSLError:
            logger.warning("SSL error for mirror %s, skipping", mirror)
            return None
        except httpx.HTTPError as e:
            logger.warning("HTTP error for mirror %s: %s", mirror, e)
            return None

    @retry(
        stop=stop_after_attempt(2),
        wait=wait_exponential(multiplier=1, min=1, max=5),
        retry=retry_if_exception_type(httpx.TimeoutException),
        reraise=True,
    )
    async def _scrape_mirror(self, mirror: str, doi: str) -> str | None:
        resp = await self._client.get(
            f"https://{mirror}/{quote(doi, safe='/')}",
            follow_redirects=True,
            timeout=15,
        )
        if resp.status_code != 200:
            return None

        soup = BeautifulSoup(resp.text, "html.parser")

        # Strategy 1: <iframe id="pdf">
        iframe = soup.find("iframe", {"id": "pdf"})
        if iframe and iframe.get("src"):
            return self._normalize_url(iframe["src"])

        # Strategy 2: <embed> tag
        embed = soup.find("embed", {"type": "application/pdf"})
        if embed and embed.get("src"):
            return self._normalize_url(embed["src"])

        # Strategy 3: direct link to .pdf
        for link in soup.find_all("a", href=True):
            if link["href"].endswith(".pdf"):
                return self._normalize_url(link["href"])

        # Strategy 4: regex for PDF URL in page source
        pdf_match = re.search(r'(https?://[^\s"\']+\.pdf)', resp.text)
        if pdf_match:
            return pdf_match.group(1)

        return None

    @staticmethod
    def _normalize_url(url: str) -> str:
        """Handle protocol-relative URLs (//example.com/path)."""
        if url.startswith("//"):
            return f"https:{url}"
        return url

    async def download_pdf(
        self,
        pdf_url: str,
        save_path: Path,
        max_size: int = 100 * 1024 * 1024,
    ) -> bool:
        """Download PDF from URL. Verifies content, enforces size limit, writes atomically."""
        tmp_path = save_path.with_suffix(".tmp")
        try:
            async with self._client.stream("GET", pdf_url, follow_redirects=True) as resp:
                content_type = resp.headers.get("content-type", "")
                if resp.status_code != 200:
                    return False

                # Check first bytes for PDF magic number if content-type is ambiguous
                first_chunk = b""
                if "pdf" not in content_type and "octet-stream" not in content_type:
                    async for chunk in resp.aiter_bytes(chunk_size=8):
                        first_chunk = chunk
                        break
                    if not first_chunk.startswith(b"%PDF"):
                        logger.warning("Not a PDF: content-type=%s", content_type)
                        return False

                save_path.parent.mkdir(parents=True, exist_ok=True)
                total_written = 0
                with open(tmp_path, "wb") as f:
                    if first_chunk:
                        f.write(first_chunk)
                        total_written += len(first_chunk)
                    async for chunk in resp.aiter_bytes(chunk_size=8192):
                        total_written += len(chunk)
                        if total_written > max_size:
                            logger.warning("PDF exceeds size limit (%d bytes), aborting", max_size)
                            break
                        f.write(chunk)

                if total_written > max_size:
                    tmp_path.unlink(missing_ok=True)
                    return False

                # Atomic rename on success
                tmp_path.rename(save_path)
                return True
        except (httpx.HTTPError, ssl.SSLError) as e:
            logger.warning("PDF download failed: %s", e)
            tmp_path.unlink(missing_ok=True)
            return False
