fn runtime() -> &'static Runtime {
    static RT: OnceLock<Runtime> = OnceLock::new();
    RT.get_or_init(|| Runtime::new().expect("create tokio runtime"))
}

fn jstring_to_string(env: &mut Env<'_>, s: JString) -> std::result::Result<String, jni::errors::Error> {
    s.try_to_string(env)
}

fn string_to_jstring(env: &mut Env<'_>, s: &str) -> jstring {
    match env.new_string(s) {
        Ok(o) => o.into_raw(),
        Err(_) => std::ptr::null_mut(),
    }
}

fn jni_call_string_method(env: &mut Env<'_>, obj: JObject, method_name: &str, method_sig: &str) -> std::result::Result<String, jni::errors::Error> {
    use std::str::FromStr;
    let class = env.get_object_class(&obj)?;
    let name_jni = jni::strings::JNIString::from(method_name);
    let sig_runtime = jni::signature::RuntimeMethodSignature::from_str(method_sig)?;
    let sig = sig_runtime.method_signature();
    let method_id = env.get_method_id(&class, name_jni, sig)?;
    // SAFETY: method_id is valid from the preceding get_method_id call, and the method exists on the class.
    let result = unsafe { env.call_method_unchecked(&obj, method_id, jni::signature::ReturnType::Object, &[])? }
        .l()?;
    // SAFETY: JNI return type guaranteed a String, so the raw jstring pointer is valid.
    let jstring = unsafe { JString::from_raw(env, result.into_raw()) };
    jstring_to_string(env, jstring)
}

fn throw_jni_error(env: &mut Env<'_>, msg: &str) {
    // If the error class cannot be found (misconfigured AAR), fall back to a
    // generic RuntimeException so the caller always gets *some* exception rather
    // than a silent null/zero return that looks like a valid result.
    let class_jni = jni::strings::JNIString::from(ERROR_CLASS);
    let msg_jni = jni::strings::JNIString::from(msg);
    if env.throw_new(&class_jni, &msg_jni).is_err() {
        let fallback = jni::strings::JNIString::from("java/lang/RuntimeException");
        let _ = env.throw_new(&fallback, &msg_jni);
    }
}

fn run_or_throw<T, F>(env: &mut Env<'_>, f: F) -> Option<T>
where
    F: FnOnce() -> T + std::panic::UnwindSafe,
{
    match std::panic::catch_unwind(f) {
        Ok(v) => Some(v),
        Err(payload) => {
            let msg = payload.downcast_ref::<String>().cloned()
                .or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string()))
                .unwrap_or_else(|| "panic in native code".to_string());
            throw_jni_error(env, &format!("native panic: {msg}"));
            None
        }
    }
}
