use std::cell::{Cell, OnceCell, RefCell};
use std::collections::HashMap;
use std::marker::PhantomData;
use std::rc::Rc;

use futures_lite::stream::StreamExt;
use glib::{clone, Enum, MainContext, Properties};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use indexmap::IndexMap;
use oo7::dbus::Collection;
use oo7::dbus::Item;
use zbus::zvariant::ObjectPath;
use zbus::{Connection, MethodFlags, Proxy};

use super::KrItem;
use crate::utils::error::DisplayError;

#[derive(Default, Debug, Copy, Clone, Enum, PartialEq)]
#[repr(u32)]
#[enum_type(name = "KrCollectionType")]
pub enum KrCollectionKind {
    #[default]
    User,
    Login,
    Session,
}

impl KrCollectionKind {
    pub fn is_system(&self) -> bool {
        *self != Self::User
    }
}

mod imp {
    use super::*;

    #[derive(Default, Debug, Properties)]
    #[properties(wrapper_type = super::KrCollection)]
    pub struct KrCollection {
        #[property(get=Self::title)]
        title: PhantomData<String>,
        #[property(get)]
        pub label: RefCell<String>,
        #[property(get)]
        pub is_locked: Cell<bool>,
        #[property(get=Self::kind, builder(Default::default()))]
        pub kind: PhantomData<KrCollectionKind>,
        #[property(get)]
        regular_items: gtk::FilterListModel,
        #[property(get)]
        flatpak_items: gtk::FilterListModel,

        regular_filter: gtk::CustomFilter,
        flatpak_filter: gtk::CustomFilter,

        pub map: RefCell<IndexMap<String, KrItem>>,
        pub dbus_collection: OnceCell<Rc<Collection<'static>>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for KrCollection {
        const NAME: &'static str = "KrCollection";
        type ParentType = glib::Object;
        type Type = super::KrCollection;
        type Interfaces = (gio::ListModel,);
    }

    #[glib::derived_properties]
    impl ObjectImpl for KrCollection {
        fn constructed(&self) {
            self.parent_constructed();

            self.regular_filter.set_filter_func(|o| {
                let item = o.downcast_ref::<KrItem>().unwrap();
                item.app_id().is_none()
            });

            self.regular_items.set_model(Some(&*self.obj()));
            self.regular_items.set_filter(Some(&self.regular_filter));

            self.flatpak_filter.set_filter_func(|o| {
                let item = o.downcast_ref::<KrItem>().unwrap();
                item.app_id().is_some()
            });

            self.flatpak_items.set_model(Some(&*self.obj()));
            self.flatpak_items.set_filter(Some(&self.flatpak_filter));
        }
    }

    impl ListModelImpl for KrCollection {
        fn item_type(&self) -> glib::Type {
            KrItem::static_type()
        }

        fn n_items(&self) -> u32 {
            self.map.borrow().len() as u32
        }

        fn item(&self, position: u32) -> Option<glib::Object> {
            self.map
                .borrow()
                .get_index(position as usize)
                .map(|o| o.1.clone().upcast::<glib::Object>())
        }
    }

    impl KrCollection {
        pub fn title(&self) -> String {
            match self.obj().kind() {
                KrCollectionKind::Login => glib::real_name().to_string_lossy().to_string(),
                KrCollectionKind::Session => "Temporary".into(),
                _ => self.obj().label(),
            }
        }

        pub fn kind(&self) -> KrCollectionKind {
            let path = self.dbus_collection.get().unwrap().path().to_string();
            if path.ends_with("/login") {
                KrCollectionKind::Login
            } else if path.ends_with("/session") {
                KrCollectionKind::Session
            } else {
                KrCollectionKind::User
            }
        }

        pub async fn receive_item_created(&self, dbus_collection: &Collection<'static>) {
            let stream = dbus_collection.receive_item_created().await;
            stream.handle_error("Couldn't listen for item creation");

            if let Ok(stream) = stream {
                futures_lite::pin!(stream);
                while let Some(dbus_item) = stream.next().await {
                    self.add_item(dbus_item)
                        .await
                        .handle_error("Unable to add item");
                }
            }
        }

        pub async fn receive_item_changed(&self, dbus_collection: &Collection<'static>) {
            let stream = dbus_collection.receive_item_changed().await;
            stream.handle_error("Couldn't listen for item changes");

            if let Ok(stream) = stream {
                futures_lite::pin!(stream);
                while let Some(dbus_item) = stream.next().await {
                    let item = self
                        .map
                        .borrow()
                        .get(&dbus_item.path().to_string())
                        .cloned();

                    match item {
                        Some(item) => item
                            .update(dbus_item.into())
                            .await
                            .handle_error("Unable to update item"),
                        None => self
                            .add_item(dbus_item)
                            .await
                            .handle_error("Unable to add item"),
                    }

                    self.regular_filter.changed(gtk::FilterChange::Different);
                    self.flatpak_filter.changed(gtk::FilterChange::Different);
                }
            }
        }

        pub async fn receive_item_deleted(&self, dbus_collection: &Collection<'static>) {
            let stream = dbus_collection.receive_item_deleted().await;
            stream.handle_error("Couldn't listen for item deletions");

            if let Ok(stream) = stream {
                futures_lite::pin!(stream);
                while let Some(path) = stream.next().await {
                    let res = { self.map.borrow_mut().shift_remove_full(&path.to_string()) };

                    match res {
                        Some((pos, _, _)) => {
                            self.obj().items_changed(pos.try_into().unwrap(), 1, 0)
                        }
                        None => eprintln!("Item not found in collection"),
                    }
                }
            }
        }

        pub async fn add_item(&self, dbus_item: Item<'static>) -> Result<(), oo7::Error> {
            let path = dbus_item.path().to_string();
            let item = KrItem::new(dbus_item.into()).await?;

            let pos = self.map.borrow_mut().insert_full(path, item);
            self.obj().items_changed(pos.0.try_into().unwrap(), 0, 1);
            Ok(())
        }
    }
}

glib::wrapper! {
    pub struct KrCollection(ObjectSubclass<imp::KrCollection>)
        @implements gio::ListModel;
}

impl KrCollection {
    pub async fn new(dbus_collection: Collection<'static>) -> Result<Self, oo7::Error> {
        let obj: Self = glib::Object::new();
        let dbus_collection = Rc::new(dbus_collection);

        let items = dbus_collection.items().await?;
        for item in items {
            obj.imp().add_item(item).await?;
        }

        let ctx = MainContext::default();
        let imp = obj.imp();
        ctx.spawn_local(clone!(@weak imp, @weak dbus_collection => async move {
             imp.receive_item_created(&dbus_collection).await;
        }));

        ctx.spawn_local(clone!(@weak imp, @weak dbus_collection => async move {
             imp.receive_item_changed(&dbus_collection).await;
        }));

        ctx.spawn_local(clone!(@weak imp, @weak dbus_collection => async move {
             imp.receive_item_deleted(&dbus_collection).await;
        }));

        imp.dbus_collection.set(dbus_collection).unwrap();
        obj.update().await?;
        Ok(obj)
    }

    pub async fn update(&self) -> Result<(), oo7::Error> {
        let imp = self.imp();
        let collection = imp.dbus_collection.get().unwrap();

        *imp.label.borrow_mut() = collection.label().await?;
        self.notify("label");

        imp.is_locked.set(collection.is_locked().await?);
        self.notify("is-locked");

        Ok(())
    }

    pub async fn lock(&self) -> Result<(), oo7::dbus::Error> {
        self.imp().dbus_collection.get().unwrap().lock().await
    }

    pub async fn unlock(&self) -> Result<(), oo7::dbus::Error> {
        self.imp().dbus_collection.get().unwrap().unlock().await
    }

    pub async fn delete(&self) -> Result<(), oo7::dbus::Error> {
        self.imp().dbus_collection.get().unwrap().delete().await
    }

    pub async fn add_password_item(
        &self,
        description: &str,
        password: &str,
    ) -> Result<(), oo7::dbus::Error> {
        let mut attr = HashMap::new();
        attr.insert("xdg:schema", "org.gnome.keyring.Note");

        self.imp()
            .dbus_collection
            .get()
            .unwrap()
            .create_item(description, &attr, password, false, "text/plain")
            .await?;

        Ok(())
    }

    pub async fn change_password(&self) -> zbus::Result<()> {
        let connection = Connection::session().await?;

        // Request a new prompt
        let message = connection
            .call_method(
                Some("org.freedesktop.secrets"),
                "/org/freedesktop/secrets",
                Some("org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface"),
                "ChangeWithPrompt",
                self.imp().dbus_collection.get().unwrap().path(),
            )
            .await?;

        let body = message.body();
        let prompt_path: ObjectPath = body.deserialize()?;

        // Activate the requested prompt
        let proxy = Proxy::new(
            &connection,
            "org.freedesktop.secrets",
            prompt_path,
            "org.freedesktop.Secret.Prompt",
        )
        .await?;

        let mut signal_stream = proxy.receive_signal("Completed").await?;

        let _: Option<()> = proxy
            .call_with_flags("Prompt", MethodFlags::NoAutoStart.into(), &(""))
            .await?;

        // Wait for `Completed` signal
        let _ = signal_stream.next().await;
        Ok(())
    }
}
