///|
pub let compiler_version : String = "0.10.20+65efd5b93"

///|
fn absolute(host : Host, path : String) -> String {
  if path.has_prefix("/") ||
    path.has_prefix("\\") ||
    (path.length() > 1 && path[1] == ':') {
    path
  } else {
    join(host.cwd, path)
  }
}

///|
fn managed(host : Host) -> String raise {
  let base = match host.environment.get("MORPHIR_HOME") {
    Some(s) if s != "" => absolute(host, s)
    _ =>
      match
        host.environment
        .get("HOME")
        .unwrap_or(host.environment.get("USERPROFILE").unwrap_or("")) {
        s if s != "" => join(s, ".morphir")
        _ =>
          raise ToolchainError(
            "Set MORPHIR_HOME when the host has no user home",
          )
      }
  }
  join(
    join(base, "toolchains"),
    compiler_version + "-" + host.system + "-" + host.architecture,
  )
}

///|
fn explicit_home(host : Host, request : Json) -> String {
  let value = string(
    request,
    "home",
    fallback=host.environment.get("MORPHIR_MOON_HOME").unwrap_or(""),
  )
  if value == "" {
    ""
  } else {
    absolute(host, value)
  }
}

///|
fn discover(host : Host, request : Json) -> String? raise {
  let explicit = explicit_home(host, request)
  if explicit != "" {
    return Some(explicit)
  }
  let managed = managed(host)
  if (host.kind)(managed) != 0 {
    return Some(managed)
  }
  if host.environment.get("MOON_HOME") is Some(home) {
    if (host.kind)(executable(host, home, "moonc")) != 0 {
      return Some(home)
    }
  }
  let separator = if host.system == "windows" { ";" } else { ":" }
  for directory in host.environment.get("PATH").unwrap_or("").split(separator) {
    let directory = directory.to_owned().replace(old="\\", new="/")
    if directory == "" {
      continue
    }
    let parts = directory.split("/").map(StringView::to_owned).collect()
    if parts.length() > 1 {
      let home = parts[:parts.length() - 1].to_owned().join("/")
      if (host.kind)(executable(host, home, "moonc")) != 0 &&
        (host.kind)(join(home, "lib/core")) == 2 {
        return Some(absolute(host, home))
      }
    }
  }
  None
}

///|
fn validate(host : Host, home : String, timeout : Int32) -> Json raise {
  for name in ["moon", "moonc"] {
    if (host.kind)(executable(host, home, name)) == 0 {
      raise ToolchainError("Missing " + name + " in toolchain: " + home)
    }
  }
  if (host.kind)(join(home, "lib/core")) != 2 {
    raise ToolchainError("Missing matching core library in toolchain: " + home)
  }
  let version = execute(
    host,
    executable(host, home, "moonc"),
    ["-v"],
    host.cwd,
    home,
    timeout,
  )
  require_success(version, "Compiler version")
  if !string(version, "stdout").contains("v" + compiler_version + " ") {
    raise ToolchainError(
      "Compiler must match pinned version " + compiler_version,
    )
  }
  let directory = (host.temporary)()
  defer (remove_tree(host, directory) catch { _ => () })
  (host.write)(
    join(directory, "moon.mod"),
    "name = \"morphir/toolchain-smoke\"\nversion = \"0.0.0\"\n",
  )
  (host.write)(join(directory, "moon.pkg"), "pkgtype(kind: \"executable\")\n")
  (host.write)(join(directory, "main.mbt"), "fn main { println(42) }\n")
  let smoke = execute(
    host,
    executable(host, home, "moon"),
    ["run", ".", "--target", "wasm"],
    directory,
    home,
    timeout,
  )
  require_success(smoke, "Toolchain/core smoke compilation and execution")
  if string(smoke, "stdout").trim().to_owned() != "42" {
    raise ToolchainError("Toolchain smoke program returned unexpected output")
  }
  let moonx = (host.kind)(executable(host, home, "moonx")) == 1
  let operations = ["info", "compile", "run", "setup", "build"]
  if moonx {
    operations.push("exec")
  }
  object([
    ("protocolVersion", Json::number(1.0)),
    ("operations", Json::array(operations.map(Json::string))),
    (
      "targets",
      Json::array(["wasm", "wasm-gc", "js", "native"].map(Json::string)),
    ),
    ("supported", Json::boolean(true)),
    ("available", Json::boolean(true)),
    ("home", Json::string(home)),
    ("compilerVersion", Json::string(compiler_version)),
    ("validated", Json::boolean(true)),
    ("mode", Json::string("process")),
    ("moonx", Json::boolean(moonx)),
  ])
}

///|
fn acquisition_target(host : Host) -> String? {
  match (host.system, host.architecture) {
    ("macos", "arm64") => Some("darwin-aarch64")
    ("linux", "x64") => Some("linux-x86_64")
    _ => None
  }
}

///|
fn acquire(host : Host, home : String, timeout : Int32) -> Json raise {
  let target = match acquisition_target(host) {
    Some(target) => target
    None =>
      raise ToolchainError(
        "No pinned acquisition archive for " +
        host.system +
        "-" +
        host.architecture,
      )
  }
  // Exclusive lock and staging directories belong to this installation only.
  let parts = home
    .replace(old="\\", new="/")
    .split("/")
    .map(StringView::to_owned)
    .collect()
  make_directories(host, parts[:parts.length() - 1].to_owned().join("/"))
  let lock = home + ".lock"
  (host.mkdir)(lock)
  defer (remove_tree(host, lock) catch { _ => () })
  if (host.kind)(home) != 0 {
    return validate(host, home, timeout)
  }
  let staging = join(lock, "installation")
  (host.mkdir)(staging)
  let encoded = compiler_version.replace(old="+", new="%2B")
  let binary_url = "https://cli.moonbitlang.com/binaries/" +
    encoded +
    "/moonbit-" +
    target +
    ".tar.gz"
  let core_url = "https://cli.moonbitlang.com/cores/core-" + encoded + ".tar.gz"
  let receipts : Array[Json] = []
  for
    (name, url, destination, digest) in [
      ("tools.tar.gz", binary_url, staging, binary_digest(target)),
      ("core.tar.gz", core_url, join(staging, "lib"), core_digest),
    ] {
    let archive = join(lock, name)
    require_success(
      execute(
        host,
        "curl",
        [
          "--fail", "--location", "--silent", "--show-error", "--proto", "=https",
          "--proto-redir", "=https", "--output", archive, url,
        ],
        host.cwd,
        "",
        timeout,
      ),
      "Download " + name,
    )
    let hash = if host.system == "macos" {
      execute(host, "shasum", ["-a", "256", archive], host.cwd, "", timeout)
    } else {
      execute(host, "sha256sum", [archive], host.cwd, "", timeout)
    }
    require_success(hash, "Archive integrity")
    if !string(hash, "stdout").has_prefix(digest + " ") {
      raise ToolchainError("Archive SHA-256 mismatch: " + name)
    }
    require_success(
      execute(
        host,
        "tar",
        ["-xf", archive, "-C", destination],
        host.cwd,
        "",
        timeout,
      ),
      "Extract " + name,
    )
    receipts.push(
      object([("url", Json::string(url)), ("sha256", Json::string(digest))]),
    )
  }
  require_success(
    execute(
      host,
      "ln",
      ["-s", "moon", executable(host, staging, "moonx")],
      host.cwd,
      "",
      timeout,
    ),
    "Create moonx alias",
  )
  require_success(
    execute(
      host,
      "chmod",
      ["-R", "u+x", join(staging, "bin")],
      host.cwd,
      "",
      timeout,
    ),
    "Make tools executable",
  )
  require_success(
    execute(
      host,
      executable(host, staging, "moon"),
      ["bundle", "--all", "--warn-list", "-a"],
      join(staging, "lib/core"),
      staging,
      timeout,
    ),
    "Bundle matching core",
  )
  ignore(validate(host, staging, timeout))
  (host.write)(
    join(staging, "morphir-toolchain.json"),
    object([
      ("compilerVersion", Json::string(compiler_version)),
      ("archives", Json::array(receipts)),
    ]).stringify(),
  )
  check_cancelled(host)
  (host.rename)(staging, home)
  validate(host, home, timeout)
}

///|
/// Version 1 request protocol, shared by CLI and process embedding hosts.
pub fn Host::request(self : Host, request : Json) -> Json raise {
  check_cancelled(self)
  let operation = if string(request, "operation") == "compile" {
    "build"
  } else {
    string(request, "operation")
  }
  if !["info", "setup", "build", "run", "exec"].contains(operation) {
    raise ToolchainError("Unknown toolchain operation: " + operation)
  }
  let timeout = match field(request, "timeout") {
    Null => 120000
    Number(n, ..) if n >= 1.0 && n <= 600000.0 && n == n.to_int().to_double() =>
      n.to_int()
    _ =>
      raise ToolchainError(
        "Timeout must be an integer from 1 through 600000 milliseconds",
      )
  }
  if !self.process {
    if operation == "info" {
      return object([
        ("protocolVersion", Json::number(1.0)),
        ("operations", Json::array([Json::string("info")])),
        ("targets", Json::array([])),
        ("supported", Json::boolean(false)),
        ("available", Json::boolean(false)),
        ("mode", Json::string("process")),
      ])
    }
    raise ToolchainError(
      "Host does not provide process execution; supply an embedded or process toolchain provider",
    )
  }
  let home = discover(self, request)
  if operation == "info" && home is None {
    let operations = ["info"]
    if acquisition_target(self) is Some(_) {
      operations.push("setup")
    }
    return object([
      ("protocolVersion", Json::number(1.0)),
      ("mode", Json::string("process")),
      ("operations", Json::array(operations.map(Json::string))),
      (
        "targets",
        Json::array(["wasm", "wasm-gc", "js", "native"].map(Json::string)),
      ),
      ("supported", Json::boolean(true)),
      ("available", Json::boolean(false)),
      ("compilerVersion", Json::string(compiler_version)),
    ])
  }
  let selected = match home {
    Some(home) => {
      if operation == "setup" && (self.kind)(home) == 0 {
        if !(field(request, "consent") is True) {
          raise ToolchainError("Toolchain acquisition requires --yes")
        }
        ignore(acquire(self, home, timeout))
      }
      home
    }
    None if operation == "setup" => {
      if !(field(request, "consent") is True) {
        raise ToolchainError("Toolchain acquisition requires --yes")
      }
      let home = managed(self)
      ignore(acquire(self, home, timeout))
      home
    }
    None =>
      raise ToolchainError(
        "No MoonBit toolchain found; run morphir toolchain setup --yes",
      )
  }
  // An explicitly selected missing home is the requested installation destination.
  let info = validate(self, selected, timeout)
  if operation == "info" || operation == "setup" {
    return info
  }
  let target = string(request, "target", fallback="wasm")
  if !["wasm", "wasm-gc", "js", "native"].contains(target) {
    raise ToolchainError("Unsupported compilation target: " + target)
  }
  let directory = absolute(self, string(request, "directory", fallback="."))
  let arguments = match field(request, "args") {
    Null => []
    Array(items) =>
      items.map(fn(item) raise {
        match item {
          String(s) => s
          _ => raise ToolchainError("Program arguments must be strings")
        }
      })
    _ => raise ToolchainError("Program arguments must be an array")
  }
  let (program, args) = match operation {
    "build" =>
      (executable(self, selected, "moon"), ["build", "--target", target])
    "run" =>
      (
        executable(self, selected, "moon"),
        [
          "run",
          string(request, "package", fallback="."),
          "--target",
          target,
          "--",
        ] +
        arguments,
      )
    _ => {
      let script = string(request, "script")
      if script == "" {
        raise ToolchainError("toolchain exec requires a .mbtx script")
      }
      if (self.kind)(executable(self, selected, "moonx")) == 0 {
        raise ToolchainError("Selected toolchain does not provide moonx")
      }
      (executable(self, selected, "moonx"), [script, "--"] + arguments)
    }
  }
  let result = execute(self, program, args, directory, selected, timeout)
  object([
    ("toolchain", info),
    ("successful", Json::boolean(successful(result))),
    ("process", result),
  ])
}