//! Publishes the Android JavaVM and application Context into `ndk_context`.
//!
//! TRACES: UR-012 | DR-223
//!
//! # Why this exists
//!
//! Seven places in this crate (five in `credentials.rs`, two in `lib.rs`) reach
//! the JNI environment through [`ndk_context::android_context`], which reads a
//! process-global pair of pointers: the `JavaVM` and a `Context` jobject.
//!
//! Nothing here ever set that global. `tao` did — the windowing layer beneath
//! `wry`, several dependencies below anything this project names. tao 0.34.5
//! called `ndk_context::initialize_android_context(...)` while starting the
//! Android activity, and our code simply read what it had left behind.
//!
//! **tao 0.35.3 stopped.** It keeps the same two pointers in a private
//! `AndroidContext` struct of its own and no longer publishes them. The moment
//! that landed (via the Tauri 2.9.5 → 2.11.5 upgrade), the first credential
//! read on Android aborted the process:
//!
//! ```text
//! PANIC at ndk-context/src/lib.rs:72: android context was not initialized
//!   8: ndk_context::android_context
//!   9: jellytau_lib::run::{{closure}}
//! ```
//!
//! Not a crash in our code, and not a change to our code: an undocumented side
//! effect of a transitive dependency disappeared. The lesson worth keeping is
//! that relying on *someone else* to populate a global is a dependency you
//! cannot see in `Cargo.toml` and will not be told about when it breaks.
//!
//! # Why restore the global rather than rewrite the call sites
//!
//! Threading a VM and Context handle through seven call sites — including the
//! credential path — is a larger and riskier change than owning the invariant
//! those call sites already depend on. This module makes the assumption true
//! instead of removing it, and the seven callers are untouched.
//!
//! # How
//!
//! `JNI_OnLoad` gives us the `JavaVM` the instant the shared library loads,
//! which is the earliest and most reliable moment available — nothing in the app
//! can run before it. It does *not* give us a Context, so that is resolved
//! lazily on first use via `ActivityThread.currentApplication()`, by which point
//! the Application object certainly exists.
//!
//! The Context published is the **Application**, not the Activity. That is what
//! the consumers want anyway (`SecureStorage.initialize()` immediately calls
//! `context.applicationContext`), and it cannot outlive its own lifetime the way
//! a retained Activity reference would.

use std::ffi::c_void;
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::OnceLock;

use jni::objects::GlobalRef;
use jni::sys::{jint, JNI_VERSION_1_6};
use jni::JavaVM;

/// The `JavaVM`, captured at library load.
static JAVA_VM: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());

/// A global reference to the Application, kept alive for the process lifetime.
///
/// `ndk_context` stores a bare pointer and does not own the reference, so the
/// `GlobalRef` must outlive every read. A local reference would be freed the
/// moment the frame that created it returned, leaving a dangling jobject that
/// only misbehaves later.
static APP_CONTEXT: OnceLock<GlobalRef> = OnceLock::new();

/// Whether the `ndk_context` global has been populated.
static PUBLISHED: OnceLock<bool> = OnceLock::new();

/// Called by the Android runtime when `libjellytau_lib.so` is loaded.
///
/// Verified that neither tao, wry nor tauri defines `JNI_OnLoad` in this
/// library, so there is nothing to collide with. Returning the JNI version is
/// mandatory — returning 0 makes `System.loadLibrary` fail.
///
/// TRACES: UR-012 | DR-223
#[no_mangle]
pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut c_void) -> jint {
    JAVA_VM.store(vm.get_java_vm_pointer().cast(), Ordering::SeqCst);
    // Deliberately no logging here: the logger is not installed this early.
    JNI_VERSION_1_6
}

/// Make [`ndk_context::android_context`] safe to call.
///
/// Idempotent and cheap after the first success. Returns an error rather than
/// panicking: a failure here means credentials fall back to the encrypted-file
/// path, which is a degraded mode the app already supports — far better than
/// aborting the process, which is what the missing global did.
///
/// TRACES: UR-012 | DR-223
pub fn ensure_initialized() -> Result<(), String> {
    if PUBLISHED.get().is_some() {
        return Ok(());
    }

    let vm_ptr = JAVA_VM.load(Ordering::SeqCst);
    if vm_ptr.is_null() {
        return Err(
            "JNI_OnLoad has not run: no JavaVM captured. The library was loaded in an \
             unexpected way, or JNI_OnLoad was stripped from the shared object."
                .to_string(),
        );
    }

    let vm = unsafe { JavaVM::from_raw(vm_ptr.cast()) }
        .map_err(|e| format!("failed to adopt the JavaVM pointer: {e}"))?;

    let mut env = vm
        .attach_current_thread()
        .map_err(|e| format!("failed to attach the current thread to the JVM: {e}"))?;

    // ActivityThread.currentApplication() is the standard way to reach the
    // Application from native code without being handed a Context. It is a
    // hidden-but-stable API; it returns null only before the Application is
    // constructed, which cannot be the case by the time anything here runs.
    let activity_thread = env
        .find_class("android/app/ActivityThread")
        .map_err(|e| format!("android.app.ActivityThread not found: {e}"))?;

    let application = env
        .call_static_method(
            activity_thread,
            "currentApplication",
            "()Landroid/app/Application;",
            &[],
        )
        .map_err(|e| format!("ActivityThread.currentApplication() failed: {e}"))?
        .l()
        .map_err(|e| format!("currentApplication() did not return an object: {e}"))?;

    if application.is_null() {
        return Err(
            "ActivityThread.currentApplication() returned null — the Application has not \
             been created yet."
                .to_string(),
        );
    }

    let global = env
        .new_global_ref(&application)
        .map_err(|e| format!("failed to pin the Application as a global reference: {e}"))?;

    // Store first, publish second: `ndk_context` will hold a bare pointer into
    // this reference, so it must already be owned somewhere permanent.
    let stored = APP_CONTEXT.get_or_init(|| global);
    let context_ptr = stored.as_obj().as_raw().cast::<c_void>();

    unsafe {
        ndk_context::initialize_android_context(vm_ptr, context_ptr);
    }
    let _ = PUBLISHED.set(true);

    log::info!("[INIT] Android JavaVM and Application published to ndk_context");
    Ok(())
}

/// Whether the global has been published, for callers that want to degrade
/// rather than attempt a JNI call.
#[allow(dead_code)]
pub fn is_initialized() -> bool {
    PUBLISHED.get().is_some()
}
