from __future__ import annotations

"""Optional PDF parser.

This parser uses :mod:`pypdf` *if available* to extract text from local PDF
files. It is designed to be an optional enhancement:

- If ``pypdf`` is installed, PDF files are parsed into a simple
  section/paragraph structure.
- If ``pypdf`` is *not* installed, attempting to parse a PDF will raise a
  :class:`ParseError` with a clear message.

This keeps neuraparse usable without extra dependencies while still
supporting richer PDF handling when the user opts in.
"""

from pathlib import Path
from typing import List

from ..core.models import DocumentNode, DocumentTree, NodeType, RawDocument, SourceType
from .base import BaseParser, ParseError


class PdfParser(BaseParser):
    """PDF parser backed by :mod:`pypdf` when available."""

    def supports(self, raw: RawDocument) -> bool:
        # Only consider local FILE sources that look like PDFs.
        if raw.source_type is not SourceType.FILE:
            return False
        path = str(raw.metadata.get("path", "")).lower()
        return path.endswith(".pdf")

    def parse(self, raw: RawDocument) -> DocumentTree:
        path_str = raw.metadata.get("path")
        if not path_str:
            raise ParseError("PDF parser requires 'path' in raw.metadata")

        try:  # local import so that pypdf remains an optional dependency
            from pypdf import PdfReader  # type: ignore[import-not-found]
        except Exception as exc:  # pragma: no cover - environment-specific
            raise ParseError(
                "PDF support requires the 'pypdf' package. "
                "Install it (e.g. `pip install pypdf`) or skip PDF parsing."
            ) from exc

        pdf_path = Path(path_str)
        try:
            reader = PdfReader(str(pdf_path))
        except Exception as exc:  # pragma: no cover - depends on test files
            raise ParseError(f"Failed to open PDF file {pdf_path!r}: {exc}") from exc

        root = DocumentNode(
            id=f"{raw.id}#root",
            node_type=NodeType.DOCUMENT,
            text=None,
            metadata={"parser": "pdf"},
        )

        counter = 0

        for page_index, page in enumerate(reader.pages):
            try:
                page_text = page.extract_text() or ""
            except Exception:  # pragma: no cover - rare parser-specific errors
                page_text = ""

            page_text = page_text.strip()
            if not page_text:
                continue

            # Treat each page as a section and split paragraphs on blank lines.
            counter += 1
            section = DocumentNode(
                id=f"{raw.id}#s{counter}",
                node_type=NodeType.SECTION,
                text=f"Page {page_index + 1}",
                metadata={"page": page_index + 1},
            )
            root.children.append(section)

            lines = page_text.splitlines()
            paragraph_buffer: List[str] = []

            def flush_paragraph() -> None:
                nonlocal counter
                if not paragraph_buffer:
                    return
                text = "\n".join(paragraph_buffer).strip()
                paragraph_buffer.clear()
                if not text:
                    return
                counter += 1
                para = DocumentNode(
                    id=f"{raw.id}#p{counter}",
                    node_type=NodeType.PARAGRAPH,
                    text=text,
                    metadata={"page": page_index + 1},
                )
                section.children.append(para)

            for line in lines:
                if not line.strip():
                    flush_paragraph()
                else:
                    paragraph_buffer.append(line)

            flush_paragraph()

        return DocumentTree(root=root, metadata={"source_id": raw.id, "parser": "pdf"})

