"""
Enhanced PDF operations for MicroPDF.

This module provides advanced PDF manipulation functions beyond the standard
MicroPDF API, including PDF merging, splitting, optimization, watermarking,
page reordering, overlay, linearization, bookmark management, and drawing.
"""

import json
from typing import List, Optional, Tuple, Union
from pathlib import Path

from .ffi_native import ffi, lib, is_native_available
from .context import Context
from .errors import MicroPDFError, ErrorCode


def _check_native() -> None:
    if not is_native_available():
        raise MicroPDFError(ErrorCode.SYSTEM, "Native library not available.")


def _check_result(result: int, operation: str) -> None:
    if result < 0:
        raise MicroPDFError(ErrorCode.GENERIC, f"{operation}: error code {result}")


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


def _get_ctx_handle(ctx: Optional[Context]) -> tuple[int, bool]:
    """Get context handle, creating one if needed. Returns (handle, should_drop)."""
    if ctx is not None:
        return ctx._handle, False
    new_ctx = Context()
    return new_ctx._handle, True


def _maybe_drop_ctx(ctx: Optional[Context], should_drop: bool, handle: int) -> None:
    """Drop context if we created it internally."""
    if should_drop:
        from .ffi_native import lib as _lib
        _lib.fz_drop_context(handle)


def get_last_error() -> Optional[str]:
    """Get the last enhanced operation error message."""
    _check_native()
    ptr = lib.mp_get_last_enhanced_error()
    if ptr == ffi.NULL:
        return None
    try:
        return ffi.string(ptr).decode('utf-8')
    finally:
        lib.mp_free_string(ptr)


def merge_pdfs(input_paths: List[str], output_path: str, ctx: Optional[Context] = None) -> int:
    """
    Merge multiple PDF files into a single output PDF.

    Args:
        input_paths: List of input PDF file paths.
        output_path: Path for the merged output PDF.
        ctx: Optional context. If None, a default context is created.

    Returns:
        Total number of pages in the merged document.

    Raises:
        ValueError: If input_paths is empty or output_path is invalid.
        MicroPDFError: If the merge operation fails.
    """
    if not input_paths:
        raise ValueError("Input PDF paths cannot be empty")

    if not output_path:
        raise ValueError("Output path cannot be empty")

    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        c_paths = [ffi.new("char[]", path.encode('utf-8')) for path in input_paths]
        c_paths_array = ffi.new("char *[]", c_paths)
        c_output_path = ffi.new("char[]", output_path.encode('utf-8'))

        result = lib.mp_merge_pdfs(
            ctx._handle, c_paths_array, len(input_paths), c_output_path
        )

        if result < 0:
            raise MicroPDFError(ErrorCode.GENERIC, f"Failed to merge PDFs: error code {result}")

        return result
    finally:
        if should_drop:
            ctx.drop()


def split_pdf(
    input_path: Union[str, Path],
    output_dir: Union[str, Path],
    ctx: Optional[Context] = None,
) -> int:
    """
    Split a PDF into individual page files.

    Args:
        input_path: Path to the PDF to split.
        output_dir: Directory for output page files.
        ctx: Optional context.

    Returns:
        Number of pages created.

    Raises:
        MicroPDFError: If splitting fails.
    """
    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        result = lib.mp_split_pdf(
            ctx._handle, _to_path_bytes(input_path), _to_path_bytes(output_dir)
        )
        if result < 0:
            raise MicroPDFError(ErrorCode.GENERIC, f"Failed to split PDF: error code {result}")
        return result
    finally:
        if should_drop:
            ctx.drop()


def reorder_pages(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    page_order: List[int],
    ctx: Optional[Context] = None,
) -> None:
    """
    Reorder pages in a PDF.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the reordered output PDF.
        page_order: List of zero-based page indices in the desired order.
        ctx: Optional context.

    Raises:
        MicroPDFError: If reordering fails.
    """
    if not page_order:
        raise ValueError("Page order cannot be empty")

    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        order_array = ffi.new("int[]", page_order)
        result = lib.mp_reorder_pages(
            ctx._handle,
            _to_path_bytes(input_path),
            _to_path_bytes(output_path),
            order_array,
            len(page_order),
        )
        _check_result(result, "reorder_pages")
    finally:
        if should_drop:
            ctx.drop()


def add_watermark(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    text: str,
    x: float = 100.0,
    y: float = 100.0,
    font_size: float = 48.0,
    opacity: float = 0.3,
    ctx: Optional[Context] = None,
) -> None:
    """
    Add a text watermark to all pages of a PDF.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the watermarked output PDF.
        text: Watermark text.
        x: X coordinate for the watermark.
        y: Y coordinate for the watermark.
        font_size: Font size in points.
        opacity: Watermark opacity (0.0 to 1.0).
        ctx: Optional context.

    Raises:
        MicroPDFError: If watermarking fails.
    """
    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        result = lib.mp_add_watermark(
            ctx._handle,
            _to_path_bytes(input_path),
            _to_path_bytes(output_path),
            text.encode('utf-8'),
            x, y, font_size, opacity,
        )
        _check_result(result, "add_watermark")
    finally:
        if should_drop:
            ctx.drop()


def overlay_pdf(
    base_path: Union[str, Path],
    output_path: Union[str, Path],
    overlay_path: Union[str, Path],
    opacity: float = 1.0,
    ctx: Optional[Context] = None,
) -> None:
    """
    Overlay one PDF on top of another.

    Args:
        base_path: Path to the base PDF.
        output_path: Path for the output PDF.
        overlay_path: Path to the overlay PDF.
        opacity: Overlay opacity (0.0 to 1.0).
        ctx: Optional context.

    Raises:
        MicroPDFError: If the overlay operation fails.
    """
    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        result = lib.mp_overlay_pdf(
            ctx._handle,
            _to_path_bytes(base_path),
            _to_path_bytes(output_path),
            _to_path_bytes(overlay_path),
            opacity,
        )
        _check_result(result, "overlay_pdf")
    finally:
        if should_drop:
            ctx.drop()


def optimize_pdf(
    path: Union[str, Path],
    ctx: Optional[Context] = None,
) -> None:
    """
    Optimize a PDF file in place (reduce file size).

    Args:
        path: Path to the PDF to optimize.
        ctx: Optional context.

    Raises:
        MicroPDFError: If optimization fails.
    """
    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        result = lib.mp_optimize_pdf(ctx._handle, _to_path_bytes(path))
        _check_result(result, "optimize_pdf")
    finally:
        if should_drop:
            ctx.drop()


def linearize_pdf(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    ctx: Optional[Context] = None,
) -> None:
    """
    Linearize a PDF for fast web viewing.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the linearized output PDF.
        ctx: Optional context.

    Raises:
        MicroPDFError: If linearization fails.
    """
    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        result = lib.mp_linearize_pdf(
            ctx._handle, _to_path_bytes(input_path), _to_path_bytes(output_path)
        )
        _check_result(result, "linearize_pdf")
    finally:
        if should_drop:
            ctx.drop()


def add_blank_page(
    doc_handle: int,
    width: float = 595.0,
    height: float = 842.0,
    ctx: Optional[Context] = None,
) -> None:
    """
    Add a blank page to a document.

    Args:
        doc_handle: Handle to the PDF document.
        width: Page width in points (default A4).
        height: Page height in points (default A4).
        ctx: Optional context.

    Raises:
        MicroPDFError: If the operation fails.
    """
    if ctx is None:
        ctx = Context()
        should_drop = True
    else:
        should_drop = False

    try:
        result = lib.mp_add_blank_page(ctx._handle, doc_handle, width, height)
        _check_result(result, "add_blank_page")
    finally:
        if should_drop:
            ctx.drop()


def get_bookmarks(pdf_path: Union[str, Path]) -> Optional[str]:
    """
    Get bookmarks/outline from a PDF as a JSON string.

    Args:
        pdf_path: Path to the PDF file.

    Returns:
        JSON string of the bookmark structure, or None if no bookmarks.

    Raises:
        MicroPDFError: If the operation fails.
    """
    _check_native()
    ptr = lib.mp_get_bookmarks(_to_path_bytes(pdf_path))
    if ptr == ffi.NULL:
        return None
    try:
        return ffi.string(ptr).decode('utf-8')
    finally:
        lib.mp_free_string(ptr)


def restore_bookmarks(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    bookmarks_json: str,
) -> None:
    """
    Restore bookmarks/outline to a PDF from a JSON string.

    Args:
        input_path: Path to the input PDF.
        output_path: Path for the output PDF with bookmarks.
        bookmarks_json: JSON string of the bookmark structure.

    Raises:
        MicroPDFError: If the operation fails.
    """
    _check_native()
    result = lib.mp_restore_bookmarks(
        _to_path_bytes(input_path),
        _to_path_bytes(output_path),
        bookmarks_json.encode('utf-8'),
    )
    _check_result(result, "restore_bookmarks")


def register_font(font_name: str, font_data: bytes) -> int:
    """
    Register a custom font for use in text overlays.

    Args:
        font_name: Name to register the font under.
        font_data: Raw font file data (TTF/OTF).

    Returns:
        Font handle for use with create_text_overlay.

    Raises:
        MicroPDFError: If registration fails.
    """
    _check_native()
    handle = lib.mp_register_font(font_name.encode('utf-8'), font_data, len(font_data))
    if handle == 0:
        raise MicroPDFError(ErrorCode.GENERIC, "Failed to register font")
    return handle


def free_font(handle: int) -> None:
    """Free a registered font handle."""
    _check_native()
    lib.mp_font_free(handle)


__all__ = [
    "get_last_error",
    "merge_pdfs",
    "split_pdf",
    "reorder_pages",
    "add_watermark",
    "overlay_pdf",
    "optimize_pdf",
    "linearize_pdf",
    "add_blank_page",
    "get_bookmarks",
    "restore_bookmarks",
    "register_font",
    "free_font",
]
