"""Convert PDF pages to image files.

This module provides functionality to convert PDF pages into image files.
It supports multiple output formats (PNG, JPG, WebP) and offers options
to customize DPI, quality, and page ranges.

Command: pdf2img <input_pdf> [output_dir] [options]
"""

from __future__ import annotations

import argparse
import logging
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING

import fitz
from pytola_core.config import ConfigMixin

if TYPE_CHECKING:
    pass

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)


class PdfToImageConfig(ConfigMixin):
    """Configuration for PDF to image conversion."""

    DPI: int = 150
    FORMAT: str = "png"
    QUALITY: int = 90
    MAX_QUALITY: int = 100


conf = PdfToImageConfig()


def parse_page_ranges(range_str: str, total_pages: int) -> list[int]:
    """Parse page range string and return list of page numbers (1-indexed).

    Args:
        range_str: Page range string, e.g., "1,3,5-10"
        total_pages: Total number of pages in the PDF

    Returns
    -------
        List of page numbers (1-indexed)
    """
    pages = []
    for part in range_str.split(","):
        part_stripped = part.strip()
        if not part_stripped:
            continue
        if "-" in part_stripped:
            start, end = part_stripped.split("-")
            start = int(start) if start else 1
            end = int(end) if end else total_pages
            pages.extend(range(start, end + 1))
        else:
            pages.append(int(part_stripped))
    return pages


class PdfToImageRunner:
    """PDF to Image converter processor.

    Processes PDF files and converts each page to an image file.
    Supports multiple output formats and customization options.
    """

    def __init__(
        self,
        input_pdf: Path,
        output_dir: Path,
        dpi: int = 150,
        img_format: str = "png",
        quality: int = 90,
        page_range: str | None = None,
    ) -> None:
        """Initialize the PDF to Image converter.

        Args:
            input_pdf: Path to the input PDF file
            output_dir: Directory to save output images
            dpi: Resolution in dots per inch (default: 150)
            img_format: Output image format: png/jpg/jpeg/webp (default: png)
            quality: Image quality 1-100 for lossy formats (default: 90)
            page_range: Page range string, e.g., "1,3,5-10" (default: all pages)
        """
        self.input_pdf = input_pdf
        self.output_dir = output_dir
        self.dpi = dpi
        self.img_format = img_format.lower()
        self.quality = quality
        self.page_range = page_range

    def _process_page(self, page_num: int) -> bool:
        """Process a single PDF page and convert to image.

        Args:
            page_num: Page number (0-indexed)

        Returns
        -------
            True if successful, False otherwise
        """
        try:
            pixmap = self._render_page(page_num)
            if pixmap:
                output_path = self._save_image(pixmap, page_num)
                logger.info(f"Created: {output_path.name}")
                return True
        except (OSError, RuntimeError, ValueError):
            logger.exception(f"Failed to convert page {page_num + 1}")
        return False

    def run(self) -> None:
        """Execute the PDF to image conversion process.

        Converts all specified pages from the PDF to image files.
        Images are saved in the output directory with sequential numbering.
        """
        logger.info(f"Start converting PDF: {self.input_pdf}")
        logger.info(f"Output directory: {self.output_dir}, Format: {self.img_format}, DPI: {self.dpi}")

        # Create output directory if it doesn't exist
        self.output_dir.mkdir(parents=True, exist_ok=True)

        # Get list of pages to convert
        page_numbers = self._get_page_numbers()

        if not page_numbers:
            logger.error("No pages to convert")
            return

        logger.info(f"Converting {len(page_numbers)} page(s)")

        # Convert each page
        converted_count = 0
        failed_pages = []
        for page_num in page_numbers:
            if self._process_page(page_num):
                converted_count += 1
            else:
                failed_pages.append(page_num + 1)

        logger.info(f"Conversion complete: {converted_count}/{len(page_numbers)} pages converted")
        if failed_pages:
            logger.warning(f"Failed pages: {failed_pages}")

    @cached_property
    def total_pages(self) -> int:
        """Get total number of pages in the PDF.

        Returns
        -------
            Total page count
        """
        doc = fitz.open(self.input_pdf)
        count = doc.page_count
        doc.close()
        return count

    def _get_page_numbers(self) -> list[int]:
        """Get list of page numbers to convert (1-indexed).

        Returns
        -------
            List of page numbers to convert
        """
        if self.page_range:
            pages = parse_page_ranges(self.page_range, self.total_pages)
            # Filter out invalid page numbers and convert to 0-indexed
            valid_pages = [p - 1 for p in pages if 1 <= p <= self.total_pages]
            # Remove duplicates while preserving order
            return sorted(set(valid_pages))

        # Convert all pages (0-indexed)
        return list(range(self.total_pages))

    def _render_page(self, page_num: int) -> fitz.Pixmap | None:
        """Render a PDF page to a pixmap.

        Args:
            page_num: Page number (0-indexed)

        Returns
        -------
            fitz.Pixmap object if successful, None otherwise
        """
        try:
            doc = fitz.open(self.input_pdf)
            page = doc[page_num]

            # Calculate zoom factor based on DPI
            zoom = self.dpi / 72.0
            matrix = fitz.Matrix(zoom, zoom)

            # Render page to pixmap
            pixmap = page.get_pixmap(matrix=matrix)

            doc.close()
        except (OSError, RuntimeError, ValueError):
            logger.exception(f"Failed to render page {page_num + 1}")
            return None
        else:
            logger.debug(f"Rendered page {page_num + 1} at {self.dpi} DPI")
            return pixmap

    def _save_image(self, pixmap: fitz.Pixmap, page_num: int) -> Path:
        """Save pixmap to image file.

        Args:
            pixmap: Pixmap object containing rendered page
            page_num: Page number (0-indexed, used for filename)

        Returns
        -------
            Path to the saved image file
        """
        # Generate output filename
        output_filename = f"{self.input_pdf.stem}_page{page_num + 1:03d}.{self.img_format}"
        output_path = self.output_dir / output_filename

        # Determine output format for save method
        output_format = self.img_format
        if output_format == "jpg":
            output_format = "jpeg"

        # Save image with appropriate settings
        lossy_formats = {"jpeg", "webp"}
        if output_format in lossy_formats:
            # Lossy formats support quality setting
            pixmap.save(str(output_path), output=output_format, jpg_quality=self.quality)
        else:
            # PNG and other lossless formats
            pixmap.save(str(output_path), output=output_format)

        logger.debug(f"Saved page {page_num + 1} to {output_path}")
        return output_path


def parse_args() -> argparse.Namespace:
    """Parse command line arguments for the pdf2img tool.

    Returns
    -------
        Namespace object containing parsed arguments
    """
    parser = argparse.ArgumentParser(
        description="Convert PDF pages to image files.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  pdf2img document.pdf                     # Convert all pages to PNG
  pdf2img document.pdf -d 300              # High resolution (300 DPI)
  pdf2img document.pdf -f jpg -q 95        # Convert to JPG with high quality
  pdf2img document.pdf -r "1,3,5-10"       # Convert specific pages
  pdf2img document.pdf -r "-5"             # Convert first 5 pages
  pdf2img document.pdf -r "10-"            # Convert from page 10 to end
        """,
    )

    parser.add_argument(
        "input",
        type=str,
        help="Input PDF file",
    )
    parser.add_argument(
        "output",
        type=str,
        nargs="?",
        default=None,
        help="Output directory (optional, defaults to '<pdf_name>_images' in same directory)",
    )
    parser.add_argument(
        "-d",
        "--dpi",
        type=int,
        default=conf.DPI,
        help=f"DPI for image output (default: {conf.DPI})",
    )
    parser.add_argument(
        "-f",
        "--format",
        type=str,
        choices=["png", "jpg", "jpeg"],
        default=conf.FORMAT,
        help=f"Output image format (default: {conf.FORMAT})",
    )
    parser.add_argument(
        "-q",
        "--quality",
        type=int,
        default=conf.QUALITY,
        help=f"Image quality 1-100 for JPG/WebP (default: {conf.QUALITY})",
    )
    parser.add_argument(
        "-r",
        "--range",
        type=str,
        default=None,
        help="Page range to convert, e.g., '1,3,5-10' (default: all pages)",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Enable verbose output",
    )

    return parser.parse_args()


def main() -> None:
    """Run entry point for the command-line interface.

    Pdf2img utility tool

    Examples
    --------
      pdf2img [options] <input_pdf> [output_dir]
    """
    args = parse_args()

    if args.verbose:
        logger.setLevel(logging.DEBUG)

    # Convert string paths to Path objects
    input_file = Path(args.input)
    output_dir = Path(args.output) if args.output else None

    # Validate input file
    if not input_file.is_file():
        logger.error(f"Input file does not exist: {input_file}")
        return

    # Set default output directory if not specified
    if output_dir is None:
        output_dir = input_file.parent / f"{input_file.stem}_images"

    # Validate quality range
    if not 1 <= args.quality <= conf.MAX_QUALITY:
        logger.error(f"Quality must be between 1 and {conf.MAX_QUALITY}")
        return

    # Create runner and execute
    runner = PdfToImageRunner(
        input_pdf=input_file,
        output_dir=output_dir,
        dpi=args.dpi,
        img_format=args.format,
        quality=args.quality,
        page_range=args.range,
    )

    runner.run()


if __name__ == "__main__":
    main()
