"""Converter functions: PDF, DOCX, CSV -> Markdown.

This module uses libraries available in the project's `.venv` as requested.
"""
from __future__ import annotations

import os
from typing import Optional

from bs4 import BeautifulSoup
import mammoth
from markdownify import ATX, markdownify as html_to_md
import pandas as pd
from pdfminer.high_level import extract_text


class ConversionError(Exception):
    """Raised when a conversion fails unexpectedly."""


class UnsupportedFormatError(ValueError):
    """Raised when a file format or engine is not supported."""


def _coerce_path(path: os.PathLike | str) -> str:
    return os.fspath(path)


def _normalize_markdown(text: str) -> str:
    if text is None:
        return ""
    normalized = text.replace("\r\n", "\n").replace("\r", "\n")
    lines = [line.rstrip() for line in normalized.split("\n")]

    out_lines: list[str] = []
    blank_run = 0
    for line in lines:
        if line == "":
            blank_run += 1
            if blank_run <= 2:
                out_lines.append("")
        else:
            blank_run = 0
            out_lines.append(line)

    while out_lines and out_lines[-1] == "":
        out_lines.pop()

    result = "\n".join(out_lines)
    if result:
        result += "\n"
    return result


def _extract_pdf_text(path: str, engine: str) -> str:
    if engine == "pdfminer":
        return extract_text(path)
    if engine == "pdftext":
        try:
            import pdftext  # type: ignore
        except Exception as exc:  # pragma: no cover - optional dependency
            raise ConversionError("pdftext engine is not available") from exc
        if hasattr(pdftext, "extract_text"):
            return pdftext.extract_text(path)  # type: ignore[attr-defined]
        if hasattr(pdftext, "PDFText"):
            reader = pdftext.PDFText(path)  # type: ignore[attr-defined]
            if hasattr(reader, "extract_text"):
                return reader.extract_text()
            if hasattr(reader, "text"):
                return reader.text
        raise ConversionError("pdftext engine API is not recognized")
    if engine == "marker":
        try:
            from marker import convert  # type: ignore
        except Exception as exc:  # pragma: no cover - optional dependency
            raise ConversionError("marker engine is not available") from exc
        try:
            result = convert(path)
        except Exception as exc:  # pragma: no cover - optional dependency
            raise ConversionError("marker conversion failed") from exc
        if isinstance(result, str):
            return result
        raise ConversionError("marker engine returned unsupported output type")
    raise UnsupportedFormatError(f"Unsupported PDF engine: {engine}")


def _ocr_pdf_text(
    path: str,
    *,
    ocr_lang: str,
    ocr_version: str,
    ocr_device: Optional[str],
    ocr_render_scale: float,
    ocr_min_score: Optional[float],
) -> str:
    try:
        from . import ocr as ocr_utils
    except Exception as exc:
        raise ConversionError("OCR support is not available") from exc
    try:
        return ocr_utils.ocr_pdf(
            path,
            lang=ocr_lang,
            ocr_version=ocr_version,
            device=ocr_device,
            render_scale=ocr_render_scale,
            min_score=ocr_min_score,
        )
    except ocr_utils.OcrError as exc:
        raise ConversionError(f"OCR failed: {exc}") from exc


def pdf_to_markdown(
    path: os.PathLike | str,
    engine: str = "pdfminer",
    *,
    ocr_mode: str = "never",
    ocr_lang: str = "es",
    ocr_version: str = "PP-OCRv5",
    ocr_device: Optional[str] = None,
    ocr_render_scale: float = 2.0,
    ocr_min_score: Optional[float] = None,
) -> str:
    """Extract text from a PDF and return simple Markdown.

    Use ocr_mode="auto" or "always" to run OCR for textless or scanned PDFs.
    """
    path_str = _coerce_path(path)
    if not os.path.isfile(path_str):
        raise FileNotFoundError(path_str)
    if ocr_mode not in {"never", "auto", "always"}:
        raise ValueError(f"Unsupported OCR mode: {ocr_mode}")
    if ocr_mode == "always":
        ocr_text = _ocr_pdf_text(
            path_str,
            ocr_lang=ocr_lang,
            ocr_version=ocr_version,
            ocr_device=ocr_device,
            ocr_render_scale=ocr_render_scale,
            ocr_min_score=ocr_min_score,
        )
        return _normalize_markdown(ocr_text)
    try:
        text = _extract_pdf_text(path_str, engine)
        if text is None:
            text = ""
    except UnsupportedFormatError:
        raise
    except Exception as exc:
        raise ConversionError(f"PDF conversion failed: {exc}") from exc
    if ocr_mode == "auto" and not text.strip():
        ocr_text = _ocr_pdf_text(
            path_str,
            ocr_lang=ocr_lang,
            ocr_version=ocr_version,
            ocr_device=ocr_device,
            ocr_render_scale=ocr_render_scale,
            ocr_min_score=ocr_min_score,
        )
        return _normalize_markdown(ocr_text)
    return _normalize_markdown(text)


def image_to_markdown(
    path: os.PathLike | str,
    *,
    ocr_lang: str = "es",
    ocr_version: str = "PP-OCRv5",
    ocr_device: Optional[str] = None,
    min_score: Optional[float] = None,
) -> str:
    """Run OCR over an image and return Markdown text."""
    path_str = _coerce_path(path)
    if not os.path.isfile(path_str):
        raise FileNotFoundError(path_str)
    try:
        from . import ocr as ocr_utils
    except Exception as exc:
        raise ConversionError("OCR support is not available") from exc
    try:
        text = ocr_utils.ocr_image(
            path_str,
            lang=ocr_lang,
            ocr_version=ocr_version,
            device=ocr_device,
            min_score=min_score,
        )
    except ocr_utils.OcrError as exc:
        raise ConversionError(f"OCR failed: {exc}") from exc
    return _normalize_markdown(text)


def docx_to_markdown(path: os.PathLike | str) -> str:
    """Convert a DOCX file to Markdown using `mammoth` -> HTML -> `markdownify`.

    Returns a Markdown string.
    """
    path_str = _coerce_path(path)
    if not os.path.isfile(path_str):
        raise FileNotFoundError(path_str)
    try:
        with open(path_str, "rb") as f:
            result = mammoth.convert_to_html(f)
            html = result.value

        # Clean up the HTML a bit
        soup = BeautifulSoup(html, "lxml")
        # Remove empty tags
        for tag in soup.find_all():
            if not tag.text.strip():
                tag.decompose()

        clean_html = str(soup)
        md = html_to_md(clean_html, heading_style=ATX, bullets="-")
        return _normalize_markdown(str(md))
    except Exception as exc:
        raise ConversionError(f"DOCX conversion failed: {exc}") from exc


def csv_to_markdown(
    path: os.PathLike | str,
    index: bool = False,
    na_rep: str = "",
    float_format: Optional[str] = None,
) -> str:
    """Read a CSV with pandas and return a Markdown table.

    By default the returned table does not include the DataFrame index.
    """
    path_str = _coerce_path(path)
    if not os.path.isfile(path_str):
        raise FileNotFoundError(path_str)
    try:
        df = pd.read_csv(path_str)
        if float_format is not None:
            md = df.to_markdown(index=index, missingval=na_rep, floatfmt=float_format)
        else:
            md = df.to_markdown(index=index, missingval=na_rep)
        return _normalize_markdown(str(md))
    except Exception as exc:
        raise ConversionError(f"CSV conversion failed: {exc}") from exc
