"""axm-bib CLI — standalone bibliographic tools.

Usage::

    axm-bib search "loop invariants"
    axm-bib resolve 10.1145/363235.363259
    axm-bib resolve 10.1145/363235.363259 arXiv:1706.03762
    axm-bib graph 10.1145/363235.363259 --direction citations
    axm-bib pdf 10.1145/363235.363259
    axm-bib pdf arXiv:2503.18813
    axm-bib extract paper.pdf
"""

from __future__ import annotations

import logging
import sys
from pathlib import Path
from typing import Annotated

import cyclopts

from axm_bib.config import get_email, resolve_email
from axm_bib.merge import check_coherence, merge_entries
from axm_bib.models import Paper
from axm_bib.pipeline import BibPipeline

logger = logging.getLogger(__name__)

__all__ = [
    "app",
    "extract",
    "graph",
    "main",
    "pdf",
    "resolve",
    "search",
]

app = cyclopts.App(
    name="axm-bib",
    help="Bibliographic tools: search papers, resolve DOIs to BibTeX, download PDFs.",
)

_MAX_DISPLAY_AUTHORS = 3


def _format_ids(paper: Paper) -> str:
    """Build the identifier display string for a paper."""
    ids: list[str] = []
    if paper.doi:
        ids.append(f"DOI:{paper.doi}")
    if paper.arxiv_id:
        ids.append(f"arXiv:{paper.arxiv_id}")
    if paper.openalex_id:
        ids.append(f"OA:{paper.openalex_id}")
    return "  ".join(ids) if ids else "N/A"


def _format_abstract(paper: Paper, abstract_len: int) -> str | None:
    """Return truncated abstract or None if absent."""
    if not paper.abstract:
        return None
    snippet = paper.abstract
    if abstract_len > 0 and len(snippet) > abstract_len:
        snippet = snippet[:abstract_len] + "..."
    return snippet


def _format_paper(
    paper: Paper,
    idx: int,
    *,
    abstract: bool,
    abstract_len: int,
) -> str:
    """Format a single paper for terminal display.

    Args:
        paper: Paper metadata.
        idx: 1-based display index.
        abstract: Whether to show abstract.
        abstract_len: Truncate abstract to N chars (0 = full).

    Returns:
        Formatted multi-line string.
    """
    authors = ", ".join(paper.authors[:_MAX_DISPLAY_AUTHORS])
    if len(paper.authors) > _MAX_DISPLAY_AUTHORS:
        authors += " et al."

    lines = [
        f"[{idx}] [{paper.source}] {paper.title}",
        f"    Authors: {authors}",
        f"    Year: {paper.year or '?'}  {_format_ids(paper)}",
    ]
    if paper.venue:
        lines.append(f"    Venue: {paper.venue}")

    if abstract:
        snippet = _format_abstract(paper, abstract_len)
        if snippet:
            lines.append(f"    {snippet}")

    return "\n".join(lines)


@app.command()
def search(  # noqa: PLR0913
    query: Annotated[
        str,
        cyclopts.Parameter(help="Search query (title, keywords)"),
    ],
    *,
    limit: Annotated[
        int,
        cyclopts.Parameter(
            name=["--limit", "-n"],
            help="Maximum results (default: 10, max: 100)",
        ),
    ] = 10,
    venue: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--venue"],
            help="Filter by venue name (case-insensitive substring)",
        ),
    ] = None,
    author: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--author"],
            help="Filter by author name (case-insensitive substring)",
        ),
    ] = None,
    year: Annotated[
        int | None,
        cyclopts.Parameter(
            name=["--year"],
            help="Filter by publication year",
        ),
    ] = None,
    abstract: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--abstract/--no-abstract"],
            help="Show paper abstracts in results",
        ),
    ] = True,
    abstract_len: Annotated[
        int,
        cyclopts.Parameter(
            name=["--abstract-len"],
            help="Truncate abstracts to N chars (0 = full)",
        ),
    ] = 0,
) -> None:
    """Search papers across all sources (-n: max results, default 10, max 100)."""
    if not query.strip():
        print("Error: query must not be empty", file=sys.stderr)
        raise SystemExit(1)

    if limit < 1:
        print(f"Error: limit must be ≥ 1, got {limit}", file=sys.stderr)
        raise SystemExit(1)
    limit = min(limit, 100)

    try:
        pipeline = BibPipeline(mailto=get_email() or "")
        result = pipeline.search(
            query,
            limit=limit,
            venue=venue,
            author=author,
            year=year,
        )
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1) from None

    if result.sources_failed:
        print(
            f"Warning: {len(result.sources_failed)} source(s) failed: "
            f"{', '.join(result.sources_failed)}",
            file=sys.stderr,
        )

    if not result.papers:
        print("No results found.")
        return

    for i, p in enumerate(result.papers, 1):
        print(_format_paper(p, i, abstract=abstract, abstract_len=abstract_len))
        print()


@app.command()
def resolve(
    ids: Annotated[
        tuple[str, ...],
        cyclopts.Parameter(
            help="One or more identifiers: DOI, arXiv ID, HAL ID, "
            "DBLP key, or OpenReview URL. "
            "Multiple IDs for the same paper are merged.",
        ),
    ],
) -> None:
    """Resolve exact identifier(s) to BibTeX (rejects titles)."""
    if not ids:
        print("Error: at least one identifier is required", file=sys.stderr)
        raise SystemExit(1)

    try:
        pipeline = BibPipeline(mailto=get_email() or "")
        entries = [pipeline.resolve(r) for r in dict.fromkeys(ids)]

        if len(entries) == 1:
            entry = entries[0]
        else:
            if not check_coherence(entries):
                print(
                    "Error: identifiers refer to different papers",
                    file=sys.stderr,
                )
                raise SystemExit(1)
            entry = merge_entries(entries)
    except SystemExit:
        raise
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1) from None

    print(entry.raw)


@app.command()
def pdf(
    ref: Annotated[
        str,
        cyclopts.Parameter(help="Identifier (DOI, arXiv ID, HAL ID, etc.)"),
    ],
    *,
    output_dir: Annotated[
        Path | None,
        cyclopts.Parameter(
            name=["--output-dir", "-o"],
            help="Download directory",
        ),
    ] = None,
) -> None:
    """Download open-access PDF for any identifier (DOI, arXiv ID, etc.)."""
    if not ref.strip():
        print("Error: identifier must not be empty", file=sys.stderr)
        raise SystemExit(1)

    try:
        email = resolve_email()
        pipeline = BibPipeline(mailto=email)
        result = pipeline.download_pdf(ref)
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1) from None

    if result is None:
        print("Paper is not open access — no PDF available.")
        return

    print(f"Downloaded: {result.paper_dir or result.path}")
    print(f"  PDF: {Path(result.path).name} ({result.size_bytes:,} bytes)")
    if result.bibtex_path:
        print(f"  BibTeX: {Path(result.bibtex_path).name}")
    if result.extraction:
        ext = result.extraction
        print(
            f"  Markdown: {Path(ext.markdown_path).name}"
            f" ({ext.word_count:,} words, {ext.char_count:,} chars,"
            f" {ext.page_count} pages)"
        )
        print(f"  Figures: {len(ext.figures)}")


@app.command()
def graph(
    paper_id: Annotated[
        str,
        cyclopts.Parameter(help="Paper ID (DOI, arXiv ID, or S2 paper ID)"),
    ],
    *,
    direction: Annotated[
        str,
        cyclopts.Parameter(
            name=["--direction", "-d"],
            help="Graph direction: citations, references, or similar",
        ),
    ] = "citations",
    limit: Annotated[
        int,
        cyclopts.Parameter(
            name=["--limit", "-n"],
            help="Maximum results (default: 20)",
        ),
    ] = 20,
    abstract: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--abstract/--no-abstract"],
            help="Show paper abstracts in results",
        ),
    ] = True,
    abstract_len: Annotated[
        int,
        cyclopts.Parameter(
            name=["--abstract-len"],
            help="Truncate abstracts to N chars (0 = full)",
        ),
    ] = 200,
) -> None:
    """Traverse citation graph for a paper (citations, references, similar)."""
    if not paper_id.strip():
        print("Error: paper_id must not be empty", file=sys.stderr)
        raise SystemExit(1)

    valid = {"citations", "references", "similar"}
    if direction not in valid:
        print(
            f"Error: direction must be one of {', '.join(sorted(valid))}",
            file=sys.stderr,
        )
        raise SystemExit(1)

    try:
        pipeline = BibPipeline(mailto=get_email() or "")
        papers = pipeline.graph(paper_id, direction=direction, limit=limit)
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1) from None

    if not papers:
        print(f"No {direction} found.")
        return

    print(f"{len(papers)} {direction}:")
    print()
    for i, p in enumerate(papers, 1):
        print(_format_paper(p, i, abstract=abstract, abstract_len=abstract_len))
        print()


@app.command()
def extract(
    pdf_path: Annotated[
        Path,
        cyclopts.Parameter(help="Path to PDF file"),
    ],
    *,
    output_dir: Annotated[
        Path | None,
        cyclopts.Parameter(
            name=["--output-dir", "-o"],
            help="Output directory (default: {stem}/ next to PDF)",
        ),
    ] = None,
    figures: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--figures/--no-figures"],
            help="Extract figures as PNG",
        ),
    ] = True,
) -> None:
    """Extract PDF to Markdown + figures (PNG)."""
    if not pdf_path.exists():
        print(f"Error: file not found: {pdf_path}", file=sys.stderr)
        raise SystemExit(1)

    try:
        from axm_bib.extract import extract_pdf

        result = extract_pdf(pdf_path, output_dir)
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1) from None

    print(f"Extracted: {result.output_dir}")
    print(
        f"  Markdown: {result.markdown_path}"
        f" ({result.word_count:,} words, {result.char_count:,} chars)"
    )
    print(f"  Pages: {result.page_count}")
    print(f"  Figures: {len(result.figures)}")
    for fig in result.figures:
        print(f"    {fig.filename} (p.{fig.page + 1}, {fig.width}x{fig.height})")


def main() -> None:
    """Main entry point."""
    app()


if __name__ == "__main__":
    main()
