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

from __future__ import annotations

import json
import sys
from typing import Any

import click
import httpx

from pyzotero import __version__
from pyzotero._helpers import (
    annotate_with_library,
    build_doi_index,
    build_doi_index_full,
    format_s2_paper,
    get_zotero_client,
    normalize_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


@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
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # 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.)
            creators = data.get("creators", [])
            creator_names = []
            for creator in creators:
                if "lastName" in creator:
                    if "firstName" in creator:
                        creator_names.append(
                            f"{creator['firstName']} {creator['lastName']}"
                        )
                    else:
                        creator_names.append(creator["lastName"])
                elif "name" in creator:
                    creator_names.append(creator["name"])

            # 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()

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


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

    Examples:
        pyzotero listcollections

        pyzotero listcollections --limit 10

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # 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))

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


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

    Examples:
        pyzotero itemtypes

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # 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))

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@main.command()
@click.pass_context
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # Call settings() to test the connection
        # This should return {} if Zotero is running and listening
        result = zot.settings()

        # 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)}")

    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)
    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@main.command()
@click.argument("key")
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # 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
            creators = data.get("creators", [])
            creator_names = []
            for creator in creators:
                if "lastName" in creator:
                    if "firstName" in creator:
                        creator_names.append(
                            f"{creator['firstName']} {creator['lastName']}"
                        )
                    else:
                        creator_names.append(creator["lastName"])
                elif "name" in creator:
                    creator_names.append(creator["name"])

            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}")

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@main.command()
@click.argument("key")
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # 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()

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@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
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # 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}")

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@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
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        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()

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@main.command()
@click.argument("dois", nargs=-1)
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output results as JSON",
)
@click.pass_context
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        # Build a mapping of normalized DOIs to (original_doi, zotero_key)
        click.echo("Building DOI index from library...", err=True)
        doi_map = {}

        # Get all items using everything() which handles pagination automatically
        all_items = zot.everything(zot.items())

        # Process all items
        for item in all_items:
            data = item.get("data", {})
            item_doi = data.get("DOI", "")

            if item_doi:
                normalized_doi = normalize_doi(item_doi)
                item_key = data.get("key", "")

                if normalized_doi and item_key:
                    # Store the original DOI from Zotero and the item key
                    doi_map[normalized_doi] = (item_doi, item_key)

        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:
            normalized_input = normalize_doi(input_doi)

            if normalized_input in doi_map:
                original_doi, zotero_key = doi_map[normalized_input]
                found.append({"doi": original_doi, "key": zotero_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}")

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@main.command()
@click.pass_context
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        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))

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@main.command()
@click.argument("key")
@click.pass_context
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

    """
    try:
        locale = ctx.obj.get("locale", "en-US")
        zot = get_zotero_client(locale)

        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))

    except Exception as e:
        click.echo(f"Error: {e!s}", err=True)
        sys.exit(1)


@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
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

    """
    try:
        # Get recommendations from Semantic Scholar
        click.echo(f"Fetching related papers for DOI: {doi}...", err=True)
        result = get_recommendations(doi, id_type="doi", limit=limit)
        papers = result.get("papers", [])

        # Apply citation filter
        if min_citations > 0:
            papers = filter_by_citations(papers, min_citations)

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

        # Optionally annotate with library status
        if check_library:
            click.echo("Checking local Zotero library...", err=True)
            locale = ctx.obj.get("locale", "en-US")
            zot = get_zotero_client(locale)
            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)
        )

    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)


@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
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

    """
    try:
        # Get citations from Semantic Scholar
        click.echo(f"Fetching citations for DOI: {doi}...", err=True)
        result = get_citations(doi, id_type="doi", limit=limit)
        papers = result.get("papers", [])

        # Apply citation filter
        if min_citations > 0:
            papers = filter_by_citations(papers, min_citations)

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

        # Optionally annotate with library status
        if check_library:
            click.echo("Checking local Zotero library...", err=True)
            locale = ctx.obj.get("locale", "en-US")
            zot = get_zotero_client(locale)
            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)
        )

    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)


@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
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

    """
    try:
        # Get references from Semantic Scholar
        click.echo(f"Fetching references for DOI: {doi}...", err=True)
        result = get_references(doi, id_type="doi", limit=limit)
        papers = result.get("papers", [])

        # Apply citation filter
        if min_citations > 0:
            papers = filter_by_citations(papers, min_citations)

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

        # Optionally annotate with library status
        if check_library:
            click.echo("Checking local Zotero library...", err=True)
            locale = ctx.obj.get("locale", "en-US")
            zot = get_zotero_client(locale)
            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)
        )

    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)


@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
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

    """
    try:
        # 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)
            locale = ctx.obj.get("locale", "en-US")
            zot = get_zotero_client(locale)
            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,
            )
        )

    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)


if __name__ == "__main__":
    main()
