use std::cell::OnceCell;

use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::clone;
use gtk::glib::Properties;
use gtk::{CompositeTemplate, glib};

use crate::data;
use crate::data::{KrFlatpak, KrItem};
use crate::utils::error::DisplayError;
use crate::widgets::KrItemRow;

mod imp {
    use gettextrs::gettext;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::KrFlatpakItemsPage)]
    #[template(file = "flatpak_items_page.ui")]
    pub struct KrFlatpakItemsPage {
        #[property(get, set, construct_only)]
        flatpak: OnceCell<KrFlatpak>,
        #[template_child]
        search_button: TemplateChild<gtk::Button>,
        #[template_child]
        search_bar: TemplateChild<gtk::SearchBar>,
        #[template_child]
        search_entry: TemplateChild<gtk::SearchEntry>,
        #[template_child]
        app_icon: TemplateChild<gtk::Image>,
        #[template_child]
        app_name: TemplateChild<gtk::Label>,
        #[template_child]
        app_description: TemplateChild<gtk::Label>,
        #[template_child]
        item_list: TemplateChild<gtk::ListBox>,

        #[template_child]
        undecryptable_items_group: TemplateChild<gtk::Widget>,
        #[template_child]
        undecryptable_items_list: TemplateChild<gtk::ListBox>,

        #[template_child]
        pub content_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub loading_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub spinner: TemplateChild<gtk::Spinner>,
        #[template_child]
        status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        no_results_status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        preferences_page: TemplateChild<adw::PreferencesPage>,
        model: gtk::SortListModel,
    }

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

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

            klass.install_action("flatpak.search", None, |obj, _, _| {
                obj.imp().search_bar.set_search_mode(true);
            });
            klass.add_binding_action(
                gtk::gdk::Key::f,
                gtk::gdk::ModifierType::CONTROL_MASK,
                "flatpak.search",
            );
        }

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

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

            self.search_bar.connect_entry(&*self.search_entry);

            flatpak
                .bind_property("icon-name", &*self.app_icon, "icon-name")
                .sync_create()
                .build();

            flatpak
                .bind_property("title", &*self.app_name, "label")
                .sync_create()
                .build();

            flatpak
                .bind_property("description", &*self.app_description, "label")
                .sync_create()
                .build();

            // Search
            let search_filter = gtk::StringFilter::new(Some(&gtk::PropertyExpression::new(
                KrItem::static_type(),
                None::<&gtk::Expression>,
                "title",
            )));
            self.search_entry
                .bind_property("text", &search_filter, "search")
                .build();
            let filter_model =
                gtk::FilterListModel::new(Some(self.model.clone()), Some(search_filter));

            filter_model.connect_items_changed(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_, _, _, _| {
                    imp.update_ui();
                }
            ));

            // Sort
            let sorter = gtk::StringSorter::new(Some(&gtk::PropertyExpression::new(
                data::KrItem::static_type(),
                gtk::Expression::NONE,
                "label",
            )));
            self.model.set_sorter(Some(&sorter));

            filter_model.connect_items_changed(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_, _, _, _| {
                    imp.update_ui();
                }
            ));

            self.item_list.bind_model(Some(&filter_model), |x| {
                let collection_item: data::KrItem = x.clone().downcast().unwrap();
                KrItemRow::new(collection_item).upcast()
            });

            self.obj()
                .flatpak()
                .undecryptable_items()
                .connect_items_changed(clone!(
                    #[weak(rename_to = imp)]
                    self,
                    move |_, _, _, _| {
                        imp.update_ui();
                    }
                ));

            let obj_weak = self.obj().downgrade();
            self.undecryptable_items_list.bind_model(
                Some(&self.obj().flatpak().undecryptable_items()),
                move |o| {
                    let any_object = o.downcast_ref::<glib::BoxedAnyObject>().unwrap();
                    let undecryptable_item = any_object.borrow::<data::UndecryptableItem>();
                    let row = adw::ActionRow::new();

                    row.set_title(&format!(
                        "{} #{}",
                        gettext("Item"),
                        undecryptable_item.index
                    ));
                    row.set_subtitle(&format!("{:?}", undecryptable_item.err));

                    let delete_button = gtk::Button::new();
                    delete_button.set_icon_name("user-trash-symbolic");
                    delete_button.set_valign(gtk::Align::Center);
                    delete_button.set_tooltip_text(Some(&gettext("Delete Key")));

                    if let Some(obj) = obj_weak.upgrade() {
                        let index = undecryptable_item.index;
                        delete_button.connect_clicked(glib::clone!(
                            #[weak]
                            obj,
                            move |_| {
                                glib::MainContext::default().spawn_local(async move {
                                    let dialog = adw::AlertDialog::builder()
                                        .heading(gettext("Delete Key?"))
                                        .body(gettext("This key will be permanently deleted"))
                                        .default_response("cancel")
                                        .build();

                                    dialog.add_response("cancel", &gettext("Cancel"));
                                    dialog.add_response("delete", &gettext("Delete"));
                                    dialog.set_response_appearance(
                                        "delete",
                                        adw::ResponseAppearance::Destructive,
                                    );

                                    if dialog.choose_future(Some(&obj)).await != "delete" {
                                        return;
                                    }

                                    let result = obj.flatpak().delete_index(index).await;

                                    //result.handle_error_in("Could not delete key", &obj);
                                });
                            }
                        ));
                    }

                    row.add_suffix(&delete_button);

                    row.upcast()
                },
            );

            glib::MainContext::default().spawn_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move {
                    imp.loading_stack.set_visible_child(&*imp.spinner);

                    let flatpak = imp.obj().flatpak();
                    imp.model.set_model(Some(&flatpak));

                    flatpak
                        .load_items()
                        .await
                        .handle_error("Unable to load keys");

                    imp.loading_stack.set_visible_child(&*imp.content_stack);
                }
            ));
        }
    }

    impl WidgetImpl for KrFlatpakItemsPage {}
    impl AccessibleImpl for KrFlatpakItemsPage {}
    impl BuildableImpl for KrFlatpakItemsPage {}
    impl NavigationPageImpl for KrFlatpakItemsPage {}

    #[gtk::template_callbacks]
    impl KrFlatpakItemsPage {
        #[template_callback]
        fn string_is_not_empty(&self, label: &str) -> bool {
            !label.is_empty()
        }

        fn update_ui(&self) {
            let obj = self.obj();
            let flatpak = obj.flatpak();

            self.loading_stack.set_visible_child(&*self.content_stack);

            if flatpak.n_total_items() == 0 && self.search_bar.is_search_mode() {
                self.loading_stack
                    .set_visible_child(&*self.no_results_status_page);
            } else {
                self.content_stack
                    .set_visible_child(&*self.preferences_page);
            }

            // Hide decryptable items if there are none
            self.item_list.set_visible(flatpak.n_items() > 0);

            // Hide undecryptable items if there are none
            self.undecryptable_items_group
                .set_visible(flatpak.undecryptable_items().n_items() > 0);

            // Hide search button if there are no decryptable items
            self.search_button.set_sensitive(flatpak.n_items() > 0);
        }
    }
}

glib::wrapper! {
    pub struct KrFlatpakItemsPage(ObjectSubclass<imp::KrFlatpakItemsPage>)
        @extends gtk::Widget, adw::NavigationPage,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl KrFlatpakItemsPage {
    pub fn new(flatpak: &KrFlatpak) -> Self {
        glib::Object::builder().property("flatpak", flatpak).build()
    }
}
