use std::convert::Infallible;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::Duration;
use async_stream::stream;
use axum::body::Body;
use axum::http::header::CONTENT_TYPE;
use axum::response::{IntoResponse, Response};
use axum::Json;
use bytes::Bytes;
use log::{debug, error, info, trace, warn};
use once_cell::sync::Lazy;
use pdfium_render::prelude::Pdfium;
use serde::{Deserialize, Serialize};
use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent};
use tauri::path::PathResolver;
use tauri::WebviewWindow;
use tauri_plugin_updater::{UpdaterExt, Update};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::broadcast;
use tokio::time;
use crate::api_token::APIToken;
use crate::clipboard::shutdown_clipboard;
use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_server};
use crate::environment::{
    is_prod, is_dev, is_flatpak, CONFIG_DIRECTORY, DATA_DIRECTORY, FLATPAK_LIBRARY_DIRECTORY,
};
use crate::log::switch_to_file_logging;
use crate::pdfium::PDFIUM_LIB_PATH;
use crate::qdrant_edge_database::{start_qdrant_edge_database, stop_qdrant_edge_database};
use crate::global_shortcuts::{RegisterShortcutRequest, ShortcutResponse};

#[cfg(debug_assertions)]
use crate::dotnet::create_startup_env_file;

#[cfg(target_os = "linux")]
use webkit2gtk::glib::Cast;

#[cfg(target_os = "linux")]
use webkit2gtk::{PermissionRequestExt, UserMediaPermissionRequestExt};

/// The Tauri main window.
pub static MAIN_WINDOW: Lazy<Mutex<Option<WebviewWindow>>> = Lazy::new(|| Mutex::new(None));

/// The update response coming from the Tauri updater.
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<Update>>> = Lazy::new(|| Mutex::new(None));

/// The event broadcast sender for Tauri events.
static EVENT_BROADCAST: Lazy<Mutex<Option<broadcast::Sender<Event>>>> = Lazy::new(|| Mutex::new(None));

/// Stores the localhost origin of the Blazor app after the .NET server is ready.
static APPROVED_APP_URL: Lazy<Mutex<Option<tauri::Url>>> = Lazy::new(|| Mutex::new(None));

/// Starts the Tauri app.
pub fn start_tauri(tauri_context: tauri::Context<tauri::Wry>) {
    info!("Starting Tauri app...");

    // Create the event broadcast channel:
    let (event_sender, root_event_receiver) = broadcast::channel(100);

    // Save a copy of the event broadcast sender for later use:
    *EVENT_BROADCAST.lock().unwrap() = Some(event_sender.clone());

    // When the last receiver is dropped, we lose the ability to send events.
    // Therefore, we spawn a task that keeps the root receiver alive:
    tauri::async_runtime::spawn(async move {
        let mut root_receiver = root_event_receiver;
        loop {
            match root_receiver.recv().await {
                Ok(event) => {
                    debug!(Source = "Tauri"; "Tauri event received: location=root receiver       , event={event:?}");
                },

                Err(broadcast::error::RecvError::Lagged(skipped)) => {
                    warn!(Source = "Tauri"; "Root event receiver lagged, skipped {skipped} messages.");
                },

                Err(broadcast::error::RecvError::Closed) => {
                    warn!(Source = "Tauri"; "Root event receiver channel closed.");
                    return;
                },
            }
        }
    });

    let app = tauri::Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
            info!(Source = "Tauri"; "Prevented second app instance from starting. cwd='{cwd}', args={args:?}");

            let Some(window) = app.get_webview_window("main") else {
                warn!(Source = "Tauri"; "Second app instance was blocked, but the main window was not available for activation.");
                return;
            };

            if let Err(error) = window.show() {
                warn!(Source = "Tauri"; "Failed to show main window after second app start: {error}");
            }

            if let Err(error) = window.unminimize() {
                warn!(Source = "Tauri"; "Failed to unminimize main window after second app start: {error}");
            }

            if let Err(error) = window.set_focus() {
                warn!(Source = "Tauri"; "Failed to focus main window after second app start: {error}");
            }
        }))
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_opener::init())
        .plugin(
            tauri::plugin::Builder::<tauri::Wry, ()>::new("external-link-handler")
                .on_navigation(|webview, url| {
                    if !should_open_in_system_browser(webview, url) {
                        return true;
                    }

                    match webview.app_handle().opener().open_url(url.as_str(), None::<&str>) {
                        Ok(_) => {
                            info!(Source = "Tauri"; "Opening external URL in system browser: {url}");
                        },
                        Err(error) => {
                            error!(Source = "Tauri"; "Failed to open external URL '{url}' in system browser: {error}");
                        },
                    }
                    false
                })
                .build(),
        )
        .plugin(tauri_plugin_global_shortcut::Builder::new().build())
        .plugin(tauri_plugin_updater::Builder::new().build())
        .setup(move |app| {

            // Get the main window:
            let window = app.get_webview_window("main").expect("Failed to get main window.");

            #[cfg(target_os = "linux")]
            register_linux_permission_request_handler(&window);

            // Register a callback for window events, such as file drops. We have to use
            // this handler in addition to the app event handler, because file drop events
            // are only available in the window event handler (is a bug, cf. https://github.com/tauri-apps/tauri/issues/14338):
            window.on_window_event(move |event| {
                debug!(Source = "Tauri"; "Tauri event received: location=window event handler, event={event:?}");
                let event_to_send = Event::from_window_event(event);
                let sender = event_sender.clone();
                tauri::async_runtime::spawn(async move {
                    match sender.send(event_to_send) {
                        Ok(_) => {},
                        Err(error) => error!(Source = "Tauri"; "Failed to channel window event: {error}"),
                    }
                });
            });

            // Save the main window for later access:
            *MAIN_WINDOW.lock().unwrap() = Some(window);

            info!(Source = "Bootloader Tauri"; "Setup is running.");
            let data_path = app.path().app_local_data_dir().unwrap();
            let data_path = data_path.join("data");

            // Get and store the data and config directories:
            DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap();
            CONFIG_DIRECTORY.set(app.path().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();

            if is_dev() {
                #[cfg(debug_assertions)]
                create_startup_env_file();
            } else {
                cleanup_dotnet_server();
                start_dotnet_server(app.handle().clone());
            }

            start_qdrant_edge_database(app.handle().clone());

            info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
            switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
            set_pdfium_path(app.path());

            Ok(())
        })
        .plugin(tauri_plugin_window_state::Builder::default().build())
        .build(tauri_context)
        .expect("Error while running Tauri application");

    // The app event handler:
    app.run(|_app_handle, event| {
        if !matches!(event, RunEvent::MainEventsCleared) {
            debug!(Source = "Tauri"; "Tauri event received: location=app event handler   , event={event:?}");
        }

        match event {
            RunEvent::WindowEvent { event, label, .. } => {
                match event {
                    WindowEvent::CloseRequested { .. } => {
                        warn!(Source = "Tauri"; "Window '{label}': close was requested.");
                    }

                    WindowEvent::Destroyed => {
                        warn!(Source = "Tauri"; "Window '{label}': was destroyed.");
                    }

                    _ => (),
                }
            }

            RunEvent::ExitRequested { .. } => {
                warn!(Source = "Tauri"; "Run event: exit was requested.");
                shutdown_clipboard();
                stop_qdrant_edge_database();
                if is_prod() {
                    warn!("Try to stop the .NET server as well...");
                    stop_dotnet_server();
                }
            }

            RunEvent::Ready => {
                info!(Source = "Tauri"; "Run event: Tauri app is ready.");
            }

            _ => {}
        }
    });

    warn!(Source = "Tauri"; "Tauri app was stopped.");
}

fn is_local_host(host: Option<&str>) -> bool {
    matches!(host, Some("localhost") | Some("127.0.0.1") | Some("::1") | Some("[::1]"))
}

fn is_tauri_asset_host(host: Option<&str>) -> bool {
    matches!(host, Some("tauri.localhost"))
}

fn is_tauri_asset_url(url: &tauri::Url) -> bool {
    matches!(url.scheme(), "http" | "https") && is_tauri_asset_host(url.host_str())
}

fn is_local_http_url(url: &tauri::Url) -> bool {
    matches!(url.scheme(), "http" | "https") && is_local_host(url.host_str())
}

fn same_origin(left: &tauri::Url, right: &tauri::Url) -> bool {
    left.scheme() == right.scheme()
        && left.host_str() == right.host_str()
        && left.port_or_known_default() == right.port_or_known_default()
}

#[cfg(any(target_os = "linux", test))]
fn should_allow_audio_capture(
    approved_app_url: Option<&tauri::Url>,
    current_webview_url: Option<&tauri::Url>,
    requests_audio: bool,
    requests_video: bool,
) -> bool {
    requests_audio
        && !requests_video
        && approved_app_url.is_some_and(is_local_http_url)
        && approved_app_url
            .zip(current_webview_url)
            .is_some_and(|(approved, current)| same_origin(approved, current))
}

#[cfg(target_os = "linux")]
fn register_linux_permission_request_handler(window: &WebviewWindow) {
    if let Err(error) = window.with_webview(|platform_webview| {
        use webkit2gtk::WebViewExt;
        use webkit2gtk::UserMediaPermissionRequest;

        let webview = platform_webview.inner();
        webview.connect_permission_request(|webview, request| {
            let Some(user_media_request) = request.downcast_ref::<UserMediaPermissionRequest>() else {
                request.deny();
                info!(Source = "Tauri"; "Denied a non-user-media WebKit permission request.");
                return true;
            };

            let current_webview_url = webview
                .uri()
                .and_then(|uri| tauri::Url::parse(uri.as_str()).ok());
            let approved_app_url = APPROVED_APP_URL.lock().unwrap().clone();
            let origin_matches = approved_app_url
                .as_ref()
                .zip(current_webview_url.as_ref())
                .is_some_and(|(approved, current)| same_origin(approved, current));
            let requests_audio = user_media_request.is_for_audio_device();
            let requests_video = user_media_request.is_for_video_device();
            let allow = should_allow_audio_capture(
                approved_app_url.as_ref(),
                current_webview_url.as_ref(),
                requests_audio,
                requests_video,
            );

            if allow {
                request.allow();
            } else {
                request.deny();
            }

            info!(
                Source = "Tauri";
                "Handled WebKit user-media permission request: allowed={allow}, origin_matches={origin_matches}, audio={requests_audio}, video={requests_video}."
            );
            true
        });
    }) {
        error!(Source = "Tauri"; "Failed to register the Linux WebKit permission request handler: {error}");
    }
}

fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, url: &tauri::Url) -> bool {
    match url.scheme() {
        "mailto" | "tel" => return true,
        "http" | "https" => {},
        _ => return false,
    }

    if is_tauri_asset_url(url) {
        return false;
    }

    if let Some(approved_app_url) = APPROVED_APP_URL.lock().unwrap().as_ref() {
        if same_origin(approved_app_url, url) {
            return false;
        }

        if is_local_http_url(url) {
            return true;
        }
    }

    if let Ok(current_url) = webview.url() && same_origin(&current_url, url) {
        return false;
    }

    !is_local_host(url.host_str())
}

/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
/// If no events are available for a certain time, we send a ping event to keep the connection alive.
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
/// The client is expected to reconnect automatically when the connection is closed and continue
/// listening for events.
pub async fn get_event_stream(_token: APIToken) -> Response {
    // Get the lock to the event broadcast sender:
    let event_broadcast_lock = EVENT_BROADCAST.lock().unwrap();

    // Get and subscribe to the event receiver:
    let mut event_receiver = event_broadcast_lock.as_ref()
        .expect("Event sender not initialized.")
        .subscribe();

    // Drop the lock to allow other access to the sender:
    drop(event_broadcast_lock);

    let stream = stream! {
        loop {
            // Wait at most 3 seconds for an event:
            match time::timeout(Duration::from_secs(3), event_receiver.recv()).await {

                // Case: we received an event
                Ok(Ok(event)) => {
                    // Serialize the event to JSON. Important is that the entire event
                    // is serialized as a single line so that the client can parse it
                    // correctly:
                    let event_json = serde_json::to_string(&event).unwrap();
                    yield Ok::<Bytes, Infallible>(Bytes::from(event_json));

                    // The client expects a newline after each event because we are using
                    // a method to read the stream line-by-line:
                    yield Ok::<Bytes, Infallible>(Bytes::from("\n"));
                },

                // Case: we lagged behind and missed some events
                Ok(Err(broadcast::error::RecvError::Lagged(skipped))) => {
                    warn!(Source = "Tauri"; "Event receiver lagged, skipped {skipped} messages.");
                },

                // Case: the event channel was closed
                Ok(Err(broadcast::error::RecvError::Closed)) => {
                    warn!(Source = "Tauri"; "Event receiver channel closed.");
                    return;
                },

                // Case: timeout. We will send a ping event to keep the connection alive.
                Err(_) => {
                    let ping_event = Event::new(TauriEventType::Ping, Vec::new());

                    // Again, we have to serialize the event as a single line:
                    let event_json = serde_json::to_string(&ping_event).unwrap();
                    yield Ok::<Bytes, Infallible>(Bytes::from(event_json));

                    // The client expects a newline after each event because we are using
                    // a method to read the stream line-by-line:
                    yield Ok::<Bytes, Infallible>(Bytes::from("\n"));
                },
            }
        }
    };

    ([(CONTENT_TYPE, "application/jsonl")], Body::from_stream(stream)).into_response()
}

/// Data structure representing a Tauri event for our event API.
#[derive(Debug, Clone, Serialize)]
pub struct Event {
    pub event_type: TauriEventType,
    pub payload: Vec<String>,
}

/// Implementation of the Event struct.
impl Event {

    /// Creates a new Event instance.
    pub fn new(event_type: TauriEventType, payload: Vec<String>) -> Self {
        Event {
            payload,
            event_type,
        }
    }

    /// Creates an Event instance from a Tauri WindowEvent.
    pub fn from_window_event(window_event: &WindowEvent) -> Self {
        match window_event {
            WindowEvent::DragDrop(drop_event) => {
                match drop_event {
                    DragDropEvent::Enter { paths, .. } => Event::new(
                        TauriEventType::FileDropHovered,
                        paths.iter().map(|p| p.display().to_string()).collect(),
                    ),

                    DragDropEvent::Drop { paths, .. } => Event::new(
                        TauriEventType::FileDropDropped,
                        paths.iter().map(|p| p.display().to_string()).collect(),
                    ),

                    DragDropEvent::Leave => Event::new(TauriEventType::FileDropCanceled, Vec::new()),

                    _ => Event::new(TauriEventType::Unknown, Vec::new()),
                }
            },

            WindowEvent::Focused(state) => if *state {
                Event::new(TauriEventType::WindowFocused,
                           Vec::new(),
                )
            } else {
                Event::new(TauriEventType::WindowNotFocused,
                           Vec::new(),
                )
            },

            _ => Event::new(TauriEventType::Unknown,
                            Vec::new(),
            ),
        }
    }
}

/// The types of Tauri events we can send through our event API.
#[derive(Debug, Serialize, Clone)]
pub enum TauriEventType {
    None,
    Ping,
    Unknown,

    WindowFocused,
    WindowNotFocused,

    FileDropHovered,
    FileDropDropped,
    FileDropCanceled,

    GlobalShortcutPressed,
    GlobalShortcutChanged,
}

/// Changes the location of the main window to the given URL.
pub async fn change_location_to(url: &str) {
    // Try to get the main window. If it is not available yet, wait for it:
    let mut main_window_ready = false;
    let mut main_window_status_reported = false;
    let main_window_spawn_clone = &MAIN_WINDOW;
    while !main_window_ready
    {
        main_window_ready = {
            let main_window = main_window_spawn_clone.lock().unwrap();
            main_window.is_some()
        };

        if !main_window_ready {
            if !main_window_status_reported {
                info!("Waiting for main window to be ready, because .NET was faster than Tauri.");
                main_window_status_reported = true;
            }

            time::sleep(Duration::from_millis(100)).await;
        }
    }

    if let Ok(parsed_url) = tauri::Url::parse(url) && is_local_http_url(&parsed_url) {
        *APPROVED_APP_URL.lock().unwrap() = Some(parsed_url);
    }

    let js_location_change = format!("window.location = '{url}';");
    let main_window = main_window_spawn_clone.lock().unwrap();
    let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
    match location_change_result {
        Ok(_) => info!("The app location was changed to {url}."),
        Err(e) => error!("Failed to change the app location to {url}: {e}."),
    }
}

/// Checks for updates.
pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
    if !self_update_allowed(is_dev(), is_flatpak()) {
        let reason = if is_flatpak() { "Flatpak installations are updated externally" } else { "the app is running in development mode" };
        warn!(Source = "Updater"; "Skipping update check because {reason}.");
        return Json(CheckUpdateResponse {
            update_is_available: false,
            error: false,
            new_version: String::from(""),
            changelog: String::from(""),
        });
    }

    let app_handle = {
        let main_window = MAIN_WINDOW.lock().unwrap();
        match main_window.as_ref() {
            Some(window) => window.app_handle().clone(),
            None => {
                error!(Source = "Updater"; "Cannot check updates: main window not available.");
                return Json(CheckUpdateResponse {
                    update_is_available: false,
                    error: true,
                    new_version: String::from(""),
                    changelog: String::from(""),
                });
            }
        }
    };
    let response = match app_handle.updater() {
        Ok(updater) => updater.check().await,
        Err(e) => {
            warn!(Source = "Updater"; "Failed to get updater instance: {e}");
            return Json(CheckUpdateResponse {
                update_is_available: false,
                error: true,
                new_version: String::from(""),
                changelog: String::from(""),
            });
        }
    };

    match response {
        Ok(Some(update)) => {
            let body_len = update.body.as_ref().map_or(0, |body| body.len());
            let date = update.date;
            let new_version = update.version.clone();
            info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={new_version}");
            let changelog = update.body.clone().unwrap_or_default();
            *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update);
            Json(CheckUpdateResponse {
                update_is_available: true,
                error: false,
                new_version,
                changelog,
            })
        }
        Ok(None) => {
            info!(Source = "Tauri"; "Updater: app is already up to date");
            Json(CheckUpdateResponse {
                update_is_available: false,
                error: false,
                new_version: String::from(""),
                changelog: String::from(""),
            })
        }
        Err(e) => {
            warn!(Source = "Tauri"; "Updater: failed to update: {e}");
            Json(CheckUpdateResponse {
                update_is_available: false,
                error: true,
                new_version: String::from(""),
                changelog: String::from(""),
            })
        }
    }
}

/// The response to the check for update request.
#[derive(Serialize)]
pub struct CheckUpdateResponse {
    update_is_available: bool,
    error: bool,
    new_version: String,
    changelog: String,
}

/// Installs the update.
pub async fn install_update(_token: APIToken) {
    if !self_update_allowed(is_dev(), is_flatpak()) {
        let reason = if is_flatpak() { "Flatpak installations are updated externally" } else { "the app is running in development mode" };
        warn!(Source = "Updater"; "Skipping update installation because {reason}.");
        return;
    }

    let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
    let app_handle = MAIN_WINDOW
        .lock()
        .unwrap()
        .as_ref()
        .map(|window| window.app_handle().clone());

    match cloned_response_option {
        Some(update_response) => {
            info!(Source = "Tauri"; "Updater: update is pending!");
            let result = update_response.download_and_install(
                |chunk_length, _content_length| {
                    trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes");
                },
                || {
                    info!(Source = "Tauri"; "Updater: update has been downloaded!");
                    warn!(Source = "Tauri"; "Try to stop the .NET server now...");

                    if is_prod() {
                        stop_dotnet_server();
                        stop_qdrant_edge_database();
                    } else {
                        warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server.");
                    }
                },
            ).await;

            match result {
                Ok(_) => {
                    info!(Source = "Tauri"; "Updater: app has been updated");
                    warn!(Source = "Tauri"; "Try to restart the app now...");

                    if is_prod() {
                        if let Some(handle) = app_handle {
                            handle.restart();
                        } else {
                            warn!(Source = "Tauri"; "Cannot restart after update: main window not available.");
                        }
                    } else {
                        warn!(Source = "Tauri"; "Development environment detected; do not restart the app.");
                    }
                }
                Err(e) => {
                    warn!(Source = "Tauri"; "Updater: failed to update: {e}");
                }
            }
        },

        None => {
            error!(Source = "Updater"; "No update available to install. Did you check for updates first?");
        },
    }
}

fn self_update_allowed(development: bool, flatpak: bool) -> bool {
    !development && !flatpak
}

/// Response for application exit requests.
#[derive(Serialize)]
pub struct AppExitResponse {
    success: bool,
    error_message: String,
}

/// Requests a controlled shutdown of the entire desktop application.
pub async fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
    let app_handle = {
        let main_window_lock = MAIN_WINDOW.lock().unwrap();
        match main_window_lock.as_ref() {
            Some(window) => window.app_handle().clone(),
            None => {
                error!(Source = "Tauri"; "Cannot exit app: main window not available.");
                return Json(AppExitResponse {
                    success: false,
                    error_message: "Main window not available".to_string(),
                });
            }
        }
    };

    info!(Source = "Tauri"; "Controlled app exit was requested by the UI.");
    tauri::async_runtime::spawn(async move {
        time::sleep(Duration::from_millis(50)).await;
        app_handle.exit(0);
    });

    Json(AppExitResponse {
        success: true,
        error_message: String::new(),
    })
}

/// Registers or updates a global shortcut. If the shortcut string is empty,
/// the existing shortcut for that name will be unregistered.
pub async fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest>) -> Json<ShortcutResponse> {
    let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().map(|window| window.app_handle().clone());
    let event_sender = EVENT_BROADCAST.lock().unwrap().clone();
    Json(crate::global_shortcuts::register(app_handle, event_sender, payload.0).await)
}

/// Request payload for validating a shortcut.
#[derive(Clone, Deserialize)]
pub struct ValidateShortcutRequest {
    /// The shortcut string to validate (e.g., "CmdOrControl+1").
    shortcut: String,
}

/// Response for shortcut validation.
#[derive(Serialize)]
pub struct ShortcutValidationResponse {
    is_valid: bool,
    error_message: String,
    has_conflict: bool,
    conflict_description: String,
}

/// Validates a shortcut string without registering it.
/// Checks if the shortcut syntax is valid and if it
/// conflicts with existing shortcuts.
pub async fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutRequest>) -> Json<ShortcutValidationResponse> {
    let shortcut = payload.shortcut.clone();

    // Empty shortcuts are always valid (means "disabled"):
    if shortcut.is_empty() {
        return Json(ShortcutValidationResponse {
            is_valid: true,
            error_message: String::new(),
            has_conflict: false,
            conflict_description: String::new(),
        });
    }

    // Check if the shortcut is already registered:
    for (name, registered_shortcut) in crate::global_shortcuts::registered_shortcuts().await {
        if registered_shortcut.eq_ignore_ascii_case(&shortcut) {
            return Json(ShortcutValidationResponse {
                is_valid: true,
                error_message: String::new(),
                has_conflict: true,
                conflict_description: format!("Already used by: {}", name),
            });
        }
    }

    // Try to parse the shortcut to validate syntax.
    // We can't easily validate without registering in Tauri 1.x,
    // so we do basic syntax validation here:
    let is_valid = validate_shortcut_syntax(&shortcut);

    if is_valid {
        Json(ShortcutValidationResponse {
            is_valid: true,
            error_message: String::new(),
            has_conflict: false,
            conflict_description: String::new(),
        })
    } else {
        Json(ShortcutValidationResponse {
            is_valid: false,
            error_message: format!("Invalid shortcut syntax: {}", shortcut),
            has_conflict: false,
            conflict_description: String::new(),
        })
    }
}

/// Suspends shortcut processing. Portal sessions remain active and ignore activations;
/// Tauri shortcuts are temporarily unregistered and restored on resume.
/// This is useful when opening a dialog to configure shortcuts, so the user can
/// press the current shortcut to re-enter it without triggering the action.
pub async fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
    let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().map(|window| window.app_handle().clone());
    Json(crate::global_shortcuts::suspend(app_handle).await)
}

/// Resumes shortcut processing by re-registering all shortcuts with the OS.
pub async fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
    let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().map(|window| window.app_handle().clone());
    let event_sender = EVENT_BROADCAST.lock().unwrap().clone();
    Json(crate::global_shortcuts::resume(app_handle, event_sender).await)
}

/// Validates the syntax of a shortcut string.
fn validate_shortcut_syntax(shortcut: &str) -> bool {
    let parts: Vec<&str> = shortcut.split('+').collect();
    if parts.is_empty() {
        return false;
    }

    let mut has_key = false;
    for part in parts {
        let part_lower = part.to_lowercase();
        match part_lower.as_str() {
            // Modifiers
            "cmdorcontrol" | "commandorcontrol" | "ctrl" | "control" | "cmd" | "command" |
            "shift" | "alt" | "meta" | "super" | "option" => continue,

            // Keys - letters
            "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" |
            "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" => has_key = true,

            // Keys - numbers
            "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => has_key = true,

            // Keys - function keys
            _ if part_lower.starts_with('f') && part_lower[1..].parse::<u32>().is_ok() => has_key = true,

            // Keys - special
            "space" | "enter" | "tab" | "escape" | "backspace" | "delete" | "insert" |
            "home" | "end" | "pageup" | "pagedown" |
            "up" | "down" | "left" | "right" |
            "arrowup" | "arrowdown" | "arrowleft" | "arrowright" |
            "minus" | "equal" | "bracketleft" | "bracketright" | "backslash" |
            "semicolon" | "quote" | "backquote" | "comma" | "period" | "slash" => has_key = true,

            // Keys - numpad
            _ if part_lower.starts_with("num") => has_key = true,

            // Unknown
            _ => return false,
        }
    }

    has_key
}

fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) {
    let resource_dir = match path_resolver.resource_dir() {
        Ok(path) => path,
        Err(error) => {
            error!(Source = "Bootloader Tauri"; "Failed to resolve resource dir: {error}");
            return;
        }
    };

    match select_pdfium_library_directory(&resource_dir, is_flatpak()) {
        Some(path) => {
            *PDFIUM_LIB_PATH.lock().unwrap() = Some(path.to_string_lossy().to_string());
        }
        None => {
            error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
        }
    }
}

fn select_pdfium_library_directory(resource_dir: &Path, include_flatpak_library_directory: bool) -> Option<PathBuf> {
    select_pdfium_library_directory_for(resource_dir, include_flatpak_library_directory, Path::new(FLATPAK_LIBRARY_DIRECTORY))
}

fn select_pdfium_library_directory_for(
    resource_dir: &Path,
    include_flatpak_library_directory: bool,
    flatpak_library_directory: &Path,
) -> Option<PathBuf> {
    let mut candidate_paths = Vec::new();

    if include_flatpak_library_directory {
        candidate_paths.push(flatpak_library_directory.to_path_buf());
    }

    candidate_paths.push(resource_dir.join("resources").join("libraries"));
    candidate_paths.push(resource_dir.join("libraries"));

    for path in candidate_paths {
        let pdfium_library_path = Pdfium::pdfium_platform_library_name_at_path(&path);
        if pdfium_library_path.exists() {
            return Some(path);
        }

        if path.exists() {
            warn!(
                Source = "Bootloader Tauri";
                "PDFium library directory exists, but the library file was not found at '{path}'.",
                path = pdfium_library_path.to_string_lossy(),
            );
        }
    }

    None
}

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

    #[test]
    fn self_update_is_disabled_in_development() {
        assert!(!self_update_allowed(true, false));
    }

    #[test]
    fn self_update_is_disabled_for_flatpak() {
        assert!(!self_update_allowed(false, true));
    }

    #[test]
    fn self_update_is_enabled_for_normal_production_installations() {
        assert!(self_update_allowed(false, false));
    }
    #[test]
    fn pdfium_library_directory_prefers_resources_libraries() {
        let temp_dir = tempfile::tempdir().unwrap();
        let resources_libraries = temp_dir.path().join("resources").join("libraries");
        let libraries = temp_dir.path().join("libraries");
        create_pdfium_library_in(&resources_libraries);
        create_pdfium_library_in(&libraries);

        assert_eq!(
            select_pdfium_library_directory(temp_dir.path(), false),
            Some(resources_libraries)
        );
    }

    #[test]
    fn pdfium_library_directory_falls_back_when_first_directory_has_no_library() {
        let temp_dir = tempfile::tempdir().unwrap();
        let resources_libraries = temp_dir.path().join("resources").join("libraries");
        let libraries = temp_dir.path().join("libraries");
        fs::create_dir_all(&resources_libraries).unwrap();
        create_pdfium_library_in(&libraries);

        assert_eq!(
            select_pdfium_library_directory(temp_dir.path(), false),
            Some(libraries)
        );
    }

    #[test]
    fn pdfium_library_directory_requires_library_file() {
        let temp_dir = tempfile::tempdir().unwrap();
        fs::create_dir_all(temp_dir.path().join("resources").join("libraries")).unwrap();
        fs::create_dir_all(temp_dir.path().join("libraries")).unwrap();

        assert_eq!(select_pdfium_library_directory(temp_dir.path(), false), None);
    }

    #[test]
    fn pdfium_library_directory_prefers_flatpak_library_directory_when_flatpak() {
        let temp_dir = tempfile::tempdir().unwrap();
        let flatpak_library_directory = temp_dir.path().join("app").join("lib");
        let resources_libraries = temp_dir.path().join("resources").join("libraries");
        create_pdfium_library_in(&flatpak_library_directory);
        create_pdfium_library_in(&resources_libraries);

        assert_eq!(
            select_pdfium_library_directory_for(temp_dir.path(), true, &flatpak_library_directory),
            Some(flatpak_library_directory)
        );
    }

    #[test]
    fn pdfium_library_directory_skips_flatpak_library_directory_when_not_flatpak() {
        let temp_dir = tempfile::tempdir().unwrap();
        let flatpak_library_directory = temp_dir.path().join("app").join("lib");
        create_pdfium_library_in(&flatpak_library_directory);

        assert_eq!(
            select_pdfium_library_directory_for(temp_dir.path(), false, &flatpak_library_directory),
            None
        );
    }

    fn create_pdfium_library_in(path: &Path) {
        fs::create_dir_all(path).unwrap();
        fs::File::create(Pdfium::pdfium_platform_library_name_at_path(path)).unwrap();
    }

    #[test]
    fn tauri_localhost_is_tauri_asset_url() {
        let https_url = tauri::Url::parse("https://tauri.localhost/index.html").unwrap();
        let http_url = tauri::Url::parse("http://tauri.localhost/index.html").unwrap();

        assert!(is_tauri_asset_url(&https_url));
        assert!(is_tauri_asset_url(&http_url));
    }

    #[test]
    fn localhost_app_url_is_not_tauri_asset_url() {
        let url = tauri::Url::parse("http://localhost:12345/").unwrap();

        assert!(!is_tauri_asset_url(&url));
        assert!(is_local_http_url(&url));
    }

    #[test]
    fn external_url_is_not_internal_url() {
        let url = tauri::Url::parse("https://example.com/").unwrap();

        assert!(!is_tauri_asset_url(&url));
        assert!(!is_local_http_url(&url));
    }

    #[test]
    fn audio_capture_is_allowed_for_exact_approved_app_origin() {
        let approved = tauri::Url::parse("http://localhost:12345/").unwrap();
        let current = tauri::Url::parse("http://localhost:12345/voice-recorder").unwrap();

        assert!(should_allow_audio_capture(Some(&approved), Some(&current), true, false));
    }

    #[test]
    fn audio_capture_is_denied_for_wrong_port() {
        let approved = tauri::Url::parse("http://localhost:12345/").unwrap();
        let current = tauri::Url::parse("http://localhost:54321/").unwrap();

        assert!(!should_allow_audio_capture(Some(&approved), Some(&current), true, false));
    }

    #[test]
    fn audio_capture_is_denied_for_external_origin() {
        let approved = tauri::Url::parse("http://localhost:12345/").unwrap();
        let current = tauri::Url::parse("https://example.com/").unwrap();

        assert!(!should_allow_audio_capture(Some(&approved), Some(&current), true, false));
    }

    #[test]
    fn video_capture_is_denied() {
        let approved = tauri::Url::parse("http://localhost:12345/").unwrap();

        assert!(!should_allow_audio_capture(Some(&approved), Some(&approved), false, true));
    }

    #[test]
    fn combined_audio_and_video_capture_is_denied() {
        let approved = tauri::Url::parse("http://localhost:12345/").unwrap();

        assert!(!should_allow_audio_capture(Some(&approved), Some(&approved), true, true));
    }
}
