use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{gio, glib, glib::clone, CompositeTemplate};
use tracing::error;

use crate::{
    components::{CheckLoadingRow, EntryAddRow, RemovableRow, SwitchLoadingRow},
    i18n::gettext_f,
    session::model::{NotificationsGlobalSetting, NotificationsSettings},
    spawn, toast,
    utils::{BoundObjectWeakRef, DummyObject},
};

mod imp {
    use std::{cell::Cell, marker::PhantomData};

    use glib::subclass::InitializingObject;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
    #[template(
        resource = "/org/gnome/Fractal/ui/session/view/account_settings/notifications_page.ui"
    )]
    #[properties(wrapper_type = super::NotificationsPage)]
    pub struct NotificationsPage {
        #[template_child]
        pub account_row: TemplateChild<SwitchLoadingRow>,
        #[template_child]
        pub session_row: TemplateChild<adw::SwitchRow>,
        #[template_child]
        pub global: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        pub global_all_row: TemplateChild<CheckLoadingRow>,
        #[template_child]
        pub global_direct_row: TemplateChild<CheckLoadingRow>,
        #[template_child]
        pub global_mentions_row: TemplateChild<CheckLoadingRow>,
        #[template_child]
        pub keywords: TemplateChild<gtk::ListBox>,
        #[template_child]
        pub keywords_add_row: TemplateChild<EntryAddRow>,
        /// The notifications settings of the current session.
        #[property(get, set = Self::set_notifications_settings, explicit_notify)]
        pub notifications_settings: BoundObjectWeakRef<NotificationsSettings>,
        /// Whether the account section is busy.
        #[property(get)]
        pub account_loading: Cell<bool>,
        /// Whether the global section is busy.
        #[property(get)]
        pub global_loading: Cell<bool>,
        /// The global notifications setting, as a string.
        #[property(get = Self::global_setting, set = Self::set_global_setting)]
        pub global_setting: PhantomData<String>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for NotificationsPage {
        const NAME: &'static str = "NotificationsPage";
        type Type = super::NotificationsPage;
        type ParentType = adw::PreferencesPage;

        fn class_init(klass: &mut Self::Class) {
            Self::bind_template(klass);
            Self::Type::bind_template_callbacks(klass);

            klass.install_property_action("notifications.set-global-default", "global-setting");
        }

        fn instance_init(obj: &InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for NotificationsPage {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            self.keywords_add_row.connect_changed(clone!(
                #[weak]
                obj,
                move |_| {
                    obj.update_keywords();
                }
            ));
        }
    }

    impl WidgetImpl for NotificationsPage {}
    impl PreferencesPageImpl for NotificationsPage {}

    impl NotificationsPage {
        /// Set the notifications settings of the current session.
        fn set_notifications_settings(
            &self,
            notifications_settings: Option<&NotificationsSettings>,
        ) {
            if self.notifications_settings.obj().as_ref() == notifications_settings {
                return;
            }
            let obj = self.obj();

            self.notifications_settings.disconnect_signals();

            if let Some(settings) = notifications_settings {
                let account_enabled_handler = settings.connect_account_enabled_notify(clone!(
                    #[weak]
                    obj,
                    move |_| {
                        obj.update_account();
                    }
                ));
                let session_enabled_handler = settings.connect_session_enabled_notify(clone!(
                    #[weak]
                    obj,
                    move |_| {
                        obj.update_session();
                    }
                ));
                let global_setting_handler = settings.connect_global_setting_notify(clone!(
                    #[weak]
                    obj,
                    move |_| {
                        obj.update_global();
                    }
                ));

                self.notifications_settings.set(
                    settings,
                    vec![
                        account_enabled_handler,
                        session_enabled_handler,
                        global_setting_handler,
                    ],
                );

                let extra_items = gio::ListStore::new::<glib::Object>();
                extra_items.append(&DummyObject::new("add"));

                let all_items = gio::ListStore::new::<glib::Object>();
                all_items.append(&settings.keywords_list());
                all_items.append(&extra_items);

                let flattened_list = gtk::FlattenListModel::new(Some(all_items));
                self.keywords.bind_model(
                    Some(&flattened_list),
                    clone!(
                        #[weak]
                        obj,
                        #[upgrade_or_else]
                        || { adw::ActionRow::new().upcast() },
                        move |item| obj.create_keyword_row(item)
                    ),
                );
            } else {
                self.keywords.bind_model(
                    None::<&gio::ListModel>,
                    clone!(
                        #[weak]
                        obj,
                        #[upgrade_or_else]
                        || { adw::ActionRow::new().upcast() },
                        move |item| obj.create_keyword_row(item)
                    ),
                );
            }

            obj.update_account();
            obj.notify_notifications_settings();
        }

        /// The global notifications setting, as a string.
        fn global_setting(&self) -> String {
            let Some(settings) = self.notifications_settings.obj() else {
                return String::new();
            };

            settings.global_setting().to_string()
        }

        /// Set the global notifications setting, as a string.
        fn set_global_setting(&self, default: &str) {
            let Ok(default) = default.parse::<NotificationsGlobalSetting>() else {
                error!("Invalid value to set global default notifications setting: {default}");
                return;
            };

            let obj = self.obj();
            spawn!(clone!(
                #[weak]
                obj,
                async move {
                    obj.global_setting_changed(default).await;
                }
            ));
        }
    }
}

glib::wrapper! {
    /// Preferences page to edit global notification settings.
    pub struct NotificationsPage(ObjectSubclass<imp::NotificationsPage>)
        @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
}

#[gtk::template_callbacks]
impl NotificationsPage {
    pub fn new(notifications_settings: &NotificationsSettings) -> Self {
        glib::Object::builder()
            .property("notifications-settings", notifications_settings)
            .build()
    }

    /// Update the section about the account.
    fn update_account(&self) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        let checked = settings.account_enabled();
        imp.account_row.set_is_active(checked);
        imp.account_row.set_sensitive(!self.account_loading());

        // Other sections will be disabled or not.
        self.update_session();
    }

    /// Update the section about the session.
    fn update_session(&self) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        imp.session_row.set_active(settings.session_enabled());
        imp.session_row.set_sensitive(settings.account_enabled());

        // Other sections will be disabled or not.
        self.update_global();
        self.update_keywords();
    }

    /// Update the section about global.
    fn update_global(&self) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        // Updates the active radio button.
        self.notify_global_setting();

        let sensitive =
            settings.account_enabled() && settings.session_enabled() && !self.global_loading();
        imp.global.set_sensitive(sensitive);
    }

    /// Update the section about keywords.
    fn update_keywords(&self) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        let sensitive = settings.account_enabled() && settings.session_enabled();
        imp.keywords.set_sensitive(sensitive);

        if !sensitive {
            // Nothing else to update.
            return;
        }

        imp.keywords_add_row
            .set_inhibit_add(!self.can_add_keyword());
    }

    fn set_account_loading(&self, loading: bool) {
        self.imp().account_loading.set(loading);
        self.notify_account_loading();
    }

    #[template_callback]
    async fn account_switched(&self) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        let enabled = imp.account_row.is_active();
        if enabled == settings.account_enabled() {
            // Nothing to do.
            return;
        }

        imp.account_row.set_sensitive(false);
        self.set_account_loading(true);

        if settings.set_account_enabled(enabled).await.is_err() {
            let msg = if enabled {
                gettext("Could not enable account notifications")
            } else {
                gettext("Could not disable account notifications")
            };
            toast!(self, msg);
        }

        self.set_account_loading(false);
        self.update_account();
    }

    #[template_callback]
    fn session_switched(&self) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        settings.set_session_enabled(imp.session_row.is_active());
    }

    fn set_global_loading(&self, loading: bool, setting: NotificationsGlobalSetting) {
        let imp = self.imp();

        // Only show the spinner on the selected one.
        imp.global_all_row
            .set_is_loading(loading && setting == NotificationsGlobalSetting::All);
        imp.global_direct_row
            .set_is_loading(loading && setting == NotificationsGlobalSetting::DirectAndMentions);
        imp.global_mentions_row
            .set_is_loading(loading && setting == NotificationsGlobalSetting::MentionsOnly);

        self.imp().global_loading.set(loading);
        self.notify_global_loading();
    }

    #[template_callback]
    async fn global_setting_changed(&self, setting: NotificationsGlobalSetting) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };
        let imp = self.imp();

        if setting == settings.global_setting() {
            // Nothing to do.
            return;
        }

        imp.global.set_sensitive(false);
        self.set_global_loading(true, setting);

        if settings.set_global_setting(setting).await.is_err() {
            toast!(
                self,
                gettext("Could not change global notifications setting")
            );
        }

        self.set_global_loading(false, setting);
        self.update_global();
    }

    /// Create a row in the keywords list for the given item.
    fn create_keyword_row(&self, item: &glib::Object) -> gtk::Widget {
        let imp = self.imp();

        if let Some(string_obj) = item.downcast_ref::<gtk::StringObject>() {
            let keyword = string_obj.string();
            let row = RemovableRow::new();
            row.set_title(&keyword);
            row.set_remove_button_tooltip_text(Some(gettext_f(
                "Remove “{keyword}”",
                &[("keyword", &keyword)],
            )));

            row.connect_remove(clone!(
                #[weak(rename_to = obj)]
                self,
                move |row| {
                    obj.remove_keyword(row);
                }
            ));

            row.upcast()
        } else {
            // It can only be the dummy item to add a new keyword.
            imp.keywords_add_row.clone().upcast()
        }
    }

    /// Remove the keyword from the given row.
    fn remove_keyword(&self, row: &RemovableRow) {
        let Some(settings) = self.notifications_settings() else {
            return;
        };

        row.set_is_loading(true);

        spawn!(clone!(
            #[weak(rename_to = obj)]
            self,
            #[weak]
            row,
            async move {
                if settings.remove_keyword(row.title().into()).await.is_err() {
                    toast!(obj, gettext("Could not remove notification keyword"));
                }

                row.set_is_loading(false);
            }
        ));
    }

    /// Whether we can add the keyword that is currently in the entry.
    fn can_add_keyword(&self) -> bool {
        let imp = self.imp();

        // Cannot add a keyword if section is disabled.
        if !imp.keywords.is_sensitive() {
            return false;
        }

        // Cannot add a keyword if a keyword is already being added.
        if imp.keywords_add_row.is_loading() {
            return false;
        }

        let text = imp.keywords_add_row.text().to_lowercase();

        // Cannot add an empty keyword.
        if text.is_empty() {
            return false;
        }

        // Cannot add a keyword without the API.
        let Some(settings) = self.notifications_settings() else {
            return false;
        };

        // Cannot add a keyword that already exists.
        let keywords_list = settings.keywords_list();
        for keyword_obj in keywords_list.iter::<glib::Object>() {
            let Ok(keyword_obj) = keyword_obj else {
                break;
            };

            if let Some(keyword) = keyword_obj
                .downcast_ref::<gtk::StringObject>()
                .map(gtk::StringObject::string)
            {
                if keyword.to_lowercase() == text {
                    return false;
                }
            }
        }

        true
    }

    /// Add the keyword that is currently in the entry.
    #[template_callback]
    async fn add_keyword(&self) {
        if !self.can_add_keyword() {
            return;
        }

        let Some(settings) = self.notifications_settings() else {
            return;
        };

        let imp = self.imp();
        imp.keywords_add_row.set_is_loading(true);

        let keyword = imp.keywords_add_row.text().into();

        if settings.add_keyword(keyword).await.is_err() {
            toast!(self, gettext("Could not add notification keyword"));
        } else {
            // Adding the keyword was successful, reset the entry.
            imp.keywords_add_row.set_text("");
        }

        imp.keywords_add_row.set_is_loading(false);
        self.update_keywords();
    }
}
