"""
High-level convenience functions for common PDF operations.

This module provides Pythonic wrappers around the MicroPDF convenience FFI functions.
These functions handle resource management internally, making them easy to use for
simple PDF tasks without needing to manage contexts, documents, or pages manually.

Example usage:
    from micropdf.convenience import get_pdf_info, extract_text, render_page_to_png

    # Get PDF information
    info = get_pdf_info("document.pdf")
    print(f"Pages: {info.page_count}, Title: {info.title}")

    # Extract all text
    text = extract_text("document.pdf")
    print(text)

    # Render a page to PNG
    png_data = render_page_to_png("document.pdf", page=0, scale=2.0)
    with open("page.png", "wb") as f:
        f.write(png_data)
"""

from dataclasses import dataclass
from pathlib import Path
from typing import Optional, List, Union
from .ffi_native import ffi, lib, is_native_available
from .errors import MicroPDFError, ErrorCode


@dataclass
class PDFInfo:
    """Information about a PDF document."""
    page_count: int
    is_encrypted: bool
    needs_password: bool
    version: Optional[str]
    title: Optional[str]
    author: Optional[str]
    subject: Optional[str]
    creator: Optional[str]


@dataclass
class PageDimensions:
    """Dimensions of a PDF page in points (1/72 inch)."""
    width: float
    height: float

    @property
    def width_inches(self) -> float:
        """Width in inches."""
        return self.width / 72.0

    @property
    def height_inches(self) -> float:
        """Height in inches."""
        return self.height / 72.0

    @property
    def width_mm(self) -> float:
        """Width in millimeters."""
        return self.width * 25.4 / 72.0

    @property
    def height_mm(self) -> float:
        """Height in millimeters."""
        return self.height * 25.4 / 72.0


@dataclass
class RenderedPage:
    """Result of rendering a PDF page."""
    data: bytes
    width: int
    height: int


@dataclass
class ExtractedText:
    """Result of text extraction."""
    text: str
    pages_processed: int


def _check_native():
    """Check if native library is available."""
    if not is_native_available():
        raise MicroPDFError(
            ErrorCode.SYSTEM,
            "Native library not available. Set MICROPDF_LIB_PATH or install micropdf with native bindings."
        )


def _check_result(result: int, operation: str):
    """Check FFI result code and raise on error."""
    if result < 0:
        error_messages = {
            -1: "Null parameter",
            -2: "Null output parameter",
            -3: "Failed to open file",
            -4: "Page number out of range",
            -5: "Operation failed",
        }
        msg = error_messages.get(result, f"Unknown error code {result}")
        raise MicroPDFError(ErrorCode.GENERIC, f"{operation}: {msg}")


def _to_path_bytes(path: Union[str, Path]) -> bytes:
    """Convert path to UTF-8 encoded bytes."""
    return str(path).encode('utf-8')


def get_pdf_info(pdf_path: Union[str, Path]) -> PDFInfo:
    """
    Get information about a PDF file.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        PDFInfo with document metadata.

    Raises:
        MicroPDFError: If the file cannot be opened or read.
    """
    _check_native()

    info = ffi.new("MpPdfInfo*")
    path_bytes = _to_path_bytes(pdf_path)

    result = lib.mp_get_pdf_info(path_bytes, info)
    _check_result(result, "get_pdf_info")

    try:
        def get_string(ptr) -> Optional[str]:
            if ptr == ffi.NULL:
                return None
            return ffi.string(ptr).decode('utf-8')

        return PDFInfo(
            page_count=info.page_count,
            is_encrypted=bool(info.is_encrypted),
            needs_password=bool(info.needs_password),
            version=get_string(info.version),
            title=get_string(info.title),
            author=get_string(info.author),
            subject=get_string(info.subject),
            creator=get_string(info.creator),
        )
    finally:
        lib.mp_free_pdf_info(info)


def get_page_count(pdf_path: Union[str, Path]) -> int:
    """
    Get the number of pages in a PDF file.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        Number of pages.

    Raises:
        MicroPDFError: If the file cannot be opened or read.
    """
    _check_native()

    path_bytes = _to_path_bytes(pdf_path)
    result = lib.mp_get_page_count(path_bytes)

    if result < 0:
        _check_result(result, "get_page_count")

    return result


def get_page_dimensions(pdf_path: Union[str, Path], page_num: int = 0) -> PageDimensions:
    """
    Get the dimensions of a specific page.

    Args:
        pdf_path: Path to the PDF file.
        page_num: Zero-based page number (default: 0).

    Returns:
        PageDimensions with width and height in points.

    Raises:
        MicroPDFError: If the file cannot be opened or page is out of range.
    """
    _check_native()

    dims = ffi.new("MpPageDimensions*")
    path_bytes = _to_path_bytes(pdf_path)

    result = lib.mp_get_page_dimensions(path_bytes, page_num, dims)
    _check_result(result, "get_page_dimensions")

    return PageDimensions(width=dims.width, height=dims.height)


def extract_text(pdf_path: Union[str, Path]) -> str:
    """
    Extract all text from a PDF file.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        Extracted text as a string.

    Raises:
        MicroPDFError: If the file cannot be opened or read.
    """
    _check_native()

    result_struct = ffi.new("MpExtractedText*")
    path_bytes = _to_path_bytes(pdf_path)

    result = lib.mp_extract_text(path_bytes, result_struct)
    _check_result(result, "extract_text")

    try:
        if result_struct.text == ffi.NULL:
            return ""
        return ffi.string(result_struct.text).decode('utf-8')
    finally:
        lib.mp_free_extracted_text(result_struct)


def extract_page_text(pdf_path: Union[str, Path], page_num: int = 0) -> str:
    """
    Extract text from a specific page of a PDF file.

    Args:
        pdf_path: Path to the PDF file.
        page_num: Zero-based page number (default: 0).

    Returns:
        Extracted text as a string.

    Raises:
        MicroPDFError: If the file cannot be opened or page is out of range.
    """
    _check_native()

    path_bytes = _to_path_bytes(pdf_path)
    text_ptr = lib.mp_extract_page_text(path_bytes, page_num)

    if text_ptr == ffi.NULL:
        raise MicroPDFError(ErrorCode.GENERIC, f"Failed to extract text from page {page_num}")

    try:
        return ffi.string(text_ptr).decode('utf-8')
    finally:
        lib.mp_free_string(text_ptr)


def render_page_to_png(
    pdf_path: Union[str, Path],
    page_num: int = 0,
    scale: float = 1.0
) -> bytes:
    """
    Render a page to PNG image data.

    Args:
        pdf_path: Path to the PDF file.
        page_num: Zero-based page number (default: 0).
        scale: Scale factor (1.0 = 72 DPI, 2.0 = 144 DPI).

    Returns:
        PNG image data as bytes.

    Raises:
        MicroPDFError: If the file cannot be opened or rendering fails.
    """
    _check_native()

    result_struct = ffi.new("MpRenderedPage*")
    path_bytes = _to_path_bytes(pdf_path)

    result = lib.mp_render_page_to_png(path_bytes, page_num, scale, result_struct)
    _check_result(result, "render_page_to_png")

    try:
        if result_struct.data == ffi.NULL or result_struct.data_len == 0:
            raise MicroPDFError(ErrorCode.GENERIC, "Rendering produced no data")
        return bytes(ffi.buffer(result_struct.data, result_struct.data_len))
    finally:
        lib.mp_free_rendered_page(result_struct)


def render_page_to_rgb(
    pdf_path: Union[str, Path],
    page_num: int = 0,
    scale: float = 1.0
) -> RenderedPage:
    """
    Render a page to raw RGB pixel data.

    Args:
        pdf_path: Path to the PDF file.
        page_num: Zero-based page number (default: 0).
        scale: Scale factor (1.0 = 72 DPI).

    Returns:
        RenderedPage with RGB pixel data and dimensions.

    Raises:
        MicroPDFError: If the file cannot be opened or rendering fails.
    """
    _check_native()

    result_struct = ffi.new("MpRenderedPage*")
    path_bytes = _to_path_bytes(pdf_path)

    result = lib.mp_render_page_to_rgb(path_bytes, page_num, scale, result_struct)
    _check_result(result, "render_page_to_rgb")

    try:
        if result_struct.data == ffi.NULL or result_struct.data_len == 0:
            raise MicroPDFError(ErrorCode.GENERIC, "Rendering produced no data")
        return RenderedPage(
            data=bytes(ffi.buffer(result_struct.data, result_struct.data_len)),
            width=result_struct.width,
            height=result_struct.height,
        )
    finally:
        lib.mp_free_rendered_page(result_struct)


def merge_pdfs(input_paths: List[Union[str, Path]], output_path: Union[str, Path]) -> None:
    """
    Merge multiple PDF files into one.

    Args:
        input_paths: List of paths to PDF files to merge.
        output_path: Path for the output merged PDF.

    Raises:
        MicroPDFError: If merging fails.
    """
    _check_native()

    if not input_paths:
        raise MicroPDFError(ErrorCode.ARGUMENT, "No input files provided")

    # Create array of path pointers
    path_bytes_list = [_to_path_bytes(p) for p in input_paths]
    paths_array = ffi.new("char*[]", [ffi.new("char[]", pb) for pb in path_bytes_list])
    output_bytes = _to_path_bytes(output_path)

    result = lib.mp_merge_pdf_files(paths_array, len(input_paths), output_bytes)
    _check_result(result, "merge_pdfs")


def split_pdf(pdf_path: Union[str, Path], output_dir: Union[str, Path]) -> int:
    """
    Split a PDF into individual page files.

    Creates files named page_001.pdf, page_002.pdf, etc. in the output directory.

    Args:
        pdf_path: Path to the PDF file to split.
        output_dir: Directory for output files.

    Returns:
        Number of pages created.

    Raises:
        MicroPDFError: If splitting fails.
    """
    _check_native()

    path_bytes = _to_path_bytes(pdf_path)
    output_bytes = _to_path_bytes(output_dir)

    result = lib.mp_split_pdf_to_pages(path_bytes, output_bytes)

    if result < 0:
        _check_result(result, "split_pdf")

    return result


def copy_pages(
    pdf_path: Union[str, Path],
    output_path: Union[str, Path],
    page_numbers: List[int]
) -> None:
    """
    Copy specific pages from a PDF to a new file.

    Args:
        pdf_path: Path to the source PDF file.
        output_path: Path for the output PDF.
        page_numbers: List of zero-based page numbers to copy.

    Raises:
        MicroPDFError: If copying fails.
    """
    _check_native()

    if not page_numbers:
        raise MicroPDFError(ErrorCode.ARGUMENT, "No page numbers provided")

    path_bytes = _to_path_bytes(pdf_path)
    output_bytes = _to_path_bytes(output_path)
    pages_array = ffi.new("int32_t[]", page_numbers)

    result = lib.mp_copy_pages(path_bytes, output_bytes, pages_array, len(page_numbers))
    _check_result(result, "copy_pages")


def is_valid_pdf(pdf_path: Union[str, Path]) -> bool:
    """
    Quick validation check on a PDF file.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        True if PDF appears valid, False otherwise.

    Raises:
        MicroPDFError: If an error occurs during validation.
    """
    _check_native()

    path_bytes = _to_path_bytes(pdf_path)
    result = lib.mp_is_valid_pdf(path_bytes)

    if result < 0:
        _check_result(result, "is_valid_pdf")

    return result == 1


def repair_pdf(pdf_path: Union[str, Path], output_path: Union[str, Path]) -> None:
    """
    Attempt to repair a damaged PDF.

    Args:
        pdf_path: Path to the damaged PDF file.
        output_path: Path for the repaired output PDF.

    Raises:
        MicroPDFError: If repair fails.
    """
    _check_native()

    path_bytes = _to_path_bytes(pdf_path)
    output_bytes = _to_path_bytes(output_path)

    result = lib.mp_repair_damaged_pdf(path_bytes, output_bytes)
    _check_result(result, "repair_pdf")


__all__ = [
    # Data classes
    "PDFInfo",
    "PageDimensions",
    "RenderedPage",
    "ExtractedText",
    # Functions
    "get_pdf_info",
    "get_page_count",
    "get_page_dimensions",
    "extract_text",
    "extract_page_text",
    "render_page_to_png",
    "render_page_to_rgb",
    "merge_pdfs",
    "split_pdf",
    "copy_pages",
    "is_valid_pdf",
    "repair_pdf",
]
