//! BikeControl desktop shell.
//!
//! This crate is *only* wiring: it owns the ride loop, exposes intents as Tauri
//! commands, and pushes state to the webview as events. The ride logic proper
//! lives in `bikecontrol-core`, and device I/O in `bikecontrol-ble` — the
//! webview reaches neither directly (§4.3).

#[cfg(target_os = "android")]
pub mod android;
pub mod backend;
pub mod commands;
pub mod controller;
pub mod derive;
pub mod devices;
pub mod events;
pub mod heart_rate;
pub mod profile_view;
pub mod recording;
pub mod samples;
pub mod session_backend;
pub mod state;
pub mod trainer;
pub mod wakelock;

use tauri::{Manager, RunEvent, WindowEvent};

use crate::state::AppState;

/// Set up `tracing` for whatever this platform calls "somewhere I can read it".
fn init_tracing() {
    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        // `bikecontrol_ble` belongs here as much as the shell does: it owns every
        // BLE conversation, so leaving it at `info` silences exactly the frames
        // NFR-8 says must be loggable — subscribe failures, bad button frames,
        // the lot. That is survivable on desktop, where RUST_LOG can override
        // it, and not on Android, where there is no environment to set.
        .unwrap_or_else(|_| "info,bikecontrol_app_lib=debug,bikecontrol_ble=debug".into());

    // Android has no stdout: the default writer would drop every line. See
    // `android::Logcat`.
    #[cfg(target_os = "android")]
    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_ansi(false)
        .with_writer(android::Logcat)
        .init();

    #[cfg(not(target_os = "android"))]
    tracing_subscriber::fmt().with_env_filter(filter).init();
}

/// The entry point, on every platform.
///
/// `main.rs` calls this on desktop. On Android there is no `main`: the
/// attribute below generates the `start_app` symbol that Tauri's generated
/// Kotlin invokes, which is why the whole app lives in a lib crate.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    init_tracing();

    // Before anything spawns: every BLE call is made from a Tauri task, and on
    // Android a task running on a thread the JVM has never seen cannot reach
    // droidplug at all. See `android::install_async_runtime`.
    #[cfg(target_os = "android")]
    android::install_async_runtime();

    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_fs::init())
        .manage(AppState::new())
        .invoke_handler(tauri::generate_handler![
            // ride lifecycle
            commands::ride_state,
            commands::start_ride,
            commands::pause_ride,
            commands::resume_ride,
            commands::toggle_pause,
            commands::stop_ride,
            commands::reset_ride,
            // recording and export
            commands::ride_summary,
            commands::save_fit,
            commands::recovered_rides,
            // control modes and targets
            commands::set_control_mode,
            commands::cycle_control_mode,
            commands::shift_gear,
            commands::set_gear,
            commands::nudge_gradient,
            commands::set_gradient,
            commands::reset_gradient,
            commands::set_target_resistance,
            commands::set_target_power,
            commands::mark_lap,
            // configuration
            commands::rider_config,
            commands::set_rider_config,
            commands::safety_limits,
            commands::set_safety_limits,
            // profiles
            commands::load_profile_from_path,
            commands::load_profile_from_text,
            commands::preview_profile_yaml,
            commands::clear_profile,
            commands::sample_profiles,
            // devices
            commands::device_list,
            commands::start_scan,
            commands::stop_scan,
            commands::connect_device,
            commands::disconnect_device,
            commands::forget_device,
            commands::trainer_controllable,
            commands::request_bluetooth_enable,
            // controller
            commands::controller_status,
            commands::connect_controller,
            commands::disconnect_controller,
            commands::swap_controller_pods,
        ])
        .setup(|app| {
            let handle = app.handle().clone();
            // NFR-7: scanning starts immediately, not on a user click.
            handle.state::<AppState>().lock().devices.start_scan();
            state::spawn_ride_loop(handle.clone());
            state::spawn_device_loop(handle.clone());
            state::spawn_controller_loop(handle.clone());
            // FR-8.4: a journal with no activity beside it is a ride the app
            // died during. Rebuilding it is the same code path a clean stop
            // uses, so the rider gets the same file they would have had.
            //
            // The result is stashed rather than emitted: nothing is listening
            // on the event channel yet, and a recovered ride is exactly the
            // thing that must not be announced to an empty room. The webview
            // collects it via `recovered_rides` when it starts.
            let recovered = recording::recover_orphans(&handle);
            handle.state::<AppState>().lock().recovered = recovered;
            recording::prune(&handle, commands::KEEP_RECORDINGS);
            state::emit_devices(&handle);
            state::emit_ride_state(&handle);
            Ok(())
        })
        .build(tauri::generate_context!())
        .expect("failed to start BikeControl")
        .run(|app, event| {
            // SAF-2 / SAF-9 — on any exit path, hand the trainer back at zero
            // load and close every link the app owns. `shutdown_devices` blocks
            // until the reset sequence has actually been written; a
            // fire-and-forget send would race the process teardown and leave the
            // rider on a loaded trainer. It is idempotent, which matters because
            // one quit delivers several of these events.
            match &event {
                RunEvent::ExitRequested { .. } | RunEvent::Exit => {
                    // NFR-11: hand the screensaver back too. The inhibitor would
                    // lapse with the process anyway, but not before a slow
                    // shutdown, and a released lock is one fewer thing to explain.
                    crate::wakelock::set(false);
                    state::shutdown_devices(app)
                }
                RunEvent::WindowEvent {
                    event: WindowEvent::Destroyed,
                    ..
                } => {
                    crate::wakelock::set(false);
                    state::shutdown_devices(app)
                }
                _ => {}
            }
        });
}
