mod auth;
mod commands;
mod connectivity;
mod credentials;
mod download;
mod jellyfin;
mod playback_mode;
mod playback_reporting;
mod player;
mod repository;
mod session_poller;
pub mod settings;
mod storage;
mod thumbnail;
pub mod utils;

#[cfg(target_os = "android")]
use log::warn;
use log::{error, info};
use std::sync::{Arc, Mutex};
use tauri::{Emitter, Manager};
use tauri_specta::Builder;
use tokio::sync::Mutex as TokioMutex;

use auth::AuthManager;
use commands::{
    auth_connect_to_server,
    auth_get_session,
    // Auth commands
    auth_initialize,
    auth_login,
    auth_logout,
    auth_reauthenticate,
    auth_set_session,
    auth_start_verification,
    auth_stop_verification,
    auth_verify_session,
    calc_progress,
    cancel_download,
    catalog_sync_status,
    clear_stale_downloads,
    // Connectivity commands
    connectivity_check_server,
    connectivity_get_status,
    connectivity_mark_reachable,
    connectivity_mark_unreachable,
    connectivity_set_server_url,
    connectivity_start_monitoring,
    connectivity_stop_monitoring,
    convert_percent_to_volume,
    convert_ticks_to_seconds,
    delete_album_downloads,
    delete_all_downloads,
    delete_download,
    delete_downloads_under,
    // Device commands
    device_get_id,
    device_set_id,
    download_album,
    download_item,
    download_item_and_start,
    download_season,
    download_series,
    download_video,
    enqueue_download,
    enqueue_video_downloads,
    // Conversion commands
    format_time_seconds,
    format_time_seconds_long,
    get_album_affinity_status,
    get_album_recommendations,
    get_download_manager_stats,
    get_download_storage_stats,
    get_downloads,
    get_downloads_allowed,
    get_smart_cache_config,
    get_smart_cache_stats,
    image_get_url,
    is_item_pinned,
    lms_create_sync_group,
    lms_dissolve_sync_group,
    // LMS multi-room sync group commands
    lms_get_sync_groups,
    lms_unsync_player,
    mark_download_completed,
    mark_download_failed,
    offline_get_items,
    offline_is_available,
    offline_search,
    pause_download,
    pin_item,
    playback_mark_played,
    // Playback mode commands
    playback_mode_get_current,
    playback_mode_get_remote_status,
    playback_mode_is_transferring,
    playback_mode_set,
    playback_mode_set_transferring,
    playback_mode_transfer_to_local,
    playback_mode_transfer_to_remote,
    playback_report_progress,
    playback_report_start,
    playback_report_stopped,
    playback_reporter_destroy,
    // Playback reporting commands
    playback_reporter_init,
    // Queue manipulation commands
    player_add_to_queue,
    player_add_track_by_id,
    player_add_tracks_by_ids,
    player_cancel_autoplay_countdown,
    player_cancel_sleep_timer,
    // Jellyfin reporting commands
    player_configure_jellyfin,
    player_cycle_repeat,
    player_disable_jellyfin,
    player_dismiss_session,
    player_enter_background_audio,
    player_exit_background_audio,
    player_get_audio_settings,
    player_get_autoplay_settings,
    player_get_cache_config,
    player_get_queue,
    // Session management commands
    player_get_session,
    player_get_sleep_timer,
    player_get_status,
    player_get_video_settings,
    player_move_in_queue,
    player_next,
    player_on_playback_ended,
    player_pause,
    player_play,
    player_play_album_track,
    player_play_item,
    player_play_next_episode,
    player_play_queue,
    player_play_tracks,
    // Preload commands
    player_preload_upcoming,
    player_previous,
    player_remove_from_queue,
    player_report_media_loaded,
    player_report_position,
    // HTML5 video state-report commands
    player_report_state,
    player_seek,
    player_seek_video,
    player_set_audio_settings,
    player_set_audio_track,
    player_set_autoplay_settings,
    player_set_cache_config,
    // Sleep timer and autoplay commands
    player_set_sleep_timer,
    player_set_subtitle_track,
    player_set_video_settings,
    player_set_volume,
    player_skip_to,
    player_stop,
    player_switch_audio_track,
    player_toggle,
    player_toggle_mute,
    player_toggle_shuffle,
    playlist_add_items,
    // Playlist commands
    playlist_create,
    playlist_delete,
    playlist_get_items,
    playlist_move_item,
    playlist_remove_items,
    playlist_rename,
    // Remote session control commands
    remote_play_on_session,
    remote_send_command,
    remote_session_seek,
    remote_session_set_volume,
    remote_session_toggle_mute,
    // Repository commands
    repository_create,
    repository_destroy,
    repository_get_audio_only_stream_url_for_video,
    repository_get_audio_stream_url,
    repository_get_channels,
    repository_get_download_disk_usage,
    repository_get_downloaded_items,
    repository_get_downloaded_libraries,
    repository_get_genres,
    repository_get_image_url,
    repository_get_item,
    repository_get_items,
    repository_get_items_by_person,
    repository_get_latest_items,
    repository_get_libraries,
    repository_get_live_tv_channels,
    repository_get_next_up_episodes,
    repository_get_person,
    repository_get_playback_info,
    repository_get_recently_played_audio,
    repository_get_rediscover_albums,
    repository_get_resume_items,
    repository_get_resume_movies,
    repository_get_similar_items,
    repository_get_subtitle_url,
    repository_get_video_download_url,
    repository_get_video_stream_url,
    repository_jray_actors_at,
    repository_mark_favorite,
    repository_open_live_stream,
    repository_report_playback_progress,
    repository_report_playback_start,
    repository_report_playback_stopped,
    repository_search,
    repository_unmark_favorite,
    resume_download,
    resume_queued_downloads,
    sessions_poll_now,
    // Session polling commands
    sessions_set_polling_hint,
    set_max_concurrent_downloads,
    set_network_state,
    set_show_server_catalog,
    start_download,
    // Storage commands
    storage_delete_server,
    storage_delete_user,
    storage_get_access_token,
    storage_get_active_session,
    storage_get_active_user,
    storage_get_item,
    storage_get_item_people,
    storage_get_items,
    // Offline cache commands
    storage_get_libraries,
    storage_get_path,
    storage_get_pending_sync_count,
    storage_get_person,
    storage_get_playback_progress,
    storage_get_security_status,
    storage_get_series_audio_preference,
    storage_get_servers,
    storage_get_size,
    storage_get_users,
    storage_init,
    storage_mark_played,
    storage_mark_synced,
    storage_save_item,
    storage_save_item_people,
    storage_save_library,
    // People cache commands
    storage_save_person,
    // Series audio preferences
    storage_save_series_audio_preference,
    storage_save_server,
    storage_save_user,
    storage_search_items,
    storage_set_active_user,
    storage_toggle_favorite,
    storage_update_playback_context,
    storage_update_playback_progress,
    sync_cleanup_completed,
    sync_clear_user,
    sync_full_catalog,
    sync_get_pending,
    sync_get_pending_count,
    sync_mark_completed,
    sync_mark_failed,
    sync_mark_processing,
    // Sync queue commands
    sync_queue_mutation,
    thumbnail_clear_cache,
    thumbnail_delete_item,
    // Thumbnail cache and image commands
    thumbnail_get_cached,
    thumbnail_get_stats,
    thumbnail_save,
    thumbnail_set_limit,
    unpin_item,
    update_smart_cache_config,
    AuthManagerWrapper,
    ConnectivityMonitorWrapper,
    CredentialStoreWrapper,
    DatabaseWrapper,
    DownloadManagerWrapper,
    MediaSessionManagerWrapper,
    PlaybackModeManagerWrapper,
    PlaybackReporterWrapper,
    PlayerStateWrapper,
    RepositoryManagerWrapper,
    SessionPollerWrapper,
    SessionVerifierWrapper,
    SmartCacheWrapper,
    ThumbnailCacheWrapper,
    VideoSettingsWrapper,
};
use connectivity::ConnectivityMonitor;
use credentials::CredentialStore;
use download::cache::{CacheConfig as SmartCacheConfig, SmartCache};
use download::DownloadManager;
use jellyfin::{HttpClient, HttpConfig};
#[cfg(target_os = "android")]
use playback_mode::PlaybackModeManager;
use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
// NullBackend is used both for platforms without a native backend AND as a graceful
// fallback when a native backend (MPV/ExoPlayer) fails to initialize, so the app can
// still launch (browse library, manage downloads, see an error) instead of crashing.
use player::NullBackend;

#[cfg(target_os = "linux")]
use player::MpvBackend;
use settings::VideoSettings;
use storage::Database;
use thumbnail::{CacheConfig as ThumbnailCacheConfig, ThumbnailCache};

#[cfg(target_os = "android")]
use credentials::initialize_secure_storage;

#[cfg(target_os = "android")]
use player::ExoPlayerBackend;

#[cfg(target_os = "android")]
use player::{
    set_media_command_handler, set_remote_volume_handler, MediaCommandHandler, RemoteVolumeHandler,
};

/// Handler for media commands from Android MediaSession (lockscreen/notification controls).
///
/// Routes commands from the system media controls to the right place depending on
/// playback mode: in local mode it drives the local `PlayerController`; in remote
/// (cast) mode it forwards transport commands to the remote Jellyfin session so
/// the lockscreen can control whatever is casting. Stop while casting requests a
/// disconnect back to local playback.
#[cfg(target_os = "android")]
struct MediaSessionHandler {
    player: Arc<TokioMutex<PlayerController>>,
    playback_mode: Arc<PlaybackModeManager>,
    event_emitter: Arc<TauriEventEmitter>,
}

#[cfg(target_os = "android")]
impl MediaSessionHandler {
    /// Forward a transport command to the active remote Jellyfin session.
    ///
    /// Runs async on the Tauri runtime because JNI callbacks arrive on arbitrary
    /// threads without a Tokio context.
    fn handle_remote_command(&self, command: &str, session_id: String) {
        use crate::player::{PlayerEventEmitter, PlayerStatusEvent};

        // Stop while casting means "disconnect and resume locally". The frontend
        // owns the remote->local transfer (it reloads the item locally), so we
        // just signal intent.
        if command == "stop" {
            self.event_emitter
                .emit(PlayerStatusEvent::RemoteDisconnectRequested);
            return;
        }

        let jellyfin_client = {
            let player = self.player.blocking_lock();
            player.jellyfin_client()
        };
        let command = command.to_string();

        tauri::async_runtime::spawn(async move {
            let client = {
                let guard = match jellyfin_client.lock() {
                    Ok(g) => g,
                    Err(e) => {
                        error!("[MediaSession] Failed to lock Jellyfin client: {}", e);
                        return;
                    }
                };
                match guard.as_ref() {
                    Some(c) => c.clone(),
                    None => {
                        warn!("[MediaSession] No Jellyfin client for remote command");
                        return;
                    }
                }
            };

            // Map lockscreen transport commands onto Jellyfin session commands.
            let result = match command.as_str() {
                "play" => client.send_session_command(session_id, "Unpause").await,
                "pause" => client.send_session_command(session_id, "Pause").await,
                "next" => client.send_session_command(session_id, "NextTrack").await,
                "previous" => {
                    client
                        .send_session_command(session_id, "PreviousTrack")
                        .await
                }
                cmd if cmd.starts_with("seek:") => match cmd[5..].parse::<f64>() {
                    Ok(seconds) => {
                        let ticks = (seconds * 10_000_000.0) as i64;
                        client.session_seek(session_id, ticks).await
                    }
                    Err(_) => {
                        warn!("[MediaSession] Bad seek command: {}", command);
                        Ok(())
                    }
                },
                _ => {
                    warn!("[MediaSession] Unknown remote command: {}", command);
                    Ok(())
                }
            };

            if let Err(e) = result {
                error!("[MediaSession] Remote command '{}' failed: {}", command, e);
            }
        });
    }

    /// Drive the local player for a transport command.
    fn handle_local_command(&self, command: &str) {
        // Use blocking_lock since this is called from a non-async JNI callback
        let controller = self.player.blocking_lock();

        let result = match command {
            "play" => controller.play(),
            "pause" => controller.pause(),
            "next" => controller.next(),
            "previous" => controller.previous(),
            "stop" => controller.stop(),
            cmd if cmd.starts_with("seek:") => match cmd[5..].parse::<f64>() {
                Ok(pos) => controller.seek(pos),
                Err(_) => {
                    warn!("[MediaSession] Bad seek command: {}", command);
                    Ok(())
                }
            },
            _ => {
                warn!("[MediaSession] Unknown command: {}", command);
                Ok(())
            }
        };

        if let Err(e) = result {
            error!("[MediaSession] Command '{}' failed: {}", command, e);
        }
    }
}

#[cfg(target_os = "android")]
impl MediaCommandHandler for MediaSessionHandler {
    fn on_command(&self, command: &str) {
        match self.playback_mode.get_mode() {
            playback_mode::PlaybackMode::Remote { session_id } => {
                self.handle_remote_command(command, session_id);
            }
            _ => self.handle_local_command(command),
        }
    }
}

/// Handler for remote volume changes from Android volume buttons when in remote playback mode.
///
/// Routes volume commands to the Jellyfin session via the playback mode manager.
#[cfg(target_os = "android")]
struct RemoteVolumeSessionHandler {
    playback_mode: Arc<PlaybackModeManager>,
}

#[cfg(target_os = "android")]
impl RemoteVolumeHandler for RemoteVolumeSessionHandler {
    fn on_remote_volume_change(&self, command: &str, volume: i32) {
        log::info!("[RemoteVolume] Command: {}, Volume: {}", command, volume);

        // Send the volume command to the remote session asynchronously
        let playback_mode = Arc::clone(&self.playback_mode);
        let command_str = command.to_string();

        // Use tauri::async_runtime::spawn instead of tokio::spawn
        // JNI callbacks happen on arbitrary threads without a Tokio runtime
        log::info!("[RemoteVolume] Spawning async task to send volume command...");
        tauri::async_runtime::spawn(async move {
            log::info!("[RemoteVolume] Async task started, calling send_remote_volume_command...");
            match playback_mode
                .send_remote_volume_command(&command_str, volume)
                .await
            {
                Ok(_) => log::info!("[RemoteVolume] Volume command completed successfully"),
                Err(e) => log::error!("[RemoteVolume] Failed to send volume command: {}", e),
            }
            log::info!("[RemoteVolume] Async task completed");
        });
        log::info!("[RemoteVolume] Async task spawned, returning from JNI callback");
    }
}

/// Payload emitted to the frontend when a native player backend fails to
/// initialize and the app falls back to a no-op backend.
#[derive(Clone, serde::Serialize)]
struct BackendInitError {
    platform: &'static str,
    backend: &'static str,
    message: String,
}

/// Log a backend-initialization failure and notify the frontend, so the UI can
/// surface "playback unavailable" instead of the app hard-crashing.
fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) {
    error!(
        "[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).",
        backend, message
    );
    let _ = app_handle.emit(
        "backend-init-failed",
        BackendInitError {
            platform: std::env::consts::OS,
            backend,
            message,
        },
    );
}

/// Create the appropriate player backend for the current platform.
fn create_player_backend(
    app_handle: tauri::AppHandle,
    playback_reporter: Arc<tokio::sync::Mutex<Option<playback_reporting::PlaybackReporter>>>,
    position_throttler: Arc<playback_reporting::EventThrottler>,
) -> Box<dyn PlayerBackend> {
    let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone()));

    #[cfg(target_os = "android")]
    {
        info!("Android platform detected - initializing ExoPlayer backend");

        // Get the Android context via ndk-context
        let ctx = ndk_context::android_context();

        // Get JavaVM and create JNI environment
        let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };

        match vm {
            Ok(java_vm) => {
                match java_vm.attach_current_thread() {
                    Ok(mut env) => {
                        let context_obj =
                            unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };

                        match ExoPlayerBackend::new(
                            &mut env,
                            &context_obj,
                            _event_emitter.clone(),
                            playback_reporter.clone(),
                            position_throttler.clone(),
                        ) {
                            Ok(backend) => {
                                info!("Successfully initialized ExoPlayer backend for Android");
                                return Box::new(backend);
                            }
                            Err(e) => {
                                // Degrade gracefully instead of crashing the app.
                                emit_backend_init_failed(&app_handle, "exoplayer", e.to_string());
                                return Box::new(NullBackend::new());
                            }
                        }
                    }
                    Err(e) => {
                        emit_backend_init_failed(
                            &app_handle,
                            "exoplayer",
                            format!("attach JNI thread failed: {}", e),
                        );
                        return Box::new(NullBackend::new());
                    }
                }
            }
            Err(e) => {
                emit_backend_init_failed(
                    &app_handle,
                    "exoplayer",
                    format!("create JavaVM failed: {}", e),
                );
                return Box::new(NullBackend::new());
            }
        }
    }

    // For Linux, use MPV backend for audio playback
    #[cfg(target_os = "linux")]
    {
        info!("Linux platform detected - initializing MPV backend for audio");
        match MpvBackend::new(Some(_event_emitter), playback_reporter, position_throttler) {
            Ok(backend) => {
                info!("Successfully initialized MPV backend for Linux");
                return Box::new(backend);
            }
            Err(e) => {
                error!("\n========================================");
                error!("FATAL ERROR: Failed to initialize MPV backend");
                error!("========================================");
                error!("Error: {}", e);
                error!("\nCommon causes:");
                error!("  1. MPV is not installed");
                error!("     Solution: Install MPV using your package manager");
                error!("     - Arch/CachyOS: sudo pacman -S mpv");
                error!("     - Ubuntu/Debian: sudo apt install mpv libmpv-dev");
                error!("     - Fedora: sudo dnf install mpv mpv-libs-devel");
                error!("\n  2. MPV version mismatch (app was built with different libmpv version)");
                error!("     Solution: Rebuild the application");
                error!("     - cd src-tauri && cargo clean && cargo build --release");
                error!("\n  3. Audio system not working");
                error!("     Solution: Verify audio works with: pactl info");
                error!("\nAudio playback will NOT work until this is fixed.");
                error!("========================================\n");

                // Degrade gracefully: launch with a no-op backend so the user can
                // still browse the library and manage downloads, and the frontend
                // can show a "playback unavailable" notice via this event.
                emit_backend_init_failed(&app_handle, "mpv", e.to_string());
                return Box::new(NullBackend::new());
            }
        }
    }

    // Fallback for other platforms
    #[cfg(not(any(target_os = "linux", target_os = "android")))]
    {
        warn!("WARNING: No audio backend available for this platform");
        Box::new(NullBackend::new())
    }
}

/// Construct the tauri-specta command builder. Shared by `run()` and the
/// bindings-export test so the TypeScript bindings always match the handler.
fn specta_builder() -> Builder<tauri::Wry> {
    Builder::<tauri::Wry>::new()
        // Throw on error so generated `commands.*` return Promise<T> and throw,
        // matching the existing frontend's invoke() try/catch convention.
        .error_handling(tauri_specta::ErrorHandlingMode::Throw)
        .events(tauri_specta::collect_events![
            crate::player::events::PlayerStatusEvent
        ])
        .commands(tauri_specta::collect_commands![
            // Player commands
            player_play_item,
            player_enter_background_audio,
            player_exit_background_audio,
            player_play_queue,
            player_play_album_track,
            player_play_tracks,
            player_play,
            player_pause,
            player_toggle,
            player_stop,
            player_next,
            player_previous,
            player_seek,
            player_seek_video,
            player_set_volume,
            player_toggle_mute,
            player_set_audio_track,
            player_switch_audio_track,
            player_set_subtitle_track,
            player_toggle_shuffle,
            player_cycle_repeat,
            player_get_status,
            player_get_queue,
            player_add_to_queue,
            player_add_track_by_id,
            player_add_tracks_by_ids,
            player_remove_from_queue,
            player_move_in_queue,
            player_skip_to,
            player_set_audio_settings,
            player_get_audio_settings,
            player_set_video_settings,
            player_get_video_settings,
            // Sleep timer and autoplay commands
            player_set_sleep_timer,
            player_cancel_sleep_timer,
            player_get_sleep_timer,
            player_get_autoplay_settings,
            player_set_autoplay_settings,
            player_cancel_autoplay_countdown,
            player_play_next_episode,
            player_on_playback_ended,
            player_report_state,
            player_report_position,
            player_report_media_loaded,
            // Preload commands
            player_preload_upcoming,
            player_set_cache_config,
            player_get_cache_config,
            // Jellyfin reporting commands
            player_configure_jellyfin,
            player_disable_jellyfin,
            // Session management commands
            player_get_session,
            player_dismiss_session,
            // Remote session control commands
            remote_play_on_session,
            remote_send_command,
            remote_session_seek,
            remote_session_set_volume,
            remote_session_toggle_mute,
            // LMS multi-room sync group commands
            lms_get_sync_groups,
            lms_create_sync_group,
            lms_unsync_player,
            lms_dissolve_sync_group,
            // Session polling commands
            sessions_set_polling_hint,
            sessions_poll_now,
            // Playback mode commands
            playback_mode_get_current,
            playback_mode_set,
            playback_mode_is_transferring,
            playback_mode_transfer_to_remote,
            playback_mode_get_remote_status,
            playback_mode_transfer_to_local,
            playback_mode_set_transferring,
            // Playback reporting commands
            playback_reporter_init,
            playback_reporter_destroy,
            playback_report_start,
            playback_report_progress,
            playback_report_stopped,
            playback_mark_played,
            // Auth commands
            auth_initialize,
            auth_connect_to_server,
            auth_login,
            auth_verify_session,
            auth_logout,
            auth_get_session,
            auth_set_session,
            auth_start_verification,
            auth_stop_verification,
            auth_reauthenticate,
            // Device commands
            device_get_id,
            device_set_id,
            // Connectivity commands
            connectivity_check_server,
            connectivity_set_server_url,
            connectivity_get_status,
            connectivity_start_monitoring,
            connectivity_stop_monitoring,
            connectivity_mark_reachable,
            connectivity_mark_unreachable,
            // Storage commands
            storage_init,
            storage_get_path,
            storage_get_size,
            storage_get_security_status,
            storage_save_server,
            storage_get_servers,
            storage_delete_server,
            storage_save_user,
            storage_get_users,
            storage_set_active_user,
            storage_get_active_user,
            storage_get_active_session,
            storage_get_access_token,
            storage_delete_user,
            // Playback progress commands
            storage_update_playback_progress,
            storage_update_playback_context,
            storage_mark_played,
            storage_get_playback_progress,
            storage_mark_synced,
            storage_toggle_favorite,
            // Download commands
            download_item,
            download_item_and_start,
            download_album,
            download_video,
            download_series,
            download_season,
            get_downloads,
            pause_download,
            resume_download,
            cancel_download,
            delete_download,
            delete_all_downloads,
            delete_album_downloads,
            delete_downloads_under,
            clear_stale_downloads,
            get_download_storage_stats,
            mark_download_completed,
            mark_download_failed,
            start_download,
            enqueue_download,
            enqueue_video_downloads,
            sync_full_catalog,
            catalog_sync_status,
            set_show_server_catalog,
            resume_queued_downloads,
            get_download_manager_stats,
            set_max_concurrent_downloads,
            get_smart_cache_stats,
            update_smart_cache_config,
            get_smart_cache_config,
            // WiFi-only download gate (UR-053)
            set_network_state,
            get_downloads_allowed,
            get_album_recommendations,
            get_album_affinity_status,
            // Pinning commands
            pin_item,
            unpin_item,
            is_item_pinned,
            // Offline commands
            offline_is_available,
            offline_get_items,
            offline_search,
            // Offline cache commands
            storage_get_libraries,
            storage_get_items,
            storage_get_item,
            storage_search_items,
            storage_save_library,
            storage_save_item,
            storage_get_pending_sync_count,
            // Sync queue commands
            sync_queue_mutation,
            sync_get_pending,
            sync_mark_processing,
            sync_mark_completed,
            sync_mark_failed,
            sync_get_pending_count,
            sync_cleanup_completed,
            sync_clear_user,
            // Thumbnail cache and image commands
            thumbnail_get_cached,
            thumbnail_save,
            thumbnail_get_stats,
            thumbnail_set_limit,
            thumbnail_clear_cache,
            thumbnail_delete_item,
            image_get_url,
            // People cache commands
            storage_save_person,
            storage_get_person,
            storage_save_item_people,
            storage_get_item_people,
            // Series audio preferences
            storage_save_series_audio_preference,
            storage_get_series_audio_preference,
            // Repository commands
            repository_create,
            repository_destroy,
            repository_get_libraries,
            repository_get_items,
            repository_get_item,
            repository_get_downloaded_libraries,
            repository_get_downloaded_items,
            repository_get_download_disk_usage,
            repository_jray_actors_at,
            repository_get_latest_items,
            repository_get_resume_items,
            repository_get_next_up_episodes,
            repository_get_recently_played_audio,
            repository_get_resume_movies,
            repository_get_rediscover_albums,
            repository_get_genres,
            repository_search,
            repository_get_playback_info,
            repository_get_video_stream_url,
            repository_get_audio_stream_url,
            repository_get_audio_only_stream_url_for_video,
            repository_get_live_tv_channels,
            repository_get_channels,
            repository_open_live_stream,
            repository_report_playback_start,
            repository_report_playback_progress,
            repository_report_playback_stopped,
            repository_get_image_url,
            repository_mark_favorite,
            repository_unmark_favorite,
            repository_get_person,
            repository_get_items_by_person,
            repository_get_similar_items,
            repository_get_subtitle_url,
            repository_get_video_download_url,
            // Playlist commands
            playlist_create,
            playlist_delete,
            playlist_rename,
            playlist_get_items,
            playlist_add_items,
            playlist_remove_items,
            playlist_move_item,
            // Conversion commands
            format_time_seconds,
            format_time_seconds_long,
            convert_ticks_to_seconds,
            calc_progress,
            convert_percent_to_volume,
        ])
}

/// Configure GStreamer (the media backend behind WebKitGTK's HTML5 `<video>`
/// element on Linux) to prefer hardware-accelerated VAAPI decoding when the
/// host provides it, falling back to software decoding otherwise.
///
/// All variables are only set if the user has not already exported them, so an
/// explicit override (e.g. forcing software decode for debugging) is respected.
/// They must be applied before WebKitGTK builds its GStreamer pipeline, hence the
/// call at the very top of `run()`.
#[cfg(target_os = "linux")]
fn enable_linux_hardware_video_decoding() {
    // Boost the rank of the modern stateless VAAPI decoders (gst-plugins-bad
    // `va` plugin) so GStreamer selects them ahead of the software decoders. The
    // `MAX` rank wins decoder autoplugging when the hardware/driver supports the
    // codec; unsupported codecs simply fall through to software.
    let rank_overrides = "vah264dec:MAX,vah265dec:MAX,vavp9dec:MAX,vaav1dec:MAX,\
                          vampeg2dec:MAX,vavp8dec:MAX";

    set_env_if_unset("GST_PLUGIN_FEATURE_RANK", rank_overrides);

    // Ensure WebKit keeps GStreamer's hardware/DMABUF video path enabled. Setting
    // this to "0" would force software decoding, so only default it to "1".
    set_env_if_unset("WEBKIT_GST_ENABLE_HW_VIDEO_DECODER", "1");

    info!("[INIT] Linux hardware video decoding (VAAPI) enabled where supported");

    log_available_vaapi_decoders();
}

/// Probe (via `gst-inspect-1.0`, which ships with GStreamer) which VAAPI hardware
/// video decoders GStreamer can actually load on this host, and log the result so
/// it is clear at startup whether hardware decoding is genuinely available or
/// whether playback will fall back to software.
#[cfg(target_os = "linux")]
fn log_available_vaapi_decoders() {
    const HW_DECODERS: &[&str] = &[
        "vah264dec",
        "vah265dec",
        "vavp9dec",
        "vaav1dec",
        "vampeg2dec",
        "vavp8dec",
    ];

    let available: Vec<&str> = HW_DECODERS
        .iter()
        .copied()
        .filter(|name| {
            std::process::Command::new("gst-inspect-1.0")
                .arg(name)
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status()
                .map(|s| s.success())
                .unwrap_or(false)
        })
        .collect();

    if available.is_empty() {
        log::warn!(
            "[INIT] No VAAPI hardware video decoders found via gst-inspect-1.0; \
             video will use software decoding. Install the GStreamer 'va' plugin \
             (gst-plugins-bad) and a VAAPI driver to enable hardware decoding."
        );
    } else {
        info!(
            "[INIT] VAAPI hardware video decoders available to GStreamer: {}",
            available.join(", ")
        );
    }
}

#[cfg(target_os = "linux")]
fn set_env_if_unset(key: &str, value: &str) {
    if std::env::var_os(key).is_none() {
        // SAFETY: called once at startup before any threads that read the
        // environment (WebKitGTK/GStreamer) are spawned.
        std::env::set_var(key, value);
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    // Initialize logger
    env_logger::Builder::from_default_env()
        .filter_level(log::LevelFilter::Info)
        .init();

    // On Linux, video plays through WebKitGTK's HTML5 <video> element, which uses
    // GStreamer as its media backend. Enable hardware-accelerated (VAAPI) decoding
    // when available so video transcoding/decoding does not fall back to the CPU.
    // These must be set before WebKitGTK initializes its GStreamer pipeline.
    #[cfg(target_os = "linux")]
    enable_linux_hardware_video_decoding();

    // NOTE: TypeScript bindings are generated by the `export_typescript_bindings`
    // test (`cargo test export_typescript_bindings`), NOT at runtime. Calling
    // `.export()` here would try to write `../src/lib/api/bindings.ts` at app
    // startup, which panics on devices (e.g. Android) where that path doesn't exist.
    let builder = specta_builder();
    let invoke_handler = builder.invoke_handler();

    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_os::init())
        .invoke_handler(invoke_handler)
        .setup(move |app| {
            // Mount tauri-specta events so PlayerStatusEvent can be emitted to and
            // listened for on the frontend via the generated bindings.
            builder.mount_events(app);

            // Initialize database with proper app data directory
            // Check for test mode environment variable first
            let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
                let test_path = std::path::PathBuf::from(test_data_dir);
                info!("[INIT] Using test data directory: {:?}", test_path);
                test_path.join("jellytau.db")
            } else {
                app
                    .path()
                    .app_data_dir()
                    .expect("Failed to get app data directory")
                    .join("jellytau.db")
            };

            info!("[INIT] Initializing database at: {:?}", db_path);

            // Create the directory if it doesn't exist
            if let Some(parent) = db_path.parent() {
                info!("[INIT] Creating database directory: {:?}", parent);
                match std::fs::create_dir_all(parent) {
                    Ok(_) => info!("[INIT] Database directory ready"),
                    Err(e) => {
                        error!("[INIT ERROR] Failed to create database directory: {}", e);
                        panic!("Failed to create database directory: {}", e);
                    }
                }
            }

            info!("[INIT] Opening database...");
            let database = match Database::open(&db_path) {
                Ok(db) => {
                    info!("[INIT] Database initialized successfully");
                    db
                }
                Err(e) => {
                    error!("[INIT ERROR] Failed to initialize database: {}", e);
                    panic!("Failed to initialize database: {}", e);
                }
            };
            let db_wrapper = DatabaseWrapper(Mutex::new(database));
            app.manage(db_wrapper);

            // On Android, initialize SecureStorage BEFORE creating CredentialStore
            #[cfg(target_os = "android")]
            {
                info!("[INIT] Initializing Android SecureStorage for credentials...");
                let ctx = ndk_context::android_context();
                let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };

                match vm {
                    Ok(java_vm) => {
                        match java_vm.attach_current_thread() {
                            Ok(mut env) => {
                                let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };

                                match initialize_secure_storage(&mut env, &context_obj) {
                                    Ok(()) => {
                                        info!("[INIT] Android SecureStorage initialized successfully");
                                    }
                                    Err(e) => {
                                        warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e);
                                    }
                                }
                            }
                            Err(e) => {
                                warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e);
                            }
                        }
                    }
                    Err(e) => {
                        warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e);
                    }
                }
            }

            // Initialize credential store (keyring with encrypted file fallback)
            info!("[INIT] Initializing credential store...");
            let credential_store = CredentialStore::new();
            let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store));
            app.manage(creds_wrapper);

            // Create shared reporter and throttler Arc wrappers before backend/controller
            info!("[INIT] Creating shared playback reporting infrastructure...");
            let playback_reporter = Arc::new(tokio::sync::Mutex::new(None));
            let position_throttler = Arc::new(playback_reporting::EventThrottler::new());

            // Create player backend with access to AppHandle for event emission
            info!("[INIT] Creating player backend...");
            let backend = create_player_backend(
                app.handle().clone(),
                playback_reporter.clone(),
                position_throttler.clone(),
            );
            let player_controller = PlayerController::new(
                backend,
                playback_reporter.clone(),
                position_throttler.clone(),
            );

            // Wire up event emitter for sleep timer and autoplay notifications
            let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone()));
            player_controller.set_event_emitter(event_emitter.clone());

            let player_arc = Arc::new(TokioMutex::new(player_controller));

            // On Android, register the player controller for autoplay decisions.
            // The MediaSession (lockscreen) handler is set up later, once the
            // playback mode manager exists, so it can route to remote sessions.
            #[cfg(target_os = "android")]
            {
                player::android::set_player_controller(player_arc.clone());
            }

            let player_state = PlayerStateWrapper(player_arc.clone());
            app.manage(player_state);

            // Initialize media session manager
            info!("[INIT] Initializing media session manager...");
            let session_manager = MediaSessionManager::new();
            let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager));
            app.manage(session_wrapper);

            // Initialize playback mode manager
            info!("[INIT] Initializing playback mode manager...");
            let jellyfin_client = {
                let player = player_arc.blocking_lock();
                player.jellyfin_client()
            };
            let playback_mode_manager = playback_mode::PlaybackModeManager::new(
                jellyfin_client.clone(),
                player_arc.clone(),
            );
            let playback_mode_arc = Arc::new(playback_mode_manager);
            // Broadcast mode changes so the frontend's mirror store reconciles to
            // this authoritative one (prevents remote/local control desync).
            playback_mode_arc.set_event_emitter(event_emitter.clone());
            let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone());
            app.manage(playback_mode_wrapper);

            // Initialize session poller manager for remote session polling
            info!("[INIT] Initializing session poller manager...");
            let session_poller = session_poller::SessionPollerManager::new(
                jellyfin_client,
                playback_mode_arc.clone(),
            );
            session_poller.set_event_emitter(event_emitter.clone());
            // Note: start() is deferred until after the connectivity monitor is
            // created below, so the poller can report reachability from its first
            // poll (it drives offline detection + recovery while the user is idle).
            let session_poller_arc = Arc::new(session_poller);
            let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
            app.manage(session_poller_wrapper);

            // On Android, set up the MediaSession (lockscreen) handler and the
            // remote volume handler. Both need the playback mode manager so they
            // can route to the active remote session while casting.
            #[cfg(target_os = "android")]
            {
                info!("[INIT] Setting up MediaSession handler for lockscreen controls...");
                let media_handler = Arc::new(MediaSessionHandler {
                    player: player_arc.clone(),
                    playback_mode: playback_mode_arc.clone(),
                    event_emitter: event_emitter.clone(),
                });
                set_media_command_handler(media_handler);

                info!("[INIT] Setting up remote volume handler for Android...");
                let handler = Arc::new(RemoteVolumeSessionHandler {
                    playback_mode: playback_mode_arc.clone(),
                });
                set_remote_volume_handler(handler);
            }

            // Initialize video settings with defaults
            let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default()));
            app.manage(video_settings);

            // Background-audio handoff base offset (UR-040).
            app.manage(commands::player::BackgroundAudioOffset::default());

            // Initialize thumbnail cache
            info!("[INIT] Initializing thumbnail cache...");
            let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
                std::path::PathBuf::from(test_data_dir)
            } else {
                app
                    .path()
                    .app_data_dir()
                    .expect("Failed to get app data directory")
            };
            let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default());
            let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache));
            app.manage(thumbnail_wrapper);

            // Initialize smart cache for preloading
            info!("[INIT] Initializing smart cache...");
            let smart_cache = SmartCache::new(SmartCacheConfig::default());
            let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache));
            app.manage(smart_cache_wrapper);

            // Initialize download manager
            info!("[INIT] Initializing download manager...");
            let download_dir = app_data_dir.join("downloads");
            let download_manager = DownloadManager::new(download_dir);
            let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager));
            app.manage(download_manager_wrapper);

            // Current network transport, for the WiFi-only download gate (UR-053).
            // Defaults to unmetered ethernet so desktop is never gated; Android
            // overwrites it via set_network_state as soon as the UI starts.
            app.manage(commands::download::NetworkStateWrapper(
                download::network::NetworkStateHandle::new(),
            ));

            // Initialize connectivity monitor
            info!("[INIT] Initializing connectivity monitor...");
            let http_config = HttpConfig::default();
            let http_client = HttpClient::new(http_config)
                .expect("Failed to create HTTP client");
            let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
            connectivity_monitor.set_app_handle(app.handle().clone());

            // Wire the connectivity reporter into the session poller so its
            // continuous background polls drive reachability (offline detection
            // + recovery) even when the user isn't browsing, then start it.
            session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
            session_poller_arc.start();

            // Wrap in Arc for sharing with AuthManager
            let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
            let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
            app.manage(connectivity_wrapper);

            // Initialize auth manager
            info!("[INIT] Initializing auth manager...");
            let auth_http_config = HttpConfig::default();
            let auth_http_client = HttpClient::new(auth_http_config)
                .expect("Failed to create HTTP client for auth");
            let mut auth_manager = AuthManager::new(auth_http_client);

            // Give auth manager a reference to connectivity monitor
            auth_manager.set_connectivity_monitor(connectivity_arc.clone());

            let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager));
            app.manage(auth_manager_wrapper);

            // Initialize session verifier wrapper (initially empty)
            info!("[INIT] Initializing session verifier wrapper...");
            let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None)));
            app.manage(session_verifier_wrapper);

            // Initialize repository manager
            info!("[INIT] Initializing repository manager...");
            let repository_manager = commands::RepositoryManager::new();
            let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
            app.manage(repository_manager_wrapper);

            // Initialize playback reporter wrapper. This MUST share the same Arc
            // the player controller and MPV progress loop report through (created
            // above at `playback_reporter`), otherwise `playback_reporter_init`
            // would populate a dead, parallel Option and no Start/Progress/Stopped
            // would ever reach Jellyfin.
            info!("[INIT] Initializing playback reporter wrapper...");
            let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
            app.manage(playback_reporter_wrapper);

            info!("[INIT] Application setup completed successfully");
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

#[cfg(test)]
mod specta_bindings {
    /// Generates `src/lib/api/bindings.ts`. Run with `cargo test export_typescript_bindings`.
    #[test]
    fn export_typescript_bindings() {
        super::specta_builder()
            .export(
                specta_typescript::Typescript::default()
                    .bigint(specta_typescript::BigIntExportBehavior::Number),
                "../src/lib/api/bindings.ts",
            )
            .expect("failed to export typescript bindings");
    }
}
