///|
/// Core value types for the object-store abstraction.
///
/// The shape follows the two designs this module is modelled on: walgit's
/// "the manifest CAS is the only commit point" and waltier's insistence that
/// a write outcome is a three-way answer, not a boolean. See
/// `docs/scalable-git-server.md`.

///|
/// Precondition attached to a write.
///
/// The backing store maps these onto its own primitive:
/// S3 / R2 use `If-None-Match: *` and `If-Match: `; GCS uses
/// `ifGenerationMatch=0` and `ifGenerationMatch=`.
pub(all) enum PutCondition {
  /// Overwrite unconditionally. Only safe for content-addressed keys.
  Unconditional
  /// Create only. Fails if the key already exists.
  IfNotExists
  /// Compare-and-swap against a known version.
  IfMatch(String)
} derive(Eq, Debug)

///|
/// Result of a write.
///
/// `Unknown` is not an error and must not be retried blindly: the write may
/// or may not have landed. The caller re-reads and reconciles. Collapsing it
/// into failure is how a WAL corrupts itself.
pub(all) enum PutOutcome {
  /// The write landed. Carries the new version tag.
  Applied(String)
  /// The precondition did not hold. The store is unchanged; re-read and retry.
  NotApplied
  /// Indeterminate (timeout, 5xx). State must be reconciled by reading.
  Unknown
} derive(Eq, Debug)

///|
/// Result of a read.
pub(all) enum GetOutcome {
  /// Body plus the version tag it was read at.
  Found(Bytes, String)
  /// The caller's `if_none_match` version is still current (HTTP 304).
  NotModified
  /// No such key.
  Missing
} derive(Eq, Debug)

///|
/// One entry in a listing.
pub(all) struct ObjEntry {
  key : String
  size : Int64
  etag : String
} derive(Eq, Debug)

///|
/// A page of a listing. `next` carries the continuation token when the store
/// truncated the result.
pub(all) struct ObjListing {
  entries : Array[ObjEntry]
  next : String?
} derive(Eq, Debug)

///|
/// Inclusive byte range, matching HTTP `Range: bytes=-`.
pub(all) struct ByteRange {
  start : Int64
  end : Int64
} derive(Eq, Debug)

///|
/// Extract the body of a successful read, raising when the key is absent.
///
/// Convenience for call sites that have already established the key must
/// exist; `Missing` there means the store is corrupt, not that the caller
/// should branch.
pub fn GetOutcome::unwrap(
  self : GetOutcome,
  key : String,
) -> (Bytes, String) raise @bit.GitError {
  match self {
    Found(body, etag) => (body, etag)
    NotModified => raise @bit.GitError::IoError("unexpected 304 for \{key}")
    Missing => raise @bit.GitError::IoError("object not found: \{key}")
  }
}

///|
/// Whether a write landed.
pub fn PutOutcome::is_applied(self : PutOutcome) -> Bool {
  match self {
    Applied(_) => true
    _ => false
  }
}

///|
pub impl Show for PutCondition with fn output(self, logger) {
  match self {
    Unconditional => logger.write_string("Unconditional")
    IfNotExists => logger.write_string("IfNotExists")
    IfMatch(etag) => logger.write_string("IfMatch(\{etag})")
  }
}

///|
pub impl Show for PutOutcome with fn output(self, logger) {
  match self {
    Applied(etag) => logger.write_string("Applied(\{etag})")
    NotApplied => logger.write_string("NotApplied")
    Unknown => logger.write_string("Unknown")
  }
}

///|
/// Bodies are shown by length, not content: a `GetOutcome` in a failing test
/// is usually a whole pack.
pub impl Show for GetOutcome with fn output(self, logger) {
  match self {
    Found(body, etag) =>
      logger.write_string("Found(\{body.length()} bytes, \{etag})")
    NotModified => logger.write_string("NotModified")
    Missing => logger.write_string("Missing")
  }
}