///|
fn maven_component(text : String) -> Bool {
  !text.is_empty() &&
  text.length() <= 128 &&
  text != "." &&
  text != ".." &&
  !text.contains("..") &&
  text
  .iter()
  .all(c => {
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-".contains(
      c.to_string(),
    )
  })
}

///|
fn maven_timestamp_snapshot(text : String) -> Bool {
  let parts = text.split("-").to_array()
  if parts.length() < 3 {
    return false
  }
  let timestamp = parts[parts.length() - 2]
  let build = parts[parts.length() - 1]
  if timestamp.length() != 15 ||
    timestamp[8] != 46 ||
    build.is_empty() ||
    !build.iter().all(c => "0123456789".contains(c.to_string())) {
    return false
  }
  for i in 0..<15 {
    if i == 8 {
      continue
    }
    let n = timestamp[i].to_int()
    if n < 48 || n > 57 {
      return false
    }
  }
  true
}

///|
pub struct MavenGroupId {
  priv value : String
} derive(Eq, Debug)

///|
pub fn MavenGroupId::parse(text : String) -> MavenGroupId? {
  if text.length() <= 255 &&
    text.split(".").all(part => maven_component(part.to_owned())) {
    Some({ value: text, })
  } else {
    None
  }
}

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

///|
pub struct MavenArtifactId {
  priv value : String
} derive(Eq, Debug)

///|
pub fn MavenArtifactId::parse(text : String) -> MavenArtifactId? {
  if maven_component(text) {
    Some({ value: text, })
  } else {
    None
  }
}

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

///|
pub struct MavenVersion {
  priv value : String
} derive(Eq, Debug)

///|
pub fn MavenVersion::parse(text : String) -> MavenVersion? {
  if maven_component(text) &&
    !text.to_upper().has_suffix("SNAPSHOT") &&
    !maven_timestamp_snapshot(text) &&
    !["LATEST", "RELEASE"].contains(text.to_upper()) {
    Some({ value: text, })
  } else {
    None
  }
}

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

///|
pub struct MavenClassifier {
  priv value : String
} derive(Eq, Debug)

///|
pub fn MavenClassifier::parse(text : String) -> MavenClassifier? {
  if maven_component(text) {
    Some({ value: text, })
  } else {
    None
  }
}

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

///|
/// Direct JAR coordinates only. No mutable snapshots, ranges or POM traversal.
pub(all) struct MavenCoordinates {
  group : MavenGroupId
  artifact : MavenArtifactId
  version : MavenVersion
  classifier : MavenClassifier?
} derive(Eq, Debug)

///|
pub fn MavenCoordinates::to_string(self : MavenCoordinates) -> String {
  let suffix = match self.classifier {
    Some(classifier) => ":" + classifier.value
    None => ""
  }
  self.group.value +
  ":" +
  self.artifact.value +
  ":" +
  self.version.value +
  suffix
}

///|
/// A repository-relative artifact location, distinct from paths inside its JAR.
pub struct MavenArtifactPath {
  priv value : String
} derive(Eq, Debug)

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

///|
pub fn MavenCoordinates::path(self : MavenCoordinates) -> MavenArtifactPath {
  let suffix = match self.classifier {
    Some(classifier) => "-" + classifier.value
    None => ""
  }
  {
    value: self.group.value.replace_all(old=".", new="/") +
    "/" +
    self.artifact.value +
    "/" +
    self.version.value +
    "/" +
    self.artifact.value +
    "-" +
    self.version.value +
    suffix +
    ".jar",
  }
}

///|
pub fn MavenCoordinates::key(self : MavenCoordinates) -> ArtifactKey {
  ArtifactKey::parse(
    Json::array([
      Json::string(self.group.value),
      Json::string(self.artifact.value),
      Json::string(self.version.value),
      match self.classifier {
        Some(classifier) => Json::string(classifier.value)
        None => Json::null()
      },
    ]).stringify(),
  ).unwrap()
}

///|
pub fn MavenCoordinates::from_key(key : ArtifactKey) -> MavenCoordinates? {
  let json = @json.parse(key.to_string()) catch { _ => return None }
  guard json is [String(group), String(artifact), String(version), classifier] &&
    MavenGroupId::parse(group) is Some(group) &&
    MavenArtifactId::parse(artifact) is Some(artifact) &&
    MavenVersion::parse(version) is Some(version) else {
    return None
  }
  let classifier = match classifier {
    Null => None
    String(text) => {
      guard MavenClassifier::parse(text) is Some(value) else { return None }
      Some(value)
    }
    _ => return None
  }
  Some({ group, artifact, version, classifier, })
}

///|
pub(all) enum MavenChecksum {
  Sha1Sidecar
  Sha256Sidecar
} derive(Eq, Debug)

///|
pub fn MavenChecksum::algorithm(self : MavenChecksum) -> ChecksumAlgorithm {
  match self {
    Sha1Sidecar => Sha1
    Sha256Sidecar => Sha256
  }
}

///|
pub(all) enum MavenVerification {
  PinnedDigest(ContentDigest)
  RepositoryChecksum(Acquisition, Checksum)
} derive(Eq, Debug)

///|
pub(all) struct MavenResolution {
  repository : RepositoryUrl
  coordinates : MavenCoordinates
  artifact : HttpUrl
  verification : MavenVerification
} derive(Eq, Debug)

///|
pub fn MavenResolution::to_string(self : MavenResolution) -> String {
  let verification = match self.verification {
    PinnedDigest(digest) => digest.to_string()
    RepositoryChecksum(_, checksum) => checksum.to_string()
  }
  "maven:" +
  self.coordinates.to_string() +
  " repository=" +
  self.repository.to_string() +
  " verification=" +
  verification
}

///|
pub(all) struct MavenArchive {
  repository : RepositoryUrl
  coordinates : MavenCoordinates
  checksum : MavenChecksum
  expected_digest : ContentDigest?
} derive(Eq, Debug)

///|
pub fn Provenance::maven_requested(
  self : Provenance,
  repository : RepositoryUrl,
  coordinates : MavenCoordinates,
) -> Provenance {
  let history = [MavenRequestStep(repository, coordinates)]
  history.append(self.history)
  { ..self, history, }
}

///|
pub fn Provenance::maven(
  self : Provenance,
  resolution : MavenResolution,
) -> Provenance {
  let history = [MavenStep(resolution)]
  history.append(self.history)
  { ..self, history, }
}

///|
pub fn MavenArchive::open(
  self : MavenArchive,
  transport : HttpTransport,
  acquisition_limits~ : AcquisitionLimits = AcquisitionLimits::new().unwrap(),
  limits~ : Limits = Limits::new(),
  cache~ : VerifiedCache? = None,
  mode~ : AcquisitionMode = Online,
  credentials~ : Credentials = Credentials::new().unwrap(),
) -> EntryTree raise ResourceError {
  self.open_resolved(
    transport,
    acquisition_limits~,
    limits~,
    cache~,
    mode~,
    credentials~,
  ).1
}

///|
fn MavenArchive::open_resolved(
  self : MavenArchive,
  transport : HttpTransport,
  acquisition_limits~ : AcquisitionLimits,
  limits~ : Limits,
  cache~ : VerifiedCache?,
  mode~ : AcquisitionMode,
  credentials~ : Credentials,
) -> (MavenResolution, EntryTree) raise ResourceError {
  let origin = Provenance::new(self.repository.location()).maven_requested(
    self.repository,
    self.coordinates,
  )
  check_limits(limits, self.repository.location())
  if limits.max_archive_bytes == 0 {
    raise ResourceError(ResourceLimit, origin, "Archive byte limit is zero")
  }
  try {
    let path = self.coordinates.path().to_string()
    let artifact = self.repository.endpoint(path)
    let verification = if self.expected_digest is Some(digest) {
      PinnedDigest(digest)
    } else {
      let data = acquire(
        self.repository.endpoint(
          path + "." + self.checksum.algorithm().to_string(),
        ),
        transport,
        limits={
          ..acquisition_limits,
          max_bytes: Int32::min(4096, acquisition_limits.max_bytes),
        },
        cache~,
        mode~,
        credentials~,
      )
      let text = @utf8.decode(data.body) catch {
        _ =>
          raise ResourceError(
            InvalidResolution,
            origin,
            "Expected a hexadecimal Maven checksum sidecar",
          )
      }
      guard Checksum::parse_hex(
          self.checksum.algorithm(),
          text.trim().to_owned(),
        )
        is Some(checksum) else {
        raise ResourceError(
          InvalidResolution,
          origin,
          "Expected a bare hexadecimal Maven " +
          self.checksum.algorithm().to_string() +
          " sidecar",
        )
      }
      RepositoryChecksum(data.metadata, checksum)
    }
    let resolution : MavenResolution = {
      repository: self.repository,
      coordinates: self.coordinates,
      artifact,
      verification,
    }
    let tree = PublicArchive::{
      url: artifact,
      format: Zip,
      expected_digest: self.expected_digest,
    }.open(
      transport,
      acquisition_limits~,
      limits~,
      cache~,
      mode~,
      credentials~,
      verify=(body, at) => {
        if verification is RepositoryChecksum(_, checksum) &&
          !checksum.matches(body) {
          raise ResourceError(
            IntegrityFailure,
            at,
            "JAR does not match repository " + checksum.to_string(),
          )
        }
      },
    ) catch {
      ResourceError(code, at, message) =>
        raise ResourceError(code, at.through(origin.maven(resolution)), message)
    }
    (resolution, { ..tree, provenance: tree.provenance.maven(resolution), })
  } catch {
    ResourceError(MissingAsset, at, message) =>
      raise ResourceError(MissingArtifact, at.through(origin), message)
    ResourceError(code, at, message) =>
      raise ResourceError(code, at.through(origin), message)
  }
}

///|
pub fn register_maven(
  registry : Registry,
  transport : HttpTransport,
  checksum~ : MavenChecksum = Sha1Sidecar,
  expected_digest~ : ContentDigest? = None,
  acquisition_limits~ : AcquisitionLimits = AcquisitionLimits::new().unwrap(),
  limits~ : Limits = Limits::new(),
  cache~ : VerifiedCache? = None,
  mode~ : AcquisitionMode = Online,
  credentials~ : Credentials = Credentials::new().unwrap(),
) -> Registry raise ResourceError {
  registry.register_provider(ProviderId::parse("maven").unwrap(), (
    repository,
    reference,
  ) => {
    guard RepositoryUrl::parse(repository.location.to_string()) is Some(base) &&
      MavenCoordinates::from_key(reference.key) is Some(coordinates) else {
      raise ResourceError(
        InvalidResolution,
        Provenance::new(Location::Source(repository.location)),
        "Maven requires a repository URL and exact JAR coordinates",
      )
    }
    let (resolution, tree) = MavenArchive::{
      repository: base,
      coordinates,
      checksum,
      expected_digest,
    }.open_resolved(
      transport,
      acquisition_limits~,
      limits~,
      cache~,
      mode~,
      credentials~,
    )
    {
      reference,
      identity: ArtifactIdentity::parse(resolution.to_string()).unwrap(),
      tree,
    }
  })
}