"""PDF parsing and chapter detection for Condensr."""

from __future__ import annotations

import os
import re
import statistics
from dataclasses import dataclass

import pymupdf


@dataclass
class Chapter:
    """A detected chapter with title and page boundaries."""

    title: str
    start_page: int  # 0-indexed
    end_page: int  # 0-indexed, inclusive


def detect_chapters(pdf_path: str, *, toc_level: int = 1) -> list[Chapter]:
    """Detect chapter boundaries in a PDF.

    Detection strategy (in order):
    1. TOC extraction via doc.get_toc()
    2. Heading heuristics (bold text, large font, "Chapter N" patterns)
    3. No chapters found -> returns empty list

    No API calls are made. This is purely heuristic.

    Args:
        pdf_path: Path to the PDF file.
        toc_level: Maximum TOC depth to include (default: 1).

    Returns:
        List of Chapter dataclasses with title and page boundaries.

    Raises:
        ValueError: If the PDF is encrypted or contains no text.
    """
    doc = pymupdf.open(pdf_path)
    try:
        _validate_pdf(doc)
        last_page = len(doc) - 1

        chapters = _detect_from_toc(doc, toc_level=toc_level)
        if chapters:
            return chapters

        chapters = _detect_from_headings(doc)
        if chapters:
            return chapters

        return []
    finally:
        doc.close()


def extract_chapter_text(pdf_path: str, chapter: Chapter) -> str:
    """Extract plain text for a specific chapter by page range.

    Args:
        pdf_path: Path to the PDF file.
        chapter: Chapter with start_page and end_page boundaries.

    Returns:
        Concatenated text from all pages in the chapter's range.
    """
    doc = pymupdf.open(pdf_path)
    try:
        text_parts = []
        # end_page is inclusive, doc.pages stop is exclusive
        for page in doc.pages(chapter.start_page, chapter.end_page + 1):
            text_parts.append(page.get_text("text"))
        return "\n".join(text_parts)
    finally:
        doc.close()


def extract_full_text(pdf_path: str) -> str:
    """Extract all text from the PDF.

    Used when no chapters are detected and the entire book
    is summarized as one unit.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        Concatenated text from all pages.
    """
    doc = pymupdf.open(pdf_path)
    try:
        text_parts = []
        for page in doc:
            text_parts.append(page.get_text("text"))
        return "\n".join(text_parts)
    finally:
        doc.close()


def get_book_title(pdf_path: str) -> str:
    """Return book title from PDF metadata, or cleaned filename as fallback.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        The book title string.
    """
    doc = pymupdf.open(pdf_path)
    try:
        title = doc.metadata.get("title", "").strip()
        if title:
            return title
        # Fallback: filename without extension, capitalized
        basename = os.path.basename(pdf_path)
        name_without_ext = basename.rsplit(".", 1)[0]
        return name_without_ext.replace("-", " ").replace("_", " ").title()
    finally:
        doc.close()


def _validate_pdf(doc: pymupdf.Document) -> None:
    """Validate that the PDF is readable and contains text.

    Raises:
        ValueError: If the PDF is encrypted or has no extractable text.
    """
    if doc.is_encrypted:
        raise ValueError(
            "PDF is password-protected. Condensr does not support encrypted PDFs."
        )

    # Sample first few pages for text content
    sample_text = ""
    pages_to_check = min(len(doc), 10)
    for i in range(pages_to_check):
        sample_text += doc[i].get_text("text")
        if len(sample_text) >= 100:
            break

    if len(sample_text) < 100:
        raise ValueError(
            "No text content found. The PDF may be scanned/image-based."
        )


_BLOCKLIST_TITLES: frozenset[str] = frozenset({
    # English
    "title page", "half title", "copyright", "copyright page",
    "dedication", "epigraph", "contents", "table of contents",
    "list of figures", "list of tables", "list of illustrations",
    "notes", "endnotes", "footnotes", "bibliography", "references",
    "glossary", "index", "appendix", "appendices",
    "acknowledgements", "acknowledgments", "about the author",
    "about the authors", "colophon",
    "suggestions for further reading", "further reading",
    "also by", "other books by",
    # French
    "page de titre", "droits d'auteur",
    "dedicace", "dédicace", "epigraphe", "épigraphe",
    "table des matieres", "table des matières", "sommaire",
    "bibliographie", "références",
    "glossaire", "annexe", "annexes",
    "remerciements", "a propos de l'auteur", "à propos de l'auteur",
    # Spanish
    "pagina de titulo", "página de título",
    "derechos de autor", "dedicatoria", "epigrafe", "epígrafe",
    "contenido", "tabla de contenido", "indice", "índice",
    "notas", "bibliografia", "bibliografía", "referencias",
    "glosario", "apendice", "apéndice",
    "agradecimientos", "sobre el autor", "sobre la autora",
    # German
    "titelseite", "impressum", "urheberrecht",
    "widmung", "inhaltsverzeichnis",
    "anmerkungen", "literaturverzeichnis", "quellenverzeichnis",
    "glossar", "stichwortverzeichnis", "anhang",
    "danksagung", "uber den autor", "über den autor",
    "uber die autorin", "über die autorin",
})


def _detect_from_toc(doc: pymupdf.Document, toc_level: int = 1) -> list[Chapter]:
    """Use doc.get_toc() to build chapter list with page boundaries.

    Filters to entries up to the given TOC level and excludes common
    non-content entries (copyright, dedication, index, etc.) using a
    multi-language blocklist. Page boundaries are computed from all
    matching entries so each chapter gets exactly its own page range
    and filtered entries' pages are excluded entirely.

    Args:
        doc: An open pymupdf Document.
        toc_level: Maximum TOC depth to include (default: 1).
    """
    toc = doc.get_toc(simple=True)
    if not toc:
        return []

    # Keep entries up to the requested TOC level for boundary calculation
    all_entries = [(title, page - 1) for level, title, page in toc if level <= toc_level]
    if not all_entries:
        return []

    last_page = len(doc) - 1
    chapters = []
    for i, (title, start_page) in enumerate(all_entries):
        # Skip blocklisted titles
        if title.strip().lower() in _BLOCKLIST_TITLES:
            continue
        # End page is bounded by the next level-1 entry (filtered or not)
        if i + 1 < len(all_entries):
            end_page = all_entries[i + 1][1] - 1
        else:
            end_page = last_page
        # Clamp pages to valid range
        start_page = max(0, min(start_page, last_page))
        end_page = max(start_page, min(end_page, last_page))
        chapters.append(Chapter(title=title, start_page=start_page, end_page=end_page))

    return chapters


_CHAPTER_PATTERN = re.compile(
    r"^(chapter|part|section)\s+(\d+|[IVXLCDM]+)",
    re.IGNORECASE,
)


def _detect_from_headings(doc: pymupdf.Document) -> list[Chapter]:
    """Scan pages for heading-like text using font metadata.

    Looks for text spans that are significantly larger than the median font size
    or are bold and match common chapter title patterns.
    """
    # First pass: collect all font sizes to compute median
    all_sizes = []
    for page in doc:
        blocks = page.get_text("dict")["blocks"]
        for block in blocks:
            if block.get("type") != 0:  # text blocks only
                continue
            for line in block["lines"]:
                for span in line["spans"]:
                    text = span["text"].strip()
                    if text:
                        all_sizes.append(span["size"])

    if not all_sizes:
        return []

    median_size = statistics.median(all_sizes)
    size_threshold = median_size * 1.3

    # Second pass: find heading candidates
    candidates = []  # (page_index, title)
    for page_idx in range(len(doc)):
        page = doc[page_idx]
        blocks = page.get_text("dict")["blocks"]
        for block in blocks:
            if block.get("type") != 0:
                continue
            for line in block["lines"]:
                line_text_parts = []
                is_heading = False
                for span in line["spans"]:
                    text = span["text"].strip()
                    if not text:
                        continue
                    line_text_parts.append(text)
                    size = span["size"]
                    is_bold = bool(span["flags"] & 16)
                    # Heading if: large font, or bold + matches chapter pattern
                    if size >= size_threshold:
                        is_heading = True
                    elif is_bold and _CHAPTER_PATTERN.match(text):
                        is_heading = True

                if is_heading and line_text_parts:
                    title = " ".join(line_text_parts)
                    # Avoid duplicates on the same page
                    if not candidates or candidates[-1] != (page_idx, title):
                        candidates.append((page_idx, title))

    if not candidates:
        return []

    # Build chapters from candidates
    last_page = len(doc) - 1
    chapters = []
    for i, (start_page, title) in enumerate(candidates):
        if i + 1 < len(candidates):
            end_page = candidates[i + 1][0] - 1
        else:
            end_page = last_page
        end_page = max(start_page, end_page)
        chapters.append(Chapter(title=title, start_page=start_page, end_page=end_page))

    return chapters
