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

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

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

mod imp {
    use super::*;

    #[derive(Debug, Default, Properties)]
    #[properties(wrapper_type=super::KrFlatpak)]
    pub struct KrFlatpak {
        #[property(get, construct_only)]
        item: OnceCell<KrItem>,
        #[property(get, construct_only)]
        app_id: OnceCell<String>,
        #[property(get=Self::icon_name)]
        icon_name: PhantomData<String>,
        #[property(get=Self::title)]
        title: PhantomData<String>,
        #[property(get=Self::description)]
        description: PhantomData<String>,
        #[property(get=Self::is_installed)]
        is_installed: PhantomData<bool>,
        #[property(get)]
        pub item_count: Cell<u32>,

        pub keyring: RefCell<Option<Rc<oo7::portal::Keyring>>>,
        monitor: OnceCell<gio::FileMonitor>,
        pub app_info: OnceCell<utils::AppInfo>,

        pub vec: RefCell<Vec<KrItem>>,
    }

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

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

            self.app_info
                .set(crate::utils::AppInfo::new(&self.obj().app_id()))
                .unwrap();

            // Reload the keyring file when the underlying KrItem is
            // updated to ensure the items can be loaded.
            //
            // This is necessary if the secret of a KrItem wasn't available yet,
            // for example because the login keychain wasn't unlocked.
            self.obj().item().connect_local(
                "updated",
                false,
                clone!(@weak self as imp => @default-panic, move |_| {
                    glib::MainContext::default().spawn_local(clone!(@weak imp => async move {
                        imp.obj().load_keyring().await.handle_error("Unable to reload keyring");
                    }));
                    None
                }),
            );

            let keyring_file = gio::File::for_path(self.keyring_path());
            let monitor = keyring_file.monitor(gio::FileMonitorFlags::NONE, gio::Cancellable::NONE);

            match monitor {
                Ok(monitor) => {
                    monitor.set_rate_limit(1000);
                    monitor.connect_changed(clone!(@weak self as imp => move |_, _, _, e| {
                        if e == gio::FileMonitorEvent::ChangesDoneHint {
                            glib::MainContext::default().spawn_local(clone!(@weak imp => async move {
                                imp.obj().load_keyring().await.handle_error("Unable to reload keyring");

                                // Only reload keyring items if they were previously loaded
                                if imp.n_items() != 0 {
                                    imp.obj().load_items().await.handle_error("Unable to reload items");
                                }
                            }));
                        }
                    }));
                    self.monitor.set(monitor).unwrap();
                }
                Err(_) => monitor.handle_error("Unable to setup file monitor"),
            }
        }
    }

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

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

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

    impl KrFlatpak {
        fn icon_name(&self) -> String {
            if self.obj().is_installed() {
                self.obj().app_id()
            } else {
                "system-component".to_string()
            }
        }

        fn title(&self) -> String {
            let app_info = self.app_info.get().unwrap();

            app_info
                .name
                .clone()
                .unwrap_or_else(|| app_info.app_id.clone())
        }

        fn description(&self) -> String {
            self.app_info.get().unwrap().description.to_string()
        }

        fn is_installed(&self) -> bool {
            self.app_info.get().unwrap().is_installed()
        }

        pub fn index(&self, item: &KrItem) -> Option<usize> {
            self.vec.borrow().iter().position(|i| i == item)
        }

        pub fn clear(&self) {
            let len = {
                let mut vec = self.vec.borrow_mut();
                let len = vec.len();
                vec.clear();
                len
            };

            self.obj().items_changed(0, len.try_into().unwrap(), 0);
        }

        pub fn keyring(&self) -> Option<Rc<oo7::portal::Keyring>> {
            self.keyring.borrow().clone()
        }

        pub fn keyring_path(&self) -> std::path::PathBuf {
            let mut path = glib::home_dir();
            path.push(".var/app");
            path.push(self.obj().app_id());
            path.push("data/keyrings/default.keyring");
            path
        }
    }
}

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

impl KrFlatpak {
    pub async fn load_items(&self) -> Result<(), oo7::Error> {
        let imp = self.imp();

        if let Some(keyring) = imp.keyring() {
            let items: Vec<_> = keyring.items().await;
            let mut vec = Vec::new();

            for item in items.into_iter() {
                let item = KrItem::new(
                    (item.map_err(|_| oo7::portal::Error::NoData)?, self.clone()).into(),
                )
                .await?;
                vec.push(item);
            }

            imp.clear();
            let len = vec.len().try_into().unwrap();
            {
                *imp.vec.borrow_mut() = vec;
            }
            self.items_changed(0, 0, len);

            imp.item_count.set(len);
            self.notify_item_count();
        } else {
            eprintln!("Unable to load items, keyring file isn't loaded");
        }

        Ok(())
    }

    pub async fn load_keyring(&self) -> Result<(), oo7::Error> {
        let imp = self.imp();

        let secret = oo7::portal::Secret::from((*(self.item().secret())).clone());
        let path = self.imp().keyring_path();

        let keyring = Rc::new(oo7::portal::Keyring::load(&path, secret).await?);

        let item_count = keyring.n_items().await.try_into().unwrap();
        imp.item_count.set(item_count);
        self.notify_item_count();

        *imp.keyring.borrow_mut() = Some(keyring.clone());

        Ok(())
    }

    pub async fn delete_item(&self, item: &KrItem) -> Result<(), oo7::Error> {
        let imp = self.imp();

        if let Some(index) = imp.index(item) {
            if let Some(keyring) = imp.keyring() {
                keyring.delete_item_index(index).await?;
            } else {
                eprint!("Unable to delete item, keyring file isn't loaded");
            }
        } else {
            eprintln!("Item {} not found in flatpak", item.label());
        }

        self.load_items().await?;
        Ok(())
    }

    pub async fn replace_item(
        &self,
        item: &KrItem,
        portal_item: &oo7::portal::Item,
    ) -> Result<(), oo7::Error> {
        let imp = self.imp();

        if let Some(index) = imp.index(item) {
            if let Some(keyring) = imp.keyring() {
                keyring.replace_item_index(index, portal_item).await?;
            } else {
                eprint!("Unable to replace item, keyring file isn't loaded");
            }
        } else {
            eprintln!("Item {} not found in flatpak", item.label());
        }

        Ok(())
    }
}

impl From<&KrItem> for KrFlatpak {
    fn from(item: &KrItem) -> KrFlatpak {
        glib::Object::builder()
            .property("app-id", item.app_id())
            .property("item", item.clone())
            .build()
    }
}
