use adw::{prelude::*, subclass::prelude::*};
use gtk::{
    gio,
    glib::{self, clone, closure},
    CompositeTemplate,
};
use tracing::warn;

mod member_menu;
mod members_list_view;

use members_list_view::{ExtraLists, MembersListView, MembershipSubpageItem};
use ruma::events::room::power_levels::PowerLevelAction;

use self::member_menu::MemberMenu;
use crate::{
    session::model::{Member, Membership, Room, User, UserActions},
    spawn,
};

mod imp {
    use std::cell::RefCell;

    use glib::subclass::InitializingObject;
    use once_cell::{sync::Lazy, unsync::OnceCell};

    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(
        resource = "/org/gnome/Fractal/ui/session/view/content/room_details/members_page/mod.ui"
    )]
    pub struct MembersPage {
        pub room: glib::WeakRef<Room>,
        #[template_child]
        pub navigation_view: TemplateChild<adw::NavigationView>,
        pub member_menu: OnceCell<MemberMenu>,
        pub can_invite_watch: RefCell<Option<gtk::ExpressionWatch>>,
    }

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

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

            klass.install_action("member.verify", None, move |widget, _, _| {
                if let Some(member) = widget.member_menu().member() {
                    widget.verify_member(member);
                } else {
                    warn!("No member was selected to be verified");
                }
            });

            klass.install_action("members.subpage", Some("u"), move |widget, _, param| {
                let Some(membership) = param.and_then(|variant| variant.get::<Membership>()) else {
                    return;
                };

                let subpage = match membership {
                    Membership::Join => "joined",
                    Membership::Invite => "invited",
                    Membership::Ban => "banned",
                    _ => return,
                };

                widget.imp().navigation_view.push_by_tag(subpage);
            });
        }

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

    impl ObjectImpl for MembersPage {
        fn properties() -> &'static [glib::ParamSpec] {
            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
                vec![
                    glib::ParamSpecObject::builder::<Room>("room")
                        .construct_only()
                        .build(),
                    glib::ParamSpecBoolean::builder("can-invite")
                        .read_only()
                        .build(),
                ]
            });

            PROPERTIES.as_ref()
        }

        fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
            let obj = self.obj();

            match pspec.name() {
                "room" => obj.set_room(value.get().unwrap()),
                _ => unimplemented!(),
            }
        }

        fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
            let obj = self.obj();

            match pspec.name() {
                "room" => obj.room().to_value(),
                "can-invite" => obj.can_invite().to_value(),
                _ => unimplemented!(),
            }
        }

        fn dispose(&self) {
            if let Some(watch) = self.can_invite_watch.take() {
                watch.unwatch();
            }
        }
    }

    impl WidgetImpl for MembersPage {}
    impl NavigationPageImpl for MembersPage {}
}

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

impl MembersPage {
    pub fn new(room: &Room) -> Self {
        glib::Object::builder().property("room", room).build()
    }

    /// The room backing all the details of the member page.
    pub fn room(&self) -> Option<Room> {
        self.imp().room.upgrade()
    }

    /// Set the room backing all the details of the member page.
    fn set_room(&self, room: &Room) {
        let imp = self.imp();

        if let Some(watch) = imp.can_invite_watch.take() {
            watch.unwatch();
        }

        self.init_members_list(room);
        self.init_can_invite(room);

        imp.room.set(Some(room));
        self.notify("room");
    }

    fn init_members_list(&self, room: &Room) {
        let imp = self.imp();

        // Sort the members list by power level, then display name.
        let sorter = gtk::MultiSorter::new();
        sorter.append(
            gtk::NumericSorter::builder()
                .expression(Member::this_expression("power-level"))
                .sort_order(gtk::SortType::Descending)
                .build(),
        );

        sorter.append(gtk::StringSorter::new(Some(Member::this_expression(
            "display-name",
        ))));

        // We should have a strong reference to the list in the main page so we can use
        // `get_or_create_members()`.
        let members = room.get_or_create_members();
        let sorted_members = gtk::SortListModel::new(Some(members.clone()), Some(sorter));

        let joined_members = self.build_filtered_list(sorted_members.clone(), Membership::Join);
        let invited_members = self.build_filtered_list(sorted_members.clone(), Membership::Invite);
        let banned_members = self.build_filtered_list(sorted_members, Membership::Ban);

        let extra_list = ExtraLists::new(
            &members,
            &MembershipSubpageItem::new(Membership::Invite, &invited_members),
            &MembershipSubpageItem::new(Membership::Ban, &banned_members),
        );
        let model_list = gio::ListStore::builder()
            .item_type(gio::ListModel::static_type())
            .build();
        model_list.append(&extra_list);
        model_list.append(&joined_members);

        let main_list = gtk::FlattenListModel::new(Some(model_list));

        let joined_view = MembersListView::new(&main_list, Membership::Join);
        self.bind_property("can-invite", &joined_view, "can-invite")
            .sync_create()
            .build();
        imp.navigation_view.add(&joined_view);
        let invited_view = MembersListView::new(&invited_members, Membership::Invite);
        self.bind_property("can-invite", &invited_view, "can-invite")
            .sync_create()
            .build();
        imp.navigation_view.add(&invited_view);
        let banned_view = MembersListView::new(&banned_members, Membership::Ban);
        self.bind_property("can-invite", &banned_view, "can-invite")
            .sync_create()
            .build();
        imp.navigation_view.add(&banned_view);
    }

    /// The object holding information needed for the menu of each `MemberRow`.
    pub fn member_menu(&self) -> &MemberMenu {
        self.imp().member_menu.get_or_init(|| {
            let menu = MemberMenu::new();

            menu.connect_notify_local(
                Some("allowed-actions"),
                clone!(@weak self as obj => move |menu, _| {
                    obj.update_actions(menu.allowed_actions());
                }),
            );
            self.update_actions(menu.allowed_actions());
            menu
        })
    }

    fn update_actions(&self, allowed_actions: UserActions) {
        self.action_set_enabled(
            "member.verify",
            allowed_actions.contains(UserActions::VERIFY),
        );
    }

    fn verify_member(&self, member: Member) {
        // TODO: show the verification immediately when started
        spawn!(clone!(@weak self as obj => async move {
            member.upcast::<User>().verify_identity().await;
        }));
    }

    fn build_filtered_list(
        &self,
        model: impl IsA<gio::ListModel>,
        state: Membership,
    ) -> gio::ListModel {
        let membership_expression = Member::this_expression("membership").chain_closure::<bool>(
            closure!(|_: Option<glib::Object>, this_state: Membership| this_state == state),
        );

        let membership_filter = gtk::BoolFilter::new(Some(&membership_expression));

        let filter_model = gtk::FilterListModel::new(Some(model), Some(membership_filter));
        filter_model.upcast()
    }

    fn init_can_invite(&self, room: &Room) {
        let can_invite = room.own_user_is_allowed_to_expr(PowerLevelAction::Invite);

        let watch = can_invite.watch(
            glib::Object::NONE,
            clone!(@weak self as obj => move || {
                obj.notify("can-invite");
            }),
        );

        self.imp().can_invite_watch.replace(Some(watch));
        self.notify("can-invite");
    }

    /// Whether our own user can send an invite in the current room.
    pub fn can_invite(&self) -> bool {
        self.imp()
            .can_invite_watch
            .borrow()
            .as_ref()
            .is_some_and(|w| {
                w.evaluate_as::<bool>()
                    .expect("Created expression is valid and a boolean")
            })
    }
}
