from __future__ import annotations

import logging
from io import BytesIO

import pdfplumber
from PyPDF2 import PdfReader

from app.clinical_pipeline.domain.models import PdfExtractionMetadata, PdfExtractionResult


logger = logging.getLogger(__name__)


class ReadPdf:
    @staticmethod
    def read(file_bytes: bytes) -> str:
        """
        Lee texto de un PDF preservando layout cuando es posible.

        pdfplumber conserva mejor columnas y saltos de página en facturas; PyPDF2 queda
        como fallback compatible para PDFs problemáticos.
        """
        return ReadPdf.read_with_metadata(file_bytes).text

    @staticmethod
    def read_with_metadata(file_bytes: bytes) -> PdfExtractionResult:
        """Extrae todas las páginas y conserva métricas sin registrar texto clínico."""

        warnings: list[str] = []
        fallback_pages: list[int] = []
        pages_without_text: list[int] = []
        page_texts: list[str] = []
        pdfplumber_pages = 0

        plumber_resource = None
        try:
            plumber_resource = pdfplumber.open(BytesIO(file_bytes))
            plumber_pdf = (
                plumber_resource.__enter__()
                if hasattr(plumber_resource, "__enter__")
                else plumber_resource
            )
        except Exception as exc:
            logger.warning("No fue posible abrir el PDF con pdfplumber: %s", exc.__class__.__name__)
            plumber_pdf = None
            warnings.append("pdfplumber_open_failed")

        try:
            pypdf_reader = PdfReader(BytesIO(file_bytes), strict=False)
        except Exception as exc:
            if plumber_pdf is None:
                raise Exception(f"No se pudo abrir el PDF: {exc.__class__.__name__}") from exc
            pypdf_reader = None
            warnings.append("pypdf2_open_failed")

        total_pages = max(
            len(plumber_pdf.pages) if plumber_pdf is not None else 0,
            len(pypdf_reader.pages) if pypdf_reader is not None else 0,
        )
        try:
            for page_index in range(total_pages):
                page_number = page_index + 1
                page_text = ""
                if plumber_pdf is not None and page_index < len(plumber_pdf.pages):
                    try:
                        page_text = (
                            plumber_pdf.pages[page_index].extract_text(
                                layout=True,
                                x_tolerance=1,
                                y_tolerance=3,
                            )
                            or ""
                        ).strip("\n")
                    except Exception as exc:
                        logger.warning(
                            "Error extrayendo página %s con pdfplumber: %s",
                            page_number,
                            exc.__class__.__name__,
                        )
                        warnings.append(f"page_{page_number}_pdfplumber_failed")

                if page_text.strip():
                    pdfplumber_pages += 1
                elif pypdf_reader is not None and page_index < len(pypdf_reader.pages):
                    try:
                        page_text = (pypdf_reader.pages[page_index].extract_text() or "").strip("\n")
                    except Exception as exc:
                        logger.warning(
                            "Error extrayendo página %s con PyPDF2: %s",
                            page_number,
                            exc.__class__.__name__,
                        )
                        warnings.append(f"page_{page_number}_pypdf2_failed")
                    if page_text.strip():
                        fallback_pages.append(page_number)
                        warnings.append(f"page_{page_number}_pypdf2_fallback")

                if page_text.strip():
                    page_texts.append(page_text)
                else:
                    pages_without_text.append(page_number)
                    warnings.append(f"page_{page_number}_without_extractable_text")
        finally:
            if plumber_resource is not None and hasattr(plumber_resource, "__exit__"):
                plumber_resource.__exit__(None, None, None)
            elif plumber_pdf is not None:
                plumber_pdf.close()

        text = "\n\f\n".join(page_texts)
        metadata = PdfExtractionMetadata(
            total_pages=total_pages,
            pages_with_text=total_pages - len(pages_without_text),
            pages_without_text=pages_without_text,
            pdfplumber_pages=pdfplumber_pages,
            pypdf2_fallback_pages=fallback_pages,
            character_count=len(text),
            warnings=warnings,
        )
        return PdfExtractionResult(text=text, metadata=metadata)

    @staticmethod
    def _read_with_pdfplumber(file_bytes: bytes) -> str:
        pages_text: list[str] = []
        with pdfplumber.open(BytesIO(file_bytes)) as pdf:
            for page_number, page in enumerate(pdf.pages, start=1):
                try:
                    page_text = page.extract_text(layout=True, x_tolerance=1, y_tolerance=3) or ""
                except Exception as exc:
                    logger.warning("Error extrayendo texto de página %s con pdfplumber: %s", page_number, exc)
                    continue
                pages_text.append(page_text.strip("\n"))
        return "\n\f\n".join(page for page in pages_text if page.strip())

    @staticmethod
    def _read_with_pypdf2(file_bytes: bytes, *, strict: bool) -> str:
        pdf_reader = PdfReader(BytesIO(file_bytes), strict=strict)
        pages_text: list[str] = []
        for page_number, page in enumerate(pdf_reader.pages, start=1):
            try:
                page_text = page.extract_text() or ""
            except Exception as exc:
                logger.warning("Error extrayendo texto de página %s con PyPDF2: %s", page_number, exc)
                continue
            pages_text.append(page_text.strip("\n"))
        return "\n\f\n".join(page for page in pages_text if page.strip())
