///|
/// Immutable image bytes. JSONL stores base64; no path or provider ID is needed
/// to replay the image. Constructors check size and raster signatures, not a
/// full pixel decode. Images are never resized or re-encoded by this module.
pub struct Image {
  media_type : String
  bytes : Bytes
} derive(Eq)

///|
/// Invalid image input, distinguished without retaining the base64 payload.
pub(all) suberror ImageError {
  TooLarge
  InvalidBase64
  UnsupportedFormat
  MediaTypeMismatch(expected~ : String, actual~ : String)
} derive(Debug, Eq)

///|
impl Show for ImageError with fn output(self, logger) {
  match self {
    TooLarge => logger.write_string("image exceeds the 5 MiB limit")
    InvalidBase64 =>
      logger.write_string("image data must be canonical padded base64")
    UnsupportedFormat =>
      logger.write_string("expected PNG, JPEG, GIF, or WebP image bytes")
    MediaTypeMismatch(expected~, actual~) =>
      logger.write_string(
        "image declares \{expected} but bytes identify \{actual}",
      )
  }
}

///|
/// Limit applies to decoded bytes; base64 input is bounded before allocating.
pub fn Image::from_bytes(bytes : Bytes) -> Image raise ImageError {
  if bytes.length() > 5 * 1024 * 1024 {
    raise TooLarge
  }
  let media_type = match bytes {
    [b'\x89', b'P', b'N', b'G', b'\r', b'\n', b'\x1a', b'\n', ..] => "image/png"
    [b'\xff', b'\xd8', b'\xff', ..] => "image/jpeg"
    [b'G', b'I', b'F', b'8', b'7' | b'9', b'a', ..] => "image/gif"
    [b'R', b'I', b'F', b'F', _, _, _, _, b'W', b'E', b'B', b'P', ..] =>
      "image/webp"
    _ => raise UnsupportedFormat
  }
  { media_type, bytes, }
}

///|
/// Decode canonical padded base64 and verify the declared MIME against its signature.
pub fn Image::from_base64(
  media_type~ : StringView,
  data : StringView,
) -> Image raise ImageError {
  if data.length() > (5 * 1024 * 1024 + 2) / 3 * 4 {
    raise TooLarge
  }
  let bytes = @base64.decode(data) catch { _ => raise InvalidBase64 }
  if @base64.encode(bytes)[:] != data {
    raise InvalidBase64
  }
  let image = Image::from_bytes(bytes)
  if image.media_type[:] != media_type {
    raise MediaTypeMismatch(
      expected=media_type.to_owned(),
      actual=image.media_type,
    )
  }
  image
}

///|
/// Build an inline URL suitable for local image display.
pub fn Image::data_url(self : Image) -> String {
  "data:\{self.media_type};base64,\{@base64.encode(self.bytes)}"
}

///|
/// Debug output must not expand a whole binary image into logs or snapshots.
pub impl Debug for Image with fn to_repr(self) {
  Repr("Image(\{self.media_type}, \{self.bytes.length()} bytes)")
}

///|
pub extend Image with Eq::{equal, not_equal}

///|
pub extend ImageError with Debug::{to_repr}

///|
pub extend ImageError with Eq::{equal, not_equal}

///|
pub extend Image with Debug::{to_repr}

///|
/// Decode an inline raster image URL, retaining its original file bytes.
pub fn Image::from_data_url(url : String) -> Image raise ImageError {
  guard url.strip_prefix("data:") is Some(value) &&
    value.split_once(";base64,") is Some((media_type, data)) else {
    raise InvalidBase64
  }
  Image::from_base64(media_type~, data)
}