use std::collections::HashSet;

use conduwuit::{Err, Event, Pdu, Result, is_not_empty, utils::ReadyExt};
use database::{Json, serialize_key};
use futures::StreamExt;
use ruma::{
	OwnedServerName, RoomId, UserId,
	api::federation::membership::RawStrippedState,
	events::{
		AnyStrippedStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType,
		StateEventType,
		direct::DirectEvent,
		room::{
			create::RoomCreateEventContent,
			member::{MembershipState, RoomMemberEventContent},
		},
	},
	serde::Raw,
};
use ruminuwuity::invite_permission_config::FilterLevel;

impl super::Service {
	/// Update current membership data.
	pub async fn update_membership(
		&self,
		room_id: &RoomId,
		user_id: &UserId,
		pdu: &Pdu,
		update_joined_count: bool,
	) -> Result {
		let membership = pdu.get_content::<RoomMemberEventContent>()?;
		let is_local = self.services.globals.user_is_local(user_id);

		if !is_local {
			self.services.users.record_remote_user(user_id);
		}

		match &membership.membership {
			| MembershipState::Join => {
				// Check if the user never joined this room
				if !self.once_joined(user_id, room_id).await {
					// Add the user ID to the join list then
					self.mark_as_once_joined(user_id, room_id);

					// Copy data from the predecessor if the user is local
					if is_local
						&& let Ok(Some(predecessor)) = self
							.services
							.state_accessor
							.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
							.await
							.map(|content: RoomCreateEventContent| content.predecessor)
					{
						// Copy old tags to new room
						if let Ok(tag_event) = self
							.services
							.account_data
							.get_room(
								&predecessor.room_id,
								user_id,
								RoomAccountDataEventType::Tag,
							)
							.await
						{
							self.services
								.account_data
								.update(
									Some(room_id),
									user_id,
									RoomAccountDataEventType::Tag,
									&tag_event,
								)
								.await
								.ok();
						}

						// Copy direct chat flag
						if let Ok(mut direct_event) = self
							.services
							.account_data
							.get_global::<DirectEvent>(
								user_id,
								GlobalAccountDataEventType::Direct,
							)
							.await
						{
							let mut room_ids_updated = false;
							for room_ids in direct_event.content.0.values_mut() {
								if room_ids.iter().any(|r| r == &predecessor.room_id) {
									room_ids.push(room_id.to_owned());
									room_ids_updated = true;
								}
							}

							if room_ids_updated {
								self.services
									.account_data
									.update(
										None,
										user_id,
										GlobalAccountDataEventType::Direct.to_string().into(),
										&serde_json::to_value(&direct_event)
											.expect("to json always works"),
									)
									.await?;
							}
						}
					}
				}

				self.mark_as_joined(user_id, room_id);
			},
			| MembershipState::Invite => {
				let invite_state = if is_local {
					self.services
						.state
						.summary_stripped(pdu, room_id, user_id, false)
						.await
				} else {
					vec![]
				};

				self.mark_as_invited(user_id, room_id, pdu.sender(), invite_state, None)
					.await?;
			},
			| MembershipState::Leave | MembershipState::Ban => {
				self.mark_as_left(user_id, room_id, Some(pdu.clone())).await;
			},
			| _ => {},
		}

		if update_joined_count {
			self.update_joined_count(room_id).await;
		}

		// Kick the target user's sync loop if they're local and this isn't a join to
		// make sure that membership changes like invites or invite rejections get
		// synced
		if is_local && !matches!(membership.membership, MembershipState::Join) {
			self.services.sync.wake(user_id).await;
		}

		Ok(())
	}

	/// Updates membership counts for the target room.
	pub async fn update_joined_count(&self, room_id: &RoomId) {
		let mut joinedcount = 0_u64;
		let mut invitedcount = 0_u64;
		let mut knockedcount = 0_u64;
		let mut joined_servers = HashSet::new();

		self.room_members(room_id)
			.ready_for_each(|joined| {
				joined_servers.insert(joined.server_name().to_owned());
				joinedcount = joinedcount.saturating_add(1);
			})
			.await;

		invitedcount = invitedcount.saturating_add(
			self.room_members_invited(room_id)
				.count()
				.await
				.try_into()
				.unwrap_or(0),
		);

		knockedcount = knockedcount.saturating_add(
			self.room_members_knocked(room_id)
				.count()
				.await
				.try_into()
				.unwrap_or(0),
		);

		self.db.roomid_joinedcount.raw_put(room_id, joinedcount);
		self.db.roomid_invitedcount.raw_put(room_id, invitedcount);
		self.db
			.roomuserid_knockedcount
			.raw_put(room_id, knockedcount);

		self.room_servers(room_id)
			.ready_for_each(|old_joined_server| {
				if joined_servers.remove(&old_joined_server) {
					return;
				}

				// Server not in room anymore
				let roomserver_id = (room_id, old_joined_server.clone());
				let serverroom_id = (old_joined_server, room_id);

				self.db.roomserverids.del(roomserver_id);
				self.db.serverroomids.del(serverroom_id);
			})
			.await;

		// Now only new servers are in joined_servers anymore
		for server in &joined_servers {
			let roomserver_id = (room_id, server);
			let serverroom_id = (server, room_id);

			self.db.roomserverids.put_raw(roomserver_id, []);
			self.db.serverroomids.put_raw(serverroom_id, []);
		}

		self.appservice_in_room_cache.write().remove(room_id);
	}

	/// Direct DB function to directly mark a user as joined. It is not
	/// recommended to use this directly. You most likely should use
	/// `update_membership` instead
	pub fn mark_as_joined(&self, user_id: &UserId, room_id: &RoomId) {
		let userroom_id = (user_id, room_id);
		let userroom_id = serialize_key(userroom_id).expect("failed to serialize userroom_id");

		let roomuser_id = (room_id, user_id);
		let roomuser_id = serialize_key(roomuser_id).expect("failed to serialize roomuser_id");

		self.db.userroomid_joined.insert(&userroom_id, []);
		self.db.roomuserid_joined.insert(&roomuser_id, []);

		self.db.userroomid_invitestate.remove(&userroom_id);
		self.db.roomuserid_invitecount.remove(&roomuser_id);
		self.db.userroomid_invitesender.remove(&userroom_id);

		self.db.userroomid_leftstate.remove(&userroom_id);
		self.db.roomuserid_leftcount.remove(&roomuser_id);

		self.db.userroomid_knockedstate.remove(&userroom_id);
		self.db.roomuserid_knockedcount.remove(&roomuser_id);

		self.db.roomid_inviteviaservers.remove(room_id);
	}

	/// Mark a user as having left a room.
	///
	/// `leave_pdu` represents the m.room.member event which the user sent to
	/// leave the room. If this is None, no event was actually sent, but we
	/// must still behave as if the user is no longer in the room. This may
	/// occur, for example, if the room being left has been server-banned by an
	/// administrator.
	pub async fn mark_as_left(&self, user_id: &UserId, room_id: &RoomId, leave_pdu: Option<Pdu>) {
		let userroom_id = (user_id, room_id);
		let userroom_id = serialize_key(userroom_id).expect("failed to serialize userroom_id");

		let roomuser_id = (room_id, user_id);
		let roomuser_id = serialize_key(roomuser_id).expect("failed to serialize roomuser_id");

		self.db
			.userroomid_leftstate
			.raw_put(&userroom_id, Json(leave_pdu));
		self.db
			.roomuserid_leftcount
			.raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap());

		self.db.userroomid_joined.remove(&userroom_id);
		self.db.roomuserid_joined.remove(&roomuser_id);

		self.db.userroomid_invitestate.remove(&userroom_id);
		self.db.roomuserid_invitecount.remove(&roomuser_id);
		self.db.userroomid_invitesender.remove(&userroom_id);

		self.db.userroomid_knockedstate.remove(&userroom_id);
		self.db.roomuserid_knockedcount.remove(&roomuser_id);

		self.db.roomid_inviteviaservers.remove(room_id);

		if self.services.globals.user_is_local(user_id)
			&& (self.services.config.forget_forced_upon_leave
				|| self.services.metadata.is_banned(room_id).await
				|| self.services.metadata.is_disabled(room_id).await)
		{
			self.forget(room_id, user_id);
		}
	}

	/// Direct DB function to directly mark a user as knocked. It is not
	/// recommended to use this directly. You most likely should use
	/// `update_membership` instead
	pub fn mark_as_knocked(
		&self,
		user_id: &UserId,
		room_id: &RoomId,
		knocked_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
	) {
		let userroom_id = (user_id, room_id);
		let userroom_id = serialize_key(userroom_id).expect("failed to serialize userroom_id");

		let roomuser_id = (room_id, user_id);
		let roomuser_id = serialize_key(roomuser_id).expect("failed to serialize roomuser_id");

		self.db
			.userroomid_knockedstate
			.raw_put(&userroom_id, Json(knocked_state.unwrap_or_default()));
		self.db
			.roomuserid_knockedcount
			.raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap());

		self.db.userroomid_joined.remove(&userroom_id);
		self.db.roomuserid_joined.remove(&roomuser_id);

		self.db.userroomid_invitestate.remove(&userroom_id);
		self.db.roomuserid_invitecount.remove(&roomuser_id);
		self.db.userroomid_invitesender.remove(&userroom_id);

		self.db.userroomid_leftstate.remove(&userroom_id);
		self.db.roomuserid_leftcount.remove(&roomuser_id);

		self.db.roomid_inviteviaservers.remove(room_id);
	}

	/// Makes a user forget a room.
	pub fn forget(&self, room_id: &RoomId, user_id: &UserId) {
		let userroom_id = (user_id, room_id);
		let roomuser_id = (room_id, user_id);

		self.db.userroomid_leftstate.del(userroom_id);
		self.db.roomuserid_leftcount.del(roomuser_id);
	}

	/// Immutably a user as having joined a room.
	fn mark_as_once_joined(&self, user_id: &UserId, room_id: &RoomId) {
		let key = (user_id, room_id);
		self.db.roomuseroncejoinedids.put_raw(key, []);
	}

	/// Marks a user as having been invited to a room, saving the relevant
	/// invite state.
	pub async fn mark_as_invited(
		&self,
		user_id: &UserId,
		room_id: &RoomId,
		sender_user: &UserId,
		last_state: Vec<RawStrippedState>,
		invite_via: Option<Vec<OwnedServerName>>,
	) -> Result<()> {
		// return an error for blocked invites. ignored invites aren't handled here
		// since the recipient's membership should still be changed to `invite`.
		// they're filtered out in the individual /sync handlers
		if matches!(
			self.services
				.users
				.invite_filter_level(sender_user, user_id)
				.await,
			FilterLevel::Block
		) {
			return Err!(Request(InviteBlocked(
				"{user_id} has blocked invites from {sender_user}."
			)));
		}

		let roomuser_id = (room_id, user_id);
		let roomuser_id = serialize_key(roomuser_id).expect("failed to serialize roomuser_id");

		let userroom_id = (user_id, room_id);
		let userroom_id = serialize_key(userroom_id).expect("failed to serialize userroom_id");

		self.db
			.userroomid_invitestate
			.raw_put(&userroom_id, Json(last_state));
		self.db
			.roomuserid_invitecount
			.raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap());
		self.db
			.userroomid_invitesender
			.insert(&userroom_id, sender_user);

		self.db.userroomid_joined.remove(&userroom_id);
		self.db.roomuserid_joined.remove(&roomuser_id);

		self.db.userroomid_leftstate.remove(&userroom_id);
		self.db.roomuserid_leftcount.remove(&roomuser_id);

		self.db.userroomid_knockedstate.remove(&userroom_id);
		self.db.roomuserid_knockedcount.remove(&roomuser_id);

		if let Some(servers) = invite_via.filter(is_not_empty!()) {
			self.add_servers_invite_via(room_id, servers).await;
		}

		Ok(())
	}
}
