"""Command-line interface for pyzotero."""

from __future__ import annotations

import functools
import json
import sys
from collections.abc import Callable
from typing import Any, TypeVar

import click
import httpx

from pyzotero import __version__
from pyzotero._helpers import (
    annotate_with_library,
    build_doi_index,
    build_doi_index_full,
    format_creators,
    format_s2_paper,
    get_zotero_client,
    normalise_doi,
)
from pyzotero.semantic_scholar import (
    PaperNotFoundError,
    RateLimitError,
    SemanticScholarError,
    filter_by_citations,
    get_citations,
    get_recommendations,
    get_references,
    search_papers,
)
from pyzotero.zotero import chunks

F = TypeVar("F", bound=Callable[..., Any])


def cli_error_handler(func: F) -> F:
    """Map exceptions raised in a CLI command to stderr messages + exit 1.

    Semantic Scholar errors get short, specific messages; everything else
    falls back to ``"Error: <str(exc)>"``.
    """

    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            return func(*args, **kwargs)
        except PaperNotFoundError:
            click.echo("Error: Paper not found in Semantic Scholar.", err=True)
            sys.exit(1)
        except RateLimitError:
            click.echo(
                "Error: Rate limit exceeded. Please wait and try again.", err=True
            )
            sys.exit(1)
        except SemanticScholarError as e:
            click.echo(f"Error: {e!s}", err=True)
            sys.exit(1)
        except Exception as e:
            click.echo(f"Error: {e!s}", err=True)
            sys.exit(1)

    return wrapper  # type: ignore[return-value]


def _zot_from_ctx(ctx: Any) -> Any:
    """Build a local-mode Zotero client using the locale from the CLI context."""
    return get_zotero_client(ctx.obj.get("locale", "en-US"))


def _run_s2_lookup(
    ctx: Any,
    doi: str,
    limit: int,
    min_citations: int,
    check_library: bool,
    lookup: Callable[..., dict[str, Any]],
    label: str,
) -> None:
    """Drive the shared Semantic-Scholar-by-DOI lookup flow.

    Fetches papers via ``lookup(doi, id_type="doi", limit=limit)``, applies
    the ``min_citations`` filter, optionally annotates each paper with its
    presence in the local Zotero library, and prints the JSON payload.
    ``label`` appears in the stderr progress message.
    """
    click.echo(f"Fetching {label} for DOI: {doi}...", err=True)
    result = lookup(doi, id_type="doi", limit=limit)
    papers = result.get("papers", [])

    if min_citations > 0:
        papers = filter_by_citations(papers, min_citations)

    if not papers:
        click.echo(json.dumps({"count": 0, "papers": []}))
        return

    if check_library:
        click.echo("Checking local Zotero library...", err=True)
        zot = _zot_from_ctx(ctx)
        doi_map = build_doi_index(zot)
        output_papers = annotate_with_library(papers, doi_map)
    else:
        output_papers = [format_s2_paper(p) for p in papers]

    click.echo(
        json.dumps({"count": len(output_papers), "papers": output_papers}, indent=2)
    )


@click.group()
@click.version_option(version=__version__, prog_name="pyzotero")
@click.option(
    "--locale",
    default="en-US",
    help="Locale for localized strings (default: en-US)",
)
@click.pass_context
def main(ctx: Any, locale: str) -> None:
    """Search local Zotero library."""
    ctx.ensure_object(dict)
    ctx.obj["locale"] = locale


@main.command()
@click.option(
    "-q",
    "--query",
    help="Search query string",
    default="",
)
@click.option(
    "--fulltext",
    is_flag=True,
    help="Search full-text content including PDFs. Retrieves parent items when attachments match.",
)
@click.option(
    "--itemtype",
    multiple=True,
    help="Filter by item type (can be specified multiple times for OR search)",
)
@click.option(
    "--collection",
    help="Filter by collection key (returns only items in this collection)",
)
@click.option(
    "--tag",
    multiple=True,
    help="Filter by tag (can be specified multiple times for AND search)",
)
@click.option(
    "--limit",
    type=int,
    default=1000000,
    help="Maximum number of results to return (default: 1000000)",
)
@click.option(
    "--offset",
    type=int,
    default=0,
    help="Number of results to skip for pagination (default: 0)",
)
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
@cli_error_handler
def search(  # noqa: PLR0912, PLR0915
    ctx: Any,
    query: str,
    fulltext: bool,
    itemtype: tuple[str, ...],
    collection: str | None,
    tag: tuple[str, ...],
    limit: int,
    offset: int,
    output_json: bool,
) -> None:
    """Search local Zotero library.

    By default, searches top-level items in titles and metadata.

    When --fulltext is enabled, searches all items including attachment content
    (PDFs, documents, etc.). If a match is found in an attachment, the parent
    bibliographic item is retrieved and included in results.

    Examples:
        pyzotero search -q "machine learning"

        pyzotero search -q "climate change" --fulltext

        pyzotero search -q "methodology" --itemtype book --itemtype journalArticle

        pyzotero search --collection ABC123 -q "test"

        pyzotero search -q "climate" --json

        pyzotero search -q "topic" --limit 20 --offset 20 --json

        pyzotero search -q "topic" --tag "climate" --tag "adaptation" --json

    """
    zot = _zot_from_ctx(ctx)

    # Build query parameters
    params = {"limit": limit}

    if offset > 0:
        params["start"] = offset

    if query:
        params["q"] = query

    if fulltext:
        params["qmode"] = "everything"

    if itemtype:
        # Join multiple item types with || for OR search
        params["itemType"] = " || ".join(itemtype)

    if tag:
        # Multiple tags are passed as a list for AND search
        params["tag"] = list(tag)

    # Execute search
    # When fulltext is enabled, use items() or collection_items() to get both
    # top-level items and attachments. Otherwise use top() or collection_items_top()
    # to only get top-level items.
    if fulltext:
        if collection:
            results = zot.collection_items(collection, **params)
        else:
            results = zot.items(**params)

        # When using fulltext, we need to retrieve parent items for any attachments
        # that matched, since most full-text content comes from PDFs and other attachments
        top_level_items = []
        attachment_items = []

        for item in results:
            data = item.get("data", {})
            if "parentItem" in data:
                attachment_items.append(item)
            else:
                top_level_items.append(item)

        # Retrieve parent items for attachments in batches of 50
        parent_items = []
        if attachment_items:
            parent_ids = list({item["data"]["parentItem"] for item in attachment_items})
            for chunk in chunks(parent_ids, 50):
                parent_items.extend(zot.get_subset(chunk))

        # Combine top-level items and parent items, removing duplicates by key
        all_items = top_level_items + parent_items
        items_dict = {item["data"]["key"]: item for item in all_items}
        results = list(items_dict.values())
    # Non-fulltext search: use top() or collection_items_top() as before
    elif collection:
        results = zot.collection_items_top(collection, **params)
    else:
        results = zot.top(**params)

    # Handle empty results
    if not results:
        if output_json:
            click.echo(json.dumps([]))
        else:
            click.echo("No results found.")
        return

    # Build output data structure
    output_items = []
    for item in results:
        data = item.get("data", {})

        title = data.get("title", "No title")
        item_type = data.get("itemType", "Unknown")
        date = data.get("date", "No date")
        item_key = data.get("key", "")
        publication = data.get("publicationTitle", "")
        volume = data.get("volume", "")
        issue = data.get("issue", "")
        doi = data.get("DOI", "")
        url = data.get("url", "")

        # Format creators (authors, editors, etc.)
        creator_names = format_creators(data.get("creators", []))

        # Check for PDF attachments
        pdf_attachments = []
        num_children = item.get("meta", {}).get("numChildren", 0)
        if num_children > 0:
            children = zot.children(item_key)
            for child in children:
                child_data = child.get("data", {})
                if child_data.get("contentType") == "application/pdf":
                    # Extract file URL from links.enclosure.href
                    file_url = (
                        child.get("links", {}).get("enclosure", {}).get("href", "")
                    )
                    if file_url:
                        pdf_attachments.append(file_url)

        # Build item object for JSON output
        item_obj = {
            "key": item_key,
            "itemType": item_type,
            "title": title,
            "creators": creator_names,
            "date": date,
            "publication": publication,
            "volume": volume,
            "issue": issue,
            "doi": doi,
            "url": url,
            "pdfAttachments": pdf_attachments,
        }
        output_items.append(item_obj)

    # Output results
    if output_json:
        click.echo(
            json.dumps({"count": len(output_items), "items": output_items}, indent=2)
        )
    else:
        click.echo(f"\nFound {len(results)} items:\n")
        for idx, item_obj in enumerate(output_items, 1):
            authors_str = (
                ", ".join(item_obj["creators"])
                if item_obj["creators"]
                else "No authors"
            )

            click.echo(f"{idx}. [{item_obj['itemType']}] {item_obj['title']}")
            click.echo(f"   Authors: {authors_str}")
            click.echo(f"   Date: {item_obj['date']}")
            click.echo(f"   Publication: {item_obj['publication']}")
            click.echo(f"   Volume: {item_obj['volume']}")
            click.echo(f"   Issue: {item_obj['issue']}")
            click.echo(f"   DOI: {item_obj['doi']}")
            click.echo(f"   URL: {item_obj['url']}")
            click.echo(f"   Key: {item_obj['key']}")

            if item_obj["pdfAttachments"]:
                click.echo("   PDF Attachments:")
                for pdf_url in item_obj["pdfAttachments"]:
                    click.echo(f"      {pdf_url}")

            click.echo()


@main.command()
@click.option(
    "--limit",
    type=int,
    help="Maximum number of collections to return (default: all)",
)
@click.pass_context
@cli_error_handler
def listcollections(ctx: Any, limit: int | None) -> None:
    """List all collections in the local Zotero library.

    Examples:
        pyzotero listcollections

        pyzotero listcollections --limit 10

    """
    zot = _zot_from_ctx(ctx)

    # Build query parameters
    params = {}
    if limit:
        params["limit"] = limit

    # Get all collections
    collections = zot.collections(**params)

    if not collections:
        click.echo(json.dumps([]))
        return

    # Build a mapping of collection keys to names for parent lookup
    collection_map = {}
    for collection in collections:
        data = collection.get("data", {})
        key = data.get("key", "")
        name = data.get("name", "")
        if key:
            collection_map[key] = name or None

    # Build JSON output
    output = []
    for collection in collections:
        data = collection.get("data", {})
        meta = collection.get("meta", {})

        name = data.get("name", "")
        key = data.get("key", "")
        num_items = meta.get("numItems", 0)
        parent_collection = data.get("parentCollection", "")

        collection_obj = {
            "id": key,
            "name": name or None,
            "items": num_items,
        }

        # Add parent information if it exists
        if parent_collection:
            parent_name = collection_map.get(parent_collection)
            collection_obj["parent"] = {
                "id": parent_collection,
                "name": parent_name,
            }
        else:
            collection_obj["parent"] = None

        output.append(collection_obj)

    # Output as JSON
    click.echo(json.dumps(output, indent=2))


@main.command()
@click.pass_context
@cli_error_handler
def itemtypes(ctx: Any) -> None:
    """List all valid item types.

    Examples:
        pyzotero itemtypes

    """
    zot = _zot_from_ctx(ctx)

    # Get all item types
    item_types = zot.item_types()

    if not item_types:
        click.echo(json.dumps([]))
        return

    # Output as JSON array
    click.echo(json.dumps(item_types, indent=2))


@main.command()
@click.pass_context
@cli_error_handler
def test(ctx: Any) -> None:
    """Test connection to local Zotero instance.

    This command checks whether Zotero is running and accepting local connections.

    Examples:
        pyzotero test

    """
    zot = _zot_from_ctx(ctx)

    try:
        # Call settings() to test the connection
        # This should return {} if Zotero is running and listening
        result = zot.settings()
    except httpx.ConnectError:
        click.echo(
            "✗ Connection failed: Could not connect to Zotero.\n\n"
            "Possible causes:\n"
            "  • Zotero might not be running\n"
            "  • Local connections might not be enabled\n\n"
            "To enable local connections:\n"
            "  Zotero > Settings > Advanced > Allow other applications on this computer to communicate with Zotero",
            err=True,
        )
        sys.exit(1)

    # If we get here, the connection succeeded
    click.echo("✓ Connection successful: Zotero is running and listening locally.")
    if result == {}:
        click.echo("  Received expected empty settings response.")
    else:
        click.echo(f"  Received response: {json.dumps(result)}")


@main.command()
@click.argument("key")
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
@cli_error_handler
def item(ctx: Any, key: str, output_json: bool) -> None:
    """Get a single item by its key.

    Returns full item data for the specified Zotero item key.

    Examples:
        pyzotero item ABC123

        pyzotero item ABC123 --json

    """
    zot = _zot_from_ctx(ctx)

    # Fetch the item
    result = zot.item(key)

    if not result:
        if output_json:
            click.echo(json.dumps(None))
        else:
            click.echo(f"Item not found: {key}")
        return

    data = result.get("data", {})

    if output_json:
        click.echo(json.dumps(result, indent=2))
    else:
        title = data.get("title", "No title")
        item_type = data.get("itemType", "Unknown")
        date = data.get("date", "No date")
        item_key = data.get("key", "")
        doi = data.get("DOI", "")
        url = data.get("url", "")

        # Format creators
        creator_names = format_creators(data.get("creators", []))
        authors_str = ", ".join(creator_names) if creator_names else "No authors"

        click.echo(f"[{item_type}] {title}")
        click.echo(f"Authors: {authors_str}")
        click.echo(f"Date: {date}")
        click.echo(f"DOI: {doi}")
        click.echo(f"URL: {url}")
        click.echo(f"Key: {item_key}")


@main.command()
@click.argument("key")
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
@cli_error_handler
def children(ctx: Any, key: str, output_json: bool) -> None:
    """Get child items (attachments, notes) of a specific item.

    Returns all child items for the specified Zotero item key.
    Useful for finding PDF attachments without the N+1 overhead during search.

    Examples:
        pyzotero children ABC123

        pyzotero children ABC123 --json

    """
    zot = _zot_from_ctx(ctx)

    # Fetch children
    results = zot.children(key)

    if not results:
        if output_json:
            click.echo(json.dumps([]))
        else:
            click.echo(f"No children found for item: {key}")
        return

    if output_json:
        click.echo(json.dumps(results, indent=2))
    else:
        click.echo(f"\nFound {len(results)} child items:\n")
        for idx, child in enumerate(results, 1):
            data = child.get("data", {})
            item_type = data.get("itemType", "Unknown")
            child_key = data.get("key", "")
            title = data.get("title", data.get("note", "No title")[:50] + "...")
            content_type = data.get("contentType", "")

            click.echo(f"{idx}. [{item_type}] {title}")
            click.echo(f"   Key: {child_key}")
            if content_type:
                click.echo(f"   Content-Type: {content_type}")

            # Show file URL for attachments
            file_url = child.get("links", {}).get("enclosure", {}).get("href", "")
            if file_url:
                click.echo(f"   File: {file_url}")
            click.echo()


@main.command()
@click.option(
    "--collection",
    help="Filter tags to a specific collection key",
)
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
@cli_error_handler
def tags(ctx: Any, collection: str | None, output_json: bool) -> None:
    """List all tags in the library.

    Returns all tags used in the library, or only tags from a specific collection.

    Examples:
        pyzotero tags

        pyzotero tags --collection ABC123

        pyzotero tags --json

    """
    zot = _zot_from_ctx(ctx)

    # Fetch tags
    if collection:
        results = zot.collection_tags(collection)
    else:
        results = zot.tags()

    if not results:
        if output_json:
            click.echo(json.dumps([]))
        else:
            click.echo("No tags found.")
        return

    if output_json:
        click.echo(json.dumps(results, indent=2))
    else:
        click.echo(f"\nFound {len(results)} tags:\n")
        for tag in sorted(results):
            click.echo(f"  {tag}")


@main.command()
@click.argument("keys", nargs=-1, required=True)
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
@cli_error_handler
def subset(ctx: Any, keys: tuple[str, ...], output_json: bool) -> None:
    """Get multiple items by their keys in a single call.

    Efficiently retrieve up to 50 items by key in a single API call.
    Far more efficient than multiple individual item lookups.

    Examples:
        pyzotero subset ABC123 DEF456 GHI789

        pyzotero subset ABC123 DEF456 --json

    """
    zot = _zot_from_ctx(ctx)

    if len(keys) > 50:  # noqa: PLR2004 - Zotero API limit
        click.echo("Error: Maximum 50 items per call.", err=True)
        sys.exit(1)

    # Fetch items
    results = zot.get_subset(list(keys))

    if not results:
        if output_json:
            click.echo(json.dumps([]))
        else:
            click.echo("No items found.")
        return

    if output_json:
        click.echo(json.dumps(results, indent=2))
    else:
        click.echo(f"\nFound {len(results)} items:\n")
        for idx, item in enumerate(results, 1):
            data = item.get("data", {})
            title = data.get("title", "No title")
            item_type = data.get("itemType", "Unknown")
            item_key = data.get("key", "")

            click.echo(f"{idx}. [{item_type}] {title}")
            click.echo(f"   Key: {item_key}")
            click.echo()


@main.command()
@click.argument("dois", nargs=-1)
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
@cli_error_handler
def alldoi(ctx: Any, dois: tuple[str, ...], output_json: bool) -> None:  # noqa: PLR0912
    """Look up DOIs in the local Zotero library and return their Zotero IDs.

    Accepts one or more DOIs as arguments and checks if they exist in the library.
    DOI matching is case-insensitive and handles common prefixes (https://doi.org/, doi:).

    If no DOIs are provided, shows "No items found" (text) or {} (JSON).

    Examples:
        pyzotero alldoi 10.1234/example

        pyzotero alldoi 10.1234/abc https://doi.org/10.5678/def doi:10.9012/ghi

        pyzotero alldoi 10.1234/example --json

    """
    zot = _zot_from_ctx(ctx)

    click.echo("Building DOI index from library...", err=True)
    doi_map = build_doi_index_full(zot)
    click.echo(f"Indexed {len(doi_map)} items with DOIs", err=True)

    # If no DOIs provided, return empty result
    if not dois:
        if output_json:
            click.echo(json.dumps({}))
        else:
            click.echo("No items found")
        return

    # Look up each input DOI
    found = []
    not_found = []

    for input_doi in dois:
        entry = doi_map.get(normalise_doi(input_doi))
        if entry is not None:
            found.append({"doi": entry["original"], "key": entry["key"]})
        else:
            not_found.append(input_doi)

    # Output results
    if output_json:
        result = {"found": found, "not_found": not_found}
        click.echo(json.dumps(result, indent=2))
    else:
        if found:
            click.echo(f"\nFound {len(found)} items:\n")
            for item in found:
                click.echo(f"  {item['doi']} → {item['key']}")
        else:
            click.echo("No items found")

        if not_found:
            click.echo(f"\nNot found ({len(not_found)}):")
            for doi in not_found:
                click.echo(f"  {doi}")


@main.command()
@click.pass_context
@cli_error_handler
def doiindex(ctx: Any) -> None:
    """Output the complete DOI-to-key mapping for the library.

    Returns a JSON mapping of normalised DOIs to item keys and original DOIs.
    This allows the skill to cache the index and avoid repeated full-library scans.

    Output format:
        {
          "10.1234/abc": {"key": "ABC123", "original": "https://doi.org/10.1234/ABC"},
          ...
        }

    Examples:
        pyzotero doiindex

        pyzotero doiindex > doi_cache.json

    """
    zot = _zot_from_ctx(ctx)

    click.echo("Building DOI index from library...", err=True)
    doi_map = build_doi_index_full(zot)
    click.echo(f"Indexed {len(doi_map)} items with DOIs", err=True)

    click.echo(json.dumps(doi_map, indent=2))


@main.command()
@click.argument("key")
@click.pass_context
@cli_error_handler
def fulltext(ctx: Any, key: str) -> None:
    """Get full-text content of an attachment.

    Returns the full-text content extracted from a PDF or other attachment.
    The key should be the key of an attachment item (not a top-level item).

    Output format:
        {
          "content": "Full-text extracted from PDF...",
          "indexedPages": 50,
          "totalPages": 50
        }

    Examples:
        pyzotero fulltext ABC123

    """
    zot = _zot_from_ctx(ctx)

    result = zot.fulltext_item(key)

    if not result:
        click.echo(json.dumps({"error": "No full-text content available"}))
        return

    click.echo(json.dumps(result, indent=2))


@main.command()
@click.option(
    "--doi",
    required=True,
    help="DOI of the paper to find related papers for",
)
@click.option(
    "--limit",
    type=int,
    default=20,
    help="Maximum number of results to return (default: 20, max: 500)",
)
@click.option(
    "--min-citations",
    type=int,
    default=0,
    help="Minimum citation count filter (default: 0)",
)
@click.option(
    "--check-library/--no-check-library",
    default=True,
    help="Check if papers exist in local Zotero (default: True)",
)
@click.pass_context
@cli_error_handler
def related(
    ctx: Any, doi: str, limit: int, min_citations: int, check_library: bool
) -> None:
    """Find papers related to a given paper using Semantic Scholar.

    Uses SPECTER2 embeddings to find semantically similar papers.

    Examples:
        pyzotero related --doi "10.1038/nature12373"

        pyzotero related --doi "10.1038/nature12373" --limit 50

        pyzotero related --doi "10.1038/nature12373" --min-citations 100

    """
    _run_s2_lookup(
        ctx,
        doi,
        limit,
        min_citations,
        check_library,
        get_recommendations,
        "related papers",
    )


@main.command()
@click.option(
    "--doi",
    required=True,
    help="DOI of the paper to find citations for",
)
@click.option(
    "--limit",
    type=int,
    default=100,
    help="Maximum number of results to return (default: 100, max: 1000)",
)
@click.option(
    "--min-citations",
    type=int,
    default=0,
    help="Minimum citation count filter (default: 0)",
)
@click.option(
    "--check-library/--no-check-library",
    default=True,
    help="Check if papers exist in local Zotero (default: True)",
)
@click.pass_context
@cli_error_handler
def citations(
    ctx: Any, doi: str, limit: int, min_citations: int, check_library: bool
) -> None:
    """Find papers that cite a given paper using Semantic Scholar.

    Examples:
        pyzotero citations --doi "10.1038/nature12373"

        pyzotero citations --doi "10.1038/nature12373" --limit 50

        pyzotero citations --doi "10.1038/nature12373" --min-citations 50

    """
    _run_s2_lookup(
        ctx, doi, limit, min_citations, check_library, get_citations, "citations"
    )


@main.command()
@click.option(
    "--doi",
    required=True,
    help="DOI of the paper to find references for",
)
@click.option(
    "--limit",
    type=int,
    default=100,
    help="Maximum number of results to return (default: 100, max: 1000)",
)
@click.option(
    "--min-citations",
    type=int,
    default=0,
    help="Minimum citation count filter (default: 0)",
)
@click.option(
    "--check-library/--no-check-library",
    default=True,
    help="Check if papers exist in local Zotero (default: True)",
)
@click.pass_context
@cli_error_handler
def references(
    ctx: Any, doi: str, limit: int, min_citations: int, check_library: bool
) -> None:
    """Find papers referenced by a given paper using Semantic Scholar.

    Examples:
        pyzotero references --doi "10.1038/nature12373"

        pyzotero references --doi "10.1038/nature12373" --limit 50

        pyzotero references --doi "10.1038/nature12373" --min-citations 100

    """
    _run_s2_lookup(
        ctx, doi, limit, min_citations, check_library, get_references, "references"
    )


@main.command()
@click.option(
    "-q",
    "--query",
    required=True,
    help="Search query string",
)
@click.option(
    "--limit",
    type=int,
    default=20,
    help="Maximum number of results to return (default: 20, max: 100)",
)
@click.option(
    "--year",
    help="Year filter (e.g., '2020', '2018-2022', '2020-')",
)
@click.option(
    "--open-access/--no-open-access",
    default=False,
    help="Only return open access papers (default: False)",
)
@click.option(
    "--sort",
    type=click.Choice(["citations", "year"], case_sensitive=False),
    help="Sort results by citation count or year (descending)",
)
@click.option(
    "--min-citations",
    type=int,
    default=0,
    help="Minimum citation count filter (default: 0)",
)
@click.option(
    "--check-library/--no-check-library",
    default=True,
    help="Check if papers exist in local Zotero (default: True)",
)
@click.pass_context
@cli_error_handler
def s2search(
    ctx: Any,
    query: str,
    limit: int,
    year: str | None,
    open_access: bool,
    sort: str | None,
    min_citations: int,
    check_library: bool,
) -> None:
    """Search for papers on Semantic Scholar.

    Search across Semantic Scholar's index of over 200M papers.

    Examples:
        pyzotero s2search -q "climate adaptation"

        pyzotero s2search -q "machine learning" --year 2020-2024

        pyzotero s2search -q "neural networks" --open-access --limit 50

        pyzotero s2search -q "deep learning" --sort citations --min-citations 100

    """
    # Search Semantic Scholar
    click.echo(f'Searching Semantic Scholar for: "{query}"...', err=True)
    result = search_papers(
        query,
        limit=limit,
        year=year,
        open_access_only=open_access,
        sort=sort,
        min_citations=min_citations,
    )
    papers = result.get("papers", [])
    total = result.get("total", len(papers))

    if not papers:
        click.echo(json.dumps({"count": 0, "total": total, "papers": []}))
        return

    # Optionally annotate with library status
    if check_library:
        click.echo("Checking local Zotero library...", err=True)
        zot = _zot_from_ctx(ctx)
        doi_map = build_doi_index(zot)
        output_papers = annotate_with_library(papers, doi_map)
    else:
        output_papers = [format_s2_paper(p) for p in papers]

    click.echo(
        json.dumps(
            {"count": len(output_papers), "total": total, "papers": output_papers},
            indent=2,
        )
    )


if __name__ == "__main__":
    main()
