use std::cell::{OnceCell, RefCell};
use std::rc::Rc;

use futures_lite::{pin, StreamExt};
use gio::subclass::prelude::*;
use glib::{clone, MainContext, Properties};
use gtk::prelude::*;
use gtk::{gio, glib};
use indexmap::IndexMap;
use oo7::dbus::{Collection, Service};

use super::{KrCollection, KrCollectionKind};
use crate::utils::error::DisplayError;

mod imp {
    use super::*;

    #[derive(Default, Debug, Properties)]
    #[properties(wrapper_type = super::KrSecretService)]
    pub struct KrSecretService {
        #[property(get)]
        login_collection: RefCell<Option<KrCollection>>,
        #[property(get)]
        session_collection: RefCell<Option<KrCollection>>,
        pub dbus_service: OnceCell<Rc<Service<'static>>>,
        map: RefCell<IndexMap<String, KrCollection>>,
    }

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

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

    impl ListModelImpl for KrSecretService {
        fn item_type(&self) -> glib::Type {
            KrCollection::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())
        }
    }

    impl KrSecretService {
        pub async fn add_collection(
            &self,
            dbus_collection: Collection<'static>,
        ) -> Result<(), oo7::Error> {
            let path = dbus_collection.path().to_string();
            let collection = KrCollection::new(dbus_collection).await?;

            if collection.kind() == KrCollectionKind::Login {
                self.login_collection
                    .borrow_mut()
                    .replace(collection.clone());
                self.obj().notify("login-collection");
            }

            if collection.kind() == KrCollectionKind::Session {
                self.session_collection
                    .borrow_mut()
                    .replace(collection.clone());
                self.obj().notify("session-collection");
            }

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

            Ok(())
        }

        pub fn remove_collection(&self, path: String) {
            let res = { self.map.borrow_mut().shift_remove_full(&path) };
            match res {
                Some((pos, _, _)) => self.obj().items_changed(pos.try_into().unwrap(), 1, 0),
                None => eprintln!("Item not found in collection"),
            }
        }

        pub async fn receive_collection_created(&self, dbus_service: &Service<'static>) {
            let stream = dbus_service.receive_collection_created().await;
            stream.handle_error("Couldn't listen for keyring creation");

            if let Ok(stream) = stream {
                pin!(stream);
                while let Some(collection) = stream.next().await {
                    self.add_collection(collection)
                        .await
                        .handle_error("Unable to add keyring");
                }
            }
        }

        pub async fn receive_collection_changed(&self, dbus_service: &Service<'static>) {
            let stream = dbus_service.receive_collection_changed().await;
            stream.handle_error("Couldn't listen for keyring changes");

            if let Ok(stream) = stream {
                pin!(stream);
                while let Some(dbus_collection) = stream.next().await {
                    let collection = self
                        .map
                        .borrow()
                        .get(&dbus_collection.path().to_string())
                        .cloned();
                    if let Some(collection) = &collection {
                        collection
                            .update()
                            .await
                            .handle_error("Unable to update keyring");
                    } else {
                        self.add_collection(dbus_collection)
                            .await
                            .handle_error("Unable to add keyring");
                    }
                }
            }
        }

        pub async fn receive_collection_deleted(&self, dbus_service: &Service<'static>) {
            let stream = dbus_service.receive_collection_deleted().await;
            stream.handle_error("Couldn't listen for keyring deletions");

            if let Ok(stream) = stream {
                pin!(stream);
                while let Some(path) = stream.next().await {
                    self.remove_collection(path.to_string());
                }
            }
        }
    }
}

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

impl KrSecretService {
    pub async fn set_dbus_service(&self, dbus_service: Service<'static>) -> Result<(), oo7::Error> {
        let imp = self.imp();
        let dbus_service = Rc::new(dbus_service);
        let ctx = MainContext::default();

        for collection in dbus_service.collections().await? {
            imp.add_collection(collection).await?;
        }

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

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

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

        imp.dbus_service.set(dbus_service).unwrap();
        Ok(())
    }

    pub async fn create_collection(&self, label: &str) -> Result<(), oo7::Error> {
        if let Some(dbus_service) = self.imp().dbus_service.get() {
            dbus_service.create_collection(label, None).await?;
        } else {
            eprint!("Unable to create collection, secret service not available.");
        }

        Ok(())
    }
}

impl Default for KrSecretService {
    fn default() -> Self {
        glib::Object::new()
    }
}
