from __future__ import annotations

from io import BytesIO
from dataclasses import dataclass, field

from rights_filter.domain.records import DataClass

FaceBox = tuple[int, int, int, int]


class PreprocessingError(ValueError):
    pass


@dataclass(frozen=True)
class ImagePayload:
    content: bytes
    width: int
    height: int
    metadata: dict[str, str] = field(default_factory=dict)
    data_class: DataClass = DataClass.ORIGINAL_IMAGE


def build_external_derivative(
    original: ImagePayload, max_side: int = 1600
) -> ImagePayload:
    if not original.content:
        raise PreprocessingError("empty image content")
    if original.width <= 0 or original.height <= 0:
        raise PreprocessingError("image dimensions must be positive")

    longest_side = max(original.width, original.height)
    if longest_side > max_side:
        scale = max_side / longest_side
        width = max(1, round(original.width * scale))
        height = max(1, round(original.height * scale))
    else:
        width = original.width
        height = original.height

    if original.metadata.get("format", "").upper() == "AVIF":
        content = _jpeg_derivative(original.content, width, height)
    else:
        # Re-encode through PIL to drop embedded EXIF/GPS metadata from the image
        # bytes. Clearing only the metadata dict (below) left camera serials and
        # GPS coordinates inside the bytes that get shipped to external APIs.
        content = _reencode_without_metadata(original.content, width, height)
    if content is None:
        content = original.content

    return ImagePayload(
        content=content,
        width=width,
        height=height,
        metadata={},
        data_class=DataClass.EXTERNAL_DERIVATIVE,
    )


def build_face_crop_derivatives(
    original: ImagePayload,
    face_boxes: list[FaceBox] | tuple[FaceBox, ...],
    max_crops: int = 3,
    padding_ratio: float = 0.25,
    max_side: int = 768,
) -> list[ImagePayload]:
    if not original.content or not face_boxes:
        return []

    try:
        from PIL import Image

        with Image.open(BytesIO(original.content)) as image:
            image = image.convert("RGB")
            crops: list[ImagePayload] = []
            image_width, image_height = image.size
            for box in face_boxes[:max_crops]:
                try:
                    bounds = _padded_bounds(box, image_width, image_height, padding_ratio)
                    if bounds is None:
                        continue
                    crop = image.crop(bounds)
                    crop = _resize_to_max_side(crop, max_side)
                    output = BytesIO()
                    crop.save(output, format="JPEG", quality=90, optimize=True)
                    crops.append(
                        ImagePayload(
                            content=output.getvalue(),
                            width=int(crop.width),
                            height=int(crop.height),
                            metadata={},
                            data_class=DataClass.EXTERNAL_DERIVATIVE,
                        )
                    )
                except Exception:
                    # Skip a single bad box instead of discarding all crops.
                    continue
            return crops
    except Exception:
        return []


def _jpeg_derivative(content: bytes, width: int, height: int) -> bytes | None:
    try:
        from PIL import Image

        with Image.open(BytesIO(content)) as image:
            image = image.convert("RGB")
            if image.size != (width, height):
                image = image.resize((width, height))
            output = BytesIO()
            image.save(output, format="JPEG", quality=90, optimize=True)
            return output.getvalue()
    except Exception:
        return None


_REENCODABLE_FORMATS = {"JPEG", "PNG", "WEBP", "GIF", "BMP", "TIFF"}


def _reencode_without_metadata(content: bytes, width: int, height: int) -> bytes | None:
    """Re-save the image (preserving format where possible) so embedded EXIF/GPS
    metadata is dropped. Returns None when PIL is unavailable or the bytes are
    not a decodable image, so the caller can fall back to the original content."""
    try:
        from PIL import Image

        with Image.open(BytesIO(content)) as image:
            source_format = (image.format or "").upper()
            save_format = source_format if source_format in _REENCODABLE_FORMATS else "JPEG"
            if save_format in {"JPEG", "BMP"} and image.mode not in {"RGB", "L"}:
                image = image.convert("RGB")
            elif image.mode == "P":
                image = image.convert("RGBA")
            if image.size != (width, height):
                image = image.resize((width, height))
            # Rebuild from raw pixels so NO metadata (EXIF/GPS/ICC) survives —
            # Pillow can otherwise re-emit image.info["exif"] when re-saving.
            clean = Image.frombytes(image.mode, image.size, image.tobytes())
            output = BytesIO()
            clean.save(output, format=save_format)
            return output.getvalue()
    except Exception:
        return None


def _padded_bounds(
    box: FaceBox,
    image_width: int,
    image_height: int,
    padding_ratio: float,
) -> tuple[int, int, int, int] | None:
    x, y, width, height = (int(value) for value in box)
    if width <= 0 or height <= 0 or image_width <= 0 or image_height <= 0:
        return None

    pad_x = round(width * padding_ratio)
    pad_y = round(height * padding_ratio)
    left = max(0, x - pad_x)
    top = max(0, y - pad_y)
    right = min(image_width, x + width + pad_x)
    bottom = min(image_height, y + height + pad_y)
    if right <= left or bottom <= top:
        return None
    return (left, top, right, bottom)


def _resize_to_max_side(image: object, max_side: int) -> object:
    longest_side = max(int(image.width), int(image.height))
    if longest_side <= max_side:
        return image

    scale = max_side / longest_side
    width = max(1, round(int(image.width) * scale))
    height = max(1, round(int(image.height) * scale))
    return image.resize((width, height))
