"""Helpers for converting signature bounding boxes into PNG crops."""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, overload

from .detector.file_result_model import FileResult
from .detector.signature_model import Signature

try:  # pragma: no cover - optional dependency
    import fitz  # type: ignore
except Exception:  # pragma: no cover - optional dependency
    fitz = None  # type: ignore[misc]


class SignatureCroppingUnavailable(RuntimeError):
    """Raised when PNG cropping cannot be performed (e.g., PyMuPDF missing)."""


@dataclass(slots=True)
class SignatureCrop:
    """PNG crop metadata and in-memory content."""

    path: Path
    image_bytes: bytes
    signature: Signature
    saved_to_disk: bool = True


@overload
def crop_signatures(
    pdf_path: Path,
    file_result: FileResult,
    *,
    output_dir: Path,
    dpi: int = 200,
    logger: logging.Logger | None = None,
    return_bytes: Literal[False] = False,
) -> list[Path]: ...


@overload
def crop_signatures(
    pdf_path: Path,
    file_result: FileResult,
    *,
    output_dir: Path,
    dpi: int = 200,
    logger: logging.Logger | None = None,
    return_bytes: Literal[True] = True,
) -> list[SignatureCrop]: ...


def crop_signatures(
    pdf_path: Path,
    file_result: FileResult,
    *,
    output_dir: Path,
    dpi: int = 200,
    logger: logging.Logger | None = None,
    return_bytes: bool = False,
    save_files: bool = True,
) -> list[Path] | list[SignatureCrop]:
    """Render each signature bounding box to a PNG image using PyMuPDF.

    Set ``return_bytes=True`` to collect in-memory PNG bytes for each crop while also writing
    the files to ``output_dir``. Set ``save_files=False`` to skip writing PNGs to disk.
    """

    if fitz is None:  # pragma: no cover - exercised when dependency absent
        raise SignatureCroppingUnavailable(
            "PyMuPDF is required for PNG crops. Install 'pymupdf' or 'sigdetect[pymupdf]'."
        )
    if not save_files and not return_bytes:
        raise ValueError("At least one of save_files or return_bytes must be True")

    pdf_path = Path(pdf_path)
    output_dir = Path(output_dir)
    if save_files:
        output_dir.mkdir(parents=True, exist_ok=True)
    generated_paths: list[Path] = []
    generated_crops: list[SignatureCrop] = []

    with fitz.open(pdf_path) as document:  # type: ignore[attr-defined]
        per_document_dir = output_dir / pdf_path.stem
        if save_files:
            per_document_dir.mkdir(parents=True, exist_ok=True)
        scale = dpi / 72.0
        matrix = fitz.Matrix(scale, scale)

        for index, signature in enumerate(file_result.Signatures, start=1):
            if not signature.BoundingBox or not signature.Page:
                continue
            try:
                page = document.load_page(signature.Page - 1)
            except Exception as exc:  # pragma: no cover - defensive
                if logger:
                    logger.warning(
                        "Failed to load page for signature crop",
                        extra={
                            "file": pdf_path.name,
                            "page": signature.Page,
                            "error": str(exc),
                        },
                    )
                continue

            clip = _to_clip_rect(page, signature.BoundingBox)
            if clip is None:
                continue

            filename = _build_filename(index, signature)
            destination = per_document_dir / filename

            try:
                image_bytes: bytes | None = None
                pixmap = page.get_pixmap(matrix=matrix, clip=clip, alpha=False)
                if save_files:
                    pixmap.save(destination)
                if return_bytes:
                    image_bytes = pixmap.tobytes("png")
            except Exception as exc:  # pragma: no cover - defensive
                if logger:
                    logger.warning(
                        "Failed to render signature crop",
                        extra={
                            "file": pdf_path.name,
                            "page": signature.Page,
                            "field": signature.FieldName,
                            "error": str(exc),
                        },
                    )
                continue

            if save_files:
                signature.CropPath = str(destination)
                generated_paths.append(destination)
            if return_bytes:
                if image_bytes is None:  # pragma: no cover - defensive
                    continue
                generated_crops.append(
                    SignatureCrop(
                        path=destination,
                        image_bytes=image_bytes,
                        signature=signature,
                        saved_to_disk=save_files,
                    )
                )

    return generated_crops if return_bytes else generated_paths


def _to_clip_rect(page, bbox: tuple[float, float, float, float]):
    width = float(page.rect.width)
    height = float(page.rect.height)

    x0, y0, x1, y1 = bbox
    left = _clamp(min(x0, x1), 0.0, width)
    right = _clamp(max(x0, x1), 0.0, width)

    top = _clamp(height - max(y0, y1), 0.0, height)
    bottom = _clamp(height - min(y0, y1), 0.0, height)

    if right - left <= 0 or bottom - top <= 0:
        return None
    return fitz.Rect(left, top, right, bottom)


def _clamp(value: float, lower: float, upper: float) -> float:
    return max(lower, min(value, upper))


def _build_filename(index: int, signature: Signature) -> str:
    base = signature.Role or signature.FieldName or "signature"
    slug = _slugify(base)
    return f"sig_{index:02d}_{slug}.png"


def _slugify(value: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9_-]+", "_", value.strip().lower())
    cleaned = cleaned.strip("_")
    return cleaned or "signature"
