use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{self, glib, glib::clone, CompositeTemplate};
use ruma::api::client::session::get_login_types::v3::LoginType;
use tracing::warn;

use super::{idp_button::IdpButton, Login};
use crate::{
    components::LoadingButton, gettext_f, prelude::*, spawn_tokio, toast, utils::BoundObjectWeakRef,
};

mod imp {
    use glib::subclass::InitializingObject;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
    #[template(resource = "/org/gnome/Fractal/ui/login/method_page.ui")]
    #[properties(wrapper_type = super::LoginMethodPage)]
    pub struct LoginMethodPage {
        #[template_child]
        pub title: TemplateChild<gtk::Label>,
        #[template_child]
        pub username_entry: TemplateChild<adw::EntryRow>,
        #[template_child]
        pub password_entry: TemplateChild<adw::PasswordEntryRow>,
        #[template_child]
        pub sso_idp_box: TemplateChild<gtk::Box>,
        #[template_child]
        pub more_sso_btn: TemplateChild<gtk::Button>,
        #[template_child]
        pub next_button: TemplateChild<LoadingButton>,
        /// The parent `Login` object.
        #[property(get, set = Self::set_login, nullable)]
        pub login: BoundObjectWeakRef<Login>,
    }

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

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

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

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

    impl WidgetImpl for LoginMethodPage {
        fn grab_focus(&self) -> bool {
            self.username_entry.grab_focus()
        }
    }

    impl NavigationPageImpl for LoginMethodPage {
        fn shown(&self) {
            self.grab_focus();
        }
    }

    impl LoginMethodPage {
        /// Set the parent `Login` object.
        fn set_login(&self, login: Option<&Login>) {
            let obj = self.obj();

            self.login.disconnect_signals();

            if let Some(login) = login {
                let domain_handler = login.connect_domain_notify(clone!(
                    #[weak]
                    obj,
                    move |_| {
                        obj.update_domain_name();
                    }
                ));
                let login_types_handler = login.connect_login_types_notify(clone!(
                    #[weak]
                    obj,
                    move |_| {
                        obj.update_sso();
                    }
                ));

                self.login
                    .set(login, vec![domain_handler, login_types_handler]);
            }

            obj.update_domain_name();
            obj.update_sso();
            obj.update_next_state();
        }
    }
}

glib::wrapper! {
    /// The login page allowing to login via password or to choose a SSO provider.
    pub struct LoginMethodPage(ObjectSubclass<imp::LoginMethodPage>)
        @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible;
}

#[gtk::template_callbacks]
impl LoginMethodPage {
    pub fn new() -> Self {
        glib::Object::new()
    }

    /// The username entered by the user.
    pub fn username(&self) -> String {
        self.imp().username_entry.text().into()
    }

    /// The password entered by the user.
    pub fn password(&self) -> String {
        self.imp().password_entry.text().into()
    }

    /// Update the domain name displayed in the title.
    pub fn update_domain_name(&self) {
        let Some(login) = self.login() else {
            return;
        };

        let title = &self.imp().title;
        if let Some(domain) = login.domain() {
            title.set_markup(&gettext_f(
                // Translators: Do NOT translate the content between '{' and '}', this is a
                // variable name.
                "Log in to {domain_name}",
                &[(
                    "domain_name",
                    &format!("<span segment=\"word\">{domain}</span>"),
                )],
            ))
        } else {
            title.set_markup(&gettext("Log in"));
        }
    }

    /// Update the SSO group.
    pub fn update_sso(&self) {
        let Some(login) = self.login() else {
            return;
        };
        let imp = self.imp();

        let login_types = login.login_types().0;
        let sso_login = match login_types.into_iter().find_map(|t| match t {
            LoginType::Sso(sso) => Some(sso),
            _ => None,
        }) {
            Some(sso) => sso,
            None => {
                imp.sso_idp_box.set_visible(false);
                imp.more_sso_btn.set_visible(false);
                return;
            }
        };

        self.clean_idp_box();

        let mut has_unknown_methods = false;
        let mut has_known_methods = false;

        for provider in &sso_login.identity_providers {
            let btn = IdpButton::new_from_identity_provider(provider);

            if let Some(btn) = btn {
                imp.sso_idp_box.append(&btn);
                has_known_methods = true;
            } else {
                has_unknown_methods = true;
            }
        }

        imp.sso_idp_box.set_visible(has_known_methods);

        if has_known_methods {
            imp.more_sso_btn.set_label(&gettext("More SSO Providers"));
            imp.more_sso_btn.set_visible(has_unknown_methods);
        } else {
            imp.more_sso_btn.set_label(&gettext("Login via SSO"));
            imp.more_sso_btn.set_visible(true);
        }
    }

    /// Whether the current state allows to login with a password.
    pub fn can_login_with_password(&self) -> bool {
        let username_length = self.username().len();
        let password_length = self.password().len();
        username_length != 0 && password_length != 0
    }

    /// Update the state of the "Next" button.
    #[template_callback]
    fn update_next_state(&self) {
        self.imp()
            .next_button
            .set_sensitive(self.can_login_with_password());
    }

    /// Login with the password login type.
    #[template_callback]
    async fn login_with_password(&self) {
        if !self.can_login_with_password() {
            return;
        }

        let Some(login) = self.login() else {
            return;
        };
        let imp = self.imp();

        imp.next_button.set_is_loading(true);
        login.freeze();

        let username = self.username();
        let password = self.password();

        let client = login.client().await.unwrap();
        let handle = spawn_tokio!(async move {
            client
                .matrix_auth()
                .login_username(&username, &password)
                .initial_device_display_name("Fractal")
                .send()
                .await
        });

        match handle.await.unwrap() {
            Ok(response) => {
                login.handle_login_response(response).await;
            }
            Err(error) => {
                warn!("Could not log in: {error}");
                toast!(self, error.to_user_facing());
            }
        }

        imp.next_button.set_is_loading(false);
        login.unfreeze();
    }

    /// Reset this page.
    pub fn clean(&self) {
        let imp = self.imp();
        imp.username_entry.set_text("");
        imp.password_entry.set_text("");
        imp.next_button.set_is_loading(false);
        self.update_next_state();
        self.clean_idp_box();
    }

    /// Empty the identity providers box.
    pub fn clean_idp_box(&self) {
        let imp = self.imp();

        let mut child = imp.sso_idp_box.first_child();
        while child.is_some() {
            imp.sso_idp_box.remove(&child.unwrap());
            child = imp.sso_idp_box.first_child();
        }
    }
}
