use std::cell::OnceCell;

use adw::prelude::*;
use adw::subclass::prelude::*;
use gettextrs::gettext;
use glib::Properties;
use gtk::glib;

use crate::app;
use crate::data::KrItem;
use crate::utils::error::*;
use gtk::CompositeTemplate;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::KrItemDialog)]
    #[template(file = "item_dialog.ui")]
    pub struct KrItemDialog {
        #[template_child]
        description_row: TemplateChild<adw::EntryRow>,
        #[template_child]
        schema_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        credentials_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        secret_plaintext_row: TemplateChild<adw::PasswordEntryRow>,
        #[template_child]
        secret_binary_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        secret_binary_show_togglebutton: TemplateChild<gtk::ToggleButton>,
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
        #[template_child]
        known_attributes_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        unknown_attributes_group: TemplateChild<adw::PreferencesGroup>,
        #[property(get, set, construct_only)]
        item: OnceCell<KrItem>,
    }

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

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

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

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

            // Optional username row
            let username_key = if let Some((key, value)) = item.username_attribute() {
                let row = self.attribute_row(&key, &value);
                row.set_title(&gettext("Username"));
                row.set_tooltip_text(Some(&key));
                self.credentials_group.add(&row);

                // Reorder so that username row is at top
                self.credentials_group
                    .remove(&self.secret_plaintext_row.get());
                self.credentials_group.add(&self.secret_plaintext_row.get());
                self.credentials_group.remove(&self.secret_binary_row.get());
                self.credentials_group.add(&self.secret_binary_row.get());
                key
            } else {
                String::new()
            };

            // Secret
            match String::from_utf8(item.secret().to_vec()) {
                Ok(plaintext) if !plaintext.contains('\0') => {
                    self.secret_plaintext_row.set_visible(true);
                    self.secret_binary_row.set_visible(false);
                    self.secret_plaintext_row
                        .set_text(&String::from_utf8_lossy(&item.secret()))
                }
                _ => {
                    self.secret_binary_row.set_visible(true);
                    self.secret_plaintext_row.set_visible(false);
                    self.hide_binary_secret();
                }
            }

            // Description
            self.description_row.set_text(&item.label());

            // Type (xdg:schema)
            let schema = item.schema_description();
            if !schema.is_empty() {
                self.schema_row.set_visible(true);
                self.schema_row.set_subtitle(&schema);
            }

            // Load item attributes
            for (key, value) in item.attributes() {
                if key == username_key {
                    continue;
                }
                if key == "xdg:schema" {
                    self.schema_row.set_tooltip_text(Some(&value));
                    continue;
                }

                self.add_attribute(&key, &value);
            }
        }
    }

    impl WidgetImpl for KrItemDialog {}

    impl AdwDialogImpl for KrItemDialog {}

    #[gtk::template_callbacks]
    impl KrItemDialog {
        fn add_attribute(&self, key: &str, value: &str) {
            let name = match key {
                "application" => gettext("Application"),
                "connection-uuid" => gettext("Connection UUID"),
                "db-path" => gettext("Database Path"),
                "device-id" => gettext("Device ID"),
                "domain" => gettext("Domain"),
                "explanation" => gettext("Explanation"),
                "goa-identity" => gettext("Online Account Identity"),
                "homeserver" => gettext("Homserver"),
                "id" => gettext("ID"),
                "keyring" => gettext("Keyring"),
                "profile" => gettext("Profile"),
                "protocol" => gettext("Protocol"),
                "repo-id" => gettext("Repository ID"),
                "server" => gettext("Server"),
                "setting-key" => gettext("Setting Key"),
                "setting-name" => gettext("Setting Name"),
                "uri" => gettext("URI"),
                "uuid" => gettext("UUID"),
                "version" => gettext("Version"),
                _ => "".into(),
            };

            if name.is_empty() {
                let row = self.attribute_row(key, value);
                self.unknown_attributes_group.add(&row);
                self.unknown_attributes_group.set_visible(true);
            } else {
                let row = self.attribute_row(&name, value);
                row.set_tooltip_text(Some(key));
                self.known_attributes_group.add(&row);
                self.known_attributes_group.set_visible(true);
            }
        }

        fn attribute_row(&self, key: &str, value: &str) -> adw::ActionRow {
            let copy = gtk::Button::builder()
                .icon_name("edit-copy-symbolic")
                .tooltip_text(gettext("Copy Property"))
                .valign(gtk::Align::Center)
                .build();

            copy.add_css_class("flat");
            let key = key.to_string();
            let value = value.to_string();

            copy.connect_clicked(glib::clone!(@weak self as this, @strong key => move |_| {
                app().window().clipboard().set_text(
                    this.obj().item().attributes().get(key.as_str()).unwrap()
                );
                let toast = adw::Toast::builder().title(gettext!("Copied “{}”", key)).timeout(2).build();
                this.obj().toast_overlay().add_toast(toast);
            }));

            let row = adw::ActionRow::builder()
                .use_markup(false)
                .title(key)
                .subtitle(value)
                .build();

            row.add_suffix(&copy);
            row.set_activatable_widget(Some(&copy));
            row.add_css_class("property");
            row
        }

        fn show_binary_secret(&self) {
            let hex = self
                .obj()
                .item()
                .secret()
                .iter()
                .enumerate()
                .map(|(n, val)| {
                    let space = if (n + 1) % 2 == 0 { " " } else { "" };
                    format!("{val:02x}{space}")
                })
                .collect::<Vec<_>>()
                .join("");
            self.secret_binary_row.set_subtitle(hex.trim());
            self.secret_binary_row.add_css_class("monospace");
            self.secret_binary_row.set_subtitle_selectable(true);
        }

        pub fn hide_binary_secret(&self) {
            self.secret_binary_row.set_subtitle("●●●●●");
            self.secret_binary_row.remove_css_class("monospace");
            self.secret_binary_row.set_subtitle_selectable(false);
        }

        #[template_callback]
        async fn on_label_apply(&self, label_entry: adw::EntryRow) {
            self.obj()
                .item()
                .set_label(&label_entry.text())
                .await
                .handle_error_in("Could not change label_entry", &*self.obj());
        }

        #[template_callback]
        fn on_toggle_binary_secret(&self) {
            if self.secret_binary_show_togglebutton.is_active() {
                self.show_binary_secret();
            } else {
                self.hide_binary_secret();
            }
        }

        #[template_callback]
        fn on_copy_secret(&self) {
            let bytes = self.obj().item().secret();
            match &std::str::from_utf8(&bytes) {
                Ok(plaintext) if !plaintext.contains('\0') => {
                    self.obj().clipboard().set_text(plaintext);
                }
                _ => {
                    let content = gtk::gdk::ContentProvider::for_bytes(
                        "application/octet-stream",
                        &glib::Bytes::from(&bytes.as_ref()),
                    );
                    self.obj()
                        .clipboard()
                        .set_content(Some(&content))
                        .handle_error_in("Failed to copy to Clipboard", &*self.obj());
                }
            }

            let toast = adw::Toast::builder()
                .title("Copied secret")
                .timeout(2)
                .build();
            self.obj().toast_overlay().add_toast(toast);
        }

        #[template_callback]
        async fn on_secret_apply(&self, secret: adw::EntryRow) {
            self.obj()
                .item()
                .set_secret(&secret.text(), "text/plain")
                .await
                .handle_error_in("Could not change secret", &*self.obj());
        }

        #[template_callback]
        async fn on_delete(&self) {
            let dialog = adw::AlertDialog::builder()
                .heading(gettext("Delete Key?"))
                .body(gettext("Delete this key from the app?"))
                .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(&*self.obj()).await != "delete" {
                return;
            }

            let result = self.obj().item().delete().await;
            result.handle_error_in("Could not delete key", &*self.obj());

            if result.is_ok() {
                self.obj().close();
            }
        }
    }
}

glib::wrapper! {
    pub struct KrItemDialog(ObjectSubclass<imp::KrItemDialog>)
        @extends gtk::Widget, adw::Dialog;
}

impl KrItemDialog {
    pub fn new(item: KrItem) -> Self {
        glib::Object::builder().property("item", item).build()
    }
}

impl ToastWindow for KrItemDialog {
    fn toast_overlay(&self) -> adw::ToastOverlay {
        self.imp().toast_overlay.clone()
    }
}
