"""Composable bibliographic pipeline — orchestrates clients for end-to-end flows.

Flows:
    title → Paper (with DOI)     via SemanticScholar → CrossRef fallback
    DOI   → BibEntry             via CrossRef content negotiation
    title → BibEntry             chains the above
    DOI   → DownloadResult       via Unpaywall + httpx download
"""

from __future__ import annotations

import re

import httpx

from axm_bib.clients.crossref import CrossRefClient
from axm_bib.clients.semantic_scholar import SemanticScholarClient
from axm_bib.clients.unpaywall import UnpaywallClient
from axm_bib.models import BibEntry, DownloadResult, Paper
from axm_bib.storage import FileSystemStorage, StorageBackend

__all__ = ["BibPipeline", "NoDOIError"]


class NoDOIError(Exception):
    """Raised when a paper has no DOI and BibTeX resolution is impossible."""


class BibPipeline:
    """Composable bibliographic pipeline.

    Orchestrates clients for title→DOI→BibTeX→PDF workflows.

    Args:
        mailto: Email for polite pool (CrossRef + Unpaywall).
        storage: PDF storage backend (default: FileSystemStorage).
    """

    def __init__(
        self,
        mailto: str = "",
        storage: StorageBackend | None = None,
    ) -> None:
        self._crossref = CrossRefClient(mailto=mailto)
        self._scholar = SemanticScholarClient()
        self._unpaywall = UnpaywallClient(email=mailto)
        self._storage = storage or FileSystemStorage()

    _MAX_LIMIT = 100

    def search(self, title: str, limit: int = 5) -> list[Paper]:
        """Search papers by title, with Semantic Scholar → CrossRef fallback.

        Args:
            title: Search query.
            limit: Maximum results (1-100, clamped if larger).

        Returns:
            List of Paper objects with metadata.

        Raises:
            ValueError: If limit < 1.
        """
        if limit < 1:
            msg = f"limit must be ≥ 1, got {limit}"
            raise ValueError(msg)
        limit = min(limit, self._MAX_LIMIT)
        try:
            return self._scholar.search_paper(title, limit=limit)
        except Exception:
            # Fallback to CrossRef
            return self._crossref.search_by_title(title, limit=limit)

    def resolve_bibtex(self, doi: str) -> BibEntry:
        """Resolve a DOI to a clean BibTeX entry.

        Args:
            doi: Digital Object Identifier.

        Returns:
            Parsed BibEntry with normalized key.
        """
        return self._crossref.doi_to_bibtex(doi)

    def search_and_resolve(self, title: str) -> BibEntry:
        """Search by title and resolve to BibTeX (full chain).

        Args:
            title: Paper title or keywords.

        Returns:
            BibEntry from the first result with a DOI.

        Raises:
            NoDOIError: If no results have a DOI.
        """
        papers = self.search(title, limit=5)

        for paper in papers:
            if paper.doi:
                return self.resolve_bibtex(paper.doi)

        msg = f"No DOI found for any result matching '{title}'"
        raise NoDOIError(msg)

    def download_pdf(self, doi: str) -> DownloadResult | None:
        """Download open-access PDF, extract content, and write BibTeX.

        Full pipeline: DOI → Unpaywall → download → {key}/ folder → extract
        markdown + figures → write paper.bib.

        Args:
            doi: Digital Object Identifier.

        Returns:
            DownloadResult with full pipeline metadata, None if not open access.
        """
        pdf_urls = self._unpaywall.get_pdf_urls(doi)

        # Fallback: arXiv DOI → direct PDF URL
        if not pdf_urls:
            arxiv_id = _extract_arxiv_id(doi)
            if arxiv_id:
                pdf_urls = [f"https://arxiv.org/pdf/{arxiv_id}"]

        if not pdf_urls:
            return None

        # Get the bib entry for naming and bibtex
        entry = self._crossref.doi_to_bibtex(doi)

        headers = {"User-Agent": "axm-bib/0.1 (academic research tool)"}

        # Try each URL in order (repos first, publishers last)
        last_error: Exception | None = None
        for url in pdf_urls:
            try:
                resp = httpx.get(
                    url,
                    timeout=60.0,
                    follow_redirects=True,
                    headers=headers,
                )
                resp.raise_for_status()
            except Exception 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

            # Save PDF via storage backend (now creates {key}/{key}.pdf)
            path = self._storage.save(
                doi=doi,
                pdf_bytes=resp.content,
                bib_key=entry.key,
            )

            # Write paper.bib
            bibtex_path = self._storage.save_bibtex(
                bib_key=entry.key,
                raw_bibtex=entry.raw,
            )

            # Extract markdown + figures
            from pathlib import Path

            from axm_bib.extract import extract_pdf

            paper_dir = str(self._storage.paper_dir(entry.key))
            extraction = extract_pdf(Path(path), Path(paper_dir))

            return DownloadResult(
                doi=doi,
                path=path,
                size_bytes=len(resp.content),
                is_open_access=True,
                paper_dir=paper_dir,
                bibtex_path=bibtex_path,
                extraction=extraction,
            )

        # All URLs failed
        if last_error:
            raise last_error

        return None


def _extract_arxiv_id(doi: str) -> str | None:
    """Extract arXiv ID from a DOI if it's an arXiv paper.

    Handles both new format (2411.19275) and old format (hep-th/0601001).
    Case-insensitive prefix matching.

    Args:
        doi: Digital Object Identifier.

    Returns:
        arXiv ID string, or None if not an arXiv DOI.
    """
    match = re.match(r"10\.48550/arXiv\.(.+)", doi, re.IGNORECASE)
    return match.group(1) if match else None
