///|
/// 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?
}

///|
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
      }
      (path[0].to_owned(), path[1].to_owned(), binary)
    }
  }
  { owner: resolved.0, repo: resolved.1, binary: resolved.2, version }
}

///|
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 {
  valid_id(value) && !value.has_prefix(".")
}

///|
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 {
  let document = @json.parse(payload)
  guard document is {
    "tag_name": String(tag),
    "assets": Array(assets),
    ..
  } else {
    fail("invalid GitHub release response")
  }
  let expected = expected_asset(pkg, target)
  for entry in assets {
    match entry {
      {
        "name": String(name),
        "browser_download_url": String(url),
        "digest": String(digest),
        ..
      } => {
        if name != expected { continue }
        let origin = "https://github.com/\{pkg.owner}/\{pkg.repo}/releases/download/"
        guard url.has_prefix(origin) && url.has_suffix("/" + expected) else {
          fail("release asset URL is not in the requested repository")
        }
        guard digest.has_prefix("sha256:") && digest.length() == 71 else {
          fail("release asset has no valid SHA-256 digest")
        }
        return { url, digest, name, tag }
      }
      _ => ()
    }
  }
  fail("no prebuilt \{expected} asset in release \{tag}")
}