// BlobRef -- how a record points at a binary attachment: an image, a video, an
// avatar.
//
// Two encodings exist and both must be READ; only one may be WRITTEN.
//
//   {"$type":"blob","ref":{"$link":"bafkrei..."},"mimeType":"image/jpeg","size":12345}
//   {"cid":"bafkrei...","mimeType":"image/jpeg"}      <- legacy, pre-1.0
//
// The legacy form has no size and no `$type`. It still appears in old records,
// so refusing to decode it would make those records unreadable -- but writing
// one is refused, because a PDS rejects it. rsky enforces the same asymmetry,
// in its case by bailing out of the write path with "Legacy blob ref at ...".
//
// The distinction is an enum rather than a struct with optional fields. The
// alternative -- rsky's `Blob`, where `ref`, `cid` and `size` are all optional
// so one struct covers both shapes -- makes it possible to build a blob with
// neither `ref` nor `cid`, and its `size: Option` with no skip attribute
// emits `"size": null`, which changes the record's CID.

///|
/// A reference to a blob, in whichever of the two encodings it arrived in.
pub(all) enum BlobRef {
  /// The current form. `size` is the byte length the server recorded.
  Typed(cid~ : @syntax.Cid, mime_type~ : String, size~ : Int64)
  /// The pre-1.0 form. Readable, never writable; carries no size.
  Legacy(cid~ : @syntax.Cid, mime_type~ : String)
} derive(Eq, Debug)

///|
pub fn BlobRef::cid(self : Self) -> @syntax.Cid {
  match self {
    Typed(cid~, ..) => cid
    Legacy(cid~, ..) => cid
  }
}

///|
pub fn BlobRef::mime_type(self : Self) -> String {
  match self {
    Typed(mime_type~, ..) => mime_type
    Legacy(mime_type~, ..) => mime_type
  }
}

///|
/// `None` for a legacy ref, which does not carry one.
///
/// Note what this is not: a guarantee. The size is what the server recorded
/// when the blob was uploaded, and nothing re-checks it.
pub fn BlobRef::size(self : Self) -> Int64? {
  match self {
    Typed(size~, ..) => Some(size)
    Legacy(..) => None
  }
}

///|
pub fn BlobRef::is_legacy(self : Self) -> Bool {
  self is Legacy(..)
}

///|
/// Accepts both encodings.
///
/// The typed form's `ref` is a `cid-link`, so by the time this sees it the JSON
/// codec has already turned `{"$link": ...}` into a `Link`. The legacy form's
/// `cid` is a plain string, and has to be parsed here.
pub fn BlobRef::from_lex(
  value : LexValue,
  path? : String = "",
) -> BlobRef raise DecodeError {
  guard value.as_object() is Some(fields) else {
    raise DecodeError(path~, reason="expected a blob, got \{value.kind()}")
  }
  let mime_type = match fields.get("mimeType") {
    Some(Str(m)) => m
    _ => raise DecodeError(path~, reason="blob is missing mimeType")
  }
  if fields.get("$type") is Some(Str("blob")) {
    let cid = match fields.get("ref") {
      Some(Link(cid)) => cid
      _ =>
        raise DecodeError(
          path=field_path(path, "ref"),
          reason="blob ref must be a cid-link",
        )
    }
    // A missing size is tolerated as zero rather than refused: the field is
    // required by the schema, but a blob that names its content is still usable
    // without it, and refusing would lose the whole record.
    let size = match fields.get("size") {
      Some(Int(size)) => size
      _ => 0L
    }
    return Typed(cid~, mime_type~, size~)
  }
  match fields.get("cid") {
    Some(Str(text)) => {
      let cid = @syntax.Cid::parse(text) catch {
        e =>
          raise DecodeError(
            path=field_path(path, "cid"),
            reason="invalid blob cid: \{e.reason()}",
          )
      }
      Legacy(cid~, mime_type~)
    }
    _ =>
      raise DecodeError(
        path~,
        reason="blob has neither a $type of \"blob\" nor a legacy cid",
      )
  }
}

///|
/// Encodes back into whichever form it came from.
///
/// A legacy ref is NOT upgraded on the way out. Rewriting it into the typed
/// form would change the record's bytes and therefore its CID, turning a
/// read-modify-write into a different record.
pub fn BlobRef::to_lex(self : Self) -> LexValue {
  match self {
    Typed(cid~, mime_type~, size~) =>
      Obj({
        "$type": Str("blob"),
        "ref": Link(cid),
        "mimeType": Str(mime_type),
        "size": Int(size),
      })
    Legacy(cid~, mime_type~) =>
      Obj({ "cid": Str(cid.to_string()), "mimeType": Str(mime_type) })
  }
}