///|
/// Print what the package says about its signature, and check it when a
/// keyring is given. Returns the problems found.
fn check_signature(
  pkg : Package,
  path : String,
  keyring : @sign.PublicKeyring?,
) -> Array[String] raise CliError {
  match pkg {
    Rpm(rpm) =>
      match rpm.signature_status(keyring) {
        Unsigned =>
          if keyring is Some(_) {
            return ["not signed"]
          } else {
            println("\{path}: signature: none")
          }
        Signed(key_id~) =>
          println(
            "\{path}: signature: signed by key \{key_id} (not checked, no --keyring)",
          )
        Good(verified) =>
          println("\{path}: signature: OK (key \{verified.fingerprint})")
        Bad(message) => return [message]
      }
    Arch(_) => {
      let sig_path = path + ".sig"
      if !@fs.path_exists(sig_path) {
        if keyring is Some(_) {
          return ["not signed (\{sig_path} is missing)"]
        }
        println("\{path}: signature: none")
      } else {
        let signature = read_bytes(sig_path)
        match keyring {
          None =>
            try {
              let info = @sign.signature_info(signature)
              println(
                "\{path}: signature: signed by key \{info.key_id} (not checked, no --keyring)",
              )
            } catch {
              err => return ["\{sig_path}: \{err}"]
            }
          Some(ring) =>
            try {
              let verified = ring.verify_detached(
                read_bytes(path)[:],
                signature,
              )
              println("\{path}: signature: OK (key \{verified.fingerprint})")
            } catch {
              err => return ["\{err}"]
            }
        }
      }
    }
    Apk(apk) =>
      match apk.signature_status(keyring) {
        Unsigned =>
          if keyring is Some(_) {
            return ["not signed"]
          } else {
            println("\{path}: signature: none")
          }
        Signed(key_id~) =>
          println(
            "\{path}: signature: signed by key \{key_id} (not checked, no --keyring)",
          )
        Good(verified) =>
          println(
            "\{path}: signature: OK (key \{verified.key_id}, \{verified.fingerprint})",
          )
        Bad(message) => return [message]
      }
    Deb(_) =>
      if keyring is Some(_) {
        raise Failed(
          "\{path}: this kind of package is not signed by itself; check the signature of the repository (`verify  --keyring KEY`)",
        )
      }
  }
  []
}

///|
fn read_optional(path : String) -> Bytes? raise CliError {
  if @fs.path_exists(path) {
    Some(read_bytes(path))
  } else {
    None
  }
}

///|
/// An `APKINDEX.tar.gz` with a signature stream in front of the index.
fn is_signed_index(data : Bytes) -> Bool {
  try @compress.gzip_members(data[:], max_size=1 << 28).length() == 2 catch {
    _ => false
  }
}

///|
/// Check the signatures of an apt flat repository and a dnf repository in
/// `dir`. Without a keyring only report which signature files are there.
fn verify_repository(
  dir : String,
  keyring : @sign.PublicKeyring?,
) -> Int32 raise CliError {
  let apt : Array[(String, Bytes)] = []
  for name in ["Release", "Release.gpg", "InRelease"] {
    if read_optional(join_path(dir, name)) is Some(data) {
      apt.push((name, data))
    }
  }
  let yum : Array[(String, Bytes)] = []
  for name in ["repodata/repomd.xml", "repodata/repomd.xml.asc"] {
    if read_optional(join_path(dir, name)) is Some(data) {
      yum.push((name, data))
    }
  }
  // Alpine: one directory per architecture with an APKINDEX.tar.gz.
  let apk : Array[(String, Bytes)] = []
  // pacman: .db.tar.gz and friends, with the .sig files next to them.
  let pacman : Array[(String, Bytes)] = []
  let pacman_names : Array[String] = []
  let listing = @fs.read_dir(dir) catch {
    err => raise Failed(describe_io(err, "cannot list \"\{dir}\""))
  }
  for name in listing {
    let sub_index = join_path(join_path(dir, name), "APKINDEX.tar.gz")
    if read_optional(sub_index) is Some(data) {
      apk.push(("\{name}/APKINDEX.tar.gz", data))
    }
    if name.has_suffix(".db.tar.gz") {
      pacman_names.push(name[0:name.length() - 10].to_owned())
    }
  }
  for repo_name in pacman_names {
    for
      suffix in [
        ".db", ".db.tar.gz", ".files", ".files.tar.gz", ".db.sig", ".db.tar.gz.sig",
        ".files.sig", ".files.tar.gz.sig",
      ] {
      if read_optional(join_path(dir, repo_name + suffix)) is Some(data) {
        pacman.push((repo_name + suffix, data))
      }
    }
  }
  if apt.length() == 0 &&
    yum.length() == 0 &&
    apk.length() == 0 &&
    pacman.length() == 0 {
    raise Failed(
      "\"\{dir}\" has no apt, dnf, pacman or apk repository metadata",
    )
  }
  let problems : Array[String] = []
  if apt.length() > 0 {
    match keyring {
      Some(ring) => {
        let found = @repo.check_apt_signature(apt, ring)
        if found.length() == 0 {
          println("\{dir}: apt: Release.gpg and InRelease OK")
        }
        for problem in found {
          problems.push("apt: \{problem}")
        }
      }
      None => {
        let signed = apt.filter(f => f.0 != "Release").map(f => f.0)
        if signed.length() == 0 {
          println("\{dir}: apt: unsigned")
        } else {
          println(
            "\{dir}: apt: signed (\{signed.join(", ")}), not checked, no --keyring",
          )
        }
      }
    }
  }
  if yum.length() > 0 {
    match keyring {
      Some(ring) => {
        let found = @repo.check_yum_signature(yum, ring)
        if found.length() == 0 {
          println("\{dir}: dnf: repomd.xml.asc OK")
        }
        for problem in found {
          problems.push("dnf: \{problem}")
        }
      }
      None =>
        if yum.length() > 1 {
          println(
            "\{dir}: dnf: signed (repomd.xml.asc), not checked, no --keyring",
          )
        } else {
          println("\{dir}: dnf: unsigned")
        }
    }
  }
  if apk.length() > 0 {
    match keyring {
      Some(ring) => {
        let found = @repo.check_apk_signature(apk, ring)
        if found.length() == 0 {
          println(
            "\{dir}: apk: APKINDEX.tar.gz of \{apk.length()} architecture(s) OK",
          )
        }
        for problem in found {
          problems.push("apk: \{problem}")
        }
      }
      None => {
        let signed = apk.filter(f => is_signed_index(f.1))
        println(
          "\{dir}: apk: \{signed.length()} of \{apk.length()} index(es) signed, not checked, no --keyring",
        )
      }
    }
  }
  for repo_name in pacman_names {
    match keyring {
      Some(ring) => {
        let found = @repo.check_arch_signature(pacman, ring, name=repo_name)
        if found.length() == 0 {
          println("\{dir}: pacman: \{repo_name}.db and .files OK")
        }
        for problem in found {
          problems.push("pacman: \{problem}")
        }
      }
      None => {
        let signed = pacman.filter(f => f.0 == repo_name + ".db.sig")
        println(
          if signed.length() > 0 {
            "\{dir}: pacman: \{repo_name}.db signed, not checked, no --keyring"
          } else {
            "\{dir}: pacman: \{repo_name}.db unsigned"
          },
        )
      }
    }
  }
  if problems.length() > 0 {
    for problem in problems {
      eprintln("\{dir}: \{problem}")
    }
    raise Mismatch(
      "\{dir}: signature check failed (\{problems.length()} problem(s))",
    )
  }
  0
}

///|
fn cmd_verify(args : Array[String]) -> Int32 raise CliError {
  let options = parse_options(args, { "--keyring": "keyring" }, Map([]))
  let path = match options.positional {
    [path] => path
    [] => raise Usage("verify needs the path of a package file")
    _ => raise Usage("verify takes exactly one package file")
  }
  let keyring = match options.value("keyring") {
    Some(file) => Some(load_keyring(file))
    None => None
  }
  let is_directory = @fs.is_dir(path) catch { _ => false }
  if is_directory {
    return verify_repository(path, keyring)
  }
  let pkg = load_package(path)
  let problems = pkg.problems(path)
  problems.append(check_signature(pkg, path, keyring))
  if problems.length() > 0 {
    for problem in problems {
      eprintln("\{path}: \{problem}")
    }
    raise Mismatch(
      "\{path}: verification failed (\{problems.length()} problem(s))",
    )
  }
  println("\{path}: OK (\{pkg.checked_files()} checksums verified)")
  0
}