///|
pub struct PackageFile {
  path : String
  size : Int
  checksum : String
}

///|
pub struct PackageManifest {
  module_name : String
  version : String
  abis : Array[String]
  files : Array[PackageFile]
}

///|
pub fn package_file(
  path : String,
  data : Bytes,
) -> PackageFile raise ConfigError {
  validate_package_path(path)
  { path, size: data.length(), checksum: sha256(data), }
}

///|
pub fn validate_package_path(path : String) -> Unit raise ConfigError {
  if path == "" ||
    path.has_prefix("/") ||
    path.contains(":") ||
    path.contains("\\") {
    raise Invalid("package: path must be relative: " + path)
  }
  for part in path.split("/") {
    if part == "" || part == "." || part == ".." {
      raise Invalid("package: unsafe path: " + path)
    }
  }
}

///|
pub fn PackageFile::to_json(self : PackageFile) -> Json {
  { "path": self.path, "size": self.size, "sha256": self.checksum }
}

///|
fn valid_checksum(text : String) -> Bool {
  if text.length() != 64 {
    return false
  }
  for char in text {
    if !(char >= '0' && char <= '9') && !(char >= 'a' && char <= 'f') {
      return false
    }
  }
  true
}

///|
pub fn package_requirements(
  module_name : String,
  abis : Array[String],
) -> Array[String] {
  let required = [
    "README.md",
    "NOTICE.NativeSDK.txt",
    "NOTICE.MoonBit.txt",
    "cmake/MoonOHOS.cmake",
    "types/lib" + module_name + "/index.d.ts",
    "types/lib" + module_name + "/oh-package.json5",
  ]
  for abi in abis {
    required.push("libs/" + abi + "/lib" + module_name + ".so")
  }
  required
}

///|
pub fn parse_package_manifest(
  text : String,
) -> PackageManifest raise ConfigError {
  let json = @json.parse(text) catch {
    _ => raise Invalid("package: invalid manifest JSON")
  }
  validate_distribution_metadata(json)
  let (module_name, version, results, entries) = match json {
    {
      "schema": "moonohos-build-v1",
      "module": String(module_name),
      "moonohosVersion": String(version),
      "sdkApi": Number(26, ..),
      "results": Array(results),
      "files": Array(entries),
      ..
    } => (module_name, version, results, entries)
    _ =>
      raise Invalid(
        "package: unsupported or incomplete manifest; checksums are required",
      )
  }
  if !identifier(module_name) {
    raise Invalid("package: invalid native module name")
  }
  if version != "0.2.0" && version != "0.3.0" {
    raise Invalid("package: unsupported generator version " + version)
  }
  let abis : Array[String] = []
  for result in results {
    match result {
      { "abi": String(abi), "build": "passed", "elf": "verified", .. } =>
        abis.push(abi)
      _ => raise Invalid("package: incomplete ABI build result")
    }
  }
  parse_abis(abis.join(",")) |> ignore
  let files : Array[PackageFile] = []
  let seen : Map[String, Bool] = Map([])
  for entry in entries {
    let file : PackageFile = match entry {
      {
        "path": String(path),
        "size": Number(size, ..),
        "sha256": String(checksum),
        ..
      } => {
        if size < 0 || size > 2147483647 || size != size.trunc() {
          raise Invalid("package: invalid file size")
        }
        { path, size: size.to_int(), checksum, }
      }
      _ => raise Invalid("package: malformed file record")
    }
    validate_package_path(file.path)
    if file.path == "manifest.json" || seen.contains(file.path) {
      raise Invalid("package: duplicate or recursive checksum entry")
    }
    if !valid_checksum(file.checksum) {
      raise Invalid("package: invalid SHA256 for " + file.path)
    }
    seen[file.path] = true
    files.push(file)
  }
  for required in package_requirements(module_name, abis) {
    if !seen.contains(required) {
      raise Invalid("package: manifest omits required file " + required)
    }
  }
  { module_name, version, abis, files, }
}

///|
pub fn PackageFile::verify(
  self : PackageFile,
  data : Bytes,
) -> Unit raise ConfigError {
  if data.length() != self.size {
    raise Invalid("package: size mismatch for " + self.path)
  }
  if sha256(data) != self.checksum {
    raise Invalid("package: SHA256 mismatch for " + self.path)
  }
}

///|
pub fn seal_manifest(
  manifest : Json,
  files : Array[PackageFile],
) -> Json raise ConfigError {
  match manifest {
    Object(fields) => {
      let result = fields.copy()
      result["files"] = files.map(f => f.to_json()).to_json()
      result.to_json()
    }
    _ => raise Invalid("package: manifest must be an object")
  }
}