// macOS desktop launch wrappers. Notification permission and delivery are
// keyed by the app bundle identifier, so `moui run macos` launches the built
// executable from a minimal development `.app` bundle instead of a bare Mach-O
// binary. The bundle stays under `_build/` and is ad-hoc signed on every run.

///|
fn macos_dev_bundle_component(value : String) -> String {
  let builder = StringBuilder()
  for index in 0..= 97 && char <= 122) ||
      (char >= 65 && char <= 90) ||
      (char >= 48 && char <= 57)
    if keep {
      builder.write_char(char.unsafe_to_char())
    } else {
      builder.write_char('-')
    }
  }
  let result = builder.to_string()
  if result == "" {
    "app"
  } else {
    result
  }
}

///|
fn macos_dev_bundle_name(flow : DesktopFlow) -> String {
  @plan.product_name(base_name(flow.app_root))
}

///|
fn macos_dev_bundle_id(flow : DesktopFlow, name : String) -> String {
  // Development launches use a `.dev` identity so a previously packaged build
  // with the release bundle id cannot own the LaunchServices/notification
  // record. Include the entrypoint leaf so multiple entrypoints in one project
  // do not overwrite each other's permission state.
  let leaf = macos_dev_bundle_component(base_name(flow.package_path)).to_lower()
  match read_project_manifest(flow.app_root) {
    Ok(manifest) =>
      match manifest.bundle_id {
        Some(value) => value + ".dev"
        None =>
          "dev.wzzc.moui.dev." +
          macos_dev_bundle_component(name).to_lower() +
          "." +
          leaf
      }
    Err(_) =>
      "dev.wzzc.moui.dev." +
      macos_dev_bundle_component(name).to_lower() +
      "." +
      leaf
  }
}

///|
fn macos_dev_bundle_dir(flow : DesktopFlow, name : String) -> String {
  join_path(flow.build_base, "_build/moui-run/" + name + ".app")
}

///|
fn prepare_macos_dev_bundle(
  flow : DesktopFlow,
  artifact : String,
) -> Result[String, String] {
  let name = macos_dev_bundle_name(flow)
  let bundle_id = macos_dev_bundle_id(flow, name)
  let bundle_dir = macos_dev_bundle_dir(flow, name)
  let contents_dir = join_path(bundle_dir, "Contents")
  let macos_dir = join_path(contents_dir, "MacOS")
  let artifact_name = base_name(artifact)
  // A `.app` executable is conventionally extensionless; LaunchServices and
  // UserNotifications both expect the declared executable to be a bundle
  // member, not a `.exe` drop-in.
  let executable = if artifact_name.has_suffix(".exe") {
    artifact_name[:artifact_name.length() - 4].to_owned()
  } else {
    artifact_name
  }
  let inner = join_path(macos_dir, executable)
  remove_tree(bundle_dir)
  match mkdir_p(macos_dir) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match copy_file(artifact, inner) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match make_executable(inner) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  let plist = "\n" +
    "\n" +
    "\n\n" +
    "  CFBundleExecutable\n  " +
    executable +
    "\n" +
    "  CFBundleIdentifier\n  " +
    bundle_id +
    "\n" +
    "  CFBundleName\n  " +
    name +
    "\n" +
    "  CFBundleDisplayName\n  " +
    name +
    "\n" +
    "  CFBundlePackageType\n  APPL\n" +
    "  CFBundleVersion\n  1\n" +
    "  CFBundleShortVersionString\n  0.0.0\n" +
    "  LSMinimumSystemVersion\n  12.0\n" +
    "  NSHighResolutionCapable\n  \n" +
    "  NSPrincipalClass\n  NSApplication\n" +
    "\n\n"
  @fs.write_bytes_to_file(
    join_path(contents_dir, "Info.plist"),
    @utf8.encode(plist[:], bom=false),
  ) catch {
    _ => return Err("could not write Info.plist")
  }
  @fs.write_bytes_to_file(
    join_path(contents_dir, "PkgInfo"),
    @utf8.encode("APPL????", bom=false),
  ) catch {
    _ => return Err("could not write PkgInfo")
  }
  let signing = match
    process_exec_argv(
      ["/usr/bin/codesign", "--force", "--sign", "-", bundle_dir],
      capture=true,
    ) {
    Ok(value) => value
    Err(message) => return Err("could not run codesign: " + message)
  }
  if signing.exit_code != 0 {
    return Err("codesign failed: " + signing.stderr + signing.stdout)
  }
  // Directly exec'ing a bundle binary skips LaunchServices registration, and
  // UserNotifications only resolves a bundle that LaunchServices knows about.
  // Register the finished bundle before launching it.
  let registrar = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
  if path_is_file(registrar) {
    let registered = match
      process_exec_argv([registrar, "-f", bundle_dir], capture=true) {
      Ok(value) => value
      Err(message) => return Err("could not run lsregister: " + message)
    }
    if registered.exit_code != 0 {
      return Err("lsregister failed: " + registered.stderr + registered.stdout)
    }
  }
  Ok(inner)
}