"""PDF download helpers — URL safety, content fetching, and DOI fallbacks.

Stateless functions used by :class:`~axm_bib.pipeline.BibPipeline`
to download open-access PDF content from scholarly repositories.
"""

from __future__ import annotations

import ipaddress
import logging
import re
import urllib.parse

from httpx import HTTPError as _HTTPError

from axm_bib import __version__
from axm_bib.http import request_with_retry

__all__ = [
    "doi_fallback_urls",
    "fallback_bib_key",
    "fetch_pdf_content",
    "is_safe_url",
]

logger = logging.getLogger(__name__)


# ── URL safety (SSRF guard) ──────────────────────────────────────────────


def is_safe_url(url: str) -> bool:
    """Block non-HTTP schemes and private/loopback IP addresses (SSRF guard).

    Args:
        url: URL to validate.

    Returns:
        True if the URL is safe to fetch.
    """
    parsed = urllib.parse.urlparse(url)
    if parsed.scheme not in ("http", "https"):
        return False
    hostname = parsed.hostname or ""
    if hostname in ("localhost", ""):
        return False
    try:
        ip = ipaddress.ip_address(hostname)
        return ip.is_global
    except ValueError:
        # hostname is a domain name, not an IP — allow it
        return True


# ── PDF content fetching ─────────────────────────────────────────────────


def fetch_pdf_content(pdf_urls: list[str]) -> bytes | None:
    """Try each URL in order and return PDF bytes on success.

    Performs SSRF validation, HTTP download, and content-type checking.
    Re-raises the last HTTP/OS error if all URLs fail with errors.

    Args:
        pdf_urls: Ordered candidate URLs (repos first, publishers last).

    Returns:
        Raw PDF bytes, or None if no URL delivered a valid PDF.
    """
    headers = {"User-Agent": f"axm-bib/{__version__} (academic research tool)"}
    last_error: Exception | None = None

    for url in pdf_urls:
        if not is_safe_url(url):
            logger.warning("Skipping unsafe URL: %s", url)
            continue

        logger.debug("Trying PDF URL: %s", url)
        try:
            resp = request_with_retry(
                "GET",
                url,
                timeout=60.0,
                follow_redirects=True,
                headers=headers,
                retry_on=(429, 500, 502, 503),
            )
        except (_HTTPError, OSError) as exc:
            last_error = exc
            continue

        # Skip non-PDF responses (e.g., HTML landing pages)
        content_type = resp.headers.get("content-type", "").lower()
        if "application/pdf" not in content_type:
            continue

        return resp.content

    if last_error:
        raise last_error
    return None


# ── Fallback bib key ─────────────────────────────────────────────────────


def fallback_bib_key(ref: str) -> str:
    """Generate a filesystem-safe bib key when BibTeX resolution fails.

    Args:
        ref: Original identifier string.

    Returns:
        Sanitized key suitable for folder naming.
    """
    # Strip scheme prefixes, replace non-alphanum with underscore
    key = re.sub(r"[^a-zA-Z0-9]", "_", ref)
    key = re.sub(r"_+", "_", key).strip("_").lower()
    return key[:80] or "unknown"


# ── DOI-based fallback rules ─────────────────────────────────────────────
# Each rule: (compiled regex matching the DOI, URL template with \1 etc.)
# Add new OA publishers here — regex must capture the variable part.

_FALLBACK_RULES: list[tuple[re.Pattern[str], str]] = [
    # arXiv: 10.48550/arXiv.2411.19275 → https://arxiv.org/pdf/2411.19275
    (re.compile(r"10\.48550/arXiv\.(.+)", re.IGNORECASE), r"https://arxiv.org/pdf/\1"),
    # ACL Anthology: 10.18653/v1/2024.acl-long.172 → https://aclanthology.org/2024.acl-long.172.pdf
    (re.compile(r"10\.18653/v1/(.+)"), r"https://aclanthology.org/\1.pdf"),
]


def doi_fallback_urls(doi: str) -> list[str]:
    """Generate PDF URLs from DOI for known open-access publishers.

    Uses deterministic URL patterns for publishers where Unpaywall
    coverage is incomplete (e.g. ACL Anthology).

    Args:
        doi: Digital Object Identifier.

    Returns:
        List of candidate PDF URLs (typically 0 or 1).
    """
    for pattern, template in _FALLBACK_RULES:
        match = pattern.match(doi)
        if match:
            return [match.expand(template)]
    return []
