//! Chanora PoC — Android mobile audio spike.
//!
//! Closes the mobile half of the PoC plan §2 audio exit criterion:
//!   "Capture/playback works on at least one desktop and one mobile target."
//!
//! Desktop half is closed by `poc/audio-capture-playback-spike` on Linux/PipeWire.
//! This crate is the mobile-Android half.
//!
//! Approach:
//!   * `cpal` 0.16 with no extra features — on Android it automatically
//!     uses the Oboe backend (AAudio + OpenSL ES fallback). Matches
//!     DEC-011 "platform-native first".
//!   * The Kotlin app calls two JNI entry points:
//!       - Java_app_chanora_poc_audio_NativeAudio_playSine440()
//!       - Java_app_chanora_poc_audio_NativeAudio_record1sToFile(path)
//!   * `tracing` is not used; on Android we route `log` macros to logcat
//!     via `android_logger` so all output is visible via `adb logcat`.

#![cfg(target_os = "android")]

use std::panic::{self, AssertUnwindSafe};
use std::sync::Once;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, SizedSample};
use hound::{SampleFormat as HoundSampleFormat, WavSpec, WavWriter};
use jni::objects::{JClass, JString};
use jni::sys::{jint, jlong, JNI_VERSION_1_6};
use jni::JNIEnv;
use log::{error, info, warn, LevelFilter};

const TAG: &str = "ChanoraAudioPoC";

static LOG_INIT: Once = Once::new();

// Stash the JavaVM* captured at library-load time. We need it to
// resolve a JNIEnv on the audio thread later if we ever want to make
// JNI calls there. cpal's Oboe backend uses `ndk_context` instead,
// which we initialise from the Kotlin side once we have a Context.
static mut JAVA_VM: *mut std::ffi::c_void = std::ptr::null_mut();

/// JNI entry point invoked by the Android runtime when our cdylib is
/// loaded via `System.loadLibrary`. Captures the JavaVM* but does NOT
/// yet initialise the ndk_context — that needs a `Context` reference,
/// which only the Activity has.
#[no_mangle]
pub extern "system" fn JNI_OnLoad(vm: *mut std::ffi::c_void, _reserved: *mut std::ffi::c_void) -> jint {
    ensure_logging();
    unsafe {
        JAVA_VM = vm;
    }
    info!("JNI_OnLoad: captured JavaVM");
    JNI_VERSION_1_6
}

/// Called by `MainActivity.onCreate` with the activity reference.
/// Initialises the `ndk_context` so cpal's Oboe backend can locate
/// the Android `Context` it needs.
#[no_mangle]
pub extern "system" fn Java_app_chanora_poc_audio_NativeAudio_initContext<'local>(
    env: JNIEnv<'local>,
    _class: JClass<'local>,
    context: jni::objects::JObject<'local>,
) {
    ensure_logging();
    // Promote the local-ref Context to a global ref so it survives
    // beyond this JNI call. ndk_context borrows the pointer.
    let global = match env.new_global_ref(&context) {
        Ok(g) => g,
        Err(e) => {
            error!("new_global_ref(context) failed: {e}");
            return;
        }
    };
    let raw_ctx = global.as_obj().as_raw() as *mut std::ffi::c_void;
    // Leak the global ref intentionally — it must live for the
    // lifetime of the library (which is the lifetime of the process).
    std::mem::forget(global);
    unsafe {
        ndk_context::initialize_android_context(JAVA_VM, raw_ctx);
    }
    info!("initContext: ndk_context initialised (vm={:p}, ctx={:p})", unsafe { JAVA_VM }, raw_ctx);
}

fn ensure_logging() {
    LOG_INIT.call_once(|| {
        android_logger::init_once(
            android_logger::Config::default()
                .with_max_level(LevelFilter::Trace)
                .with_tag(TAG),
        );
        // Route panics into logcat so they don't disappear into the void.
        panic::set_hook(Box::new(|info| {
            let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
                (*s).to_string()
            } else if let Some(s) = info.payload().downcast_ref::<String>() {
                s.clone()
            } else {
                "<unknown panic payload>".to_string()
            };
            let location = info
                .location()
                .map(|l| format!("{}:{}", l.file(), l.line()))
                .unwrap_or_else(|| "<unknown location>".to_string());
            error!("RUST PANIC at {location}: {payload}");
        }));
        info!("logging initialised");
    });
}

/// Run a closure that produces a result, catching any panic and
/// converting it to an error string. Required because our JNI
/// functions are `extern "system"` and panicking across that
/// boundary is undefined behaviour (and aborts the process).
fn catch<F: FnOnce() -> Result<u64, String>>(label: &str, f: F) -> jlong {
    let r = panic::catch_unwind(AssertUnwindSafe(f));
    match r {
        Ok(Ok(n)) => {
            info!("{label} ok n={n}");
            n as jlong
        }
        Ok(Err(e)) => {
            error!("{label} returned err: {e}");
            -1
        }
        Err(_) => {
            // The panic hook already logged the details.
            error!("{label} panicked (caught)");
            -2
        }
    }
}

/// Synth a 440 Hz mono sine for `duration_ms` directly through the
/// default output device. Returns a status code (0 = OK, non-zero =
/// error code). Errors are also logged to logcat.
#[no_mangle]
pub extern "system" fn Java_app_chanora_poc_audio_NativeAudio_playSine440(
    _env: JNIEnv,
    _class: JClass,
    duration_ms: jlong,
) -> jlong {
    ensure_logging();
    let dur = Duration::from_millis(duration_ms.max(0) as u64);
    catch("playSine440", || play_sine_inner(440.0, dur))
}

fn play_sine_inner(freq_hz: f32, duration: Duration) -> Result<u64, String> {
    let host = cpal::default_host();
    let device = host
        .default_output_device()
        .ok_or_else(|| "no default output device".to_string())?;
    let device_name = device.name().unwrap_or_else(|_| "<unknown>".to_string());
    let cfg = device
        .default_output_config()
        .map_err(|e| format!("default_output_config: {e}"))?;
    info!(
        "playback device='{}' sample_rate={} channels={} fmt={:?}",
        device_name,
        cfg.sample_rate().0,
        cfg.channels(),
        cfg.sample_format()
    );

    let sample_rate = cfg.sample_rate().0 as f32;
    let channels = cfg.channels() as usize;
    let total_frames = (duration.as_secs_f32() * sample_rate) as u64;
    let phase = Arc::new(Mutex::new(0.0f32));
    let frames_emitted = Arc::new(Mutex::new(0u64));
    let done = Arc::new(Mutex::new(false));

    let stream_config: cpal::StreamConfig = cfg.clone().into();

    fn err_fn(e: cpal::StreamError) {
        error!("playback stream error: {e}");
    }

    let stream = match cfg.sample_format() {
        SampleFormat::F32 => build_play_stream::<f32>(
            &device,
            &stream_config,
            sample_rate,
            freq_hz,
            channels,
            total_frames,
            phase.clone(),
            frames_emitted.clone(),
            done.clone(),
            err_fn,
        )?,
        SampleFormat::I16 => build_play_stream::<i16>(
            &device,
            &stream_config,
            sample_rate,
            freq_hz,
            channels,
            total_frames,
            phase.clone(),
            frames_emitted.clone(),
            done.clone(),
            err_fn,
        )?,
        SampleFormat::U16 => build_play_stream::<u16>(
            &device,
            &stream_config,
            sample_rate,
            freq_hz,
            channels,
            total_frames,
            phase.clone(),
            frames_emitted.clone(),
            done.clone(),
            err_fn,
        )?,
        other => return Err(format!("unsupported sample format: {other:?}")),
    };

    stream
        .play()
        .map_err(|e| format!("play(): {e}"))?;

    // Wait until either the producer reaches total_frames or a generous timeout.
    let timeout = duration + Duration::from_millis(500);
    let start = std::time::Instant::now();
    loop {
        if *done.lock().unwrap() {
            break;
        }
        if start.elapsed() > timeout {
            warn!("playback timeout reached");
            break;
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    drop(stream);
    let final_frames = *frames_emitted.lock().unwrap();
    Ok(final_frames)
}

fn build_play_stream<T>(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    sample_rate: f32,
    freq_hz: f32,
    channels: usize,
    total_frames: u64,
    phase: Arc<Mutex<f32>>,
    frames_emitted: Arc<Mutex<u64>>,
    done: Arc<Mutex<bool>>,
    err_fn: fn(cpal::StreamError),
) -> Result<cpal::Stream, String>
where
    T: SizedSample + FromSineSample + Send + 'static,
{
    let phase_inc = 2.0 * std::f32::consts::PI * freq_hz / sample_rate;
    let stream = device
        .build_output_stream(
            config,
            move |out: &mut [T], _| {
                let mut ph = phase.lock().unwrap();
                let mut emitted = frames_emitted.lock().unwrap();
                for frame in out.chunks_mut(channels) {
                    if *emitted >= total_frames {
                        for s in frame.iter_mut() {
                            *s = T::from_f32(0.0);
                        }
                        *done.lock().unwrap() = true;
                    } else {
                        let v = (*ph).sin() * 0.5;
                        *ph = (*ph + phase_inc) % (2.0 * std::f32::consts::PI);
                        for s in frame.iter_mut() {
                            *s = T::from_f32(v);
                        }
                        *emitted += 1;
                    }
                }
            },
            err_fn,
            None,
        )
        .map_err(|e| format!("build_output_stream: {e}"))?;
    Ok(stream)
}

pub trait FromSineSample {
    fn from_f32(v: f32) -> Self;
}
impl FromSineSample for f32 {
    fn from_f32(v: f32) -> Self {
        v
    }
}
impl FromSineSample for i16 {
    fn from_f32(v: f32) -> Self {
        (v.clamp(-1.0, 1.0) * f32::from(i16::MAX)) as i16
    }
}
impl FromSineSample for u16 {
    fn from_f32(v: f32) -> Self {
        let s = (v.clamp(-1.0, 1.0) * f32::from(i16::MAX)) as i32;
        (s + i32::from(i16::MAX) + 1) as u16
    }
}

/// Record `duration_ms` of mono 16-bit PCM to `path` from the default
/// input device. Returns frames captured (>= 0) or -1 on error.
#[no_mangle]
pub extern "system" fn Java_app_chanora_poc_audio_NativeAudio_record1sToFile(
    mut env: JNIEnv,
    _class: JClass,
    path: JString,
    duration_ms: jlong,
) -> jlong {
    ensure_logging();
    let path_str: String = match env.get_string(&path) {
        Ok(s) => s.into(),
        Err(e) => {
            error!("get_string(path) failed: {e}");
            return -1;
        }
    };
    let dur = Duration::from_millis(duration_ms.max(0) as u64);
    catch("record1sToFile", move || record_inner(&path_str, dur))
}

fn record_inner(path: &str, duration: Duration) -> Result<u64, String> {
    let host = cpal::default_host();
    let device = host
        .default_input_device()
        .ok_or_else(|| "no default input device".to_string())?;
    let device_name = device.name().unwrap_or_else(|_| "<unknown>".to_string());
    let cfg = device
        .default_input_config()
        .map_err(|e| format!("default_input_config: {e}"))?;
    info!(
        "capture device='{}' sample_rate={} channels={} fmt={:?}",
        device_name,
        cfg.sample_rate().0,
        cfg.channels(),
        cfg.sample_format()
    );
    let spec = WavSpec {
        channels: 1,
        sample_rate: cfg.sample_rate().0,
        bits_per_sample: 16,
        sample_format: HoundSampleFormat::Int,
    };
    let writer = Arc::new(Mutex::new(Some(
        WavWriter::create(path, spec).map_err(|e| format!("wav create: {e}"))?,
    )));
    let frames = Arc::new(Mutex::new(0u64));
    let channels = cfg.channels() as usize;

    let stream_config: cpal::StreamConfig = cfg.clone().into();

    fn err_fn(e: cpal::StreamError) {
        error!("capture stream error: {e}");
    }

    let stream = match cfg.sample_format() {
        SampleFormat::F32 => build_capture_stream::<f32>(
            &device, &stream_config, writer.clone(), frames.clone(), channels, err_fn,
        )?,
        SampleFormat::I16 => build_capture_stream::<i16>(
            &device, &stream_config, writer.clone(), frames.clone(), channels, err_fn,
        )?,
        SampleFormat::U16 => build_capture_stream::<u16>(
            &device, &stream_config, writer.clone(), frames.clone(), channels, err_fn,
        )?,
        other => return Err(format!("unsupported sample format: {other:?}")),
    };

    stream
        .play()
        .map_err(|e| format!("capture play(): {e}"))?;
    std::thread::sleep(duration);
    drop(stream);

    if let Some(w) = writer.lock().unwrap().take() {
        w.finalize().map_err(|e| format!("wav finalize: {e}"))?;
    }
    let n = *frames.lock().unwrap();
    Ok(n)
}

fn build_capture_stream<T>(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    writer: Arc<Mutex<Option<WavWriter<std::io::BufWriter<std::fs::File>>>>>,
    frames: Arc<Mutex<u64>>,
    channels: usize,
    err_fn: fn(cpal::StreamError),
) -> Result<cpal::Stream, String>
where
    T: SizedSample + ToI16Sample + Send + 'static,
{
    let stream = device
        .build_input_stream(
            config,
            move |data: &[T], _| {
                let mut w_guard = writer.lock().unwrap();
                if let Some(w) = w_guard.as_mut() {
                    let mut n = 0u64;
                    for frame in data.chunks(channels) {
                        let mut acc: i32 = 0;
                        for s in frame {
                            acc += s.to_i16_sample() as i32;
                        }
                        let mono = (acc / frame.len() as i32) as i16;
                        let _ = w.write_sample(mono);
                        n += 1;
                    }
                    *frames.lock().unwrap() += n;
                }
            },
            err_fn,
            None,
        )
        .map_err(|e| format!("build_input_stream: {e}"))?;
    Ok(stream)
}

pub trait ToI16Sample {
    fn to_i16_sample(&self) -> i16;
}
impl ToI16Sample for i16 {
    fn to_i16_sample(&self) -> i16 {
        *self
    }
}
impl ToI16Sample for u16 {
    fn to_i16_sample(&self) -> i16 {
        (i32::from(*self) - i32::from(i16::MAX) - 1) as i16
    }
}
impl ToI16Sample for f32 {
    fn to_i16_sample(&self) -> i16 {
        let v = (*self * f32::from(i16::MAX)).clamp(f32::from(i16::MIN), f32::from(i16::MAX));
        v as i16
    }
}
