"""Extract commands for extracting content from PDFs."""

import json
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console

from papercut.cli.utils import handle_errors, is_quiet
from papercut.output import get_formatter

app = typer.Typer(
    help="Extract content from PDFs.",
    no_args_is_help=True,
)


def _get_console() -> Console:
    """Get a console for output messages."""
    return Console()


def parse_pages(pages_str: Optional[str]) -> Optional[list[int]]:
    """Parse page range string like '1-5,8,10-12' into list of page numbers.

    Args:
        pages_str: Page specification string (e.g., '1-5,8,10-12').

    Returns:
        List of 0-indexed page numbers, or None if no pages specified.

    Raises:
        typer.BadParameter: If the pages string is invalid.
    """
    if not pages_str or not pages_str.strip():
        return None

    pages = []
    try:
        for part in pages_str.split(","):
            part = part.strip()
            if not part:
                continue
            if "-" in part:
                start, end = part.split("-", 1)
                if not start or not end:
                    raise ValueError(f"Invalid range: '{part}' (use format like '1-5')")
                start_int = int(start)
                end_int = int(end)
                if start_int < 1 or end_int < 1:
                    raise ValueError("Page numbers must be positive (1 or greater)")
                if start_int > end_int:
                    raise ValueError(f"Invalid range: {start} > {end}")
                pages.extend(range(start_int - 1, end_int))  # Convert to 0-indexed
            else:
                page_int = int(part)
                if page_int < 1:
                    raise ValueError("Page numbers must be positive (1 or greater)")
                pages.append(page_int - 1)  # Convert to 0-indexed
    except ValueError as e:
        raise typer.BadParameter(f"Invalid pages format: {e}")

    return sorted(set(pages)) if pages else None


@app.command()
@handle_errors
def text(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Output file (default: stdout)",
    ),
    chunk_size: Optional[int] = typer.Option(
        None,
        "--chunk-size",
        help="Split text into chunks of this character size",
    ),
    overlap: int = typer.Option(
        200,
        "--overlap",
        help="Overlap between chunks (in characters)",
    ),
    pages: Optional[str] = typer.Option(
        None,
        "--pages",
        "-p",
        help="Page range to extract (e.g., '1-5,8,10-12')",
    ),
    include_metadata: bool = typer.Option(
        False,
        "--include-metadata",
        "-m",
        help="Include chunk metadata (page, section, figure/table refs)",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract clean text from PDF.

    Returns JSON with text content for LLM processing.
    Use --include-metadata with --chunk-size for LLM-ready chunks with
    page numbers, section info, and figure/table references.
    """
    from papercut.core.text import TextExtractor
    from papercut.extractors.pdfplumber import PdfPlumberExtractor

    console = _get_console()
    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())
    extractor = TextExtractor(PdfPlumberExtractor())
    page_list = parse_pages(pages)

    # Validate chunk parameters
    if chunk_size is not None:
        if chunk_size <= 0:
            raise typer.BadParameter("chunk-size must be a positive integer")
        if overlap < 0:
            raise typer.BadParameter("overlap must be non-negative")
        if overlap >= chunk_size:
            raise typer.BadParameter(
                f"overlap ({overlap}) must be less than chunk-size ({chunk_size})"
            )

    if chunk_size:
        if include_metadata:
            # Use enhanced chunking with metadata
            from papercut.index import DocumentIndexer

            indexer = DocumentIndexer(use_cache=True)
            doc_index = indexer.index(pdf_path)
            sections = doc_index.sections if doc_index.type == "paper" else []

            chunks = extractor.extract_chunked_with_metadata(
                pdf_path,
                chunk_size=chunk_size,
                overlap=overlap,
                pages=page_list,
                sections=sections,
            )
            result = {
                "success": True,
                "file": str(pdf_path.name),
                "chunked": True,
                "chunk_size": chunk_size,
                "overlap": overlap,
                "count": len(chunks),
                "chunks": [c.to_dict() for c in chunks],
            }
        else:
            # Standard chunking (list of strings)
            chunks = extractor.extract_chunked(
                pdf_path,
                chunk_size=chunk_size,
                overlap=overlap,
                pages=page_list,
            )
            result = {
                "success": True,
                "file": str(pdf_path.name),
                "chunked": True,
                "chunk_size": chunk_size,
                "overlap": overlap,
                "count": len(chunks),
                "chunks": chunks,
            }
    elif include_metadata:
        console.print("[yellow]Warning:[/yellow] --include-metadata requires --chunk-size")
        text_content = extractor.extract(pdf_path, pages=page_list)
        result = {
            "success": True,
            "file": str(pdf_path.name),
            "pages": list(page_list) if page_list else "all",
            "content": {
                "text": text_content,
                "word_count": len(text_content.split()),
                "char_count": len(text_content),
            },
        }
    else:
        text_content = extractor.extract(pdf_path, pages=page_list)
        result = {
            "success": True,
            "file": str(pdf_path.name),
            "pages": list(page_list) if page_list else "all",
            "content": {
                "text": text_content,
                "word_count": len(text_content.split()),
                "char_count": len(text_content),
            },
        }

    if output:
        json_output = json.dumps(result, indent=2)
        output.write_text(json_output)
        if not is_quiet():
            console.print(f"[green]Saved:[/green] {output}")
    else:
        formatter.output(result)


@app.command()
@handle_errors
def table(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    table_id: int = typer.Option(
        ...,
        "--id",
        "-i",
        help="Table ID to extract",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract a single table by ID.

    Returns JSON with table data.
    """
    from papercut.cache import get_cache
    from papercut.core.tables import TableExtractor
    from papercut.extractors.pdfplumber import PdfPlumberExtractor

    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())
    cache = get_cache()

    # Check cache first
    cached = cache.get_table(pdf_path, table_id)
    if cached:
        result = {"success": True, "cached": True, "file": str(pdf_path.name), "table": cached}
        formatter.output(result)
        return

    # Extract all tables to find the one we want
    extractor = TableExtractor(PdfPlumberExtractor())
    all_tables = extractor.extract(pdf_path)

    if table_id < 1 or table_id > len(all_tables):
        result = {
            "success": False,
            "error": f"Table {table_id} not found. Document has {len(all_tables)} table(s).",
        }
        formatter.output(result)
        raise typer.Exit(1)

    table_obj = all_tables[table_id - 1]
    table_data = {
        "id": table_id,
        "page": table_obj.page,
        "headers": table_obj.headers,
        "rows": table_obj.data,
        "row_count": len(table_obj.data),
        "col_count": table_obj.cols,
        "csv": table_obj.to_csv(),
    }

    # Cache it
    cache.set_table(pdf_path, table_id, table_data)

    result = {"success": True, "file": str(pdf_path.name), "table": table_data}
    formatter.output(result)


@app.command()
@handle_errors
def tables(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Output directory for tables",
    ),
    pages: Optional[str] = typer.Option(
        None,
        "--pages",
        "-p",
        help="Page range to extract (e.g., '1-5,8,10-12')",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract all tables from PDF.

    Returns JSON with all tables.
    """
    from papercut.core.tables import TableExtractor
    from papercut.extractors.pdfplumber import PdfPlumberExtractor

    console = _get_console()
    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())
    extractor = TableExtractor(PdfPlumberExtractor())
    page_list = parse_pages(pages)

    extracted_tables = extractor.extract(pdf_path, pages=page_list)

    tables_data = [
        {
            "id": i + 1,
            "page": t.page,
            "headers": t.headers,
            "rows": t.data,
            "row_count": len(t.data),
            "col_count": t.cols,
        }
        for i, t in enumerate(extracted_tables)
    ]

    result = {
        "success": True,
        "file": str(pdf_path.name),
        "count": len(tables_data),
        "tables": tables_data,
    }

    if output:
        output.mkdir(parents=True, exist_ok=True)
        for i, t in enumerate(extracted_tables, 1):
            csv_path = output / f"table_{i}.csv"
            csv_path.write_text(t.to_csv())
        if not is_quiet():
            console.print(f"[green]Saved {len(extracted_tables)} table(s) to:[/green] {output}")
    else:
        formatter.output(result)


@app.command()
@handle_errors
def figure(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    figure_id: int = typer.Option(
        ...,
        "--id",
        "-i",
        help="Figure ID to extract",
    ),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Output file path for figure",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract a single figure by ID.

    Returns path to cached figure or saves to specified output.
    """
    from papercut.cache import get_cache
    from papercut.core.figures import FigureExtractor

    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())
    cache = get_cache()

    # Check cache first
    cached_path = cache.get_figure_path(pdf_path, figure_id)
    if cached_path:
        if output:
            import shutil
            shutil.copy(cached_path, output)
            result = {"success": True, "file": str(pdf_path.name), "figure_path": str(output)}
        else:
            result = {
                "success": True,
                "cached": True,
                "file": str(pdf_path.name),
                "figure_path": str(cached_path),
            }
        formatter.output(result)
        return

    # Extract figure
    try:
        extractor = FigureExtractor()
        fig = extractor.extract_one(pdf_path, figure_id)

        if not fig:
            result = {"success": False, "error": f"Figure {figure_id} not found"}
            formatter.output(result)
            raise typer.Exit(1)

        # Save to cache or output
        if output:
            fig.save(output)
            fig_path = str(output)
        else:
            fig_path = str(cache.set_figure(pdf_path, figure_id, fig.image_data))

        result = {
            "success": True,
            "file": str(pdf_path.name),
            "figure": fig.to_dict(),
            "figure_path": fig_path,
        }
        formatter.output(result)

    except ImportError as e:
        result = {"success": False, "error": str(e)}
        formatter.output(result)
        raise typer.Exit(1)


@app.command()
@handle_errors
def figures(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Output directory for figures",
    ),
    pages: Optional[str] = typer.Option(
        None,
        "--pages",
        "-p",
        help="Page range to extract (e.g., '1-5,8,10-12')",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract all figures from PDF.

    Requires PyMuPDF: pip install pymupdf
    """
    from papercut.core.figures import FigureExtractor

    console = _get_console()
    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())

    try:
        extractor = FigureExtractor()
        page_list = parse_pages(pages)

        extracted_figures = extractor.extract(pdf_path, pages=page_list)

        figures_data = [fig.to_dict() for fig in extracted_figures]

        result = {
            "success": True,
            "file": str(pdf_path.name),
            "count": len(figures_data),
            "figures": figures_data,
        }

        if output:
            output.mkdir(parents=True, exist_ok=True)
            for fig in extracted_figures:
                fig_path = output / f"fig_{fig.id}.png"
                fig.save(fig_path)
            result["output_dir"] = str(output)
            if not is_quiet():
                console.print(f"[green]Saved {len(extracted_figures)} figure(s) to:[/green] {output}")

        formatter.output(result)

    except ImportError as e:
        result = {"success": False, "error": str(e)}
        formatter.output(result)
        raise typer.Exit(1)


def _infer_refs_format(output_path: Optional[Path], explicit_format: str) -> str:
    """Infer output format from file extension or explicit flag.

    Args:
        output_path: Output file path (may be None for stdout).
        explicit_format: Explicitly specified format from --format flag.

    Returns:
        Format string: "bibtex" or "json".
    """
    # Explicit non-default format takes precedence
    if explicit_format and explicit_format != "json":
        return explicit_format

    # Infer from file extension
    if output_path:
        ext = output_path.suffix.lower()
        if ext == ".bib":
            return "bibtex"
        elif ext == ".json":
            return "json"

    return explicit_format or "json"


@app.command()
@handle_errors
def refs(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Output file (default: stdout). Format auto-detected from extension (.bib, .json).",
    ),
    format: str = typer.Option(
        "json",
        "--format",
        "-f",
        help="Output format: bibtex or json. Overrides extension auto-detection.",
    ),
    search: Optional[str] = typer.Option(
        None,
        "--search",
        "-s",
        help="Filter references by search term",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract references/bibliography from PDF.

    Returns JSON with references data. Output format is auto-detected from
    file extension (.bib for BibTeX, .json for JSON) or can be explicitly
    set with --format.
    """
    from papercut.core.references import ReferenceExtractor
    from papercut.extractors.pdfplumber import PdfPlumberExtractor

    console = _get_console()
    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())
    extractor = ReferenceExtractor(PdfPlumberExtractor())
    all_refs = extractor.extract(pdf_path)

    # Infer format from extension if not explicitly specified
    effective_format = _infer_refs_format(output, format)

    # Filter by search if specified
    if search:
        search_lower = search.lower()
        all_refs = [
            r for r in all_refs
            if search_lower in r.raw.lower()
            or (r.title and search_lower in r.title.lower())
            or any(search_lower in a.lower() for a in r.authors)
        ]

    if effective_format == "bibtex":
        bibtex_output = "\n\n".join(ref.to_bibtex() for ref in all_refs)
        result = {
            "success": True,
            "file": str(pdf_path.name),
            "count": len(all_refs),
            "format": "bibtex",
            "bibtex": bibtex_output,
        }
    else:
        result = {
            "success": True,
            "file": str(pdf_path.name),
            "count": len(all_refs),
            "references": [ref.to_dict() for ref in all_refs],
        }

    if output:
        if effective_format == "bibtex":
            output.write_text(bibtex_output)
        else:
            json_output = json.dumps(result, indent=2)
            output.write_text(json_output)
        if not is_quiet():
            console.print(f"[green]Saved:[/green] {output}")
    else:
        formatter.output(result)


def _find_section(sections: list, query: str):
    """Find section by ID or partial title match.

    Args:
        sections: List of Section objects with id, title attributes.
        query: Section ID (as string) or partial title.

    Returns:
        Matched section or None.
    """
    query_lower = query.lower().strip()

    # Try exact ID match first
    for s in sections:
        if str(s.id) == query:
            return s

    # Try exact title match (case-insensitive)
    for s in sections:
        if s.title.lower() == query_lower:
            return s

    # Try partial title match
    for s in sections:
        if query_lower in s.title.lower():
            return s

    return None


@app.command()
@handle_errors
def section(
    pdf_path: Path = typer.Argument(..., help="Path to PDF file"),
    section_name: Optional[str] = typer.Option(
        None,
        "--section",
        "-s",
        help="Section name or ID to extract (partial match supported)",
    ),
    list_sections: bool = typer.Option(
        False,
        "--list",
        "-l",
        help="List all detected sections",
    ),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Output file (default: stdout)",
    ),
    use_json: bool = typer.Option(
        False,
        "--json",
        help="Force JSON output",
    ),
    pretty: bool = typer.Option(
        False,
        "--pretty",
        help="Force pretty output",
    ),
):
    """Extract text from a specific section or list sections.

    Use --list to see all detected sections.
    Use --section to extract a specific section's text.

    Examples:

        papercut extract section paper.pdf --list

        papercut extract section paper.pdf --section "Methods"

        papercut extract section paper.pdf -s 3

        papercut extract section paper.pdf -s Introduction -o intro.txt
    """
    from papercut.core.text import TextExtractor
    from papercut.extractors.pdfplumber import PdfPlumberExtractor
    from papercut.index import DocumentIndexer

    console = _get_console()
    formatter = get_formatter(json_flag=use_json, pretty_flag=pretty, quiet=is_quiet())

    # Get document index for section detection
    indexer = DocumentIndexer(use_cache=True)
    doc_index = indexer.index(pdf_path)

    if doc_index.type == "book":
        result = {
            "success": False,
            "error": "This document is detected as a book. Use 'papercut chapters' instead.",
        }
        formatter.output(result)
        raise typer.Exit(1)

    sections = doc_index.sections

    if not sections:
        result = {
            "success": False,
            "error": "No sections detected in this document.",
            "hint": "Try 'papercut index' to see the full document structure.",
        }
        formatter.output(result)
        raise typer.Exit(1)

    # List mode: show all sections
    if list_sections:
        result = {
            "success": True,
            "file": str(pdf_path.name),
            "count": len(sections),
            "sections": [
                {"id": s.id, "title": s.title, "pages": list(s.pages)}
                for s in sections
            ],
        }
        if output:
            json_output = json.dumps(result, indent=2)
            output.write_text(json_output)
            if not is_quiet():
                console.print(f"[green]Saved:[/green] {output}")
        else:
            formatter.output(result)
        return

    # Extract mode: need a section name
    if not section_name:
        result = {
            "success": False,
            "error": "Please specify --section/-s or --list/-l",
            "hint": "Use --list to see available sections",
        }
        formatter.output(result)
        raise typer.Exit(1)

    # Find matching section
    matched = _find_section(sections, section_name)

    if not matched:
        available = ", ".join(f'"{s.title}"' for s in sections[:5])
        if len(sections) > 5:
            available += f" ... and {len(sections) - 5} more"
        result = {
            "success": False,
            "error": f"Section not found: '{section_name}'",
            "hint": f"Available sections: {available}",
        }
        formatter.output(result)
        raise typer.Exit(1)

    # Extract text for the section
    extractor = TextExtractor(PdfPlumberExtractor())
    start_page = matched.pages[0] - 1  # Convert to 0-indexed
    end_page = matched.pages[1]  # End is exclusive in range
    pages = list(range(start_page, end_page))
    section_text = extractor.extract(pdf_path, pages=pages)

    result = {
        "success": True,
        "file": str(pdf_path.name),
        "section": {
            "id": matched.id,
            "title": matched.title,
            "pages": list(matched.pages),
        },
        "content": {
            "text": section_text,
            "word_count": len(section_text.split()) if section_text else 0,
            "char_count": len(section_text) if section_text else 0,
        },
    }

    if output:
        # For file output, just save the text content
        output.write_text(section_text)
        if not is_quiet():
            console.print(f"[green]Saved:[/green] {output}")
    else:
        formatter.output(result)
