///|
pub(all) enum AcquisitionMode {
  Online
  Offline
} derive(Eq, Debug)

///|
/// Versioned keys scoped by provider. Providers may add pinned-reference mappings
/// without using repository text or content digests as physical paths.
pub struct CacheKey {
  priv value : String
} derive(Eq, Hash, Debug)

///|
pub fn CacheKey::reference(
  provider : ProviderId,
  reference : ArtifactKey,
) -> CacheKey {
  let encoded = Json::array([
    Json::string(provider.to_string()),
    Json::string(reference.to_string()),
  ]).stringify()
  {
    value: "ref-v1-" +
    ContentDigest::sha256(@utf8.encode(encoded)).to_string()[7:].to_owned(),
  }
}

///|
pub fn CacheKey::content(digest : ContentDigest) -> CacheKey {
  { value: "sha256-" + digest.to_string()[7:].to_owned(), }
}

///|
pub fn CacheKey::to_string(self : CacheKey) -> String {
  self.value
}

///|
/// Host storage must publish complete values atomically. Missing values return
/// None; read/write failures raise CacheIo. Reads must enforce the supplied bound.
pub(all) struct CacheStore {
  read : (CacheKey, Int32) -> Bytes? raise ResourceError
  publish : (CacheKey, Bytes) -> Unit raise ResourceError
}

///|
pub struct VerifiedCache {
  priv store : CacheStore
}

///|
pub fn VerifiedCache::new(store : CacheStore) -> VerifiedCache {
  { store, }
}

///|
pub(all) struct AcquiredBytes {
  metadata : Acquisition
  body : Bytes
}

///|
fn http_cache_key(
  url : HttpUrl,
  authorization : Authorization?,
  accept : HttpAccept,
) -> CacheKey {
  if authorization is Some(_) || accept != AnyContent {
    let identity : Json = [
      url.to_string(),
      authorization
      .map(a => ContentDigest::sha256(@utf8.encode(a.header)).to_string())
      .unwrap_or("anonymous"),
      if accept == OctetStream {
        "octet-stream"
      } else {
        "any"
      },
    ]
    return CacheKey::reference(
      ProviderId::parse("authorized-http").unwrap(),
      ArtifactKey::parse(identity.stringify()).unwrap(),
    )
  }
  CacheKey::reference(
    ProviderId::parse("public-http").unwrap(),
    ArtifactKey::parse(url.to_string()).unwrap(),
  )
}

///|
fn cache_metadata(
  bytes : Bytes,
  requested : HttpUrl,
) -> Acquisition raise ResourceError {
  fn decode() -> Acquisition? raise {
    let text = @utf8.decode(bytes)
    let json = @json.parse(text)
    guard json
      is {
        "version": 1,
        "requested": String(a),
        "resolved": String(b),
        "digest": String(c),
        ..
      } else {
      return None
    }
    guard HttpUrl::parse(a) is Some(a) &&
      a == requested &&
      HttpUrl::parse(b) is Some(b) &&
      ContentDigest::parse(c) is Some(c) else {
      return None
    }
    Some(Acquisition::{ requested: a, resolved: b, digest: c, })
  }
  let parsed = decode() catch { _ => None }
  guard parsed is Some(metadata) else {
    raise ResourceError(
      CacheCorrupt,
      Provenance::new(requested.location()),
      "Invalid cached acquisition metadata",
    )
  }
  metadata
}

///|
/// Shared byte acquisition policy, usable independently of archive decoding.
/// Online hits are reused without revalidation over HTTP. Corruption always fails
/// closed; callers must remove the damaged cache entry before fetching again.
pub fn acquire(
  requested : HttpUrl,
  transport : HttpTransport,
  expected_digest~ : ContentDigest? = None,
  limits~ : AcquisitionLimits = AcquisitionLimits::new().unwrap(),
  cache~ : VerifiedCache? = None,
  mode~ : AcquisitionMode = Online,
  verify~ : IntegrityVerifier = (_, _) => (),
  credentials~ : Credentials = Credentials::new().unwrap(),
  accept~ : HttpAccept = AnyContent,
) -> AcquiredBytes raise ResourceError {
  let origin = Provenance::new(requested.location())
  let authorization = credentials.for_url(requested)
  let reference_key = http_cache_key(requested, authorization, accept)
  fn content_key(digest : ContentDigest) -> CacheKey {
    match authorization {
      None => CacheKey::content(digest)
      Some(auth) =>
        CacheKey::reference(
          ProviderId::parse("authorized-content").unwrap(),
          ArtifactKey::parse(
            Json::array([
              Json::string(requested.origin().to_string()),
              Json::string(digest.to_string()),
              Json::string(
                ContentDigest::sha256(@utf8.encode(auth.header)).to_string(),
              ),
            ]).stringify(),
          ).unwrap(),
        )
    }
  }
  if cache is Some(cache) &&
    (cache.store.read)(reference_key, 32768) is Some(bytes) {
    let metadata = cache_metadata(bytes, requested)
    let at = Provenance::new(metadata.resolved.location()).acquired(metadata)
    guard (cache.store.read)(content_key(metadata.digest), limits.max_bytes)
      is Some(body) else {
      raise ResourceError(
        CacheCorrupt,
        at,
        "Cached acquisition content is missing",
      )
    }
    if body.length() > limits.max_bytes {
      raise ResourceError(
        ResourceLimit,
        at,
        "Cached content exceeds archive byte limit",
      )
    }
    if ContentDigest::sha256(body) != metadata.digest {
      raise ResourceError(
        CacheCorrupt,
        at,
        "Cached content does not match its SHA-256 digest",
      )
    }
    if expected_digest is Some(expected) && expected != metadata.digest {
      raise ResourceError(
        IntegrityFailure,
        at,
        "Expected \{expected}, received \{metadata.digest}",
      )
    }
    verify(body, at)
    return { metadata, body, }
  }
  if mode == Offline {
    raise ResourceError(
      OfflineMiss,
      origin,
      "No verified cached acquisition for requested URL",
    )
  }
  let response = transport({ url: requested, authorization, accept, }, limits)
  if response.body.length() > limits.max_bytes {
    raise ResourceError(
      ResourceLimit,
      origin,
      "Download exceeds archive byte limit",
    )
  }
  let metadata : Acquisition = {
    requested,
    resolved: response.resolved,
    digest: ContentDigest::sha256(response.body),
  }
  let at = Provenance::new(response.resolved.location()).acquired(metadata)
  if response.status != 200 {
    let code = match response.status {
      429 => RateLimited
      401 | 403 => AuthenticationRequired
      404 | 410 => MissingAsset
      _ => HttpStatus
    }
    raise ResourceError(code, at, "HTTP status \{response.status}")
  }
  if expected_digest is Some(expected) && expected != metadata.digest {
    raise ResourceError(
      IntegrityFailure,
      at,
      "Expected \{expected}, received \{metadata.digest}",
    )
  }
  verify(response.body, at)
  if cache is Some(cache) {
    (cache.store.publish)(content_key(metadata.digest), response.body)
    let json : Json = {
      "version": 1,
      "requested": requested.to_string(),
      "resolved": metadata.resolved.to_string(),
      "digest": metadata.digest.to_string(),
    }
    (cache.store.publish)(reference_key, @utf8.encode(json.stringify()))
  }
  { metadata, body: response.body, }
}