///|
/// Only explicit GitHub coordinates and the three owned aliases are accepted.
/// No shell invocation is constructed from these values.
pub struct Package {
  owner : String
  repo : String
  binary : String
  version : String?
}

///|
/// A Mooncakes package coordinate within a versioned module.
pub struct MooncakesPackage {
  module_path : String
  package_path : String
  binary : String
  version : String?
}

///|
/// A source coordinate accepted by the installer.
pub enum InstallCoordinate {
  GitHub(Package)
  Mooncakes(MooncakesPackage)
}

///|
pub fn parse_package(input : String) -> Package raise {
  let parts = input.split("@").collect()
  guard parts.length() >= 1 && parts.length() <= 2 else {
    fail("expected owner/repo[@tag]")
  }
  let coordinate = parts[0].to_owned()
  let version = if parts.length() == 2 {
    guard valid_tag(parts[1].to_owned()) else { fail("unsafe or empty tag") }
    Some(parts[1].to_owned())
  } else {
    None
  }
  let resolved = match coordinate {
    "turtles" | "turtles.mbt" | "gpui-mbt/turtles.mbt" =>
      ("gpui-mbt", "turtles.mbt", "turtles")
    "hotpath" | "hotpath.mbt" | "gpui-mbt/hotpath.mbt" =>
      ("gpui-mbt", "hotpath.mbt", "hotpath-report")
    "dsh" | "dsh.mbt" | "f4ah6o/dsh.mbt" => ("f4ah6o", "dsh.mbt", "dsh")
    other => {
      let path = other.split("/").collect()
      guard path.length() == 2 else {
        fail("expected owner/repo[@tag]; aliases: turtles, hotpath, dsh")
      }
      guard valid_id(path[0].to_owned()) && valid_id(path[1].to_owned()) else {
        fail("unsafe GitHub repository coordinate")
      }
      // Outside the aliases, a repo whose name ends with .mbt is treated
      // as a generic CLI named after the repo without the .mbt suffix.
      let name = path[1].to_owned()
      let binary = if name.has_suffix(".mbt") {
        name[:name.length() - 4].to_owned()
      } else {
        name
      }
      guard binary != "" else {
        fail("repository name has an empty binary name")
      }
      (path[0].to_owned(), path[1].to_owned(), binary)
    }
  }
  { owner: resolved.0, repo: resolved.1, binary: resolved.2, version, }
}

///| Parse either the compatible GitHub coordinate or a Mooncakes package path.

///| `owner/module/package` is a Mooncakes coordinate; two path segments remain

///|
/// GitHub `owner/repository` for backwards compatibility.
pub fn parse_coordinate(input : String) -> InstallCoordinate raise {
  let parts = input.split("@").collect()
  guard parts.length() >= 1 && parts.length() <= 2 else {
    fail("expected owner/repo[@tag] or owner/module/package[@version]")
  }
  let coordinate = parts[0].to_owned()
  let path = coordinate.split("/").collect()
  if path.length() < 3 {
    return GitHub(parse_package(input))
  }
  let version = if parts.length() == 2 {
    guard valid_registry_version(parts[1].to_owned()) else {
      fail("unsafe or empty version")
    }
    Some(parts[1].to_owned())
  } else {
    None
  }
  guard valid_id(path[0].to_owned()) && valid_id(path[1].to_owned()) else {
    fail("unsafe Mooncakes module coordinate")
  }
  let mut package_path = ""
  for i = 2; i < path.length(); i = i + 1 {
    guard valid_id(path[i].to_owned()) else {
      fail("unsafe Mooncakes package path")
    }
    if package_path != "" {
      package_path = package_path + "/"
    }
    package_path = package_path + path[i].to_owned()
  }
  let binary = binary_from_package_path(package_path)
  guard binary != "" else { fail("package path has an empty binary name") }
  Mooncakes({
    module_path: path[0].to_owned() + "/" + path[1].to_owned(),
    package_path,
    binary,
    version,
  })
}

///|
/// Parse a package path when the module is supplied separately with --module.
pub fn parse_module_package(
  module_path : String,
  input : String,
) -> MooncakesPackage raise {
  let parts = input.split("@").collect()
  guard parts.length() >= 1 && parts.length() <= 2 else {
    fail("expected package/path[@version]")
  }
  let module_parts = module_path.split("/").collect()
  guard module_parts.length() == 2 &&
    valid_id(module_parts[0].to_owned()) &&
    valid_id(module_parts[1].to_owned()) else {
    fail("--module must be owner/module")
  }
  let version = if parts.length() == 2 {
    guard valid_registry_version(parts[1].to_owned()) else {
      fail("unsafe or empty version")
    }
    Some(parts[1].to_owned())
  } else {
    None
  }
  let package_parts = parts[0].to_owned().split("/").collect()
  guard package_parts.length() > 0 else { fail("missing package path") }
  for part in package_parts {
    guard valid_id(part.to_owned()) else {
      fail("unsafe Mooncakes package path")
    }
  }
  let package_path = parts[0].to_owned()
  let binary = binary_from_package_path(package_path)
  guard binary != "" else { fail("package path has an empty binary name") }
  MooncakesPackage::{ module_path, package_path, binary, version, }
}

///|
fn binary_from_package_path(package_path : String) -> String {
  let parts = package_path.split("/").collect()
  let leaf = parts[parts.length() - 1].to_owned()
  if leaf.has_suffix(".mbt") {
    leaf[:leaf.length() - 4].to_owned()
  } else {
    leaf
  }
}

///|
fn valid_registry_version(value : String) -> Bool {
  if value == "" || value.has_prefix(".") || value.has_suffix(".") {
    return false
  }
  for c in value {
    if !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '.' ||
      c == '+') {
      return false
    }
  }
  true
}

///|
fn valid_id(value : String) -> Bool {
  if value == "" || value == "." || value == ".." {
    return false
  }
  for c in value {
    if !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '_' ||
      c == '.') {
      return false
    }
  }
  true
}

///|
fn valid_tag(value : String) -> Bool {
  if value == "" || value == "." || value == ".." || value.has_prefix(".") {
    return false
  }
  for c in value {
    if !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '_' ||
      c == '.' ||
      c == '+') {
      return false
    }
  }
  true
}

///|
pub fn release_url(pkg : Package) -> String {
  let root = "https://api.github.com/repos/\{pkg.owner}/\{pkg.repo}/releases"
  match pkg.version {
    Some(tag) => root + "/tags/" + tag
    None => root + "/latest"
  }
}

///|
/// GitHub Releases assets have a stable, archive-free format. Unrecognized
/// systems fail closed instead of downloading a binary for another CPU.
pub fn platform_key(os : String, cpu : String) -> String raise {
  let system = match os {
    "Linux" => "linux"
    "Darwin" => "darwin"
    _ => fail("unsupported OS: \{os} (Linux/macOS only)")
  }
  let arch = match cpu {
    "x86_64" | "amd64" => "x86_64"
    "arm64" | "aarch64" => "aarch64"
    _ => fail("unsupported CPU: \{cpu}")
  }
  "\{system}-\{arch}"
}

///|
pub fn expected_asset(pkg : Package, target : String) -> String {
  "\{pkg.binary}-\{target}"
}

///|
pub struct ReleaseAsset {
  url : String
  digest : String
  name : String
  tag : String
}

///|
/// A release without a GitHub SHA-256 digest is not safe to install.
/// The URL is also pinned to the requested repository's release namespace.
pub fn resolve_release(
  payload : String,
  pkg : Package,
  target : String,
) -> ReleaseAsset raise {
  resolve_release_names(
    payload,
    pkg.owner,
    pkg.repo,
    [expected_asset(pkg, target)],
    pkg.version,
  )
}

///|
/// Resolve an asset by name and bind its URL to the API's exact release path.
pub fn resolve_release_names(
  payload : String,
  owner : String,
  repo : String,
  expected_names : Array[String],
  expected_tag : String?,
) -> ReleaseAsset raise {
  match
    resolve_release_names_optional(
      payload, owner, repo, expected_names, expected_tag,
    ) {
    Some(asset) => asset
    None => fail("no matching prebuilt asset in release")
  }
}

///|
/// Resolve the first present candidate, returning None only when none of the
/// expected names exists. A matching but malformed entry is always an error.
pub fn resolve_release_names_optional(
  payload : String,
  owner : String,
  repo : String,
  expected_names : Array[String],
  expected_tag : String?,
) -> ReleaseAsset? raise {
  let document = @json.parse(payload)
  guard document is { "tag_name": String(tag), "assets": Array(assets), .. } else {
    fail("invalid GitHub release response")
  }
  guard expected_names.length() > 0 else { fail("no expected release assets") }
  match expected_tag {
    Some(expected) =>
      if tag != expected {
        fail("GitHub returned tag \{tag}, expected \{expected}")
      }
    None => ()
  }
  let origin = "https://github.com/\{owner}/\{repo}/releases/download/\{tag}/"
  for expected in expected_names {
    guard valid_asset_name(expected) else {
      fail("unsafe expected release asset name")
    }
    let mut matched : ReleaseAsset? = None
    for entry in assets {
      match entry {
        { "name": String(name), .. } if name == expected => {
          if matched is Some(_) {
            fail("duplicate release asset name: \{name}")
          }
          let (url, digest) = match entry {
            {
              "browser_download_url": String(url),
              "digest": String(digest),
              ..
            } => (url, digest)
            _ => fail("release asset metadata is incomplete: \{name}")
          }
          guard valid_github_digest(digest) else {
            fail("release asset has no valid SHA-256 digest")
          }
          guard url == origin + expected else {
            fail("release asset URL is not in the requested repository")
          }
          matched = Some({
            url,
            digest: "sha256:" + digest[7:].to_owned(),
            name,
            tag,
          })
        }
        _ => ()
      }
    }
    if matched is Some(asset) {
      return Some(asset)
    }
  }
  None
}

///|
fn valid_asset_name(name : String) -> Bool {
  if name == "" || name == "." || name == ".." || name.has_prefix("-") {
    return false
  }
  for c in name {
    if !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '_' ||
      c == '.' ||
      c == '+') {
      return false
    }
  }
  true
}

///|
fn valid_github_digest(value : String) -> Bool {
  value.has_prefix("sha256:") && valid_sha256_hex(value[7:].to_owned())
}