#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supported.");

mod cdp;
#[cfg(target_os = "macos")]
mod cef_preflight;
mod cef_profile;
mod core_process;
mod core_rpc;
mod dictation_hotkeys;
mod discord_scanner;
mod fake_camera;
mod file_logging;
mod gmessages_scanner;
mod imessage_scanner;
#[cfg(target_os = "macos")]
mod mascot_native_window;
mod meet_audio;
mod meet_call;
mod meet_scanner;
mod meet_video;
mod native_notifications;
mod notification_settings;
mod process_kill;
mod process_recovery;
mod screen_capture;
mod slack_scanner;
mod telegram_scanner;
mod webview_accounts;
mod webview_apis;
mod whatsapp_scanner;
mod window_state;

#[cfg(target_os = "macos")]
use tauri::WindowEvent;
#[cfg(not(target_os = "linux"))]
use tauri::{
    menu::{Menu, MenuItem},
    tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, RunEvent, WebviewWindow};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState};

#[cfg(any(windows, target_os = "linux"))]
use tauri_plugin_deep_link::DeepLinkExt;

#[cfg(target_os = "macos")]
use objc2::runtime::{AnyClass, AnyObject};
#[cfg(target_os = "macos")]
use objc2::ClassType;
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSPanel, NSWindowCollectionBehavior, NSWindowStyleMask};

// CEF is the only runtime; alias kept so command handlers thread the runtime generic uniformly.
pub(crate) type AppRuntime = tauri::Cef;

#[tauri::command]
fn core_rpc_url() -> String {
    crate::core_rpc::core_rpc_url_value()
}

/// Tauri command: return the per-process bearer token that must be sent with
/// every core RPC request as `Authorization: Bearer <token>`.
///
/// The token is generated by the Tauri shell at startup (inside
/// [`CoreProcessHandle::new`]), injected into the core child process via
/// `OPENHUMAN_CORE_TOKEN`, and stored in the handle — available immediately
/// with no file I/O or timing issues.
#[tauri::command]
fn core_rpc_token(state: tauri::State<'_, core_process::CoreProcessHandle>) -> String {
    log::debug!("[auth] core_rpc_token: returning token to frontend");
    state.inner().rpc_token().to_string()
}

#[tauri::command]
fn overlay_parent_rpc_url() -> Option<String> {
    let url = std::env::var("OPENHUMAN_CORE_RPC_URL").ok()?;
    let trimmed = url.trim();
    if trimmed.is_empty() {
        return None;
    }
    Some(trimmed.to_string())
}

#[tauri::command]
fn process_diagnostics_list_owned() -> Result<Vec<process_recovery::ProcessInfo>, String> {
    match process_recovery::enumerate_openhuman_processes() {
        Ok(processes) => {
            log::info!(
                "[startup-recovery] diagnostics listed {} owned OpenHuman processes",
                processes.len()
            );
            Ok(processes)
        }
        Err(err) => {
            log::warn!("[startup-recovery] diagnostics process enumeration failed: {err}");
            Err(err)
        }
    }
}

#[allow(dead_code)] // Overlay disabled in tauri.conf.json; helper kept for future re-enable.
fn pin_overlay_bottom_right(window: &WebviewWindow<AppRuntime>) {
    let Ok(Some(monitor)) = window.current_monitor() else {
        log::warn!("[overlay] could not resolve current monitor for positioning");
        return;
    };
    let Ok(size) = window.outer_size() else {
        log::warn!("[overlay] could not resolve overlay size for positioning");
        return;
    };

    let margin = 20i32;
    let x = monitor.position().x + monitor.size().width as i32 - size.width as i32 - margin;
    let y = monitor.position().y + monitor.size().height as i32 - size.height as i32 - margin;

    if let Err(err) = window.set_position(PhysicalPosition::new(x, y)) {
        log::warn!("[overlay] failed to pin overlay bottom-right: {err}");
    } else {
        log::info!("[overlay] pinned overlay bottom-right at {},{}", x, y);
    }
}

#[cfg(target_os = "macos")]
#[allow(dead_code)] // Overlay disabled in tauri.conf.json; helper kept for future re-enable.
fn configure_overlay_window_macos(window: &WebviewWindow<AppRuntime>) {
    // Standard NSWindow cannot float above fullscreen apps on macOS because
    // fullscreen apps run in a separate Space. Only NSPanel can do this.
    //
    // Tauri/tao hardcodes NSWindow as the window class, so we use
    // object_setClass() to reclass the existing NSWindow into an NSPanel
    // at runtime. This avoids creating a new window (which crashes because
    // Tao's window delegate is tightly coupled to the original NSWindow).
    //
    // After reclassing, we set the NonactivatingPanel style mask and
    // Transient collection behavior — matching the working Swift overlay
    // helper (accessibility/helper.rs OverlayController) which is confirmed
    // to float above fullscreen apps on macOS Sonoma.
    //
    // Previous attempts that FAILED:
    // 1. CGShieldingWindowLevel + CanJoinAllSpaces + FullScreenAuxiliary → hidden
    // 2. Window level i32::MAX-17 + Stationary → hidden
    // 3. CGS private API CGSSetWindowTags sticky bit → hidden
    // 4. object_setClass WITHOUT NonactivatingPanel style mask → hidden
    // 5. Create new NSPanel + reparent webview → CRASH (Tao delegate panic)
    //
    // See: https://github.com/tauri-apps/tauri/issues/11488

    match window.ns_window() {
        Ok(ns_window_raw) => unsafe {
            let ns_window = ns_window_raw as *mut AnyObject;

            // ── Reclass NSWindow → NSPanel ──────────────────────────
            let panel_class: *const AnyClass = NSPanel::class();
            objc2::ffi::object_setClass(ns_window, panel_class);
            log::info!("[overlay] reclassed NSWindow → NSPanel via object_setClass");

            // Cast to NSPanel for method calls
            let panel: &NSPanel = &*(ns_window as *const NSPanel);

            // ── Style mask: add NonactivatingPanel ──────────────────
            // This is the KEY piece the Swift helper uses. Without it,
            // the panel doesn't behave as a proper non-activating panel
            // and won't float above fullscreen Spaces.
            let current_style = panel.styleMask();
            panel.setStyleMask(current_style | NSWindowStyleMask::NonactivatingPanel);

            // ── Collection behavior ─────────────────────────────────
            // The Swift helper uses .canJoinAllSpaces + .transient
            // (NOT .stationary or .fullScreenAuxiliary alone).
            // Transient means the panel follows the active Space and
            // appears above fullscreen apps.
            panel.setCollectionBehavior(
                NSWindowCollectionBehavior::CanJoinAllSpaces
                    | NSWindowCollectionBehavior::Transient
                    | NSWindowCollectionBehavior::FullScreenAuxiliary
                    | NSWindowCollectionBehavior::IgnoresCycle,
            );

            // ── Window level: status bar tier ───────────────────────
            // NSStatusWindowLevel = 25. The Swift helper uses .statusBar
            // which is the same value.
            panel.setLevel(25);

            // ── Panel-specific properties ───────────────────────────
            panel.setFloatingPanel(true);
            panel.setHidesOnDeactivate(false);
            panel.setBecomesKeyOnlyIfNeeded(true);
            panel.setWorksWhenModal(true);

            // Make sure it's ordered front
            panel.orderFrontRegardless();

            log::info!(
                "[overlay] NSPanel configured — level=25, \
                 NonactivatingPanel+canJoinAllSpaces+transient, \
                 floatingPanel={}, hidesOnDeactivate={}",
                panel.isFloatingPanel(),
                panel.hidesOnDeactivate(),
            );
        },
        Err(err) => {
            log::warn!("[overlay] failed to access native NSWindow handle: {err}");
        }
    }
}

/// Core update is handled by the Tauri shell auto-updater (`tauri-plugin-updater`)
/// since the core ships in-process with the app. This command is kept as a
/// no-op stub so the frontend's `checkCoreUpdate` keeps working without errors;
/// it always reports the running version as up-to-date.
#[tauri::command]
async fn check_core_update(
    _state: tauri::State<'_, core_process::CoreProcessHandle>,
) -> Result<serde_json::Value, String> {
    let version = env!("CARGO_PKG_VERSION");
    Ok(serde_json::json!({
        "running_version": version,
        "minimum_version": version,
        "outdated": false,
        "latest_version": version,
        "update_available": false,
    }))
}

/// Stub kept for frontend compatibility — use `apply_app_update` instead.
#[tauri::command]
async fn apply_core_update(
    _state: tauri::State<'_, core_process::CoreProcessHandle>,
    _app: tauri::AppHandle<AppRuntime>,
) -> Result<(), String> {
    Err("core ships in-process; use the Tauri shell updater (apply_app_update) instead".into())
}

#[tauri::command]
async fn restart_core_process(
    state: tauri::State<'_, core_process::CoreProcessHandle>,
) -> Result<(), String> {
    log::info!("[core] restart_core_process: command invoked from frontend");
    let _guard = state.inner().restart_lock().await;
    log::debug!("[core] restart_core_process: acquired restart lock");
    state.inner().restart().await
}

/// Start the embedded core process on demand.
///
/// Called by the BootCheckGate (Local mode) before the version check.  The
/// core no longer auto-spawns at Tauri setup — the UI is responsible for
/// driving the lifecycle so it can surface startup failures and version
/// mismatches to the user.
///
/// Idempotent: `ensure_running` is a no-op if the core is already up.
#[tauri::command]
async fn start_core_process(
    state: tauri::State<'_, core_process::CoreProcessHandle>,
) -> Result<(), String> {
    log::info!("[core] start_core_process: command invoked from frontend");
    state.inner().ensure_running().await
}

/// Cleanly exit the application.
///
/// Called by the BootCheckGate "Quit" button when the core is unreachable and
/// the user chooses to close the app rather than switch modes.
#[tauri::command]
async fn app_quit(app: tauri::AppHandle<AppRuntime>) -> Result<(), String> {
    log::info!("[app] app_quit: quit requested from frontend");
    app.exit(0);
    Ok(())
}

#[tauri::command]
async fn restart_app(app: tauri::AppHandle<AppRuntime>) -> Result<(), String> {
    log::info!("[app] restart_app invoked from frontend");
    // Persist main-window geometry and hide the window before exit so
    // the macOS WindowServer doesn't briefly black-out the desktop layer
    // on the (now defunct) display when the focused app dies, and so
    // the new process can land its window on the same display+position
    // the user had it on. (#900 secondary fixes)
    if let Some(window) = app.get_webview_window("main") {
        window_state::save_main(&window);
        if let Err(err) = window.hide() {
            log::warn!("[app] hide main window before restart failed: {err}");
        }
    }

    log::info!("[app] restart_app — starting early teardown before restart");
    perform_early_teardown_async(&app).await;
    log::info!("[app] restart_app — early teardown complete, restarting");

    app.restart();
    // restart() does not return, but we must satisfy the signature
    Ok(())
}

/// Read the authoritative active user id from `active_user.toml` so the
/// frontend can seed `userScopedStorage` BEFORE redux-persist hydrates.
///
/// The previous frontend-only seed (a `localStorage` key) was bound to the
/// per-user CEF profile dir, so on every restart-driven user flip the new
/// process read whatever value the new profile's `localStorage` happened to
/// hold from a prior session — usually stale, triggering a false re-flip and
/// a restart loop. The Rust core writes `active_user.toml` atomically as part
/// of `auth_store_session`, so it's the only profile-independent source of
/// truth available to the UI at boot. Reuses
/// `cef_profile::default_root_openhuman_dir()` so the lookup honors
/// `OPENHUMAN_WORKSPACE` overrides used in test harnesses. (#900)
#[tauri::command]
fn get_active_user_id() -> Result<Option<String>, String> {
    let dir = cef_profile::default_root_openhuman_dir()?;
    Ok(cef_profile::read_active_user_id(&dir))
}

#[tauri::command]
async fn schedule_cef_profile_purge(user_id: Option<String>) -> Result<String, String> {
    let queued = cef_profile::queue_profile_purge_for_user(user_id.as_deref())?;
    Ok(queued.display().to_string())
}

/// Information about an available shell-app update returned to the frontend.
#[derive(Debug, Clone, serde::Serialize)]
struct AppUpdateInfo {
    /// The currently-running app version (matches `tauri.conf.json::version`).
    current_version: String,
    /// True when the configured updater endpoint advertises a newer version.
    available: bool,
    /// Newer version reported by the updater endpoint, if any.
    available_version: Option<String>,
    /// Release notes / body for the new version, if the manifest provided one.
    body: Option<String>,
}

/// Probe the updater endpoint and report whether a newer shell build is available.
/// Does NOT download or install. Pair with `apply_app_update` to actually upgrade.
#[tauri::command]
async fn check_app_update(app: tauri::AppHandle<AppRuntime>) -> Result<AppUpdateInfo, String> {
    use tauri_plugin_updater::UpdaterExt;

    let current_version = app.package_info().version.to_string();
    log::info!("[app-update] check requested (current: {current_version})");

    let updater = app
        .updater()
        .map_err(|e| format!("updater plugin not initialized: {e}"))?;

    match updater.check().await {
        Ok(Some(update)) => {
            log::info!(
                "[app-update] update available: {} -> {}",
                current_version,
                update.version
            );
            Ok(AppUpdateInfo {
                current_version,
                available: true,
                available_version: Some(update.version.clone()),
                body: update.body.clone(),
            })
        }
        Ok(None) => {
            log::info!("[app-update] no update available");
            Ok(AppUpdateInfo {
                current_version,
                available: false,
                available_version: None,
                body: None,
            })
        }
        Err(e) => {
            log::warn!("[app-update] check failed: {e}");
            Err(format!("update check failed: {e}"))
        }
    }
}

/// Download and install the latest shell update, then relaunch.
///
/// Shuts the core sidecar down before download begins so the install step
/// (which on macOS replaces the entire `.app` bundle) does not race against
/// a live sidecar holding file handles inside `Contents/Resources/`. The
/// new bundled sidecar is launched fresh after `app.restart()`.
///
/// Emits Tauri events `app-update:status` and `app-update:progress` so the
/// frontend can show a snackbar / progress bar.
#[tauri::command]
async fn apply_app_update(
    state: tauri::State<'_, core_process::CoreProcessHandle>,
    app: tauri::AppHandle<AppRuntime>,
) -> Result<(), String> {
    use tauri::Emitter;
    use tauri_plugin_updater::UpdaterExt;

    log::info!("[app-update] manual apply_app_update invoked from frontend");

    let updater = app
        .updater()
        .map_err(|e| format!("updater plugin not initialized: {e}"))?;

    let _ = app.emit("app-update:status", "checking");

    let update = match updater.check().await {
        Ok(Some(u)) => u,
        Ok(None) => {
            log::info!("[app-update] no update available");
            let _ = app.emit("app-update:status", "up_to_date");
            return Ok(());
        }
        Err(e) => {
            log::warn!("[app-update] check failed: {e}");
            let _ = app.emit("app-update:status", "error");
            return Err(format!("update check failed: {e}"));
        }
    };

    let new_version = update.version.clone();
    log::info!(
        "[app-update] downloading {} (size hint: {:?})",
        new_version,
        update.signature
    );
    let _ = app.emit("app-update:status", "downloading");

    // Shut the core sidecar down before the install step replaces the .app.
    // We hold the restart lock until app.restart() so nothing tries to
    // respawn the sidecar from the in-flight (or freshly-replaced) bundle.
    let _guard = state.inner().restart_lock().await;
    log::debug!("[app-update] acquired core restart lock");
    state.inner().shutdown().await;

    let progress_app = app.clone();
    let install_app = app.clone();
    let download_result = update
        .download_and_install(
            move |chunk_length, content_length| {
                let payload = serde_json::json!({
                    "chunk": chunk_length,
                    "total": content_length,
                });
                let _ = progress_app.emit("app-update:progress", payload);
            },
            move || {
                log::info!("[app-update] download complete — installing");
                let _ = install_app.emit("app-update:status", "installing");
            },
        )
        .await;

    if let Err(e) = download_result {
        log::error!("[app-update] download/install failed: {e}");
        // Same recovery as `install_app_update`: the .app wasn't swapped,
        // so revive the in-process core we shut down above.
        if let Err(start_err) = state.inner().ensure_running().await {
            log::error!("[app-update] failed to restart core after apply error: {start_err}");
        }
        let _ = app.emit("app-update:status", "error");
        return Err(format!("download_and_install failed: {e}"));
    }

    log::info!("[app-update] install complete — relaunching");
    let _ = app.emit("app-update:status", "restarting");

    log::info!("[app-update] starting early teardown before restart");
    perform_early_teardown_async(&app).await;

    // Note: app.restart() never returns. Anything after this is unreachable.
    app.restart();
}

/// Holds an `Update` handle plus its downloaded bytes between the
/// `download_app_update` (background) and `install_app_update` (user
/// confirmed restart) commands. Sized at ~100MB on macOS for the .app
/// bundle, which is fine to keep in RAM until the user is ready.
struct PendingAppUpdate {
    update: tauri_plugin_updater::Update,
    bytes: Vec<u8>,
    version: String,
}

/// Tauri-managed state slot for the in-flight pending update. `None` means
/// "no update has been downloaded since launch"; `Some(_)` means the bytes
/// are ready and `install_app_update` can finalize without re-downloading.
#[derive(Default)]
struct PendingAppUpdateState(tokio::sync::Mutex<Option<PendingAppUpdate>>);

/// Result returned to the frontend after a download attempt.
#[derive(Debug, Clone, serde::Serialize)]
struct AppUpdateDownloadResult {
    /// True when an update was found and the bytes are now staged.
    ready: bool,
    /// Version of the staged update (if any).
    version: Option<String>,
    /// Release notes for the staged update.
    body: Option<String>,
}

/// Probe the updater endpoint and, if a newer build is advertised, download
/// the bundle bytes into memory but do NOT install. The frontend can then
/// surface a "Restart to apply" prompt at a moment that's safe for the user
/// (no in-flight conversation, etc.) before calling `install_app_update`.
///
/// Emits the same `app-update:status` and `app-update:progress` events as
/// `apply_app_update`, so the React state machine can drive a single UI off
/// either path. Status sequence: `checking` → `downloading` → `ready_to_install`,
/// or `up_to_date` / `error`.
#[tauri::command]
async fn download_app_update(
    app: tauri::AppHandle<AppRuntime>,
    state: tauri::State<'_, PendingAppUpdateState>,
) -> Result<AppUpdateDownloadResult, String> {
    use tauri::Emitter;
    use tauri_plugin_updater::UpdaterExt;

    log::info!("[app-update] download_app_update invoked from frontend");

    let updater = app
        .updater()
        .map_err(|e| format!("updater plugin not initialized: {e}"))?;

    let _ = app.emit("app-update:status", "checking");

    let update = match updater.check().await {
        Ok(Some(u)) => u,
        Ok(None) => {
            log::info!("[app-update] no update available");
            let _ = app.emit("app-update:status", "up_to_date");
            return Ok(AppUpdateDownloadResult {
                ready: false,
                version: None,
                body: None,
            });
        }
        Err(e) => {
            log::warn!("[app-update] check failed: {e}");
            let _ = app.emit("app-update:status", "error");
            return Err(format!("update check failed: {e}"));
        }
    };

    let new_version = update.version.clone();
    let body = update.body.clone();
    log::info!("[app-update] downloading {} (background)", new_version);
    let _ = app.emit("app-update:status", "downloading");

    let progress_app = app.clone();
    let download_result = update
        .download(
            move |chunk_length, content_length| {
                let payload = serde_json::json!({
                    "chunk": chunk_length,
                    "total": content_length,
                });
                let _ = progress_app.emit("app-update:progress", payload);
            },
            || {
                log::info!("[app-update] download complete — staging for install");
            },
        )
        .await;

    let bytes = match download_result {
        Ok(b) => b,
        Err(e) => {
            log::error!("[app-update] download failed: {e}");
            let _ = app.emit("app-update:status", "error");
            return Err(format!("download failed: {e}"));
        }
    };

    let mut slot = state.0.lock().await;
    *slot = Some(PendingAppUpdate {
        update,
        bytes,
        version: new_version.clone(),
    });
    drop(slot);

    log::info!(
        "[app-update] staged {} — awaiting user-initiated install",
        new_version
    );
    let _ = app.emit("app-update:status", "ready_to_install");

    Ok(AppUpdateDownloadResult {
        ready: true,
        version: Some(new_version),
        body,
    })
}

/// Install the previously-downloaded update bytes (staged by
/// `download_app_update`), then relaunch. Errors with `no pending update`
/// if `download_app_update` hasn't run yet — the frontend should fall back
/// to a fresh `apply_app_update` in that case.
///
/// Acquires the core restart lock + shuts the in-process core server down
/// before install, same as `apply_app_update`, so the macOS .app bundle
/// replacement does not race against a live core holding file handles.
#[tauri::command]
async fn install_app_update(
    core_state: tauri::State<'_, core_process::CoreProcessHandle>,
    pending: tauri::State<'_, PendingAppUpdateState>,
    app: tauri::AppHandle<AppRuntime>,
) -> Result<(), String> {
    use tauri::Emitter;

    log::info!("[app-update] install_app_update invoked from frontend");

    let staged = {
        let mut slot = pending.0.lock().await;
        slot.take()
    };
    let staged = match staged {
        Some(s) => s,
        None => {
            log::warn!("[app-update] install requested but no staged update");
            let _ = app.emit("app-update:status", "error");
            return Err("no pending update — call download_app_update first".into());
        }
    };

    log::info!("[app-update] installing staged version {}", staged.version);

    let _guard = core_state.inner().restart_lock().await;
    log::debug!("[app-update] acquired core restart lock");
    core_state.inner().shutdown().await;

    let _ = app.emit("app-update:status", "installing");
    if let Err(e) = staged.update.install(staged.bytes) {
        log::error!("[app-update] install failed: {e}");
        // The .app on disk wasn't replaced, so we keep running the
        // pre-install build — bring the core back up before returning
        // so the user can keep working instead of being silently offline.
        if let Err(start_err) = core_state.inner().ensure_running().await {
            log::error!("[app-update] failed to restart core after install error: {start_err}");
        }
        let _ = app.emit("app-update:status", "error");
        return Err(format!("install failed: {e}"));
    }

    log::info!("[app-update] install complete — relaunching");
    let _ = app.emit("app-update:status", "restarting");

    log::info!("[app-update] starting early teardown before restart");
    perform_early_teardown_async(&app).await;

    // Note: app.restart() never returns. Anything after this is unreachable.
    app.restart();
}

/// Register (or re-register) the global dictation toggle hotkey.
/// Emits `dictation://toggle` to all webviews when the shortcut is pressed.
#[tauri::command]
async fn register_dictation_hotkey(
    app: AppHandle<AppRuntime>,
    shortcut: String,
) -> Result<(), String> {
    log::info!("[dictation] register_dictation_hotkey: shortcut={shortcut}");

    let old_shortcuts = {
        let state = app.state::<dictation_hotkeys::DictationHotkeyState>();
        let guard = state.0.lock().unwrap();
        guard.clone()
    };

    let expanded_shortcuts = dictation_hotkeys::expand_dictation_shortcuts(&shortcut);
    if expanded_shortcuts.is_empty() {
        return Err("Shortcut cannot be empty".to_string());
    }
    log::info!(
        "[dictation] expanded shortcuts: {}",
        expanded_shortcuts.join(", ")
    );

    let register_shortcut = |shortcut_variant: &str| -> Result<(), String> {
        let app_clone = app.clone();
        app.global_shortcut()
            .on_shortcut(shortcut_variant, move |_app, _sc, event| {
                if event.state == ShortcutState::Pressed {
                    log::debug!("[dictation] hotkey pressed — emitting dictation://toggle");
                    if let Err(e) = app_clone.emit("dictation://toggle", ()) {
                        log::warn!("[dictation] emit failed: {e}");
                    }
                }
            })
            .map_err(|e| format!("Failed to register shortcut '{shortcut_variant}': {e}"))
    };

    let mut unregistered_old: Vec<String> = Vec::new();
    for old in &old_shortcuts {
        log::debug!("[dictation] unregistering previous shortcut: {old}");
        if let Err(e) = app.global_shortcut().unregister(old.as_str()) {
            for restored in &unregistered_old {
                if let Err(restore_err) = register_shortcut(restored.as_str()) {
                    log::warn!(
                        "[dictation] rollback failed while restoring old shortcut '{restored}': {restore_err}"
                    );
                }
            }
            return Err(format!(
                "Failed to unregister previous shortcut '{old}': {e}"
            ));
        }
        unregistered_old.push(old.clone());
    }

    let mut newly_registered: Vec<String> = Vec::new();
    for shortcut_variant in &expanded_shortcuts {
        if let Err(err) = register_shortcut(shortcut_variant.as_str()) {
            log::error!("[dictation] failed to register shortcut '{shortcut_variant}': {err}");
            for registered in &newly_registered {
                if let Err(unregister_err) = app.global_shortcut().unregister(registered.as_str()) {
                    log::warn!(
                        "[dictation] rollback failed while unregistering '{registered}': {unregister_err}"
                    );
                }
            }
            for old in &old_shortcuts {
                if let Err(restore_err) = register_shortcut(old.as_str()) {
                    log::warn!(
                        "[dictation] rollback failed while restoring old shortcut '{old}': {restore_err}"
                    );
                }
            }
            return Err(err);
        }
        newly_registered.push(shortcut_variant.clone());
    }

    // Persist all newly registered shortcuts.
    {
        let state = app.state::<dictation_hotkeys::DictationHotkeyState>();
        let mut guard = state.0.lock().unwrap();
        *guard = expanded_shortcuts.clone();
    }

    log::info!(
        "[dictation] shortcuts registered: {}",
        expanded_shortcuts.join(", ")
    );
    Ok(())
}

/// Unregister the global dictation hotkey (if any).
#[tauri::command]
async fn unregister_dictation_hotkey(app: AppHandle<AppRuntime>) -> Result<(), String> {
    log::info!("[dictation] unregister_dictation_hotkey: called");
    let state = app.state::<dictation_hotkeys::DictationHotkeyState>();
    let mut guard = state.0.lock().unwrap();
    if guard.is_empty() {
        log::debug!("[dictation] no shortcut registered — nothing to unregister");
    } else {
        let old_shortcuts = guard.clone();
        guard.clear();
        for old in old_shortcuts {
            log::debug!("[dictation] unregistering shortcut: {old}");
            app.global_shortcut()
                .unregister(old.as_str())
                .map_err(|e| {
                    log::warn!("[dictation] failed to unregister '{old}': {e}");
                    format!("Failed to unregister shortcut '{old}': {e}")
                })?;
            log::info!("[dictation] shortcut unregistered: {old}");
        }
    }
    Ok(())
}

fn is_daemon_mode() -> bool {
    std::env::args().any(|arg| arg == "daemon" || arg == "--daemon")
}

/// Tauri command: bring the main window to front from any webview (e.g. overlay orb click).
#[tauri::command]
fn activate_main_window(app: AppHandle<AppRuntime>) -> Result<(), String> {
    log::debug!("[window] activate_main_window called from overlay");
    show_main_window(&app)
}

/// Show the floating mascot. macOS: native NSPanel + WKWebView (so the
/// window is actually transparent — vendored tauri-cef can't render
/// transparent windowed-mode browsers). Loads the Vite dev URL in
/// development and the bundled `index.html` in production. Other OSes:
/// not yet wired up.
#[tauri::command]
fn mascot_window_show(app: AppHandle<AppRuntime>) -> Result<(), String> {
    log::info!("[mascot-window] show requested");
    #[cfg(target_os = "macos")]
    {
        return mascot_native_window::show(&app);
    }
    #[cfg(not(target_os = "macos"))]
    {
        let _ = app;
        Err("floating mascot window is macOS-only for now".into())
    }
}

/// Hide the floating mascot.
#[tauri::command]
fn mascot_window_hide(app: AppHandle<AppRuntime>) -> Result<(), String> {
    log::info!("[mascot-window] hide requested");
    #[cfg(target_os = "macos")]
    {
        let _ = app;
        mascot_native_window::hide();
        Ok(())
    }
    #[cfg(not(target_os = "macos"))]
    {
        let _ = app;
        Ok(())
    }
}

#[cfg(target_os = "macos")]
fn mascot_native_window_is_open() -> bool {
    mascot_native_window::is_open()
}

#[cfg(not(target_os = "macos"))]
fn mascot_native_window_is_open() -> bool {
    false
}

fn show_main_window(app: &AppHandle<AppRuntime>) -> Result<(), String> {
    let window = app
        .get_webview_window("main")
        .ok_or_else(|| "main window not found".to_string())?;
    window
        .show()
        .map_err(|err| format!("failed to show main window: {err}"))?;
    window
        .unminimize()
        .map_err(|err| format!("failed to unminimize main window: {err}"))?;
    window
        .set_focus()
        .map_err(|err| format!("failed to focus main window: {err}"))?;
    Ok(())
}
#[cfg(target_os = "linux")]
fn setup_tray(app: &AppHandle<AppRuntime>) -> tauri::Result<()> {
    let _ = app;
    log::warn!(
        "[tray] skipping tray setup on linux: tray menu creation still panics inside GTK during packaged runs"
    );
    Ok(())
}

#[cfg(not(target_os = "linux"))]
fn setup_tray(app: &AppHandle<AppRuntime>) -> tauri::Result<()> {
    log::info!("[tray] setting up tray icon");

    let show_item = MenuItem::with_id(
        app,
        "tray_show_window",
        "Open OpenHuman",
        true,
        None::<&str>,
    )?;
    let quit_item = MenuItem::with_id(app, "tray_quit", "Quit", true, None::<&str>)?;
    // The floating mascot has a native NSPanel + WKWebView host, so the
    // tray entry only does anything on macOS. Don't surface a menu item
    // on Windows that's guaranteed to error — gate it to the platform
    // where `mascot_window_show` actually works.
    #[cfg(target_os = "macos")]
    let menu = {
        let mascot_item = MenuItem::with_id(
            app,
            "tray_toggle_mascot",
            "Toggle floating mascot",
            true,
            None::<&str>,
        )?;
        Menu::with_items(app, &[&show_item, &mascot_item, &quit_item])?
    };
    #[cfg(not(target_os = "macos"))]
    let menu = Menu::with_items(app, &[&show_item, &quit_item])?;

    let icon = app
        .default_window_icon()
        .cloned()
        .ok_or_else(|| tauri::Error::AssetNotFound("default window icon".to_string()))?;

    TrayIconBuilder::with_id("openhuman-tray")
        .icon(icon)
        .menu(&menu)
        .on_menu_event(|app, event| match event.id().as_ref() {
            "tray_show_window" => {
                log::info!("[tray] action=show_window source=menu");
                if let Err(err) = show_main_window(app) {
                    log::warn!("[tray] failed to show main window from menu: {err}");
                }
            }
            "tray_toggle_mascot" => {
                log::info!("[tray] action=toggle_mascot source=menu");
                if mascot_native_window_is_open() {
                    if let Err(err) = mascot_window_hide(app.clone()) {
                        log::error!("[tray] failed to hide mascot window: {err}");
                    }
                } else if let Err(err) = mascot_window_show(app.clone()) {
                    log::error!("[tray] failed to show mascot window: {err}");
                }
            }
            "tray_quit" => {
                log::info!("[tray] action=quit source=menu");
                shutdown_app_sync(app, 0);
            }
            _ => {}
        })
        .on_tray_icon_event(|tray, event| {
            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                log::info!("[tray] action=show_window source=left_click");
                if let Err(err) = show_main_window(tray.app_handle()) {
                    log::warn!("[tray] failed to show main window from tray click: {err}");
                }
            }
        })
        .build(app)?;

    log::info!("[tray] tray icon ready");
    Ok(())
}

const CEF_PREWARM_LABEL: &str = "cef-prewarm";

/// Spawn a hidden 1×1 child webview at `about:blank` on the main window so
/// CEF's child-webview render path is hot before the user clicks an
/// account. The first `webview_account_open` then skips the cold
/// renderer-process spinup. Idempotent — bails if the prewarm webview
/// already exists.
fn spawn_cef_prewarm(app: &AppHandle<AppRuntime>) -> Result<(), String> {
    use tauri::webview::WebviewBuilder;
    use tauri::WebviewUrl;

    if app.get_webview(CEF_PREWARM_LABEL).is_some() {
        return Ok(());
    }
    let parent = app
        .get_window("main")
        .ok_or_else(|| "main window not found".to_string())?;
    let url: tauri::Url = "about:blank"
        .parse()
        .map_err(|e| format!("about:blank parse: {e}"))?;
    let builder = WebviewBuilder::new(CEF_PREWARM_LABEL, WebviewUrl::External(url));
    parent
        .add_child(
            builder,
            tauri::LogicalPosition::new(-20000.0, -20000.0),
            tauri::LogicalSize::new(1.0, 1.0),
        )
        .map_err(|e| format!("add_child failed: {e}"))?;
    log::info!("[cef-prewarm] hidden warmup webview spawned");
    Ok(())
}

/// Drop the prewarm webview if still alive. Called from `RunEvent::Exit`
/// so its CEF browser is torn down before `cef::shutdown()` runs.
fn teardown_cef_prewarm<R: tauri::Runtime>(app: &AppHandle<R>) -> Result<(), String> {
    let Some(wv) = app.get_webview(CEF_PREWARM_LABEL) else {
        return Err("no prewarm webview".into());
    };
    wv.close().map_err(|e| e.to_string())?;
    log::info!("[cef-prewarm] teardown ok");
    Ok(())
}

const CEF_CLOSE_FIXED_YIELD: std::time::Duration = std::time::Duration::from_millis(20);
const CEF_CLOSE_POLL_BUDGET: std::time::Duration = std::time::Duration::from_millis(300);
const CEF_CLOSE_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(20);

fn close_early_cef_webviews<R: tauri::Runtime>(app: &AppHandle<R>) -> Vec<String> {
    let mut closed_labels = Vec::new();
    if teardown_cef_prewarm(app).is_ok() {
        closed_labels.push(CEF_PREWARM_LABEL.to_string());
    }
    if let Some(state) = app.try_state::<webview_accounts::WebviewAccountsState>() {
        closed_labels.extend(state.shutdown_all(app));
    }
    closed_labels
}

fn shutdown_imessage_scanner<R: tauri::Runtime>(app: &AppHandle<R>) {
    if let Some(registry) = app.try_state::<std::sync::Arc<imessage_scanner::ScannerRegistry>>() {
        registry.inner().shutdown();
    }
}

fn pending_cef_webview_labels<R: tauri::Runtime>(
    app: &AppHandle<R>,
    labels: &[String],
) -> Vec<String> {
    let mut seen = std::collections::HashSet::new();
    labels
        .iter()
        .filter(|label| seen.insert((*label).clone()))
        .filter(|label| app.get_webview(label.as_str()).is_some())
        .cloned()
        .collect()
}

async fn wait_for_cef_webviews_to_close_async<R: tauri::Runtime>(
    app: &AppHandle<R>,
    labels: &[String],
) {
    if labels.is_empty() {
        return;
    }
    log::info!(
        "[app] waiting for CEF webview close requests labels={:?}",
        labels
    );
    tokio::time::sleep(CEF_CLOSE_FIXED_YIELD).await;
    let start = std::time::Instant::now();
    let mut pending = pending_cef_webview_labels(app, labels);
    while !pending.is_empty() && start.elapsed() < CEF_CLOSE_POLL_BUDGET {
        tokio::time::sleep(CEF_CLOSE_POLL_INTERVAL).await;
        pending = pending_cef_webview_labels(app, labels);
    }
    if pending.is_empty() {
        log::info!(
            "[app] CEF webview close poll drained labels={:?} elapsed_ms={}",
            labels,
            start.elapsed().as_millis()
        );
    } else {
        log::info!(
            "[app] CEF webview close poll still pending labels={:?} elapsed_ms={} (will continue in runtime shutdown)",
            pending,
            start.elapsed().as_millis()
        );
    }
}

/// Shared early teardown logic before CEF's shutdown to prevent races and zombie processes.
///
/// Synchronous entry used from `RunEvent::ExitRequested` and tray quit. We intentionally
/// **do not** poll here with `std::thread::sleep` — that would block the Tauri / CEF main
/// event loop and prevent close messages from being processed. Close requests are issued
/// in [`close_early_cef_webviews`]; the exit pump drains them. Use
/// [`perform_early_teardown_async`] when an async caller can await
/// [`wait_for_cef_webviews_to_close_async`] without starving the UI loop.
fn perform_early_teardown_sync(app_handle: &AppHandle<AppRuntime>) {
    log::info!("[app] perform_early_teardown_sync — early teardown");

    let closed_labels = close_early_cef_webviews(app_handle);
    shutdown_imessage_scanner(app_handle);

    webview_apis::server::stop();

    if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
        let core = core.inner().clone();
        // Aborts the embedded server task. Synchronous and safe on
        // the UI thread — `JoinHandle::abort` returns immediately.
        tauri::async_runtime::block_on(async move {
            core.send_terminate_signal().await;
        });
    }

    if !closed_labels.is_empty() {
        log::info!(
            "[app] sync early teardown: close requested for labels={:?} — skipping main-thread poll so the event loop can drain CEF",
            closed_labels
        );
    }

    log::info!("[app] perform_early_teardown_sync — early teardown complete");
}

/// Shared early teardown logic before CEF's shutdown to prevent races and zombie processes.
/// Asynchronous version to be called from async Tauri commands (e.g. `restart_app`, updates).
async fn perform_early_teardown_async(app_handle: &AppHandle<AppRuntime>) {
    log::info!("[app] perform_early_teardown_async — early teardown");

    let closed_labels = close_early_cef_webviews(app_handle);
    shutdown_imessage_scanner(app_handle);

    webview_apis::server::stop();

    if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
        let core = core.inner().clone();
        core.send_terminate_signal().await;
    }

    wait_for_cef_webviews_to_close_async(app_handle, &closed_labels).await;

    log::info!("[app] perform_early_teardown_async — early teardown complete");
}

/// Explicitly winds down CEF and Tauri before an app.exit(0)
fn shutdown_app_sync(app_handle: &AppHandle<AppRuntime>, exit_code: i32) {
    log::info!("[app] shutdown_app_sync — starting early teardown");
    perform_early_teardown_sync(app_handle);
    log::info!("[app] shutdown_app_sync — early teardown complete, exiting");
    app_handle.exit(exit_code);
}

pub fn run() {
    // Initialize Sentry for the Tauri shell (desktop host) process before any
    // other startup work. Reads `OPENHUMAN_TAURI_SENTRY_DSN` at runtime first,
    // then falls back to the value baked in at compile time via the release
    // workflow. Missing/empty DSN ⇒ `sentry::init` returns a no-op guard.
    //
    // The guard is held for the entire lifetime of `run()` so events queued
    // during shutdown still flush. Only invoked here (and not in `main.rs`)
    // so renderer/GPU CEF helper subprocesses (re-exec'd via
    // `tauri::cef_entry_point`) and the `OpenHuman core …` in-process core
    // path do NOT spin up a second client — those have their own reporting
    // surfaces.
    let _sentry_guard = sentry::init(sentry::ClientOptions {
        dsn: std::env::var("OPENHUMAN_TAURI_SENTRY_DSN")
            .ok()
            .filter(|s| !s.is_empty())
            .or_else(|| option_env!("OPENHUMAN_TAURI_SENTRY_DSN").map(|s| s.to_string()))
            .filter(|s| !s.is_empty())
            .and_then(|s| s.parse().ok()),
        release: Some(std::borrow::Cow::Owned(build_sentry_release_tag())),
        environment: Some(std::borrow::Cow::Owned(resolve_sentry_environment())),
        send_default_pii: false,
        before_send: Some(std::sync::Arc::new(|mut event| {
            // Strip server_name (hostname) to avoid leaking machine identity.
            event.server_name = None;
            event.user = None;
            Some(event)
        })),
        sample_rate: 1.0,
        ..sentry::ClientOptions::default()
    });
    // Tag every Sentry event with CPU architecture and OS so Intel-specific
    // crashes (issue #1012 — SIGABRT in CrBrowserMain on x86_64 macOS) are
    // clearly identified without needing a separate build identifier.
    sentry::configure_scope(|scope| {
        scope.set_tag("cpu_arch", std::env::consts::ARCH);
        scope.set_tag("os_name", std::env::consts::OS);
        #[cfg(target_os = "macos")]
        if let Some(ver) = macos_os_version() {
            scope.set_tag("os_version", ver);
        }
    });

    // Optional smoke trigger for verifying the Sentry pipeline end-to-end.
    // Run with `OPENHUMAN_TAURI_SENTRY_TEST=panic` to fire a panic, or
    // `=message` to send a captured-message event. No-op when unset.
    if let Ok(mode) = std::env::var("OPENHUMAN_TAURI_SENTRY_TEST") {
        match mode.as_str() {
            "panic" => panic!("OPENHUMAN_TAURI_SENTRY_TEST=panic — local Sentry smoke test"),
            "message" => {
                sentry::capture_message(
                    "OPENHUMAN_TAURI_SENTRY_TEST=message — local Sentry smoke test",
                    sentry::Level::Error,
                );
                let _ = sentry::Hub::current().client().map(|c| c.flush(None));
            }
            other => log::warn!(
                "OPENHUMAN_TAURI_SENTRY_TEST={other:?} — unknown mode (use 'panic' or 'message')"
            ),
        }
    }

    let daemon_mode = is_daemon_mode();

    // Install the unified tracing subscriber + daily-rotated file appender
    // before any other startup work so CEF preflight failures, sentry
    // smoke-test events, and the rest of `run()` are captured in
    // `<data_dir>/logs/openhuman-YYYY-MM-DD.log`. The shell's `log::*` calls
    // are bridged into the same subscriber via `tracing_log::LogTracer`,
    // replacing the previous stderr-only `env_logger`.
    file_logging::init();

    // Log platform identity early so every log session is tagged with arch
    // and OS version — essential for reproducing and triaging Intel-only
    // crashes like issue #1012 (SIGABRT in CrBrowserMain on x86_64 macOS).
    {
        let arch = std::env::consts::ARCH;
        let os = std::env::consts::OS;
        #[cfg(target_os = "macos")]
        let os_ver = macos_os_version().unwrap_or_else(|| "unknown".to_string());
        #[cfg(not(target_os = "macos"))]
        let os_ver = "n/a".to_string();
        log::info!("[startup] platform: arch={arch} os={os} os_version={os_ver}");
    }

    // The vendored tauri-cef dev-server proxy builds a reqwest 0.13 client
    // (see vendor/tauri-cef/crates/tauri/src/protocol/tauri.rs) which calls
    // rustls 0.23's `CryptoProvider::get_default()`. rustls 0.23 no longer
    // picks a provider implicitly — without one installed, the proxy panics
    // with "No provider set" the first time `tauri dev` forwards a request.
    // Install the ring provider once before any HTTPS client is built.
    let _ = rustls::crypto::ring::default_provider().install_default();

    // CEF cache-lock preflight (macOS only): if another OpenHuman instance
    // is already holding the CEF user-data-dir, the vendored
    // `tauri-runtime-cef` panics inside `cef::initialize` with a Rust
    // backtrace and no actionable message (issue #864). Catch the collision
    // here and exit cleanly with a message that names the lock-holder PID
    // and the workaround. Stale locks (PID dead) are removed and we
    // continue, matching Chromium's own startup recovery.
    match cef_profile::prepare_process_cache_path() {
        Ok(path) => log::debug!("[cef-profile] startup cache path={}", path.display()),
        Err(error) => {
            log::error!(
                "[cef-profile] failed to configure per-user CEF cache; refusing to start with shared/global cache: {error}"
            );
            std::process::exit(1);
        }
    }

    #[cfg(target_os = "macos")]
    process_recovery::reap_stale_openhuman_processes();

    #[cfg(target_os = "macos")]
    if let Err(e) = cef_preflight::check_default_cache() {
        eprintln!("\n[openhuman] {e}\n");
        std::process::exit(1);
    }

    let builder = {
        // Bypass macOS Keychain. Without this, every embedded service that
        // touches password / cookie / encryption-key storage triggers a
        // "Allow access to your keychain?" prompt — WhatsApp Web hits it on
        // every cold start, Chromium's own component-update store also does.
        // `use-mock-keychain` swaps the Keychain backend for an in-process
        // mock; `password-store=basic` is the equivalent for the password
        // manager. Both are no-ops on Windows/Linux, so safe to always set.
        //
        // In debug builds we additionally expose the Chrome DevTools
        // Protocol on localhost:19222 so every CEF webview can be
        // inspected from a regular browser (right-click "Inspect" does
        // not propagate to CEF child webviews on macOS). Release builds
        // intentionally do NOT open the CDP port — it would let any
        // process on the machine drive the embedded WhatsApp/Slack/etc.
        // webviews.
        //
        // The port was 9222 (Chromium's default) but ollama's
        // OpenAI-compatible server squats on 127.0.0.1:9222 in some
        // installs, which silently broke CDP attach (our client hit
        // ollama, the WS handshake failed, child webviews stayed at
        // about:blank → black screen). Picked 19222 to dodge that
        // collision; if you change it here also update
        // `cdp::CDP_PORT` and `whatsapp_scanner::CDP_PORT`.
        //
        // NOTE: flags must be prefixed with `--`. The runtime's
        // `on_before_command_line_processing` dispatch (in
        // `tauri-runtime-cef/src/cef_impl.rs`) routes value-less args that
        // don't start with `-` to `append_argument` (positional) instead of
        // `append_switch`, which means Chromium silently ignores them.
        let mut args: Vec<(&str, Option<&str>)> = vec![
            ("--use-mock-keychain", None),
            ("--password-store", Some("basic")),
            // Enable SharedArrayBuffer so embedded apps that need WebRTC
            // audio worklets / Opus encoders (Slack Huddles, Meet
            // real-time features, Discord voice) can actually initialise.
            // Chromium gates SharedArrayBuffer behind cross-origin
            // isolation by default; web apps embedded inside CEF rarely
            // send COOP/COEP headers, so without this flag the feature
            // silently disappears and huddle/call buttons no-op.
            ("--enable-features", Some("SharedArrayBuffer")),
            // Defeat Chromium's modern throttlers that ignore the
            // legacy `--disable-background-timer-throttling` flag.
            // Empirically with that flag *alone*, the producer in the
            // (visible but non-key) main window still got pinned to
            // 1Hz worker timers as soon as the off-screen Meet window
            // opened. These three feature flags are the canonical
            // additional knobs (Electron / Puppeteer use them).
            (
                "--disable-features",
                Some(
                    "IntensiveWakeUpThrottling,CalculateNativeWinOcclusion,\
                     AutofillActorMode,GlicActorUi,LensOverlay",
                ),
            ),
            // Allow autoplay (audio + video) without a prior user gesture.
            // CEF inherits Chromium's default policy, which leaves an
            // AudioContext suspended until the user interacts with the
            // page; @remotion/player gates its rAF frame loop on
            // AudioContext.state === 'running', so on a cold tab the
            // mascot SVG paints frame 0 and freezes there until the user
            // alt-tabs / clicks somewhere (which counts as a gesture and
            // resumes the context — the "fast on revisit" symptom). With
            // this flag the AudioContext starts in 'running' immediately
            // and the mascot animates from first paint. We control every
            // surface in this webview, so dropping the gesture gate is
            // safe.
            ("--autoplay-policy", Some("no-user-gesture-required")),
            // Background-throttling defeaters. The MeetCallProducer
            // pumps mascot frames at 24 fps from the *main* OpenHuman
            // window, but as soon as the off-screen Meet webview opens
            // (or the user clicks anywhere outside main), macOS demotes
            // the renderer's priority and Chromium throttles its
            // setInterval / worker timers / rAF down to ~1 Hz — the
            // mascot stream collapses to 1 fps. Page-level workarounds
            // (silent AudioContext, muted <audio>) are unreliable in
            // CEF: AudioContext starts suspended pre-gesture; the muted
            // <audio> trick depends on the renderer being polled at all.
            // The canonical fix is the Chromium command-line flag set
            // Electron / Puppeteer use for the same scenario.
            //
            // Process-wide is fine: every CEF webview we own is part of
            // the agent flow (no idle low-priority background tabs we
            // care about saving battery on).
            ("--disable-background-timer-throttling", None),
            ("--disable-renderer-backgrounding", None),
            ("--disable-backgrounding-occluded-windows", None),
        ];
        // Mascot fake-camera: bake the SVG into a one-frame Y4M and
        // point Chromium's fake-video-capture pipeline at it so any
        // CEF webview that calls `getUserMedia({video:true})` sees the
        // mascot as the agent's webcam. `--use-fake-ui-for-media-stream`
        // auto-allows the permission prompt so Meet's join page doesn't
        // get stuck behind it. The flags are process-level (affect every
        // CEF webview), which is fine today: only the Meet call window
        // intentionally requests a camera, and other webviews don't ask
        // for one. The path string is leaked with `Box::leak` so its
        // `&str` outlives the args vec we hand to `command_line_args`.
        let fake_camera_arg: Option<&'static str> =
            match fake_camera::ensure_mascot_y4m(&file_logging::resolve_data_dir()) {
                Ok(path) => {
                    let leaked: &'static str =
                        Box::leak(path.to_string_lossy().into_owned().into_boxed_str());
                    log::info!("[cef-startup] fake-camera y4m path={leaked}");
                    Some(leaked)
                }
                Err(err) => {
                    log::warn!(
                        "[cef-startup] mascot fake-camera unavailable: {err} \
                     (Meet will see no camera)"
                    );
                    None
                }
            };
        if let Some(path) = fake_camera_arg {
            args.push(("--use-fake-device-for-media-stream", None));
            args.push(("--use-fake-ui-for-media-stream", None));
            args.push(("--use-file-for-fake-video-capture", Some(path)));
        }
        // Always expose the CDP port, not just in debug. The webview-accounts
        // CDP session opener navigates each embedded provider webview from its
        // `about:blank#openhuman-acct-...` placeholder to the real provider URL
        // via `Page.navigate`. Without this port available in release builds,
        // the CDP client can't attach (`browser_ws_url()` 404s on /json/version),
        // the navigation never fires, and the embedded webview stays on
        // `about:blank` (blank panel for Telegram / WhatsApp / Slack / Discord).
        // Same port the `cdp::CDP_HOST`/`cdp::CDP_PORT` constants expect.
        args.push(("--remote-debugging-port", Some("19222")));
        // Issue #1012 — Intel macOS (x86_64) crashes with EXC_CRASH (SIGABRT)
        // inside CrBrowserMain when CEF 146 tries to use GPU compositing via
        // Metal on Intel GPU hardware/drivers. Disable GPU compositing on
        // x86_64 macOS so the browser process falls back to software
        // compositing instead of aborting. This flag is a no-op on Apple
        // Silicon (arm64) and on non-macOS targets; all other GPU paths
        // (WebGL, video decode) remain unaffected.
        #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
        {
            args.push(("--disable-gpu-compositing", None));
            log::info!("[cef-startup] Intel macOS detected: adding --disable-gpu-compositing (issue #1012)");
        }
        tauri::Builder::<tauri::Cef>::new().command_line_args::<&str, &str>(args)
    };

    let builder = builder
        // Single-instance guard — MUST be the first plugin registered so the
        // secondary-process exit path triggers before any other plugin setup
        // (and before `Builder::build()` reaches `CefRuntime::init`). Without
        // this, launching a second instance races into CEF init while the
        // primary still holds the cache lock; `cef::initialize` returns 0 and
        // the vendored runtime asserts (Sentry OPENHUMAN-TAURI-A — 442 events
        // across Win10/11 + Linux, all releases). The callback receives the
        // secondary's argv/cwd; we forward deep-link args and focus the main
        // window. Deep-link payloads stay handled by `tauri-plugin-deep-link`
        // — we just need to wake the primary so it observes them.
        .plugin(tauri_plugin_single_instance::init(
            |app: &AppHandle<AppRuntime>, args, cwd| {
                // Don't log raw argv/cwd: deep-link callbacks (OAuth codes,
                // magic links) can carry auth tokens that would otherwise leak
                // into desktop logs and Sentry breadcrumbs. CodeRabbit on #1510.
                log::info!(
                    "[single-instance] secondary launch detected, focusing primary (argc={}, cwd_present={})",
                    args.len(),
                    !cwd.is_empty()
                );
                if let Err(err) = show_main_window(app) {
                    log::warn!("[single-instance] failed to focus main window: {err}");
                }
            },
        ))
        // Explicitly disable `open_js_links_on_click`: tauri-plugin-opener
        // defaults to injecting `init-iife.js` into *every* webview — a
        // global click listener that invokes `plugin:opener|open_url` via
        // HTTP-IPC. That violates our "no JS injection into CEF child
        // webviews" rule (see CLAUDE.md) and also fails in practice
        // because third-party origins (web.telegram.org, linkedin, …)
        // trip Tauri's Origin header check and return 500. External link
        // handling for `acct_*` webviews runs natively via
        // `on_navigation` / `on_new_window` in webview_accounts/mod.rs;
        // the main window uses `openUrl()` from `utils/openUrl.ts` when
        // it needs to hand off a URL.
        .plugin(
            tauri_plugin_opener::Builder::default()
                .open_js_links_on_click(false)
                .build(),
        )
        .plugin(tauri_plugin_deep_link::init())
        .plugin(tauri_plugin_notification::init())
        .plugin(tauri_plugin_global_shortcut::Builder::new().build())
        // Auto-updater for the Tauri shell. Endpoint and minisign pubkey live
        // in `tauri.conf.json` under `plugins.updater`. Releases are signed at
        // build time with `TAURI_SIGNING_PRIVATE_KEY` (+ `_PASSWORD`); see
        // gitbooks/overview/auto-update.md for the full pipeline.
        .plugin(tauri_plugin_updater::Builder::new().build())
        .manage(dictation_hotkeys::DictationHotkeyState(
            std::sync::Mutex::new(Vec::new()),
        ))
        .manage(webview_accounts::WebviewAccountsState::default())
        .manage(notification_settings::NotificationSettingsState::new())
        .manage(PendingAppUpdateState::default());
    let builder = builder.manage(std::sync::Arc::new(imessage_scanner::ScannerRegistry::new()));
    let builder = builder.manage(std::sync::Arc::new(
        gmessages_scanner::ScannerRegistry::new(),
    ));
    let builder = builder.manage(whatsapp_scanner::ScannerRegistry::new());
    let builder = builder.manage(slack_scanner::ScannerRegistry::new());
    let builder = builder.manage(discord_scanner::ScannerRegistry::new());
    let builder = builder.manage(telegram_scanner::ScannerRegistry::new());
    let builder = builder.manage(screen_capture::ScreenShareState::new());
    let builder = builder.manage(meet_call::MeetCallState::new());
    let builder = builder.manage(meet_audio::MeetAudioState::new());
    let builder = builder.manage(meet_video::frame_bus::MeetVideoFrameBusState::new());
    builder
        .setup(move |app| {
            #[cfg(any(windows, target_os = "linux"))]
            {
                if let Err(err) = app.deep_link().register_all() {
                    log::warn!("[deep-link] register_all failed (non-fatal): {err}");
                }
            }

            // Start the webview_apis WebSocket bridge BEFORE spawning core —
            // core reads OPENHUMAN_WEBVIEW_APIS_PORT on first connect, and
            // connects lazily, so the env var must be set before the spawn.
            //
            // If the bridge fails to bind we clear any inherited port env so
            // the core child can't accidentally connect to whichever loopback
            // process already owns that port, then abort setup — the bridge
            // is load-bearing for every webview_apis RPC method.
            let bridge_ok = tauri::async_runtime::block_on(async {
                match webview_apis::start().await {
                    Ok(port) => {
                        std::env::set_var(webview_apis::server::PORT_ENV, port.to_string());
                        log::info!("[webview_apis] bridge ready on port {port}");
                        true
                    }
                    Err(err) => {
                        log::error!("[webview_apis] failed to start bridge: {err}");
                        std::env::remove_var(webview_apis::server::PORT_ENV);
                        false
                    }
                }
            });
            if !bridge_ok {
                return Err("webview_apis bridge failed to start — aborting setup".into());
            }

            // Purge stray LaunchAgent left over from a prior worktree's
            // `service install`. KeepAlive=true on the plist re-spawns the
            // daemon after every SIGKILL, fighting `ensure_running`'s
            // stale-listener takeover and re-binding port 7788 on cold boot.
            // (Symptom: "Failed to start local core: signaled pid <X> but
            // port 7788 remained bound after 5000ms".)
            //
            // Tightly scoped to avoid clobbering a legitimate `service
            // install`:
            //   - dev builds only (`cfg!(debug_assertions)`)
            //   - skip when this process IS the daemon (`!daemon_mode`)
            //   - only purge when the plist's ProgramArguments[0] points
            //     somewhere other than the currently-running executable —
            //     i.e. a sibling worktree's stale binary, not us.
            #[cfg(target_os = "macos")]
            if cfg!(debug_assertions) && !daemon_mode {
                const STALE_LABEL: &str = "com.openhuman.core";

                if let Ok(home) = std::env::var("HOME") {
                    let plist = std::path::PathBuf::from(&home)
                        .join("Library")
                        .join("LaunchAgents")
                        .join(format!("{STALE_LABEL}.plist"));

                    let plist_targets_us = std::fs::read_to_string(&plist)
                        .ok()
                        .and_then(|contents| {
                            // ProgramArguments[0] is the first <string>...</string>
                            // after the <key>ProgramArguments</key> marker. The
                            // service installer always writes it as an absolute
                            // path to the openhuman-core binary (see
                            // src/openhuman/service/macos.rs).
                            let after_key = contents.split("<key>ProgramArguments</key>").nth(1)?;
                            let start = after_key.find("<string>")? + "<string>".len();
                            let rest = &after_key[start..];
                            let end = rest.find("</string>")?;
                            Some(std::path::PathBuf::from(rest[..end].trim()))
                        })
                        .zip(std::env::current_exe().ok())
                        .map(|(plist_bin, self_bin)| plist_bin == self_bin)
                        .unwrap_or(false);

                    if plist.exists() && !plist_targets_us {
                        let uid = std::process::Command::new("id")
                            .arg("-u")
                            .output()
                            .ok()
                            .and_then(|o| String::from_utf8(o.stdout).ok())
                            .map(|s| s.trim().to_string());

                        if let Some(uid) = uid {
                            let target = format!("gui/{uid}/{STALE_LABEL}");
                            let _ = std::process::Command::new("launchctl")
                                .arg("bootout")
                                .arg(&target)
                                .status();
                        }

                        match std::fs::remove_file(&plist) {
                            Ok(()) => log::warn!(
                                "[boot] removed stale LaunchAgent plist at {} \
                                 (points at a different binary than this build — \
                                 likely a sibling worktree's `service install`)",
                                plist.display()
                            ),
                            Err(err) => log::warn!(
                                "[boot] failed to remove stale LaunchAgent plist {}: {err}",
                                plist.display()
                            ),
                        }
                    }
                }
            }

            let core_handle =
                core_process::CoreProcessHandle::new(core_process::default_core_port());
            std::env::set_var("OPENHUMAN_CORE_RPC_URL", core_handle.rpc_url());

            // Expose the shared CEF cookies SQLite path to the core sidecar
            // so `check_onboarding_status` can detect which webview
            // providers (whatsapp, slack, telegram, …) already have a live
            // session cookie. Best-effort — if we can't resolve the path
            // the core treats every provider as logged_out.
            if let Some(cache_dir) = cef_profile::configured_cache_path_from_env() {
                let cookies_db = cache_dir.join("Default").join("Cookies");
                log::debug!("[webview_accounts] exposing cookies DB path to core");
                std::env::set_var("OPENHUMAN_CEF_COOKIES_DB", &cookies_db);
            } else {
                // Clear any inherited value so the core can't pick up a
                // stale path from a previous run or the parent shell.
                std::env::remove_var("OPENHUMAN_CEF_COOKIES_DB");
                log::warn!(
                    "[webview_accounts] could not resolve configured CEF cache dir — core \
                     will report all webview providers as logged_out"
                );
            }

            app.manage(core_handle.clone());
            // NOTE: the core is NOT auto-spawned here. The BootCheckGate UI
            // calls `start_core_process` (Local mode) after the user picks a
            // mode, which lets the frontend surface startup failures and
            // version mismatches before the rest of the app mounts.
            //
            // In daemon mode (headless) we spawn immediately so the tray
            // agent is available without waiting for a UI interaction.
            if daemon_mode {
                let core_handle_daemon = core_handle.clone();
                tauri::async_runtime::spawn(async move {
                    if let Err(err) = core_handle_daemon.ensure_running().await {
                        log::error!("[core] daemon_mode — failed to start embedded core: {err}");
                        return;
                    }
                    log::info!("[core] daemon_mode — embedded core ready");
                });
            }

            // Restore last-known window position+size before showing the
            // window so the user's first paint after a restart-driven flow
            // (#900 identity flip) lands on the same display they used,
            // not back at the default centered initial size on the
            // primary monitor. `tauri.conf.json` ships `visible: false`
            // / `center: false` for the main window so the placement
            // happens before the first paint and there's no jump.
            if let Some(window) = app.get_webview_window("main") {
                if !window_state::restore_main(&window) {
                    window_state::center_main(&window);
                }
                if !daemon_mode {
                    if let Err(err) = window.show() {
                        log::warn!("[window-state] show main window failed: {err}");
                    }
                }
            }

            if daemon_mode {
                if let Some(window) = app.get_webview_window("main") {
                    let _ = window.hide();
                    log::info!("[tray] daemon_mode=true window_hidden_on_startup");
                }
            }

            // Overlay window is currently disabled in `tauri.conf.json` (the
            // `overlay` entry under `app.windows` was removed), so we skip
            // the macOS NSPanel reclass + bottom-right pin + initial show
            // here. The helpers (`configure_overlay_window_macos`,
            // `pin_overlay_bottom_right`) and the React entry point
            // (`src/overlay/OverlayApp.tsx`) are kept intact so the overlay
            // can be re-enabled by restoring the config entry and the two
            // setup blocks below.
            //
            //   #[cfg(target_os = "macos")]
            //   if let Some(window) = app.get_webview_window("overlay") {
            //       configure_overlay_window_macos(&window);
            //   }
            //   if let Some(window) = app.get_webview_window("overlay") {
            //       pin_overlay_bottom_right(&window);
            //       let _ = window.show();
            //   }

            // Tray icon setup moved to RunEvent::Ready (see below) — GTK is only
            // initialized after the event loop starts, so we must delay tray creation
            // until the Ready event fires. Creating the tray here would panic on
            // Linux with "GTK has not been initialized".
            log::info!("[tray] deferring tray setup to RunEvent::Ready");

            // CEF cold-start warmup. Spawns a 1×1 hidden child webview on
            // the main window at `about:blank` so CEF's render-process /
            // compositor for child webviews is hot before the user clicks
            // an account — first cold open of a real provider drops from
            // "spin up renderer + navigate" to just "navigate".
            //
            // Earlier builds had this disabled because of a "blank webview
            // on first onboarding open" report; we now park the warmup at
            // a far off-screen position and never reveal it (matching the
            // 1×1-on-screen pattern used for cold account spawns), and
            // tear it down in the shutdown sequence below. Disable at
            // runtime with `OPENHUMAN_CEF_PREWARM=0` if it regresses.
            {
                let prewarm_enabled = std::env::var("OPENHUMAN_CEF_PREWARM")
                    .map(|v| {
                        let v = v.trim().to_ascii_lowercase();
                        !(v == "0" || v == "false" || v == "no" || v == "off")
                    })
                    .unwrap_or(true);
                if prewarm_enabled {
                    let app_handle = app.handle().clone();
                    tauri::async_runtime::spawn(async move {
                        // Defer one tick so the main window finishes its
                        // first paint before we attach a sibling webview.
                        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
                        if let Err(e) = spawn_cef_prewarm(&app_handle) {
                            log::warn!("[cef-prewarm] failed (non-fatal): {e}");
                        }
                    });
                } else {
                    log::info!("[cef-prewarm] disabled via OPENHUMAN_CEF_PREWARM");
                }
            }

            // Dev convenience: if OPENHUMAN_DEV_AUTO_WHATSAPP=<account-id>
            // is set, spawn that account's webview at startup so the
            // CDP/IndexedDB scanner can iterate without manual UI clicks.
            // The same account-id reuses the persistent data dir, so a
            // previously-logged-in WhatsApp session stays logged in.
            if let Ok(account_id) = std::env::var("OPENHUMAN_DEV_AUTO_WHATSAPP") {
                let account_id = account_id.trim().to_string();
                if !account_id.is_empty() {
                    let app_handle = app.handle().clone();
                    tauri::async_runtime::spawn(async move {
                        // Wait for the window to be fully ready.
                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                        let state = app_handle.state::<webview_accounts::WebviewAccountsState>();
                        let args = webview_accounts::OpenArgs {
                            account_id: account_id.clone(),
                            provider: "whatsapp".to_string(),
                            url: None,
                            bounds: Some(webview_accounts::Bounds {
                                x: 100.0,
                                y: 100.0,
                                width: 900.0,
                                height: 700.0,
                            }),
                            prewarm: false,
                        };
                        match webview_accounts::webview_account_open(
                            app_handle.clone(),
                            state,
                            args,
                        )
                        .await
                        {
                            Ok(label) => log::info!(
                                "[dev-auto-whatsapp] spawned label={} account={}",
                                label,
                                account_id
                            ),
                            Err(e) => log::error!(
                                "[dev-auto-whatsapp] failed: {} (account={})",
                                e,
                                account_id
                            ),
                        }
                    });
                }
            }

            // Same dev helper, Slack flavour. OPENHUMAN_DEV_AUTO_SLACK=<uuid>
            // opens the Slack account webview on startup so the CDP scanner
            // can iterate without manual UI clicks.
            if let Ok(account_id) = std::env::var("OPENHUMAN_DEV_AUTO_SLACK") {
                let account_id = account_id.trim().to_string();
                if !account_id.is_empty() {
                    let app_handle = app.handle().clone();
                    tauri::async_runtime::spawn(async move {
                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                        let state = app_handle.state::<webview_accounts::WebviewAccountsState>();
                        let args = webview_accounts::OpenArgs {
                            account_id: account_id.clone(),
                            provider: "slack".to_string(),
                            url: None,
                            bounds: Some(webview_accounts::Bounds {
                                x: 100.0,
                                y: 100.0,
                                width: 900.0,
                                height: 700.0,
                            }),
                            prewarm: false,
                        };
                        match webview_accounts::webview_account_open(
                            app_handle.clone(),
                            state,
                            args,
                        )
                        .await
                        {
                            Ok(label) => log::info!(
                                "[dev-auto-slack] spawned label={} account={}",
                                label,
                                account_id
                            ),
                            Err(e) => log::error!(
                                "[dev-auto-slack] failed: {} (account={})",
                                e,
                                account_id
                            ),
                        }
                    });
                }
            }

            // Same dev helper, Telegram flavour. OPENHUMAN_DEV_AUTO_TELEGRAM=<uuid>
            // opens the Telegram Web K account webview on startup so the CDP
            // scanner can iterate without manual UI clicks.
            if let Ok(account_id) = std::env::var("OPENHUMAN_DEV_AUTO_TELEGRAM") {
                let account_id = account_id.trim().to_string();
                if !account_id.is_empty() {
                    let app_handle = app.handle().clone();
                    tauri::async_runtime::spawn(async move {
                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                        let state = app_handle.state::<webview_accounts::WebviewAccountsState>();
                        let args = webview_accounts::OpenArgs {
                            account_id: account_id.clone(),
                            provider: "telegram".to_string(),
                            url: None,
                            bounds: Some(webview_accounts::Bounds {
                                x: 100.0,
                                y: 100.0,
                                width: 900.0,
                                height: 700.0,
                            }),
                            prewarm: false,
                        };
                        match webview_accounts::webview_account_open(
                            app_handle.clone(),
                            state,
                            args,
                        )
                        .await
                        {
                            Ok(label) => log::info!(
                                "[dev-auto-telegram] spawned label={} account={}",
                                label,
                                account_id
                            ),
                            Err(e) => log::error!(
                                "[dev-auto-telegram] failed: {} (account={})",
                                e,
                                account_id
                            ),
                        }
                    });
                }
            }
            // Same dev helper, Google Meet flavour.
            // OPENHUMAN_DEV_AUTO_GOOGLE_MEET=<uuid> opens the gmeet account
            // webview at startup so the caption-capture recipe runs
            // without manual UI clicks. Use in combination with:
            //   tail -F /tmp/oh-cef.log | grep -E --line-buffered \
            //     "\[gmeet\]|memory_doc_ingest|orchestrator"
            // to verify captions flow → transcript persist → thread handoff.
            if let Ok(account_id) = std::env::var("OPENHUMAN_DEV_AUTO_GOOGLE_MEET") {
                let account_id = account_id.trim().to_string();
                if !account_id.is_empty() {
                    let app_handle = app.handle().clone();
                    tauri::async_runtime::spawn(async move {
                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                        let state = app_handle.state::<webview_accounts::WebviewAccountsState>();
                        // Dev mode: size the child webview to the parent
                        // window's inner bounds so Meet controls (CC toggle,
                        // mic/cam, leave) are reachable without overflowing.
                        let (w, h) = app_handle
                            .get_webview_window("main")
                            .and_then(|main| {
                                let scale = main.scale_factor().unwrap_or(1.0);
                                main.inner_size()
                                    .ok()
                                    .map(|s| ((s.width as f64) / scale, (s.height as f64) / scale))
                            })
                            .unwrap_or((1100.0, 780.0));
                        let args = webview_accounts::OpenArgs {
                            account_id: account_id.clone(),
                            provider: "google-meet".to_string(),
                            url: None,
                            bounds: Some(webview_accounts::Bounds {
                                x: 0.0,
                                y: 0.0,
                                width: w,
                                height: h,
                            }),
                            prewarm: false,
                        };
                        match webview_accounts::webview_account_open(
                            app_handle.clone(),
                            state,
                            args,
                        )
                        .await
                        {
                            Ok(label) => log::info!(
                                "[dev-auto-gmeet] spawned label={} account={}",
                                label,
                                account_id
                            ),
                            Err(e) => log::error!(
                                "[dev-auto-gmeet] failed: {} (account={})",
                                e,
                                account_id
                            ),
                        }
                    });
                }
            }
            // Dev helper: OPENHUMAN_DEV_AUTO_MEET_CALL=<https://meet.google.com/...>
            // auto-spawns a meet-call window at startup so the camera +
            // audio bridges + frame-bus + producer pipeline can be
            // exercised end-to-end without manual UI clicks. Pair with
            // `tail -F ~/.openhuman/logs/openhuman.<date>.log` to see
            // the periodic [meet-camera] bridge stats logged by the
            // diagnostics poller in meet_video::inject.
            if let Ok(meet_url) = std::env::var("OPENHUMAN_DEV_AUTO_MEET_CALL") {
                let meet_url = meet_url.trim().to_string();
                if !meet_url.is_empty() {
                    let app_handle = app.handle().clone();
                    tauri::async_runtime::spawn(async move {
                        // Wait for the main window + core to be ready.
                        tokio::time::sleep(std::time::Duration::from_secs(3)).await;
                        let state = app_handle.state::<meet_call::MeetCallState>();
                        let request_id = format!(
                            "dev-auto-{}",
                            std::time::SystemTime::now()
                                .duration_since(std::time::UNIX_EPOCH)
                                .map(|d| d.as_secs())
                                .unwrap_or(0)
                        );
                        let args = meet_call::OpenWindowArgs {
                            request_id: request_id.clone(),
                            meet_url: meet_url.clone(),
                            display_name: "OpenHuman Dev".to_string(),
                        };
                        match meet_call::meet_call_open_window(app_handle.clone(), state, args)
                            .await
                        {
                            Ok(label) => log::info!(
                                "[dev-auto-meet] spawned label={label} request_id={request_id} url={meet_url}"
                            ),
                            Err(e) => log::error!(
                                "[dev-auto-meet] failed: {e} (url={meet_url})"
                            ),
                        }
                    });
                }
            }

            #[cfg(target_os = "macos")]
            {
                use std::sync::Arc;
                // The scanner task self-gates on `channels_config.imessage` via
                // JSON-RPC each tick — it stays idle until the user connects
                // iMessage and stops ingesting as soon as they disconnect. We
                // spawn it here just so the loop is live and picks up state
                // changes without requiring an app restart.
                if let Some(registry) = app.try_state::<Arc<imessage_scanner::ScannerRegistry>>() {
                    let registry = registry.inner().clone();
                    let app_handle = app.handle().clone();
                    registry.ensure_scanner(app_handle, "default".to_string());
                    log::info!("[imessage] scanner scheduled (gates on config each tick)");
                }
            }
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            core_rpc_url,
            core_rpc_token,
            overlay_parent_rpc_url,
            process_diagnostics_list_owned,
            check_core_update,
            apply_core_update,
            check_app_update,
            apply_app_update,
            download_app_update,
            install_app_update,
            restart_core_process,
            start_core_process,
            app_quit,
            restart_app,
            get_active_user_id,
            schedule_cef_profile_purge,
            register_dictation_hotkey,
            unregister_dictation_hotkey,
            webview_accounts::webview_account_open,
            webview_accounts::webview_account_prewarm,
            webview_accounts::webview_account_close,
            webview_accounts::webview_account_purge,
            webview_accounts::webview_account_bounds,
            webview_accounts::webview_account_reveal,
            webview_accounts::webview_account_hide,
            webview_accounts::webview_account_show,
            webview_accounts::webview_recipe_event,
            webview_accounts::webview_notification_permission_state,
            webview_accounts::webview_notification_permission_request,
            webview_accounts::webview_notification_set_dnd,
            webview_accounts::webview_notification_mute_account,
            webview_accounts::webview_notification_get_bypass_prefs,
            webview_accounts::webview_set_focused_account,
            notification_settings::notification_settings_get,
            notification_settings::notification_settings_set,
            screen_capture::screen_share_begin_session,
            screen_capture::screen_share_thumbnail,
            screen_capture::screen_share_finalize_session,
            native_notifications::notification_permission_state,
            native_notifications::notification_permission_request,
            activate_main_window,
            native_notifications::show_native_notification,
            mascot_window_show,
            mascot_window_hide,
            file_logging::reveal_logs_folder,
            file_logging::logs_folder_path,
            meet_call::meet_call_open_window,
            meet_call::meet_call_close_window
        ])
        .build(tauri::generate_context!())
        .expect("error while building tauri application")
        .run(move |app_handle, event| match event {
            RunEvent::Ready => {
                log::info!("[app] RunEvent::Ready — GTK initialized, setting up tray");
                if let Err(err) = setup_tray(app_handle) {
                    log::warn!(
                    "[tray] failed to setup tray icon (non-fatal in headless environment): {err}"
                );
                }
            }
            #[cfg(target_os = "macos")]
            RunEvent::WindowEvent {
                label,
                event: WindowEvent::CloseRequested { api, .. },
                ..
            } if label == "main" => {
                log::info!(
                    "[window] close requested on main window — hiding instead of destroying"
                );
                api.prevent_close();
                if let Some(window) = app_handle.get_webview_window("main") {
                    let _ = window.hide();
                }
            }
            #[cfg(target_os = "macos")]
            RunEvent::Reopen { .. } => {
                log::info!("[window] reopen event — showing main window");
                if let Err(err) = show_main_window(app_handle) {
                    log::error!("[macos] failed to show main window on reopen: {err}");
                }
            }
            RunEvent::ExitRequested { .. } => {
                // Run our cleanup BEFORE CEF's own Exit handler does
                // `close_all_windows() → cef::shutdown()`. Doing this in
                // RunEvent::Exit instead races CEF's teardown and the
                // `browser_count == 0` CHECK in `cef::shutdown` panics on
                // macOS Cmd+Q (issue #920). The order matters:
                //   1. close our child webviews so CEF processes the
                //      close requests during the Exit-phase message pump
                //      (gives them time to settle before cef::shutdown).
                //   2. abort our long-lived tokio tasks so they're not
                //      driving CDP traffic against CEF as it tears down.
                //   3. stop the webview_apis WS listener so its accept
                //      loop releases the loopback port.
                //   4. SIGTERM the core sidecar (non-blocking). Tauri
                //      spawned the child so we own its lifecycle, but we
                //      do not wait — that would block the main thread
                //      and starve CEF's UI loop. The kernel reaps the
                //      child after Tauri exits.
                perform_early_teardown_sync(app_handle);
            }
            RunEvent::Exit => {
                log::info!("[app] RunEvent::Exit — cef::shutdown follows");
            }
            _ => {}
        });

    // Belt-and-suspenders sweep: after Tauri's event loop returns the
    // vendored runtime has already called `cef::shutdown()`. In normal
    // operation every CEF helper (GPU / Network / Utility / Renderer) is
    // gone by now. If anything is still alive — e.g. a renderer that was
    // mid-spawn when the user quit — it would otherwise be re-parented to
    // launchd on macOS / init on Linux and survive the GUI exit. Sweep its
    // children before this process actually exits.
    //
    // We don't `wait()` on them: the kernel will reap them as our exit
    // unwinds. Give stubborn helpers a short grace period, then force-kill
    // anything still parented to us so the GUI exit leaves no background
    // processes behind.
    process_kill::sweep_orphan_children();
}

pub fn run_core_from_args(args: &[String]) -> Result<(), String> {
    // Core lives in-process: dispatch directly through the linked `openhuman_core`
    // library instead of shelling out to a separate binary. The Tauri main()
    // routes `OpenHuman core <args>` here so users can still drive the core CLI
    // from the bundled app.
    openhuman_core::run_core_from_args(args).map_err(|e| format!("{e:#}"))
}

// ---------------------------------------------------------------------------
// Sentry release / environment resolution (Tauri shell)
// ---------------------------------------------------------------------------

/// Canonical release tag: `openhuman@<version>[+<short_sha>]`.
///
/// Mirrors `build_release_tag` in the core sidecar's `src/main.rs` and the
/// `SENTRY_RELEASE` value computed in `app/vite.config.ts` so events from
/// every surface (React frontend, core sidecar, Tauri shell) group under the
/// same release in Sentry and benefit from the same source-map / debug-info
/// upload.
fn build_sentry_release_tag() -> String {
    let version = env!("CARGO_PKG_VERSION");
    let sha = option_env!("OPENHUMAN_BUILD_SHA").unwrap_or("").trim();
    let sha_short: String = sha.chars().take(12).collect();
    if sha_short.is_empty() {
        format!("openhuman@{version}")
    } else {
        format!("openhuman@{version}+{sha_short}")
    }
}

/// Resolve the Sentry environment tag from `OPENHUMAN_APP_ENV` (runtime) or
/// `VITE_OPENHUMAN_APP_ENV` (compile-time fallback). Defaults to
/// `production` so unmarked release builds don't pollute the dev/staging
/// streams.
fn resolve_sentry_environment() -> String {
    if let Ok(value) = std::env::var("OPENHUMAN_APP_ENV") {
        let trimmed = value.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }
    if let Some(value) = option_env!("VITE_OPENHUMAN_APP_ENV") {
        let trimmed = value.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }
    "production".to_string()
}

/// Returns the macOS product version string (e.g. `"14.5"`) by reading
/// `sw_vers -productVersion`. Returns `None` on non-macOS targets or when
/// the command is unavailable. Used to tag Sentry events and startup logs
/// with OS version so Intel-specific crashes (issue #1012) can be filtered
/// by macOS release.
#[cfg(target_os = "macos")]
fn macos_os_version() -> Option<String> {
    std::process::Command::new("sw_vers")
        .arg("-productVersion")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

#[cfg(test)]
mod tests {
    use super::*;

    // Tests that read/write process-global env vars must serialize through this
    // mutex. Rust's test runner executes tests in parallel by default; without
    // coordination, concurrent set_var / remove_var calls race and produce
    // spurious failures.
    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

    /// Test that is_daemon_mode correctly detects daemon flag variations
    #[test]
    fn is_daemon_mode_detects_daemon_flag() {
        // Note: This test relies on the current process args, so in test mode
        // it will typically return false. We verify the function is callable.
        let _result = is_daemon_mode();
    }

    /// Test core_rpc_url returns expected format
    #[test]
    fn core_rpc_url_returns_expected_format() {
        let _g = ENV_LOCK.lock().unwrap();
        let original = std::env::var("OPENHUMAN_CORE_RPC_URL").ok();

        std::env::set_var("OPENHUMAN_CORE_RPC_URL", "http://localhost:9999/rpc");
        let url = core_rpc_url();
        assert_eq!(url, "http://localhost:9999/rpc");

        std::env::remove_var("OPENHUMAN_CORE_RPC_URL");
        let url = core_rpc_url();
        assert_eq!(url, "http://127.0.0.1:7788/rpc");

        match original {
            Some(v) => std::env::set_var("OPENHUMAN_CORE_RPC_URL", v),
            None => std::env::remove_var("OPENHUMAN_CORE_RPC_URL"),
        }
    }

    /// Test overlay_parent_rpc_url handles empty env var
    #[test]
    fn overlay_parent_rpc_url_handles_empty() {
        let _g = ENV_LOCK.lock().unwrap();
        let original = std::env::var("OPENHUMAN_CORE_RPC_URL").ok();

        std::env::set_var("OPENHUMAN_CORE_RPC_URL", "");
        assert!(overlay_parent_rpc_url().is_none());

        std::env::set_var("OPENHUMAN_CORE_RPC_URL", "   ");
        assert!(overlay_parent_rpc_url().is_none());

        std::env::set_var("OPENHUMAN_CORE_RPC_URL", "http://127.0.0.1:7788/rpc");
        assert_eq!(
            overlay_parent_rpc_url(),
            Some("http://127.0.0.1:7788/rpc".to_string())
        );

        match original {
            Some(v) => std::env::set_var("OPENHUMAN_CORE_RPC_URL", v),
            None => std::env::remove_var("OPENHUMAN_CORE_RPC_URL"),
        }
    }

    /// Tests for setup_tray conditional compilation
    /// The PR adds two versions of setup_tray():
    /// 1. No-op for linux + cef: logs warning and returns Ok(())
    /// 2. Full implementation for other platforms
    ///
    /// These tests verify the function signatures are correct and
    /// the compile-time cfg blocks are properly set up.

    /// Verify setup_tray function exists and has correct signature
    /// This test passes if the code compiles, as the function signature
    /// is validated by the compiler.
    #[test]
    fn setup_tray_function_signature_compiles() {
        // This test exists to ensure the conditional compilation
        // of setup_tray is valid. The function is not actually called
        // here because it requires a full Tauri AppHandle.
        // The cfg attributes ensure only one version exists at compile time.
    }

    /// Test that AppRuntime is defined for the current feature set
    #[test]
    fn app_runtime_type_exists() {
        // This test verifies AppRuntime is properly defined
        // based on the cef feature flag.
        // The type alias exists at module scope and is used throughout.
        fn _check_runtime<R: tauri::Runtime>() {}
        // _check_runtime::<AppRuntime>(); // Would require importing
    }

    /// Verify tray logging patterns exist (grep-friendly)
    #[test]
    fn tray_setup_logging_patterns_exist() {
        // These log patterns from the PR are grep-friendly:
        // "[tray] skipping tray setup on linux+cef: ..."
        // "[tray] setting up tray icon"
        // "[tray] tray icon ready"
        // "[tray] action=show_window ..."
        // "[tray] action=quit ..."
        // "[tray] failed to setup tray icon ..."
        // "[app] RunEvent::Ready — GTK initialized, setting up tray"
        //
        // This test passes if the code compiles with these log messages.
    }

    // -------------------------------------------------------------------------
    // macos_os_version (issue #1012)
    // -------------------------------------------------------------------------

    /// On macOS, sw_vers is always present and must return a version string.
    #[cfg(target_os = "macos")]
    #[test]
    fn macos_os_version_returns_some() {
        assert!(
            macos_os_version().is_some(),
            "sw_vers -productVersion must succeed on macOS"
        );
    }

    /// The returned version must be a non-empty trimmed string.
    #[cfg(target_os = "macos")]
    #[test]
    fn macos_os_version_is_nonempty() {
        let ver = macos_os_version().expect("sw_vers must return a version on macOS");
        assert!(!ver.is_empty());
        // No leading/trailing whitespace (the impl trims).
        assert_eq!(ver, ver.trim());
    }

    /// The version string must look like dot-separated integers ("14.5", "13.2.1").
    #[cfg(target_os = "macos")]
    #[test]
    fn macos_os_version_is_dotted_integer_format() {
        let ver = macos_os_version().expect("sw_vers must return a version on macOS");
        let all_numeric_parts = ver
            .split('.')
            .all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()));
        assert!(
            all_numeric_parts,
            "os version {ver:?} must be dot-separated integers (e.g. '14.5')"
        );
    }

    /// The version must have at least one component (e.g. a bare major "15" is valid).
    #[cfg(target_os = "macos")]
    #[test]
    fn macos_os_version_has_at_least_one_component() {
        let ver = macos_os_version().expect("sw_vers must return a version on macOS");
        assert!(
            !ver.split('.').next().unwrap_or("").is_empty(),
            "version must have at least one numeric component"
        );
    }

    // -------------------------------------------------------------------------
    // Platform constants (issue #1012 Sentry tagging)
    // -------------------------------------------------------------------------

    /// cpu_arch tag is derived from std::env::consts::ARCH which must be non-empty.
    #[test]
    fn platform_arch_constant_is_nonempty() {
        assert!(
            !std::env::consts::ARCH.is_empty(),
            "ARCH constant used for Sentry cpu_arch tag must be non-empty"
        );
    }

    /// os_name tag is derived from std::env::consts::OS which must be non-empty.
    #[test]
    fn platform_os_constant_is_nonempty() {
        assert!(
            !std::env::consts::OS.is_empty(),
            "OS constant used for Sentry os_name tag must be non-empty"
        );
    }

    /// On a macOS build the OS constant must equal "macos".
    #[cfg(target_os = "macos")]
    #[test]
    fn platform_os_is_macos_on_macos_build() {
        assert_eq!(std::env::consts::OS, "macos");
    }

    /// On an Intel macOS build the ARCH constant must equal "x86_64".
    /// This is the architecture that triggers --disable-gpu-compositing.
    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
    #[test]
    fn platform_arch_is_x86_64_on_intel_build() {
        assert_eq!(std::env::consts::ARCH, "x86_64");
    }

    /// On Apple Silicon the ARCH constant must equal "aarch64"; the GPU flag
    /// must NOT be compiled in (verified by this test existing in the binary).
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    #[test]
    fn platform_arch_is_aarch64_on_apple_silicon_build() {
        assert_eq!(std::env::consts::ARCH, "aarch64");
    }

    // -------------------------------------------------------------------------
    // build_sentry_release_tag
    // -------------------------------------------------------------------------

    #[test]
    fn sentry_release_tag_starts_with_openhuman() {
        let tag = build_sentry_release_tag();
        assert!(
            tag.starts_with("openhuman@"),
            "release tag must start with 'openhuman@', got: {tag:?}"
        );
    }

    #[test]
    fn sentry_release_tag_contains_cargo_pkg_version() {
        let tag = build_sentry_release_tag();
        let version = env!("CARGO_PKG_VERSION");
        assert!(
            tag.contains(version),
            "release tag {tag:?} must embed CARGO_PKG_VERSION {version:?}"
        );
    }

    #[test]
    fn sentry_release_tag_version_part_is_nonempty() {
        let tag = build_sentry_release_tag();
        let after_prefix = tag.strip_prefix("openhuman@").unwrap_or("");
        assert!(!after_prefix.is_empty(), "version part must not be empty");
    }

    /// When a SHA is baked in the tag takes the form `openhuman@<ver>+<sha12>`.
    /// When it is not, the tag is simply `openhuman@<ver>` with no `+`.
    /// Either way the full tag must be non-empty.
    #[test]
    fn sentry_release_tag_is_nonempty() {
        assert!(!build_sentry_release_tag().is_empty());
    }

    // -------------------------------------------------------------------------
    // resolve_sentry_environment
    // -------------------------------------------------------------------------

    #[test]
    fn sentry_environment_reads_openhuman_app_env() {
        let _g = ENV_LOCK.lock().unwrap();
        let key = "OPENHUMAN_APP_ENV";
        let original = std::env::var(key).ok();
        std::env::set_var(key, "staging");
        let env = resolve_sentry_environment();
        match original {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
        assert_eq!(env, "staging");
    }

    #[test]
    fn sentry_environment_trims_whitespace_from_openhuman_app_env() {
        let _g = ENV_LOCK.lock().unwrap();
        let key = "OPENHUMAN_APP_ENV";
        let original = std::env::var(key).ok();
        std::env::set_var(key, "  dev  ");
        let env = resolve_sentry_environment();
        match original {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
        assert_eq!(env, "dev");
    }

    #[test]
    fn sentry_environment_skips_empty_openhuman_app_env() {
        let _g = ENV_LOCK.lock().unwrap();
        let key = "OPENHUMAN_APP_ENV";
        let original = std::env::var(key).ok();
        std::env::set_var(key, "");
        let env = resolve_sentry_environment();
        match original {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
        // Falls through to VITE_ compile-time value or "production"; must be non-empty.
        assert!(!env.is_empty());
    }

    #[test]
    fn sentry_environment_skips_whitespace_only_openhuman_app_env() {
        let _g = ENV_LOCK.lock().unwrap();
        let key = "OPENHUMAN_APP_ENV";
        let original = std::env::var(key).ok();
        std::env::set_var(key, "   ");
        let env = resolve_sentry_environment();
        match original {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
        assert!(!env.is_empty());
    }

    /// When neither runtime env var nor compile-time VITE_ is set, the fallback
    /// must be "production". Guard with a compile-time check so this test only
    /// asserts the hard default when no compile-time override is present.
    #[test]
    fn sentry_environment_defaults_to_production_when_unset() {
        let _g = ENV_LOCK.lock().unwrap();
        if option_env!("VITE_OPENHUMAN_APP_ENV").is_some() {
            // A compile-time override is baked in; skip — the fallback path is
            // exercised by sentry_environment_skips_empty_openhuman_app_env.
            return;
        }
        let key = "OPENHUMAN_APP_ENV";
        let original = std::env::var(key).ok();
        std::env::remove_var(key);
        let env = resolve_sentry_environment();
        match original {
            Some(v) => std::env::set_var(key, v),
            None => std::env::remove_var(key),
        }
        assert_eq!(env, "production");
    }
}
