// Window-hosted Android packaging.
// Android APK packaging now uses the window-hosted template at
// `window/modules/window/android/template/` +
// `examples//android_window_hosted` entrypoint.
//
// `build_android` generates MoonBit C, stages the window-hosted Gradle
// template, applies app identity (applicationId, namespace, label), and
// invokes `gradle :app:assembleDebug` with the `MBW_*` project properties
// consumed by the template's `app/src/main/cpp/CMakeLists.txt`.

///|
priv struct BuildAndroidOptions {
  app : MobileApp
  workspace_root : String
  skia_root : String
  abi : String
  compile_sdk : Int
  target_sdk : Int
  renderer : String
  build_dir : String
  output_apk : String
  fallback_skia : Bool
  prepare_only : Bool
}

///|
priv struct BuildResult {
  build_dir : String
  output_artifact : String
}

///|
/// Recursively copy a directory tree from `source` into `target` (which must
/// already exist). Files are byte-copied; symlinks are not followed.
fn copy_tree(source : String, target : String) -> Result[Unit, String] {
  let entries = @fs.read_dir(source) catch {
    _ => return Err("could not read directory: " + source)
  }
  for entry in entries {
    let from = join_path(source, entry)
    let to = join_path(target, entry)
    if path_is_dir(from) {
      match mkdir_p(to) {
        Ok(_) => ()
        Err(message) => return Err(message)
      }
      match copy_tree(from, to) {
        Ok(_) => ()
        Err(message) => return Err(message)
      }
    } else if path_is_file(from) {
      match read_file_bytes_optional(from) {
        Some(bytes) =>
          match write_file_bytes_result(to, bytes) {
            Ok(_) => ()
            Err(message) => return Err(message)
          }
        None => return Err("could not read file: " + from)
      }
    }
  }
  Ok(())
}

///|
/// Locate the Gradle wrapper at `/gradlew` or fall back to a
/// `gradle` on PATH. Mirrors the dispatch at lines 48-59 of
/// `window/modules/window/android/scripts/build-hosted-apk.sh`.
fn resolve_gradle_command(
  workspace_root : String,
) -> Result[(String, Array[String]), String] {
  // Prefer a previously staged Gradle wrapper inside an existing Android
  // project (e.g. artifacts/android//android-project/gradlew). This
  // mirrors the upstream script's first lookup.
  let staged_gradlew = join_path(
    join_path(workspace_root, "artifacts/android"),
    "android-project/gradlew",
  )
  if path_is_executable(staged_gradlew) {
    return Ok((staged_gradlew, []))
  }
  let gradlew = join_path(workspace_root, "gradlew")
  if path_is_executable(gradlew) {
    return Ok((gradlew, []))
  }
  match find_in_path("gradle", detect_host_platform()) {
    Some(path) => Ok((path, []))
    None => Err("Gradle was not found; install Gradle or add a gradlew wrapper")
  }
}

///|
/// Stage the window-hosted Android Gradle template into `target_project` and
/// write the `.moui-window-hosted-stage` marker so future runs can replace
/// it safely.
fn stage_android_window_hosted_project(
  template_root : String,
  target_project : String,
) -> Result[Unit, String] {
  let stage_marker = join_path(target_project, ".moui-window-hosted-stage")
  if path_is_dir(target_project) && !path_is_file(stage_marker) {
    return Err(
      "refusing to replace an unowned Android project: " + target_project,
    )
  }
  remove_tree(target_project)
  match mkdir_p(target_project) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match copy_tree(template_root, target_project) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match write_file_bytes_result(stage_marker, b"") {
    Ok(_) => Ok(())
    Err(message) => Err(message)
  }
}

///|
/// Apply mobile app identity onto a staged window-hosted Android template.
/// Patches `app/build.gradle` (applicationId, namespace) and
/// `app/src/main/AndroidManifest.xml` (android:label).
fn apply_android_window_hosted_identity(
  project_root : String,
  app : MobileApp,
  compile_sdk : Int,
  target_sdk : Int,
  _renderer : String,
) -> Result[Unit, String] {
  let android = match app.android {
    Some(value) => value
    None => return Err("moui.mobile.json missing android section")
  }
  let application_id = android.application_id
  // Patch app/build.gradle: namespace, applicationId, compileSdk, targetSdk, minSdk.
  let app_gradle_path = join_path(project_root, "app/build.gradle")
  let app_gradle_text = match read_file_string_optional(app_gradle_path) {
    Some(value) => value
    None => return Err("could not read: " + app_gradle_path)
  }
  let patched_gradle = patch_android_build_gradle(
    app_gradle_text,
    application_id,
    application_id,
    compile_sdk,
    target_sdk,
    android.min_sdk,
    app.display_name,
  )
  match
    write_file_bytes_result(
      app_gradle_path,
      @utf8.encode(patched_gradle[:], bom=false),
    ) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  // Patch AndroidManifest.xml android:label.
  let manifest_path = join_path(
    project_root, "app/src/main/AndroidManifest.xml",
  )
  match read_file_string_optional(manifest_path) {
    Some(manifest_text) => {
      let patched = patch_android_manifest_label(
        manifest_text,
        app.display_name,
      )
      match
        write_file_bytes_result(
          manifest_path,
          @utf8.encode(patched[:], bom=false),
        ) {
        Ok(_) => ()
        Err(message) => return Err(message)
      }
    }
    None => () // Tolerate missing manifest (template guarantees it).
  }
  log_info(
    "[moui-window-hosted-android] Applied identity applicationId=" +
    application_id,
  )
  Ok(())
}

///|
/// Patch `namespace = "..."` and `applicationId = "..."` and SDK versions
/// inside a window-hosted `app/build.gradle` template. Rewrites the lines
/// in place; preserves all other Gradle content.
fn patch_android_build_gradle(
  text : String,
  application_id : String,
  mo_namespace : String,
  compile_sdk : Int,
  target_sdk : Int,
  min_sdk : Int,
  app_label : String,
) -> String {
  let out : Array[String] = []
  for line_view in text.split("\n") {
    let line = line_view.to_owned()
    let trimmed = line.trim().to_owned()
    if trimmed.has_prefix("namespace =") {
      out.push("    namespace = \"" + mo_namespace + "\"")
    } else if trimmed.has_prefix("applicationId =") {
      out.push("        applicationId = \"" + application_id + "\"")
    } else if trimmed.has_prefix("compileSdk =") {
      out.push("    compileSdk = " + compile_sdk.to_string())
    } else if trimmed.has_prefix("minSdk =") {
      out.push("        minSdk = " + min_sdk.to_string())
    } else if trimmed.has_prefix("targetSdk =") {
      out.push("        targetSdk = " + target_sdk.to_string())
    } else if line.contains("android:label=") {
      // Replace any `android:label="..."` literal on this line.
      out.push(patch_android_label_inline(line, app_label))
    } else {
      out.push(line)
    }
  }
  out.join("\n")
}

///|
/// Replace `android:label="..."` substring within a single line, returning
/// the patched line. Used as a non-regex fallback for
/// `patch_android_build_gradle`.
fn patch_android_label_inline(line : String, label : String) -> String {
  let marker = "android:label=\""
  let pos = line.find(marker)
  match pos {
    None => line
    Some(start) => {
      let after_start = start + marker.length()
      let after = line[after_start:]
      let end_rel = after.find("\"")
      match end_rel {
        None => line
        Some(end) => {
          // Reconstruct:  + label +
          //  + .
          let prefix = line[:after_start].to_owned()
          let suffix = after[end + 1:].to_owned()
          prefix + label + "\"" + suffix
        }
      }
    }
  }
}

///|
/// Patch `android:label="..."` in an AndroidManifest.xml document. Falls
/// back to inline line-by-line replacement when no label attribute is
/// present.
fn patch_android_manifest_label(text : String, label : String) -> String {
  let lines : Array[String] = []
  for line_view in text.split("\n") {
    lines.push(line_view.to_owned())
  }
  let out : Array[String] = []
  let mut patched = false
  for line in lines {
    if line.contains("android:label=") {
      out.push(patch_android_label_inline(line, label))
      patched = true
    } else {
      out.push(line)
    }
  }
  if !patched {
    // Inject `android:label` on the first `` of this line.
        let idx = line.find(">")
        match idx {
          Some(pos) => {
            injected.push(
              line[:pos].to_owned() +
              " android:label=\"" +
              label +
              "\"" +
              line[pos:].to_owned(),
            )
            did_inject = true
          }
          None => injected.push(line)
        }
      } else {
        injected.push(line)
      }
    }
    return injected.join("\n")
  }
  out.join("\n")
}

///|
/// Build the Android APK by orchestrating MoonBit C generation + Gradle.
/// Mirrors `window/modules/window/android/scripts/build-hosted-apk.sh`.
fn build_android(
  options : BuildAndroidOptions,
  host : HostPlatform,
) -> Result[BuildResult, String] {
  let _ = host
  // Make build_dir + workspace_root absolute so MBW_* project properties
  // passed to Gradle (which runs with cwd=android-project) resolve
  // correctly regardless of the user's original cwd.
  let workspace_root_abs = make_absolute(
    options.workspace_root,
    @env.get_env_var("PWD").unwrap_or("."),
  )
  let build_dir_abs = make_absolute(options.build_dir, workspace_root_abs)
  // 2. Resolve the upstream window package root and template.
  let window_root = window_package_root(workspace_root_abs)
  let template_root = window_template_root(workspace_root_abs, "android")
  if !path_is_file(join_path(template_root, "app/src/main/cpp/CMakeLists.txt")) ||
    !path_is_file(join_path(template_root, "app/build.gradle")) ||
    !path_is_file(join_path(template_root, "settings.gradle")) {
    return Err(
      "MoUI window-hosted Android template is incomplete: " + template_root,
    )
  }
  // 3. Generate MoonBit C from the entrypoint package.
  let entrypoint_package = match
    mobile_entrypoint_package(
      options.app.metadata_path,
      "android",
      workspace_root_abs,
    ) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  let moonbit_c = match
    generate_moonbit_c(
      workspace_root_abs,
      entrypoint_package,
      base_name(entrypoint_package) + ".c",
      build_dir_abs,
    ) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  if !path_is_file(moonbit_c) {
    return Err("MoonBit generated C was not found: " + moonbit_c)
  }
  // Resolve real Skia link flags via `node moui_skia/build.js`. Mirrors the
  // iOS path in `build_ios.mbt`: when fallback_skia is false, build.js returns
  // `-DMOUI_SKIA_HAS_SKIA ...` (stub_flags) + `libskia.a` or `-lskia` plus
  // Android runtime libs (link_flags). When fallback_skia is true, returns an
  // empty SkiaLink and the stubs link without `MOUI_SKIA_HAS_SKIA`, so
  // `skia_available()` returns false at runtime — the stub-only build-evidence
  // path that presents a black surface.
  let skia_arch = match
    lookup_static_map(android_abi_to_skia_arch, options.abi) {
    Some(value) => value
    None => return Err("unsupported Android ABI: " + options.abi)
  }
  let renderer_selected = if options.fallback_skia {
    "skia-raster"
  } else if options.renderer == "skia-raster" {
    "skia-raster"
  } else {
    "skia-gpu"
  }
  let link_mode = @env.get_env_var("MOUI_SKIA_LINK_MODE").unwrap_or_else(() => {
    if renderer_selected == "skia-gpu" {
      "static"
    } else {
      "dynamic"
    }
  })
  let gpu_env : Array[(String, String)] = if renderer_selected == "skia-gpu" {
    [("MOUI_SKIA_ENABLE_GPU_VULKAN", "1"), ("MOUI_SKIA_ENABLE_GPU_EGL", "1")]
  } else {
    []
  }
  let skia_root_abs = make_absolute(options.skia_root, workspace_root_abs)
  let skia_link = match
    resolve_skia(
      skia_root_abs,
      "android",
      skia_arch,
      link_mode,
      options.fallback_skia,
      gpu_env,
    ) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  // Write Skia flags into response files so the CMake template can read them
  // via `file(READ ...)` + `separate_arguments`. Direct `-D` passing through
  // Gradle `externalNativeBuild.cmake.arguments` would split on spaces and
  // break `-I/path with spaces` and multi-flag stub strings.
  let native_dir = join_path(build_dir_abs, "native")
  let skia_cxx_rsp = join_path(native_dir, "skia-cxx-flags.rsp")
  let skia_link_rsp = join_path(native_dir, "skia-link-flags.rsp")
  match write_rsp(skia_cxx_rsp, skia_link.stub_flags) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match write_rsp(skia_link_rsp, skia_link.link_flags) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  // When the dynamic Skia link mode is selected, `libskia.so` must be packaged
  // into the APK's jniLibs// directory. The static link mode embeds Skia
  // into libwindow_android_app.so and needs no extra packaging.
  let jni_libs_dir : String? = if contains_flag(skia_link.link_flags, "-lskia") {
    let dir = join_path(build_dir_abs, "jniLibs")
    let sdk_root = android_sdk_home()
    if sdk_root != "" {
      let ndk_home = resolve_android_ndk_home(sdk_root)
      if ndk_home != "" {
        match
          copy_android_shared_libs(
            options.abi,
            ndk_home,
            skia_link.link_flags,
            dir,
          ) {
          Ok(_) => ()
          Err(message) => return Err(message)
        }
      }
    }
    Some(dir)
  } else {
    None
  }
  // 4. Stage the window-hosted template into /android-project.
  let android_project = join_path(build_dir_abs, "android-project")
  match stage_android_window_hosted_project(template_root, android_project) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match
    apply_android_window_hosted_identity(
      android_project,
      options.app,
      options.compile_sdk,
      options.target_sdk,
      options.renderer,
    ) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  if options.prepare_only {
    log_info(
      "[moui-window-hosted-android] Prepared " +
      build_dir_abs +
      " (moonbit_c=" +
      moonbit_c +
      ")",
    )
    return Ok({ build_dir: build_dir_abs, output_artifact: "", })
  }
  // 5. Resolve Gradle command.
  let (gradle_cmd, _) = match resolve_gradle_command(workspace_root_abs) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  // 6. Resolve MOON_HOME.
  let moon_home = make_absolute(
    @env.get_env_var("MOON_HOME").unwrap_or_else(() => {
      let home = @env.get_env_var("HOME").unwrap_or("")
      join_path(home, ".moon")
    }),
    workspace_root_abs,
  )
  // 7. Invoke `gradle :app:assembleDebug` with MBW_* project properties.
  let gradle_args = [
    "-p",
    android_project,
    ":app:assembleDebug",
    "-PmbwWorkspaceRoot=" + workspace_root_abs,
    "-PmbwWindowRoot=" + window_root,
    "-PmbwMoonHome=" + moon_home,
    "-PmbwMoonbitC=" + moonbit_c,
    "-PmbwAbi=" + options.abi,
    "-PmbwSkiaCxxRsp=" + skia_cxx_rsp,
    "-PmbwSkiaLinkRsp=" + skia_link_rsp,
    "-PmbwJniLibsDir=" +
    (match jni_libs_dir {
      Some(dir) => dir
      None => ""
    }),
    "--no-daemon",
  ]
  let gradle_env = moon_build_env()
  log_command("gradle", [gradle_cmd, ..gradle_args])
  match
    process_exec_argv(
      [gradle_cmd, ..gradle_args],
      cwd=android_project,
      env=gradle_env,
      capture=false,
    ) {
    Ok(result) if result.exit_code == 0 => ()
    Ok(result) =>
      return Err("Gradle failed with exit code " + result.exit_code.to_string())
    Err(message) => return Err(message)
  }
  // 8. Copy APK to output_apk.
  let apk_path = join_path(
    join_path(android_project, "app/build/outputs/apk/debug"),
    "app-debug.apk",
  )
  if !path_is_file(apk_path) {
    return Err("Gradle APK was not found: " + apk_path)
  }
  match mkdir_p(parent_dir(options.output_apk)) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  match copy_file_atomic(apk_path, options.output_apk) {
    Ok(_) => ()
    Err(message) => return Err(message)
  }
  log_info("[moui-window-hosted-android] Wrote " + options.output_apk)
  Ok({ build_dir: build_dir_abs, output_artifact: options.output_apk, })
}

///|
/// Copy `source` to `target` byte-for-byte. Used to stage the final APK.
fn copy_file_atomic(source : String, target : String) -> Result[Unit, String] {
  match read_file_bytes_optional(source) {
    Some(bytes) => write_file_bytes_result(target, bytes)
    None => Err("could not read source file: " + source)
  }
}