"""On-demand bounding-box rendering for detection alerts."""

from __future__ import annotations

from collections.abc import Sequence
from importlib import import_module
from io import BytesIO
import math
from typing import Any

from cmvr_edge_ai.contracts import Detection, EncodedImage, ImageFrame


class ImageAnnotationDependencyError(RuntimeError):
    """Raised when the optional image annotation runtime is unavailable."""


class ImageAnnotationError(ValueError):
    """Raised when an input frame cannot be rendered safely."""


def ensure_annotation_support() -> None:
    """Fail early with an actionable message when Pillow is not installed."""

    _pillow_modules()


def render_annotated_jpeg(
    frame: ImageFrame,
    detections: Sequence[Detection],
    *,
    jpeg_quality: int,
) -> EncodedImage:
    """Draw valid detection boxes and encode one decoded frame as JPEG."""

    if isinstance(jpeg_quality, bool) or not isinstance(jpeg_quality, int):
        raise ImageAnnotationError("jpeg_quality must be an integer")
    if not 1 <= jpeg_quality <= 95:
        raise ImageAnnotationError("jpeg_quality must be between 1 and 95")
    _validate_frame(frame)
    image_module, image_draw_module = _pillow_modules()

    raw_mode = "BGR" if frame.pixel_format.upper() == "BGR8" else "RGB"
    image = image_module.frombytes(
        "RGB",
        (frame.width, frame.height),
        frame.data,
        "raw",
        raw_mode,
    )
    draw = image_draw_module.Draw(image)
    for detection in detections:
        if not isinstance(detection, Detection):
            raise ImageAnnotationError(
                "detections must contain Detection instances"
            )
        box = _clipped_box(detection, frame.width, frame.height)
        if box is None:
            # One corrupt backend box must not prevent the other evidence boxes
            # or the structured safety alert from being produced.
            continue
        color = _label_color(detection.label)
        draw.rectangle(box, outline=color, width=3)
        label = f"{detection.label} {detection.confidence:.2f}"
        text_box = draw.textbbox((box[0], box[1]), label)
        text_width = max(1, text_box[2] - text_box[0])
        text_height = max(1, text_box[3] - text_box[1])
        text_y = max(0.0, box[1] - text_height - 4)
        background = (
            box[0],
            text_y,
            min(float(frame.width - 1), box[0] + text_width + 4),
            min(float(frame.height - 1), text_y + text_height + 4),
        )
        draw.rectangle(background, fill=color)
        draw.text((box[0] + 2, text_y + 2), label, fill=(255, 255, 255))

    output = BytesIO()
    image.save(output, format="JPEG", quality=jpeg_quality)
    return EncodedImage(
        data=output.getvalue(),
        media_type="image/jpeg",
        width=frame.width,
        height=frame.height,
    )


def _pillow_modules() -> tuple[Any, Any]:
    try:
        return import_module("PIL.Image"), import_module("PIL.ImageDraw")
    except ImportError as exc:
        raise ImageAnnotationDependencyError(
            "alert images require Pillow; install the 'image' extra "
            "(uv sync --extra image)"
        ) from exc


def _validate_frame(frame: ImageFrame) -> None:
    if not isinstance(frame, ImageFrame):
        raise ImageAnnotationError(
            f"expected ImageFrame, got {type(frame).__name__}"
        )
    if not isinstance(frame.codec, str):
        raise ImageAnnotationError("frame codec must be a string")
    if frame.codec.lower() not in {"", "none", "raw"}:
        raise ImageAnnotationError("alert images require a decoded ImageFrame")
    if not isinstance(frame.pixel_format, str) or frame.pixel_format.upper() not in {
        "BGR8",
        "RGB8",
    }:
        raise ImageAnnotationError(
            "alert images support only packed BGR8 or RGB8 frames"
        )
    if (
        isinstance(frame.width, bool)
        or not isinstance(frame.width, int)
        or frame.width < 1
        or isinstance(frame.height, bool)
        or not isinstance(frame.height, int)
        or frame.height < 1
    ):
        raise ImageAnnotationError("frame dimensions must be positive integers")
    expected_size = frame.width * frame.height * 3
    if not isinstance(frame.data, bytes):
        raise ImageAnnotationError("packed frame buffer must be bytes")
    if len(frame.data) != expected_size:
        raise ImageAnnotationError(
            f"packed frame buffer has {len(frame.data)} bytes; "
            f"expected {expected_size}"
        )


def _clipped_box(
    detection: Detection,
    width: int,
    height: int,
) -> tuple[float, float, float, float] | None:
    values = (
        detection.box.x_min,
        detection.box.y_min,
        detection.box.x_max,
        detection.box.y_max,
    )
    if any(
        isinstance(value, bool)
        or not isinstance(value, (int, float))
        or not math.isfinite(float(value))
        for value in values
    ):
        return None
    x_min, y_min, x_max, y_max = (float(value) for value in values)
    if x_max <= x_min or y_max <= y_min:
        return None
    clipped = (
        min(max(x_min, 0.0), float(width - 1)),
        min(max(y_min, 0.0), float(height - 1)),
        min(max(x_max, 0.0), float(width - 1)),
        min(max(y_max, 0.0), float(height - 1)),
    )
    if clipped[2] <= clipped[0] or clipped[3] <= clipped[1]:
        return None
    return clipped


def _label_color(label: str) -> tuple[int, int, int]:
    # Stable across processes unlike Python's randomized hash().
    seed = sum((index + 1) * byte for index, byte in enumerate(label.encode("utf-8")))
    return (64 + seed % 192, 64 + (seed * 5) % 192, 64 + (seed * 11) % 192)


__all__ = [
    "ImageAnnotationDependencyError",
    "ImageAnnotationError",
    "ensure_annotation_support",
    "render_annotated_jpeg",
]
