mod commands;
// System tray uses Tauri's `tray-icon` feature which is desktop-only.
// Android has no tray surface — drop the module entirely on that target.
#[cfg(not(target_os = "android"))]
mod tray;

use std::sync::Arc;
use std::time::Instant;

use sqlx::SqlitePool;
use tauri::Manager;

use kon_core::types::EngineName;
use kon_llm::LlmEngine;
use kon_storage::{
    database_path, get_setting, init as init_db, prune_error_log, set_setting,
};

/// How long to retain `error_log` rows. Pruned once on startup.
/// 90 days is long enough to triage "this happened a few weeks ago"
/// reports and short enough that the table stays kilobyte-scale.
const ERROR_LOG_RETENTION_DAYS: i64 = 90;
use kon_transcription::LocalEngine;

/// Shared app state holding the transcription engines and database pool.
pub struct AppState {
    pub whisper_engine: Arc<LocalEngine>,
    pub parakeet_engine: Arc<LocalEngine>,
    pub db: SqlitePool,
    pub llm_engine: Arc<LlmEngine>,
}

/// Holds the preferences init script for injection into secondary windows.
pub struct PreferencesScript(pub String);

/// Build a JavaScript snippet that applies saved preferences to the DOM
/// before Svelte mounts, preventing any flash of unstyled content.
fn build_preferences_script(prefs_json: Option<String>) -> String {
    let json = prefs_json.unwrap_or_default();
    if json.is_empty() {
        return String::new();
    }
    // Serialise the JSON string as a JS string literal for safe embedding
    let js_str = serde_json::to_string(&json).unwrap_or_else(|_| "\"\"".to_string());
    format!(
        r#"(function() {{
  try {{
    var p = JSON.parse({js_str});
    var el = document.documentElement;
    if (p.theme === 'system') {{
      el.dataset.theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
    }} else if (p.theme) {{
      el.dataset.theme = p.theme;
    }}
    if (p.zone && p.zone !== 'default') el.dataset.zone = p.zone;
    if (p.accessibility) {{
      var a = p.accessibility;
      var fonts = {{ lexend: "'Lexend', system-ui, sans-serif", atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif", opendyslexic: "'OpenDyslexic', system-ui, sans-serif" }};
      if (a.fontFamily) {{ el.style.setProperty('--font-family-body', fonts[a.fontFamily] || fonts.lexend); el.dataset.fontFamily = a.fontFamily; }}
      if (a.fontSize) el.style.setProperty('--font-size-body', a.fontSize + 'px');
      if (a.letterSpacing != null) el.style.setProperty('--letter-spacing-body', a.letterSpacing + 'em');
      if (a.lineHeight) el.style.setProperty('--line-height-body', String(a.lineHeight));
      if (a.transcriptSize) el.style.setProperty('--text-transcript', a.transcriptSize + 'px');
      if (a.bionicReading) el.dataset.bionicReading = 'true';
      if (a.reduceMotion === 'on' || (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) el.dataset.reduceMotion = 'true';
    }}
  }} catch (e) {{ console.warn('Preferences injection failed:', e); }}
}})();"#
    )
}

/// Save preferences JSON to the SQLite settings table.
#[tauri::command]
async fn save_preferences(
    state: tauri::State<'_, AppState>,
    preferences: String,
) -> Result<(), String> {
    set_setting(&state.db, "kon_preferences", &preferences)
        .await
        .map_err(|e| e.to_string())
}

/// On Linux Wayland sessions (KDE, GNOME) WebKitGTK + DMABUF rendering hits
/// known crashes that the HANDOVER documents working around with a manual
/// env-var prefix:
///
/// ```sh
/// env GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 \
///     WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev
/// ```
///
/// Detect the Wayland session at startup and apply the env vars before
/// anything else loads, so users do not need to remember the prefix and
/// the dev launcher / packaged app both work out of the box.
///
/// Inspired by Open-Whispr's similar Chromium self-relaunch (with
/// --ozone-platform=x11). Day 6 of the upgrade plan.
#[cfg(target_os = "linux")]
fn ensure_x11_on_wayland() {
    // If the user explicitly opted in to Wayland (XDG_SESSION_TYPE set by
    // their session, KDE / GNOME compositor flags), force WebKitGTK + GDK
    // onto X11 via XWayland. Idempotent: if a value is already set, keep
    // it (the user knows best).
    let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
    let is_wayland = session_type.eq_ignore_ascii_case("wayland");
    if !is_wayland {
        return;
    }

    let set_if_unset = |key: &str, value: &str| {
        if std::env::var_os(key).is_none() {
            // SAFETY: setting env vars before any threads spawn (we are
            // pre-Tauri-Builder here). This block is the only place these
            // are written.
            unsafe {
                std::env::set_var(key, value);
            }
            eprintln!("[startup] Wayland workaround: {key}={value}");
        }
    };

    set_if_unset("GDK_BACKEND", "x11");
    set_if_unset("WINIT_UNIX_BACKEND", "x11");
    set_if_unset("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
    // PipeWire screen-capture portal still works fine through XWayland;
    // we only redirect the GUI rendering path.
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    #[cfg(target_os = "linux")]
    ensure_x11_on_wayland();

    // Capture Rust panics to disk so the diagnostic-report bundler in
    // Settings → About can attach them. Local only; nothing transmitted.
    commands::diagnostics::install_panic_hook();

    let builder = tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_dialog::init())
        // Phase 6 nudges: OS-native notifications. The plugin exposes
        // isPermissionGranted / requestPermission / sendNotification on
        // the JS side; our deliver_nudge wrapper guards those calls
        // against the nudge-bus suppression rules and restricts
        // invocation to the main window via ensure_main_window.
        .plugin(tauri_plugin_notification::init());

    // Desktop-only plugins. Each is either unsupported on Android
    // (global-shortcut, autostart) or structurally meaningless on a
    // single-window mobile app (window-state). Gating here matches the
    // Cargo.toml `cfg(not(target_os = "android"))` block that drops the
    // crates entirely on Android targets.
    #[cfg(not(target_os = "android"))]
    let builder = builder
        .plugin(tauri_plugin_global_shortcut::Builder::new().build())
        // Phase 5 rituals: autostart. The plugin registers JS-facing
        // commands (isEnabled / enable / disable) that the Settings
        // toggle and first-run prompt invoke directly — no bespoke
        // Rust commands needed. `LaunchAgent` is the non-root macOS
        // install path (per-user, no sudo).
        .plugin(tauri_plugin_autostart::init(
            tauri_plugin_autostart::MacosLauncher::LaunchAgent,
            None,
        ))
        // Remember size + position of every window across app restarts.
        // Without this, secondary windows (preview overlay, task float,
        // transcript viewer) open at whatever spot the compositor picks,
        // which feels random. State is persisted per-window-label to
        // app-data/window-state.json.
        .plugin(tauri_plugin_window_state::Builder::default().build());

    builder
        .setup(|app| {
            // Initialise database (blocking in setup — runs once at startup)
            let db_path = database_path();
            let t0 = Instant::now();
            let db = tauri::async_runtime::block_on(async { init_db(&db_path).await })
                .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
            eprintln!("[startup] DB init: {:?}", t0.elapsed());

            // Prune old `error_log` rows so the table doesn't grow unbounded
            // across months of dogfooding. Best-effort — a prune failure is
            // not worth blocking startup over.
            let t_prune = Instant::now();
            let pruned = tauri::async_runtime::block_on(async {
                prune_error_log(&db, ERROR_LOG_RETENTION_DAYS).await
            });
            match pruned {
                Ok(n) if n > 0 => eprintln!(
                    "[startup] Error log prune: {n} rows removed (>{ERROR_LOG_RETENTION_DAYS}d) in {:?}",
                    t_prune.elapsed()
                ),
                Ok(_) => {}
                Err(e) => eprintln!("[startup] Error log prune failed: {e}"),
            }

            // Load saved preferences for webview injection
            let t1 = Instant::now();
            let prefs_json = tauri::async_runtime::block_on(async {
                get_setting(&db, "kon_preferences").await.unwrap_or(None)
            });
            eprintln!("[startup] Preferences load: {:?}", t1.elapsed());
            let init_script = build_preferences_script(prefs_json);

            // Apply preferences to the main window (defined in tauri.conf.json)
            if let Some(main_window) = app.get_webview_window("main") {
                if !init_script.is_empty() {
                    // Tauri's WebviewWindow.eval() is the standard API for
                    // running JS in a webview — not the JS eval() function
                    let _ = main_window.eval(&init_script);
                }

                // Auto-grant microphone permission on Linux (WebKitGTK).
                // WebKitGTK doesn't show a permission dialog — it just denies
                // getUserMedia by default. We connect to the permission-request
                // signal and grant audio capture requests automatically.
                #[cfg(target_os = "linux")]
                {
                    main_window
                        .with_webview(|webview| {
                            use webkit2gtk::glib::prelude::Cast;
                            use webkit2gtk::{
                                PermissionRequest, PermissionRequestExt, SettingsExt,
                                UserMediaPermissionRequest, UserMediaPermissionRequestExt,
                                WebViewExt,
                            };

                            let wv: webkit2gtk::WebView = webview.inner().clone();

                            // Enable media stream in WebKit settings
                            if let Some(settings) = WebViewExt::settings(&wv) {
                                settings.set_enable_media_stream(true);
                                settings.set_enable_media_capabilities(true);
                            }

                            // Auto-grant microphone capture only. Other WebKitGTK
                            // permission requests are denied so future surfaces do
                            // not inherit camera/geolocation/pointer-lock access.
                            WebViewExt::connect_permission_request(
                                &wv,
                                |_wv, request: &PermissionRequest| {
                                    if let Ok(media) =
                                        request.clone().downcast::<UserMediaPermissionRequest>()
                                    {
                                        if media.is_for_audio_device()
                                            && !media.is_for_video_device()
                                        {
                                            request.allow();
                                        } else {
                                            request.deny();
                                        }
                                    } else {
                                        request.deny();
                                    }
                                    true
                                },
                            );
                        })
                        .unwrap_or_else(|e| {
                            // Non-fatal: WebKitGTK may already have media
                            // capture wired by some compositors, or the
                            // signal binding may fail on unusual builds.
                            // Falling back means getUserMedia() prompts (or
                            // silently denies) instead of auto-granting,
                            // which is degraded but recoverable.
                            eprintln!(
                                "[startup] failed to configure webview media permissions: {e}",
                            );
                        });
                }

                // Close-to-tray: hide the window instead of exiting so the
                // tray icon stays as the canonical entry point. Desktop-only;
                // mobile has no tray and "hide on close" maps to the
                // platform's own background-app behaviour.
                #[cfg(not(target_os = "android"))]
                {
                    let win = main_window.clone();
                    main_window.on_window_event(move |event| {
                        if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                            api.prevent_close();
                            let _ = win.hide();
                        }
                    });
                }
            }

            // Store init script for secondary windows (float, viewer)
            app.manage(PreferencesScript(init_script));

            app.manage(commands::hotkey::HotkeyState::new());
            app.manage(commands::audio::NativeCaptureState::new());
            app.manage(commands::live::LiveTranscriptionState::default());
            app.manage(commands::tts::TtsState::new());
            app.manage(commands::meeting::MeetingState::new());

            app.manage(AppState {
                whisper_engine: Arc::new(LocalEngine::new(EngineName::new("whisper"))),
                parakeet_engine: Arc::new(LocalEngine::new(EngineName::new("parakeet"))),
                db,
                llm_engine: Arc::new(LlmEngine::new()),
            });

            // Runtime-warning banner: push CPU-feature + Vulkan-loader
            // fallbacks to the frontend so Settings can render a one-line
            // hint. No-ops on a fully-supported box.
            crate::commands::models::emit_runtime_warnings(app.handle());

            #[cfg(not(target_os = "android"))]
            if let Err(e) = tray::setup(app) {
                eprintln!("Failed to setup tray: {e}");
            }

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            // Preferences
            save_preferences,
            // Whisper model management
            commands::models::download_model,
            commands::models::check_model,
            commands::models::list_models,
            commands::models::load_model,
            commands::models::prewarm_default_model_cmd,
            commands::models::check_engine,
            commands::models::get_runtime_capabilities,
            // Local LLM management
            commands::llm::recommend_llm_tier,
            commands::llm::check_llm_model,
            commands::llm::download_llm_model,
            commands::llm::load_llm_model,
            commands::llm::unload_llm_model,
            commands::llm::delete_llm_model,
            commands::llm::get_llm_status,
            commands::llm::test_llm_model,
            commands::llm::cleanup_transcript_text_cmd,
            // Parakeet model management
            commands::models::download_parakeet_model,
            commands::models::check_parakeet_model,
            commands::models::list_parakeet_models,
            commands::models::load_parakeet_model,
            commands::models::check_parakeet_engine,
            // Transcription
            commands::transcription::transcribe_pcm,
            commands::transcription::transcribe_file,
            commands::transcription::transcribe_pcm_parakeet,
            // Audio
            commands::audio::save_audio,
            commands::audio::start_native_capture,
            commands::audio::stop_native_capture,
            commands::audio::list_audio_devices,
            // Tasks (canonical SQLite-backed task CRUD)
            commands::tasks::create_task_cmd,
            commands::tasks::list_tasks_cmd,
            commands::tasks::update_task_cmd,
            commands::tasks::complete_task_cmd,
            commands::tasks::delete_task_cmd,
            commands::tasks::uncomplete_task_cmd,
            commands::tasks::set_task_energy_cmd,
            commands::tasks::decompose_and_store,
            commands::tasks::extract_tasks_from_transcript_cmd,
            commands::tasks::list_subtasks_cmd,
            commands::tasks::complete_subtask_cmd,
            commands::tasks::list_recent_completions_cmd,
            // B2a — archive surface (Inbox-only auto-sweep + per-row toggle)
            commands::tasks::list_archived_tasks_cmd,
            commands::tasks::archive_task_cmd,
            commands::tasks::unarchive_task_cmd,
            commands::tasks::archive_old_inbox_cmd,
            // B2a — task_lists CRUD (replaces the localStorage source of truth)
            commands::task_lists::list_task_lists_cmd,
            commands::task_lists::create_task_list_cmd,
            commands::task_lists::update_task_list_cmd,
            commands::task_lists::delete_task_list_cmd,
            commands::task_lists::import_task_lists_cmd,
            // B2b — templates CRUD (replaces kon_templates localStorage)
            commands::templates::list_templates_cmd,
            commands::templates::create_template_cmd,
            commands::templates::update_template_cmd,
            commands::templates::delete_template_cmd,
            commands::templates::import_templates_cmd,
            // HITL feedback (Phase 2 roadmap)
            commands::feedback::record_feedback,
            commands::feedback::list_feedback_examples_cmd,
            // Read aloud (Phase 4 roadmap)
            commands::tts::tts_speak,
            commands::tts::tts_stop,
            commands::tts::tts_list_voices,
            // Rituals (Phase 5 roadmap)
            commands::rituals::get_last_morning_triage,
            commands::rituals::mark_morning_triage_shown,
            // Nudges (Phase 6 roadmap)
            commands::nudges::deliver_nudge,
            // Implementation intentions (Phase 7 roadmap)
            commands::intentions::list_implementation_rules,
            commands::intentions::create_implementation_rule,
            commands::intentions::set_implementation_rule_enabled,
            commands::intentions::mark_implementation_rule_fired,
            commands::intentions::delete_implementation_rule,
            // Profiles + profile terms (canonical SQLite-backed profile CRUD) — Task 12
            commands::profiles::list_profiles_cmd,
            commands::profiles::get_profile_cmd,
            commands::profiles::create_profile_cmd,
            commands::profiles::update_profile_cmd,
            commands::profiles::delete_profile_cmd,
            commands::profiles::list_profile_terms_cmd,
            commands::profiles::add_profile_term_cmd,
            commands::profiles::learn_profile_terms_from_edit_cmd,
            commands::profiles::delete_profile_term_cmd,
            // Transcripts (canonical SQLite-backed history) — Day 4
            commands::transcripts::add_transcript,
            commands::transcripts::list_transcripts,
            commands::transcripts::count_transcripts_command,
            commands::transcripts::get_transcript,
            commands::transcripts::update_transcript,
            commands::transcripts::update_transcript_meta_cmd,
            commands::transcripts::delete_transcript,
            commands::transcripts::search_transcripts,
            // Diagnostics (panic + error capture, manual report bundle)
            commands::diagnostics::log_frontend_error,
            commands::diagnostics::list_recent_errors_command,
            commands::diagnostics::list_crash_files,
            commands::diagnostics::generate_diagnostic_report,
            commands::diagnostics::save_diagnostic_report,
            commands::diagnostics::get_os_info,
            commands::live::start_live_transcription_session,
            commands::live::stop_live_transcription_session,
            // Windows
            commands::windows::open_task_window,
            commands::windows::open_viewer_window,
            commands::windows::open_preview_window,
            commands::windows::close_preview_window,
            // Clipboard
            commands::clipboard::copy_to_clipboard,
            // Filesystem (Phase 9 save-dialog path)
            commands::fs::write_text_file_cmd,
            // LLM content tags (Phase 9)
            commands::llm::extract_content_tags_cmd,
            // Auto-title generation (also via per-row + bulk in History)
            commands::llm::generate_title_cmd,
            // Paste (auto-insert at cursor)
            commands::paste::paste_text,
            commands::paste::paste_text_replacing,
            commands::paste::detect_paste_backends,
            // Meeting auto-capture (process-list poll)
            commands::meeting::detect_meeting_processes,
            // Hardware
            commands::hardware::probe_system,
            commands::hardware::rank_models,
            // Hotkey (evdev — Wayland-compatible)
            commands::hotkey::is_wayland_session,
            commands::hotkey::check_hotkey_access,
            commands::hotkey::start_evdev_hotkey,
            commands::hotkey::update_evdev_hotkey,
            commands::hotkey::stop_evdev_hotkey,
            // Updater
            commands::update::check_for_update,
            commands::update::install_update,
        ])
        .run(tauri::generate_context!())
        .expect("error while running Kon");
}
