///|
pub fn UpdateManifest::parse(
  text : String,
) -> Result[UpdateManifest, Array[String]] {
  let json = @json.parse(text) catch {
    error => return Err(["invalid update manifest json: \{error}"])
  }
  match json {
    Object(object) => update_manifest_from_json_object(object)
    _ => Err(["update manifest must be a json object"])
  }
}

///|
fn update_manifest_from_json_object(
  object : Map[String, Json],
) -> Result[UpdateManifest, Array[String]] {
  let problems : Array[String] = []
  update_manifest_schema_version(object, problems)
  let identifier = update_manifest_string(object, "identifier", problems)
  let product_name = update_manifest_string(object, "productName", problems)
  let version = update_manifest_string(object, "version", problems)
  let channel = update_manifest_string(object, "channel", problems)
  let public_key = update_manifest_optional_string(
    object, "publicKey", problems,
  )
  let artifacts = update_manifest_artifacts(object, problems)
  let manifest = UpdateManifest::new(
    identifier~,
    product_name~,
    version~,
    channel~,
    public_key?,
    artifacts~,
  )
  for problem in manifest.validate() {
    if !problems.contains(problem) {
      problems.push(problem)
    }
  }
  if problems.is_empty() {
    Ok(manifest)
  } else {
    Err(problems)
  }
}

///|
fn update_manifest_schema_version(
  object : Map[String, Json],
  problems : Array[String],
) -> Unit {
  match object.get("schemaVersion") {
    Some(Number(value, ..)) =>
      if value.to_int() != 1 {
        problems.push("update schemaVersion must be 1")
      }
    Some(_) => problems.push("update schemaVersion number is required")
    None => problems.push("update schemaVersion number is required")
  }
}

///|
fn update_manifest_string(
  object : Map[String, Json],
  key : String,
  problems : Array[String],
) -> String {
  match object.get(key) {
    Some(String(value)) => {
      if value == "" {
        problems.push("update \{key} must not be empty")
      }
      value
    }
    _ => {
      problems.push("update \{key} string is required")
      ""
    }
  }
}

///|
fn update_manifest_optional_string(
  object : Map[String, Json],
  key : String,
  problems : Array[String],
) -> String? {
  match object.get(key) {
    Some(String(value)) => Some(value)
    Some(Null) => None
    Some(_) => {
      problems.push("update \{key} must be a string or null")
      None
    }
    None => None
  }
}

///|
fn update_manifest_artifacts(
  object : Map[String, Json],
  problems : Array[String],
) -> Array[UpdateManifestArtifact] {
  match object.get("artifacts") {
    Some(Array(items)) => {
      let artifacts : Array[UpdateManifestArtifact] = []
      for index, item in items {
        match item {
          Object(artifact) =>
            match update_manifest_artifact(artifact, index, problems) {
              Some(value) => artifacts.push(value)
              None => ()
            }
          _ => problems.push("update artifact must be a json object: \{index}")
        }
      }
      artifacts
    }
    _ => {
      problems.push("update artifacts array is required")
      []
    }
  }
}

///|
fn update_manifest_artifact(
  object : Map[String, Json],
  index : Int,
  problems : Array[String],
) -> UpdateManifestArtifact? {
  let target = update_manifest_target(
    update_manifest_string(object, "target", problems),
    index,
    problems,
  )
  let kind = update_manifest_artifact_kind(
    update_manifest_string(object, "kind", problems),
    index,
    problems,
  )
  let path = update_manifest_string(object, "path", problems)
  let url = update_manifest_string(object, "url", problems)
  let sha256 = update_manifest_string(object, "sha256", problems)
  let signature = update_manifest_optional_string(object, "signature", problems)
  match (target, kind) {
    (Some(target), Some(kind)) =>
      Some(
        UpdateManifestArtifact::new(
          target~,
          kind~,
          path~,
          url~,
          sha256~,
          signature?,
        ),
      )
    _ => None
  }
}

///|
fn update_manifest_target(
  value : String,
  index : Int,
  problems : Array[String],
) -> BundleTarget? {
  match value {
    "macos" => Some(MacOS)
    "windows" => Some(Windows)
    "linux" => Some(Linux)
    "" => None
    _ => {
      problems.push("update artifact target is invalid at \{index}: \{value}")
      None
    }
  }
}

///|
fn update_manifest_artifact_kind(
  value : String,
  index : Int,
  problems : Array[String],
) -> BundleArtifactKind? {
  match value {
    "app-bundle" => Some(AppBundleArtifact)
    "disk-image" => Some(DiskImageArtifact)
    "executable" => Some(ExecutableArtifact)
    "installer" => Some(InstallerArtifact)
    "portable-directory" => Some(PortableDirectoryArtifact)
    "desktop-entry" => Some(DesktopEntryArtifact)
    "" => None
    _ => {
      problems.push("update artifact kind is invalid at \{index}: \{value}")
      None
    }
  }
}