use reqwest::Client;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::time;

use crate::config::{APP_ID, VERSION};
use crate::content_page::ContentPage;
use crate::error::NewsFlashGtkError;
use crate::global_actions::SyncActions;
use crate::gobject_models::PluginFeautres;
use crate::i18n::{i18n, i18n_f};
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use crate::settings::{AdvancedSettings, Settings, SyncIntervalType};
use crate::util::{DesktopSettings, JOIN_ERROR, Util, constants};
use gdk4::{Display, prelude::*};
use gio::{
    ActionEntry, ApplicationFlags, NetworkConnectivity, Notification, NotificationPriority, SimpleAction, ThemedIcon,
    subclass::prelude::*,
};
use glib::{ControlFlow, ExitCode, OptionArg, OptionFlags, Propagation, Properties, SourceId, clone};
use gtk4::{CssProvider, IconTheme, prelude::*};
use libadwaita::subclass::prelude::*;
use news_flash::models::{FeedID, LoginData, PluginCapabilities};
use news_flash::{NewsFlash, error::NewsFlashError};
use once_cell::sync::Lazy;
use std::sync::RwLock as SyncRwLock;
use tokio::sync::RwLock;
use tokio::time::Duration;

pub static CONFIG_DIR: Lazy<PathBuf> = Lazy::new(|| glib::user_config_dir().join("news-flash"));
pub static DATA_DIR: Lazy<PathBuf> = Lazy::new(|| glib::user_data_dir().join("news-flash"));
pub static STATE_DIR: Lazy<PathBuf> = Lazy::new(|| glib::user_state_dir().join("news-flash"));
pub static IMAGE_DATA_DIR: Lazy<PathBuf> = Lazy::new(|| DATA_DIR.join("pictures"));
pub static CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| glib::user_cache_dir().join("news-flash"));
pub static ARTICLE_CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| CACHE_DIR.join("html2gtk"));

#[derive(Debug, Clone)]
pub struct NotificationCounts {
    pub new: HashMap<FeedID, i64>,
    pub unread: HashMap<FeedID, i64>,
    pub names: HashMap<FeedID, String>,
}

mod imp {
    use super::*;

    #[derive(Properties)]
    #[properties(wrapper_type = super::App)]
    pub struct App {
        pub news_flash: Arc<RwLock<Option<NewsFlash>>>,
        pub news_flash_error: RefCell<Option<NewsFlashGtkError>>,
        pub sync_source_id: RefCell<Option<SourceId>>,
        pub client: Arc<RwLock<Option<Client>>>,
        pub shutdown_in_progress: Cell<bool>,
        pub start_headless: Cell<bool>,
        pub was_online: Cell<bool>,
        pub desktop_settings: DesktopSettings,
        pub network_changed_timeout: Rc<RefCell<Option<SourceId>>>,

        #[property(get = Self::get_settings, name = "settings", type = Settings)]
        #[property(get, set, name = "plugin-id", nullable)]
        pub plugin_id: RefCell<Option<String>>,

        #[property(get, set, name = "is-syncing")]
        pub is_syncing: Cell<bool>,

        #[property(get, set, name = "is-marking-all")]
        pub is_marking_all: Cell<bool>,

        #[property(get, set, name = "is-scraping-content")]
        pub is_scraping_content: Cell<bool>,

        #[property(get, set = Self::set_is_offline, name = "is-offline")]
        pub is_offline: Cell<bool>,

        #[property(get, set, name = "is-exporting-article")]
        pub is_exporting_article: Cell<bool>,

        #[property(get, set)]
        pub features: RefCell<PluginFeautres>,

        #[property(get, set)]
        pub volume: Cell<f64>,

        #[property(get, set, nullable)]
        pub recoloring: RefCell<Option<CssProvider>>,

        #[property(get, set, nullable)]
        pub theme_tiles: RefCell<Option<CssProvider>>,

        pub settings: SyncRwLock<Settings>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for App {
        const NAME: &'static str = "NewsFlashApp";
        type Type = super::App;
        type ParentType = libadwaita::Application;

        fn new() -> Self {
            Self {
                news_flash: Arc::new(RwLock::new(None)),
                news_flash_error: RefCell::new(None),
                settings: SyncRwLock::new(Settings::default()),
                sync_source_id: RefCell::new(None),
                client: Arc::new(RwLock::new(None)),
                shutdown_in_progress: Cell::new(false),
                start_headless: Cell::new(false),
                features: RefCell::new(PluginFeautres::default()),
                desktop_settings: DesktopSettings::default(),
                network_changed_timeout: Rc::new(RefCell::new(None)),
                plugin_id: RefCell::new(None),
                is_syncing: Cell::new(false),
                is_marking_all: Cell::new(false),
                is_scraping_content: Cell::new(false),
                is_offline: Cell::new(false),
                was_online: Cell::new(false),
                is_exporting_article: Cell::new(false),
                volume: Cell::new(1.0),
                recoloring: RefCell::new(None),
                theme_tiles: RefCell::new(None),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for App {}

    impl GtkApplicationImpl for App {}

    impl ApplicationImpl for App {
        fn startup(&self) {
            // Workaround to still load style.css if app-id has '.Devel' suffix
            if APP_ID.ends_with(constants::DEVEL_SUFFIX)
                && let Some(id_str) = APP_ID.strip_suffix(constants::DEVEL_SUFFIX)
            {
                let new_id = format!("/{}/", id_str.replace('.', "/"));
                self.obj().set_resource_base_path(Some(&new_id));
            }

            gio::resources_register_include!("styles.gresource").unwrap();
            gio::resources_register_include!("icons.gresource").unwrap();
            gio::resources_register_include!("appdata.gresource").unwrap();

            if let Some(display) = Display::default() {
                IconTheme::for_display(&display).add_resource_path("/com/gitlab/news_flash/icons/");
            }

            self.parent_startup();
        }

        fn activate(&self) {
            let obj = self.obj();
            obj.withdraw_notification(constants::SYSTEM_NOTIFICATION_ID);

            if let Some(window) = obj.active_window() {
                window.set_visible(true);
                window.present();
                return;
            }

            tracing::info!(%VERSION, %APP_ID, "Newsflash");

            let settings = Settings::open().expect("Failed to access settings file");
            *self.settings.write().unwrap() = settings;

            let news_flash_lib = NewsFlash::builder()
                .data_dir(&*DATA_DIR)
                .config_dir(&*CONFIG_DIR)
                .try_load()
                .ok();

            if let Some(news_flash_lib) = news_flash_lib {
                tracing::info!("Successful load from config");
                self.news_flash.blocking_write().replace(news_flash_lib);
            } else {
                tracing::warn!("No account configured");
            }

            let css_provider = CssProvider::new();
            if let Some(display) = Display::default() {
                gtk4::style_context_add_provider_for_display(
                    &display,
                    &css_provider,
                    gtk4::STYLE_PROVIDER_PRIORITY_THEME + 1,
                );
            }
            self.recoloring.replace(Some(css_provider));

            let css_provider = CssProvider::new();
            if let Some(display) = Display::default() {
                gtk4::style_context_add_provider_for_display(
                    &display,
                    &css_provider,
                    gtk4::STYLE_PROVIDER_PRIORITY_THEME + 1,
                );
            }
            self.theme_tiles.replace(Some(css_provider));

            self.desktop_settings.init();

            let main_window = MainWindow::default();
            obj.add_window(&main_window);
            main_window.init();
            main_window.connect_close_request(Self::on_window_close);

            MainWindow::restore_state();

            // update client if network changes
            let network_monitor = gio::NetworkMonitor::default();
            network_monitor.connect_network_changed(Self::on_network_changed);
            network_monitor.emit_by_name::<()>("network-changed", &[&network_monitor.is_network_available()]);

            if self.start_headless.get() {
                main_window.set_visible(false);
            } else {
                main_window.present();
            }

            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move {
                    imp.update_features().await;
                    imp.load_plugin_id().await;

                    if super::App::default().settings().general().sync_on_startup() {
                        let network_monitor = gio::NetworkMonitor::default();
                        if !network_monitor.is_network_available()
                            && network_monitor.connectivity() != NetworkConnectivity::Full
                        {
                            tracing::warn!("Omitting startup sync due to no network connectivity");
                        } else {
                            tracing::info!("Startup Sync");
                            SyncActions::sync().await;
                        }
                    }

                    // callback will only be executed if news_flash is initialized
                    super::App::default().schedule_sync();
                }
            ));
        }
    }

    impl AdwApplicationImpl for App {}

    impl App {
        fn get_settings(&self) -> Settings {
            self.settings.read().unwrap().clone()
        }

        fn on_window_close(win: &MainWindow) -> Propagation {
            win.set_visible(false);
            let app = super::App::default();

            if app.imp().shutdown_in_progress.get() {
                return Propagation::Stop;
            }
            if app.settings().general().keep_running_in_background() && win.is_page_visible(constants::CONTENT_PAGE) {
                MainWindow::harvest_restore_relevant_state();
                unsafe {
                    if libc::malloc_trim(0) > 0 {
                        tracing::info!("malloc_trim freed memory");
                    }
                }
                return Propagation::Stop;
            }

            app.queue_quit();
            Propagation::Stop
        }

        fn on_network_changed(monitor: &gio::NetworkMonitor, network_available: bool) {
            let obj = super::App::default();
            let connectivity = monitor.connectivity();
            let ping_urls = obj.settings().advanced().ping_url.clone();

            if let Some(timeout_id) = obj.imp().network_changed_timeout.take() {
                timeout_id.remove();
            }

            let timeout_id = glib::timeout_add_local_once(Duration::from_secs(3), move || {
                super::App::default().imp().network_changed_timeout.take();
                let network_changed_future = Self::runtime_online_check(connectivity, network_available, ping_urls);
                glib::spawn_future_local(network_changed_future);
            });

            obj.imp().network_changed_timeout.replace(Some(timeout_id));
        }

        async fn runtime_online_check(
            connectivity: NetworkConnectivity,
            network_available: bool,
            ping_urls: Vec<String>,
        ) {
            let settings = super::App::default().settings().advanced();
            let client = super::App::rebuild_client(settings).await;

            let future = TokioRuntime::spawn(async move {
                let news_flash = super::App::news_flash();
                let news_flash_guard = news_flash.read().await;
                let Some(news_flash) = news_flash_guard.as_ref() else {
                    return false;
                };

                let is_online = Util::is_online(connectivity, network_available, news_flash, &client, &ping_urls).await;
                _ = news_flash.set_offline(!is_online, &client).await;
                is_online
            });

            let is_online = future.await.expect(JOIN_ERROR);

            let app = super::App::default();
            // check again, just to make sure network did not connect while we waited for the ping timeout
            let network_monitor = gio::NetworkMonitor::default();
            let connectivity = network_monitor.connectivity();
            let network_available = network_monitor.is_network_available();
            let is_online = is_online || (network_available && connectivity == NetworkConnectivity::Full);

            app.set_is_offline(!is_online);
        }

        async fn load_plugin_id(&self) {
            let future = TokioRuntime::spawn(async move {
                let news_flash = super::App::news_flash();
                let news_flash_guard = news_flash.read().await;
                let news_flash = news_flash_guard.as_ref()?;
                news_flash.id().await
            });
            let plugin_id = future.await.expect(JOIN_ERROR);

            tracing::info!(?plugin_id);
            self.obj().set_plugin_id(plugin_id.map(|id| id.as_str().to_owned()));
        }

        async fn update_features(&self) {
            let future = TokioRuntime::spawn(async move {
                let news_flash = super::App::news_flash();
                let news_flash_guad = news_flash.read().await;
                let news_flash = news_flash_guad.as_ref()?;
                news_flash.features().await.ok()
            });

            if let Some(features) = future.await.expect(JOIN_ERROR) {
                self.obj().set_features(PluginFeautres::from(features));
            }
        }

        fn set_is_offline(&self, offline: bool) {
            if self.is_offline.get() == offline {
                return;
            }

            if self.was_online.get() {
                let content_page = ContentPage::instance();
                content_page.dismiss_notification();

                if offline {
                    content_page.simple_message(&i18n("NewsFlash is offline"));
                } else {
                    content_page.simple_message(&i18n("NewsFlash is online"));
                }
            } else if !offline {
                self.was_online.set(true);
            }

            self.is_offline.set(offline);
        }
    }
}

glib::wrapper! {
    pub struct App(ObjectSubclass<imp::App>)
        @extends gio::Application, gtk4::Application, libadwaita::Application,
        @implements gio::ActionMap, gio::ActionGroup;
}

impl Default for App {
    fn default() -> Self {
        gio::Application::default()
            .expect("Failed to get default gio::Application")
            .downcast::<App>()
            .expect("failed to downcast gio::Application to App")
    }
}

impl App {
    pub fn new() -> Self {
        let app: App = glib::Object::builder()
            .property("application-id", Some(APP_ID))
            .property("flags", ApplicationFlags::HANDLES_COMMAND_LINE)
            .build();
        app.add_main_option(
            "headless",
            b'l'.into(),
            OptionFlags::NONE,
            OptionArg::None,
            "start without showing a window",
            None,
        );
        app.add_main_option(
            "subscribe",
            b's'.into(),
            OptionFlags::IN_MAIN,
            OptionArg::String,
            "subscribe to a feed",
            Some("feed Url"),
        );
        app.add_main_option(
            "oauth",
            b'o'.into(),
            OptionFlags::IN_MAIN,
            OptionArg::String,
            "oauth callback",
            Some("callback Url"),
        );

        app.connect_command_line(|app, command_line| {
            let options = command_line.options_dict();
            let arguments = command_line
                .arguments()
                .into_iter()
                .filter_map(|osstr| osstr.to_str().map(ToString::to_string))
                .collect::<Vec<_>>();

            if !command_line.is_remote() && options.contains("headless") {
                tracing::info!("starting headless");
                app.imp().start_headless.set(true);
            }

            app.activate();

            glib::idle_add_local(move || {
                if let Ok(Some(subscribe_url)) = options.lookup::<String>("subscribe") {
                    tracing::info!(%subscribe_url);
                    MainWindow::activate_action("add-feed-dialog", Some(&subscribe_url.to_variant()));
                }

                if let Ok(Some(callback_url)) = options.lookup::<String>("oauth") {
                    tracing::info!(%callback_url);
                    MainWindow::instance().oauth_callback(&callback_url);
                }

                for raw_arg in &arguments {
                    if raw_arg.starts_with("newsflash://") {
                        MainWindow::instance().oauth_callback(raw_arg);
                    }

                    if raw_arg.starts_with("feed://") {
                        let mut feed_uri = raw_arg.trim_start_matches("feed:").to_string();
                        if !feed_uri.starts_with("https") {
                            feed_uri = format!("https:{feed_uri}");
                        }
                        MainWindow::activate_action("add-feed-dialog", Some(&feed_uri.to_variant()));
                    }
                }

                ControlFlow::Break
            });

            ExitCode::SUCCESS
        });

        app.add_action_entries([ActionEntry::builder("quit")
            .activate(|app: &App, _action, _parameter| app.queue_quit())
            .build()]);
        app
    }

    pub fn news_flash() -> Arc<RwLock<Option<NewsFlash>>> {
        App::default().imp().news_flash.clone()
    }

    pub async fn client() -> Client {
        let locked = App::default().imp().client.clone();
        if let Some(client) = locked.read().await.as_ref() {
            client.clone()
        } else {
            let settings = App::default().settings().advanced();
            Self::rebuild_client(settings).await
        }
    }

    pub async fn rebuild_client(settings: AdvancedSettings) -> Client {
        let new_client = Util::build_client(settings);
        let new_client_clone = new_client.clone();
        let locked = App::default().imp().client.clone();

        TokioRuntime::spawn(async move {
            locked.write().await.replace(new_client);
        })
        .await
        .expect("task join");

        new_client_clone
    }

    pub fn desktop_settings(&self) -> &DesktopSettings {
        &self.imp().desktop_settings
    }

    pub fn set_newsflash_error(&self, error: NewsFlashGtkError) {
        self.imp().news_flash_error.replace(Some(error));
    }

    pub fn show_notification(counts: NotificationCounts) {
        // don't notify when window is visible and focused
        let window = MainWindow::instance();
        if window.is_visible() && window.is_active() {
            return;
        }

        let mut new_count = 0;
        let mut unread_count = 0;

        let feed_settings = App::default().settings().get_merged_feed_settings_map();

        for (feed_id, new) in counts.new.into_iter() {
            if feed_settings
                .get(&feed_id)
                .map(|feed_settings| feed_settings.mute_notifications)
                .unwrap_or(false)
            {
                tracing::debug!(%feed_id, "notifications muted");
                continue;
            }

            tracing::debug!(%feed_id, %new, "feed new count");
            new_count += new;
            unread_count += *counts.unread.get(&feed_id).unwrap_or(&0);
        }

        if new_count == 0 || unread_count == 0 {
            return;
        }

        let message = if new_count == 1 {
            i18n_f("There is 1 new article ({} unread)", &[&unread_count.to_string()])
        } else {
            i18n_f(
                "There are {} new articles ({} unread)",
                &[&new_count.to_string(), &unread_count.to_string()],
            )
        };

        let notification = Notification::new(&i18n("New Articles"));
        notification.set_body(Some(&message));
        notification.set_priority(NotificationPriority::Normal);
        notification.set_icon(&ThemedIcon::new(APP_ID));

        App::default().send_notification(Some(constants::SYSTEM_NOTIFICATION_ID), &notification);
    }

    pub async fn login(data: LoginData) {
        let id = data.id();
        let user_api_secret = match &data {
            LoginData::OAuth(oauth_data) => oauth_data.custom_api_secret.clone(),
            _ => None,
        };

        let data_clone = data.clone();
        let future = TokioRuntime::spawn(async move {
            let news_flash_lib = NewsFlash::builder()
                .plugin(id)
                .config_dir(&*CONFIG_DIR)
                .data_dir(&*DATA_DIR)
                .api_secret(user_api_secret)
                .create()?;

            let client = App::client().await;
            news_flash_lib.login(data_clone, &client).await?;

            let id = news_flash_lib.id().await.ok_or(NewsFlashError::NotLoggedIn)?;
            let features = news_flash_lib.features().await?;

            App::news_flash().write().await.replace(news_flash_lib);

            Ok((id, features))
        });

        match future.await.expect(JOIN_ERROR) {
            Err(error) => {
                App::default().set_plugin_id(None::<String>);
                App::default().set_features(PluginFeautres::default());
                MainWindow::instance().show_login_error(error, &data);
            }
            Ok((plugin_id, features)) => {
                App::default().set_plugin_id(Some(plugin_id.as_str()));
                App::default().set_features(PluginFeautres::from(features));

                // show content page
                MainWindow::instance().show_content_page();

                // schedule initial sync
                App::default().activate_action("init-sync", None);
                App::default().schedule_sync();

                // features might have changed after login
                // - check if discover is allowed
                // - update add popover features
                if let Some(discover_dialog_action) = MainWindow::instance().lookup_action("discover") {
                    discover_dialog_action
                        .downcast::<SimpleAction>()
                        .expect("downcast Action to SimpleAction")
                        .set_enabled(
                            App::default()
                                .features()
                                .as_ref()
                                .contains(PluginCapabilities::ADD_REMOVE_FEEDS),
                        );
                }
            }
        }
    }

    pub async fn reset_account() {
        let future = TokioRuntime::spawn(async move {
            let news_flash = App::news_flash();
            if let Some(news_flash_lib) = news_flash.read().await.as_ref() {
                let client = App::client().await;
                news_flash_lib.logout(&client).await?;
            }
            news_flash.write().await.take();
            Ok(())
        });

        match future.await.expect(JOIN_ERROR) {
            Ok(()) => {
                ContentPage::instance().clear();
                _ = App::default().settings().delete_old_feed_settings(&HashSet::new());
                _ = App::default().settings().delete_old_category_settings(&HashSet::new());
                MainWindow::instance().show_welcome_page()
            }
            Err(error) => {
                MainWindow::instance().reset_account_failed(error);
            }
        }
    }

    pub fn schedule_sync(&self) {
        let imp = self.imp();
        if let Some(a) = imp.sync_source_id.take() {
            SourceId::remove(a)
        }
        let general_settings = self.settings().general();
        let sync_interval = match general_settings.sync_type() {
            SyncIntervalType::Never => None,
            SyncIntervalType::Predefined => general_settings.predefined_sync_interval().as_seconds(),
            SyncIntervalType::Custom => Some(general_settings.custom_sync_interval()),
        };
        tracing::debug!(?sync_interval, "schedule sync");
        let sync_on_metered_connection = self.settings().general().sync_on_metered();

        let Some(sync_interval) = sync_interval else {
            return;
        };

        let timeout = glib::timeout_add_seconds_local(sync_interval, move || {
            let network_monitor = gio::NetworkMonitor::default();
            let power_profile_monitor = gio::PowerProfileMonitor::get_default();

            // check if on metered connection and only sync if chosen in preferences
            // also only sync if power saving mode (low battery, etc.) is not enabled
            if (!network_monitor.is_network_metered() || sync_on_metered_connection)
                && !power_profile_monitor.is_power_saver_enabled()
            {
                App::default().activate_action("sync", None);
            }

            ControlFlow::Continue
        });

        imp.sync_source_id.borrow_mut().replace(timeout);
    }

    pub fn queue_quit(&self) {
        self.imp().shutdown_in_progress.set(true);
        MainWindow::instance().close();
        ContentPage::instance().dismiss_notification();

        // wait for ongoing sync to finish, but limit waiting to max 3s
        let start_wait_time = time::SystemTime::now();
        let max_wait_time = time::Duration::from_secs(4);

        glib::spawn_future_local(Self::wait_for_sync_or_undo_action(start_wait_time, max_wait_time));
    }

    async fn wait_for_sync_or_undo_action(start_wait_time: time::SystemTime, max_wait_time: time::Duration) {
        let future = TokioRuntime::spawn(async move {
            let news_flash = App::news_flash();
            let guard = news_flash.read().await;
            guard.as_ref().map(NewsFlash::is_sync_ongoing).unwrap_or(false)
        });

        let is_sync_ongoing = future.await.expect(JOIN_ERROR);
        let elapsed = start_wait_time.elapsed().expect("shutdown timer elapsed error");
        let is_executing_undo_action = ContentPage::instance().executing_undo_action();

        if (is_executing_undo_action || is_sync_ongoing) && elapsed < max_wait_time {
            glib::MainContext::default().iteration(false);
            glib::spawn_future_local(Self::wait_for_sync_or_undo_action(start_wait_time, max_wait_time));
        } else {
            App::default().force_quit();
        }
    }

    fn force_quit(&self) {
        tracing::info!("Shutdown!");
        MainWindow::harvest_restore_relevant_state();
        self.quit();
    }

    pub fn request_background_permission(autostart: bool) {
        let root = MainWindow::instance().native().unwrap();

        tracing::info!(%autostart, "request background permissions");

        glib::spawn_future_local(async move {
            if !ashpd::is_sandboxed() {
                return;
            }

            let binary_name = APP_ID.strip_suffix(constants::DEVEL_SUFFIX).unwrap_or(APP_ID);
            let identifier = ashpd::WindowIdentifier::from_native(&root).await;

            let response = ashpd::desktop::background::Background::request()
                .identifier(identifier)
                .reason(constants::BACKGROUND_IDLE)
                .auto_start(autostart)
                .command([binary_name, "--headless"])
                .send()
                .await
                .and_then(|request| request.response());

            if let Err(error) = response {
                tracing::error!(%error, "Requesting background permission failed");
            }

            let proxy = ashpd::desktop::background::BackgroundProxy::new().await;
            let status_options =
                ashpd::desktop::background::SetStatusOptions::default().set_message(constants::BACKGROUND_IDLE);

            if let Ok(proxy) = proxy
                && let Err(error) = proxy.set_status(status_options).await
            {
                tracing::error!(%error, "Failed to set background message");
            }
        });
    }

    pub fn set_background_status(message: &'static str) {
        glib::spawn_future_local(async move {
            if !ashpd::is_sandboxed() {
                return;
            }

            let proxy = ashpd::desktop::background::BackgroundProxy::new().await;
            let status_options = ashpd::desktop::background::SetStatusOptions::default().set_message(message);

            if let Ok(proxy) = proxy
                && let Err(error) = proxy.set_status(status_options).await
            {
                tracing::error!(%error, "Failed to set background message");
            }
        });
    }

    pub fn open_url_in_default_browser(url: &str) {
        let launcher = gtk4::UriLauncher::new(url);

        glib::spawn_future_local(async move {
            if let Err(error) = launcher.launch_future(Some(&MainWindow::instance())).await {
                ContentPage::instance().simple_message(&i18n_f("Failed open URL: {}", &[&error.to_string()]));
            }
        });
    }
}
