"""Image sanitization for user-uploaded photos.

Re-encodes inbound images through Pillow to:
  - strip EXIF (GPS lat/lon, device fingerprint, owner metadata)
  - normalize byte stream (a polyglot JPEG with appended payload survives MIME check
    but is dropped on re-encode — only the actual image survives)
  - enforce max dimension (defend against pixel-flood DoS in downstream renders)
  - produce uniform JPEG output (smaller storage; no transparency support needed
    for apartment photos)

Closes finding #6 from 2026-05-24 trade-in audit.
"""

from __future__ import annotations

import io
import logging

from PIL import Image, ImageOps, UnidentifiedImageError

logger = logging.getLogger(__name__)

# Max longest edge after re-encode. 2400px = enough for retina apt photo,
# small enough to bound storage & decode cost.
_MAX_DIMENSION = 2400

# JPEG quality. 85 is industry standard for photos — visually lossless,
# ~70% size reduction vs original phone JPEGs.
_JPEG_QUALITY = 85

# Output content type after sanitization (all inputs transcoded to JPEG).
SANITIZED_CONTENT_TYPE = "image/jpeg"


class ImageSanitizationError(ValueError):
    """Raised when inbound bytes cannot be decoded as an image."""


def sanitize_image(content: bytes) -> tuple[bytes, str]:
    """Decode arbitrary image bytes and re-encode as clean JPEG.

    Args:
        content: raw uploaded bytes from the client.

    Returns:
        (sanitized_bytes, "image/jpeg") — both ready for storage/serving.

    Raises:
        ImageSanitizationError: bytes are not a recognized image format.
    """
    try:
        with Image.open(io.BytesIO(content)) as img:
            img.load()  # force full decode; surfaces UnidentifiedImageError early
            # Bake EXIF Orientation into pixels BEFORE stripping EXIF, else phone
            # portrait photos (Orientation=6/8) end up rotated 90/270° once the tag
            # is gone. No-op when there is no orientation tag.
            img = ImageOps.exif_transpose(img)
            # Convert any mode (RGBA, P, CMYK, ...) to RGB for JPEG output.
            if img.mode != "RGB":
                img = img.convert("RGB")
            # Resize if oversized; preserve aspect ratio.
            if max(img.size) > _MAX_DIMENSION:
                img.thumbnail((_MAX_DIMENSION, _MAX_DIMENSION), Image.Resampling.LANCZOS)
            buf = io.BytesIO()
            # save() does NOT carry over .info / EXIF unless we pass exif=...; we don't.
            img.save(buf, format="JPEG", quality=_JPEG_QUALITY, optimize=True)
            return buf.getvalue(), SANITIZED_CONTENT_TYPE
    except UnidentifiedImageError as e:
        raise ImageSanitizationError("not a recognized image format") from e
    except Exception as e:
        # PIL can raise broad errors (DecompressionBombError, OSError on truncated, ...);
        # normalize to one user-facing error type.
        logger.warning("image sanitization failed: %s", e)
        raise ImageSanitizationError(f"image decode failed: {e}") from e
