///|
/// A package file opened for inspection or verification.
priv enum Package {
  Deb(@deb.DebPackage)
  Rpm(@rpm.RpmPackage)
  Arch(@arch.ArchPackage)
  Apk(@apk.ApkPackage)
}

///|
/// Turn a package library error into the matching CLI error. A damaged but
/// recognizable package is an integrity failure (`Mismatch`, exit 2), anything
/// else (unsupported features, bad input) a plain failure (exit 1).
fn rpm_failure(path : String, err : @rpm.RpmError) -> CliError {
  match err {
    Corrupt(..) => Mismatch("\{path}: \{err}")
    _ => Failed("\{path}: \{err}")
  }
}

///|
/// Load a package and tell its format from the first bytes.
fn load_package(path : String) -> Package raise CliError {
  let data = read_bytes(path)
  if data.length() >= 8 && data[0:8] == b"!\n"[:] {
    let deb = @deb.read(data) catch {
      Corrupt(message~) => raise Mismatch("\{path}: \{message}")
      err => raise Failed("\{path}: \{err}")
    }
    return Deb(deb)
  }
  if data.length() >= 4 && data[0:4] == b"\xed\xab\xee\xdb"[:] {
    let rpm = @rpm.read(data) catch { err => raise rpm_failure(path, err) }
    return Rpm(rpm)
  }
  // Alpine packages are two or three gzip streams in a row. An Arch Linux
  // package compressed with gzip is a single stream, so try apk first.
  if data.length() >= 2 && data[0:2] == b"\x1f\x8b"[:] {
    try @apk.read(data) catch {
      @apk.ApkError::Corrupt(message~) =>
        if path.has_suffix(".apk") {
          raise Mismatch("\{path}: \{message}")
        }
      err => if path.has_suffix(".apk") { raise Failed("\{path}: \{err}") }
    } noraise {
      apk => return Apk(apk)
    }
  }
  // Arch Linux packages are plain, zstd, xz or gzip compressed tar files.
  let looks_arch = path.contains(".pkg.tar")
  let arch = @arch.read(data) catch {
    @arch.ArchError::Corrupt(message~) =>
      if looks_arch && !message.contains("no .PKGINFO") {
        raise Mismatch("\{path}: \{message}")
      } else {
        raise Failed("\{path}: not a deb, rpm, apk or Arch Linux package")
      }
    err => raise Failed("\{path}: \{err}")
  }
  Arch(arch)
}

///|
fn Package::report(self : Package, path : String) -> Json raise CliError {
  match self {
    Deb(deb) => deb.to_json()
    Rpm(rpm) => rpm.to_json() catch { err => raise rpm_failure(path, err) }
    Arch(arch) => arch.to_json()
    Apk(apk) => apk.to_json()
  }
}

///|
/// Integrity problems, one per line; empty when the package is sound.
fn Package::problems(
  self : Package,
  path : String,
) -> Array[String] raise CliError {
  match self {
    Deb(deb) => deb.verify()
    Rpm(rpm) => rpm.verify() catch { err => raise rpm_failure(path, err) }
    Arch(arch) => arch.verify()
    Apk(apk) => apk.verify()
  }
}

///|
/// How many checksums `problems` compared.
fn Package::checked_files(self : Package) -> Int32 {
  match self {
    Deb(deb) => deb.md5sums.length()
    Arch(arch) => arch.checked_files()
    Apk(apk) => apk.checked_files()
    Rpm(rpm) => {
      let mut count = 0
      for digest in rpm.header.strings(1035) {
        if digest != "" {
          count += 1
        }
      }
      count
    }
  }
}