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

use adw::prelude::*;
use gettextrs::gettext;
use glib::subclass::prelude::*;
use glib::subclass::Signal;
use gtk::glib::{self, WeakRef};
use once_cell::sync::Lazy;
use zeroize::Zeroizing;

use super::KrFlatpak;

mod imp {
    use super::*;

    #[derive(Debug, Default, glib::Properties)]
    #[properties(wrapper_type=super::KrItem)]
    pub struct KrItem {
        #[property(get)]
        #[property(get=Self::title, name="title")]
        #[property(get=Self::subtitle, name="subtitle")]
        #[property(get=Self::app_id, name="app-id", type=Option<String>)]
        #[property(get=Self::schema_description, name="schema-description")]
        #[property(get=Self::icon_name, name="icon-name")]
        pub label: RefCell<String>,
        #[property(get)]
        pub is_locked: Cell<bool>,

        pub item: RefCell<Option<Rc<Item>>>,
        pub secret: RefCell<Zeroizing<Vec<u8>>>,
        pub attributes: RefCell<HashMap<String, String>>,
        pub schema: RefCell<ItemSchema>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for KrItem {
        const NAME: &'static str = "KrItem";
        type Type = super::KrItem;
        type ParentType = glib::Object;
    }

    #[glib::derived_properties]
    impl ObjectImpl for KrItem {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> =
                Lazy::new(|| vec![Signal::builder("updated").build()]);
            SIGNALS.as_ref()
        }
    }

    impl KrItem {
        pub fn title(&self) -> String {
            if self.obj().is_locked() && self.obj().label().is_empty() {
                gettext("Locked element")
            } else {
                self.obj().label()
            }
        }

        pub fn subtitle(&self) -> String {
            if let Some(username) = self.obj().username_attribute() {
                if !self.schema_description().is_empty() {
                    format!("{} ({})", self.schema_description(), username.1)
                } else {
                    username.1
                }
            } else {
                self.schema_description()
            }
        }

        pub fn app_id(&self) -> Option<String> {
            self.obj()
                .attributes()
                .get("app_id")
                .cloned()
                .and_then(|app_id| {
                    if app_id.is_empty() {
                        None
                    } else {
                        Some(app_id)
                    }
                })
        }

        pub fn schema_description(&self) -> String {
            self.schema.borrow().description()
        }

        pub fn icon_name(&self) -> String {
            self.schema.borrow().icon_name()
        }
    }
}

glib::wrapper! {
    pub struct KrItem(ObjectSubclass<imp::KrItem>);
}

impl KrItem {
    pub async fn new(item: Item) -> Result<Self, oo7::Error> {
        let obj: Self = glib::Object::new();
        obj.update(item).await?;
        Ok(obj)
    }

    pub async fn update(&self, item: Item) -> Result<(), oo7::Error> {
        let is_locked = match &item {
            Item::Collection(item) => item.is_locked().await?,
            Item::Flatpak(_, _) => false,
        };
        self.imp().is_locked.set(is_locked);
        self.notify_is_locked();

        let label = match &item {
            Item::Collection(item) => item.label().await?,
            Item::Flatpak(item, _) => item.label().to_string(),
        };
        self.imp().label.replace(label);
        self.notify_label();
        self.notify_title();

        let attributes: HashMap<String, String> = match &item {
            Item::Collection(item) => item
                .attributes()
                .await?
                .iter()
                .map(|(k, v)| (k.clone(), v.clone()))
                .collect(),
            Item::Flatpak(item, _) => item
                .attributes()
                .iter()
                .map(|(k, v)| (k.clone(), v.to_string()))
                .collect(),
        };

        let schema = if let Some(schema) = attributes.get("xdg:schema") {
            ItemSchema::from(schema.as_str())
        } else {
            ItemSchema::None
        };
        *self.imp().schema.borrow_mut() = schema;

        self.imp().attributes.replace(attributes);
        self.notify_subtitle();
        self.notify_icon_name();
        self.notify_app_id();

        if !is_locked {
            let secret = match &item {
                Item::Collection(item) => item.secret().await?,
                Item::Flatpak(item, _) => item.secret(),
            };
            self.imp().secret.replace(secret);
        } else {
            self.imp().secret.replace(Vec::new().into());
        }

        self.imp().item.replace(Some(Rc::new(item)));
        self.emit_by_name::<()>("updated", &[]);

        Ok(())
    }

    pub async fn set_label(&self, label: &str) -> Result<(), oo7::Error> {
        let item = { self.imp().item.borrow().as_ref().unwrap().clone() };

        match &*item {
            Item::Collection(item) => item.set_label(label).await?,
            Item::Flatpak(item, flatpak) => {
                let mut item = item.clone();
                item.set_label(label);
                flatpak.upgrade().unwrap().replace_item(self, &item).await?;
            }
        };

        *self.imp().label.borrow_mut() = label.to_string();
        self.notify_label();
        self.notify_title();

        Ok(())
    }

    pub fn secret(&self) -> Zeroizing<Vec<u8>> {
        self.imp().secret.borrow().clone()
    }

    pub async fn set_secret(
        &self,
        secret: impl AsRef<[u8]>,
        content_type: &str,
    ) -> Result<(), oo7::Error> {
        let item = { self.imp().item.borrow().as_ref().unwrap().clone() };

        match &*item {
            Item::Collection(item) => item.set_secret(secret, content_type).await?,
            Item::Flatpak(item, flatpak) => {
                let mut item = item.clone();
                item.set_secret(secret);
                flatpak.upgrade().unwrap().replace_item(self, &item).await?;
            }
        };

        Ok(())
    }

    pub async fn delete(&self) -> Result<(), oo7::Error> {
        let item = { self.imp().item.borrow().as_ref().unwrap().clone() };

        match &*item {
            Item::Collection(item) => item.delete().await?,
            Item::Flatpak(_, flatpak) => flatpak.upgrade().unwrap().delete_item(self).await?,
        };

        Ok(())
    }

    pub fn attributes(&self) -> HashMap<String, String> {
        self.imp().attributes.borrow().clone()
    }

    pub fn username_attribute(&self) -> Option<(String, String)> {
        for (key, value) in self.attributes() {
            if [
                "name",
                "user",
                "username",
                "account",
                "accountname",
                "login",
                "loginname",
            ]
            .contains(&key.as_str())
            {
                return Some((key, value));
            }
        }

        None
    }
}

#[derive(Debug)]
pub enum Item {
    Collection(oo7::dbus::Item<'static>),
    Flatpak(oo7::portal::Item, WeakRef<KrFlatpak>),
}

impl From<oo7::dbus::Item<'static>> for Item {
    fn from(value: oo7::dbus::Item<'static>) -> Self {
        Self::Collection(value)
    }
}

impl From<(oo7::portal::Item, KrFlatpak)> for Item {
    fn from(value: (oo7::portal::Item, KrFlatpak)) -> Self {
        Self::Flatpak(value.0, value.1.downgrade())
    }
}

#[derive(Default, Debug, Clone, PartialEq)]
pub enum ItemSchema {
    ChromeCrypt,
    ChromeDummy,
    EpiphanyForm,
    EpiphanySync,
    EvolutionDataSource,
    Generic,
    Gpg,
    GvfsLuks,
    NetworkConnection,
    NetworkPassword,
    Note,
    OnlineAccounts,
    RdpCredentials,
    VncPassword,
    Unknown(String),
    #[default]
    None,
}

impl ItemSchema {
    fn description(&self) -> String {
        match self {
            Self::ChromeCrypt => gettext("Chrome Safe Storage Password"),
            Self::ChromeDummy => gettext("Chrome Dummy Schema"),
            Self::EpiphanyForm => gettext("Saved Web Password"),
            Self::EpiphanySync => gettext("Web Sync Password"),
            Self::EvolutionDataSource => gettext("Evolution Data Source Secret"),
            Self::Generic => gettext("Generic Password"),
            Self::Gpg => gettext("GPG Passphrase"),
            Self::GvfsLuks => gettext("Disk Encryption Password"),
            Self::NetworkConnection => gettext("Network Connection Password"),
            Self::NetworkPassword => gettext("Network Password"),
            Self::Note => gettext("Note"),
            Self::OnlineAccounts => gettext("Online Account Password"),
            Self::RdpCredentials => gettext("RDP Credentials"),
            Self::VncPassword => gettext("VNC Password"),
            Self::Unknown(schema) => {
                let info = crate::utils::AppInfo::new(schema);
                if let Some(name) = info.name {
                    gettext!("{} Secret", name)
                } else {
                    schema.clone()
                }
            }
            Self::None => String::new(),
        }
    }

    fn icon_name(&self) -> String {
        let i = match self {
            Self::ChromeCrypt | Self::ChromeDummy => "channel-secure-symbolic",
            Self::EpiphanyForm | Self::EpiphanySync => "web-browser-symbolic",
            Self::EvolutionDataSource => "mail-unread-symbolic",
            Self::Generic => "dialog-password-symbolic",
            Self::Gpg => "dialog-password-symbolic",
            Self::GvfsLuks => "drive-harddisk-symbolic",
            Self::NetworkConnection => "network-server-symbolic",
            Self::NetworkPassword => "network-transmit-receive-symbolic",
            Self::Note => "dialog-password-symbolic",
            Self::OnlineAccounts => "goa-panel-symbolic",
            Self::RdpCredentials => "screen-shared-symbolic",
            Self::VncPassword => "screen-shared-symbolic",
            Self::Unknown(schema) => {
                let info = crate::utils::AppInfo::new(schema);
                if info.is_installed() {
                    &format!("{schema}-symbolic")
                } else {
                    "dialog-password-symbolic"
                }
            }
            Self::None => "dialog-password-symbolic",
        };
        i.to_string()
    }
}

impl From<&str> for ItemSchema {
    fn from(schema: &str) -> Self {
        match schema {
            "org.epiphany.FormPassword" => Self::EpiphanyForm,
            "org.epiphany.SyncSecrets" => Self::EpiphanySync,
            "org.freedesktop.NetworkManager.Connection" => Self::NetworkConnection,
            "org.freedesktop.NetworkManager" => Self::NetworkConnection,
            "org.freedesktop.Secret.Generic" => Self::Generic,
            "org.gnome.Evolution.Data.Source" => Self::EvolutionDataSource,
            "org.gnome.GVfs.Luks.Password" => Self::GvfsLuks,
            "org.gnome.keyring.NetworkPassword" => Self::NetworkPassword,
            "org.gnome.keyring.Note" => Self::Note,
            "org.gnome.OnlineAccounts" => Self::OnlineAccounts,
            "org.gnome.RemoteDesktop.RdpCredentials" => Self::RdpCredentials,
            "org.gnome.RemoteDesktop.VncPassword" => Self::VncPassword,
            "org.gnupg.Passphrase" => Self::Gpg,
            "chrome_libsecret_os_crypt_password_v2" => Self::ChromeCrypt,
            "_chrome_dummy_schema_for_unlocking" => Self::ChromeDummy,
            _ => Self::Unknown(schema.to_string()),
        }
    }
}
