///|
let mobile_config_schema_version : Int = 1

///|
priv struct MobileAndroid {
  application_id : String
  min_sdk : Int
}

///|
priv struct MobileIos {
  bundle_id : String
  product_name : String
}

///|
priv struct MobileHarmonyos {
  bundle_name : String
  app_name : String?
  module_name : String?
  module_description : String?
  entry_description : String?
  compatible_sdk_version : Int
}

///|
priv struct MobileApp {
  id : String
  display_name : String
  metadata_path : String
  android : MobileAndroid?
  ios : MobileIos?
  harmonyos : MobileHarmonyos?
}

///|
fn mobile_optional_string(
  fields : Map[String, Json],
  key : String,
  label : String,
) -> Result[String?, String] {
  match fields.get(key) {
    Some(String(value)) if value.trim() != "" => Ok(Some(value))
    Some(_) => Err(label + "." + key + " must be a non-empty string")
    None => Ok(None)
  }
}

///|
fn mobile_numeric_dotted_version_at_least(
  value : String,
  major : Int,
  minor : Int,
) -> Bool {
  let parts = value.split(".").to_array()
  if parts.is_empty() || parts.length() > 3 {
    return false
  }
  let parsed : Array[Int] = []
  for part in parts {
    let text = part.to_owned()
    if text == "" || !text.all(char => char >= '0' && char <= '9') {
      return false
    }
    let number = Ref(0)
    for unit in text {
      number.val = number.val * 10 + unit.to_int() - '0'.to_int()
    }
    parsed.push(number.val)
  }
  let actual_major = parsed[0]
  let actual_minor = if parsed.length() > 1 { parsed[1] } else { 0 }
  actual_major > major || (actual_major == major && actual_minor >= minor)
}

///|
fn parse_mobile_android(
  value : Json,
  label : String,
) -> Result[MobileAndroid, String] {
  let fields = match
    json_exact_object(
      value,
      ["applicationId", "minSdk"],
      ["applicationId", "minSdk"],
      label,
    ) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let application_id = match
    json_required_string(fields, "applicationId", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let min_sdk = match json_required_int(fields, "minSdk", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  if min_sdk < 23 {
    return Err(label + ".minSdk must be at least 23")
  }
  Ok({ application_id, min_sdk, })
}

///|
fn parse_mobile_ios(value : Json, label : String) -> Result[MobileIos, String] {
  let fields = match
    json_exact_object(
      value,
      ["bundleId", "productName", "deploymentTarget"],
      ["bundleId", "productName", "deploymentTarget"],
      label,
    ) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let bundle_id = match json_required_string(fields, "bundleId", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let product_name = match json_required_string(fields, "productName", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let _deployment_target = match
    json_required_string(fields, "deploymentTarget", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  if !mobile_numeric_dotted_version_at_least(_deployment_target, 15, 0) {
    return Err(label + ".deploymentTarget must be at least 15.0")
  }
  Ok({ bundle_id, product_name, })
}

///|
fn parse_mobile_harmonyos(
  value : Json,
  label : String,
) -> Result[MobileHarmonyos, String] {
  let fields = match
    json_exact_object(
      value,
      [
        "bundleName", "productName", "appName", "moduleName", "moduleDescription",
        "entryDescription", "compatibleSdkVersion",
      ],
      ["bundleName", "productName", "compatibleSdkVersion"],
      label,
    ) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let bundle_name = match json_required_string(fields, "bundleName", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let _product_name = match json_required_string(fields, "productName", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let app_name = match mobile_optional_string(fields, "appName", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let module_name = match mobile_optional_string(fields, "moduleName", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let module_description = match
    mobile_optional_string(fields, "moduleDescription", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let entry_description = match
    mobile_optional_string(fields, "entryDescription", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let compatible_sdk_version = match
    json_required_int(fields, "compatibleSdkVersion", label) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  if compatible_sdk_version < 20 {
    return Err(label + ".compatibleSdkVersion must be at least 20")
  }
  Ok({
    bundle_name,
    app_name,
    module_name,
    module_description,
    entry_description,
    compatible_sdk_version,
  })
}

///|
fn read_mobile_app_at(metadata_path : String) -> Result[MobileApp, String] {
  let text = @fs.read_file_to_string(metadata_path) catch {
    _ => return Err("mobile configuration not found: " + metadata_path)
  }
  let parsed = @json.parse(text) catch {
    _ => return Err("mobile configuration is invalid JSON: " + metadata_path)
  }
  let fields = match
    json_exact_object(
      parsed,
      [
        "schemaVersion", "id", "displayName", "artifactName", "appPackage", "android",
        "ios", "harmonyos",
      ],
      ["schemaVersion", "id", "displayName", "artifactName", "appPackage"],
      metadata_path,
    ) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let schema_version = match
    json_required_int(fields, "schemaVersion", metadata_path) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  if schema_version != mobile_config_schema_version {
    return Err(metadata_path + ".schemaVersion must be 1")
  }
  let id = match json_required_string(fields, "id", metadata_path) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let display_name = match
    json_required_string(fields, "displayName", metadata_path) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let _artifact_name = match
    json_required_string(fields, "artifactName", metadata_path) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let _app_package = match
    json_required_string(fields, "appPackage", metadata_path) {
    Ok(result) => result
    Err(message) => return Err(message)
  }
  let android = match fields.get("android") {
    Some(value) =>
      match parse_mobile_android(value, metadata_path + ".android") {
        Ok(result) => Some(result)
        Err(message) => return Err(message)
      }
    None => None
  }
  let ios = match fields.get("ios") {
    Some(value) =>
      match parse_mobile_ios(value, metadata_path + ".ios") {
        Ok(result) => Some(result)
        Err(message) => return Err(message)
      }
    None => None
  }
  let harmonyos = match fields.get("harmonyos") {
    Some(value) =>
      match parse_mobile_harmonyos(value, metadata_path + ".harmonyos") {
        Ok(result) => Some(result)
        Err(message) => return Err(message)
      }
    None => None
  }
  if android is None && ios is None && harmonyos is None {
    return Err(
      metadata_path +
      " must configure at least one of android, ios, or harmonyos",
    )
  }
  Ok({ id, display_name, metadata_path, android, ios, harmonyos, })
}

///|
fn read_mobile_app(project_root : String) -> Result[MobileApp, String] {
  read_mobile_app_at(join_path(project_root, "moui.mobile.json"))
}

///|
fn mobile_entrypoint_package(
  metadata_path : String,
  platform : String,
  workspace_root : String,
) -> Result[String, String] {
  let app_root = parent_dir(metadata_path)
  let selected = join_path(app_root, platform + "_window_hosted")
  let has_pkg = @fs.is_file(join_path(selected, "moon.pkg")) catch {
    _ => false
  }
  if !has_pkg {
    return Err(
      "window-hosted " + platform + " entrypoint is missing under " + app_root,
    )
  }
  match mobile_path_relative_to(workspace_root, selected) {
    Ok(value) => Ok(value)
    Err(_) => Ok(selected)
  }
}

///|
fn mobile_path_relative_to(
  workspace_root : String,
  target : String,
) -> Result[String, String] {
  match
    mobile_relative_path_from_normalized(
      normalized_path(workspace_root),
      normalized_path(target),
    ) {
    Ok(value) => return Ok(value)
    Err(_) => ()
  }
  let root = match canonical_path(workspace_root) {
    Some(value) => normalized_path(value)
    None => return Err("target is not inside workspace root")
  }
  let candidate = match canonical_path(target) {
    Some(value) => normalized_path(value)
    None => return Err("target is not inside workspace root")
  }
  mobile_relative_path_from_normalized(root, candidate)
}

///|
fn mobile_relative_path_from_normalized(
  root : String,
  candidate : String,
) -> Result[String, String] {
  if !candidate.has_prefix(root) {
    return Err("target is not inside workspace root")
  }
  let prefix_length = root.length()
  if candidate.length() == prefix_length {
    Ok("")
  } else if candidate.length() > prefix_length &&
    candidate[prefix_length] == '/' {
    Ok(candidate[prefix_length + 1:].to_owned())
  } else {
    Err("target is not inside workspace root")
  }
}