use std::{
	net::IpAddr,
	time::{Duration, SystemTime},
};

use conduwuit::{
	Err, Result, debug_error, debug_warn, err, error, info, trace,
	utils::{self, stream::TryIgnore},
	warn,
};
use database::{Deserialized, Json};
use futures::{FutureExt, Stream, StreamExt};
use lettre::Address;
use ruma::{
	MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
	api::client::profile::PropagateTo,
	events::{
		GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
		push_rules::PushRulesEvent, room::message::RoomMessageEventContent,
	},
	profile::ProfileFieldValue,
	push::Ruleset,
};
use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigEvent};

use crate::{
	appservice::RegistrationInfo,
	users::{HashedPassword, UserSuspension, profile::ProfileFieldChange},
};

/// The status of an access token.
pub enum AccessTokenStatus {
	Valid,
	Expired,
}

/// The status of a user account.
#[derive(Clone, Copy)]
pub enum AccountStatus {
	NotFound,
	Active,
	Deactivated,
}

impl AccountStatus {
	#[must_use]
	pub fn is_found(&self) -> bool { !matches!(self, Self::NotFound) }

	#[must_use]
	pub fn is_active(&self) -> bool { matches!(self, Self::Active) }

	pub fn ensure_active(&self) -> Result<()> {
		match self {
			| Self::Active => Ok(()),
			| Self::Deactivated => Err!(Request(UserDeactivated("This account is deactivated."))),
			| Self::NotFound => Err!(Request(NotFound("This account does not exist."))),
		}
	}
}

impl super::Service {
	/// Returns true/false based on whether the recipient/receiving user has
	/// ignored the sender.
	pub async fn user_is_ignored(&self, sender_user: &UserId, recipient_user: &UserId) -> bool {
		self.services
			.account_data
			.get_global(recipient_user, GlobalAccountDataEventType::IgnoredUserList)
			.await
			.is_ok_and(|ignored: IgnoredUserListEvent| {
				ignored
					.content
					.ignored_users
					.keys()
					.any(|blocked_user| blocked_user == sender_user)
			})
	}

	/// Returns the recipient's filter level for an invite from the sender.
	///
	/// If the sender is ignored by the recipient, `Ignore` is returned.
	/// Otherwise, the resulting value depends on their invite blocking or
	/// invite filtering configuration.
	pub async fn invite_filter_level(
		&self,
		sender_user: &UserId,
		recipient_user: &UserId,
	) -> FilterLevel {
		if self.user_is_ignored(sender_user, recipient_user).await {
			FilterLevel::Ignore
		} else {
			let (stable, unstable) = tokio::join!(
				self.services
					.account_data
					.get_global::<InvitePermissionConfigEvent>(
						recipient_user,
						GlobalAccountDataEventType::InvitePermissionConfig
					),
				self.services
					.account_data
					.get_global::<InvitePermissionConfigEvent>(
						recipient_user,
						"org.matrix.msc4155.invite_permission_config".into()
					) // TODO: MSC4155 probably needs upstreaming to ruma at some point
			);
			if stable.is_err() && unstable.is_err() {
				return FilterLevel::Allow;
			}
			stable
				.unwrap_or_else(|_| unstable.unwrap())
				.content
				.user_filter_level(sender_user)
		}
	}

	/// Check if a user is an admin
	#[inline]
	pub async fn is_admin(&self, user_id: &UserId) -> bool {
		self.services.admin.user_is_admin(user_id).await
	}

	/// Create a new shadow user account on this server. Shadow accounts
	/// have no password and cannot be logged into.
	pub async fn create_shadow_account(&self, user_id: &UserId) -> Result<()> {
		assert!(self.services.globals.user_is_local(user_id), "user id must be local");

		if self.status(user_id).await.is_found() {
			return Err!(Request(UserInUse("An account with this user ID already exists.")));
		}

		self.db.userid_password.insert(user_id, "");

		Ok(())
	}

	/// Create a new account for a local human or bot user. If `password` is
	/// None, the account will be a shadow account.
	pub async fn create_local_account(
		&self,
		user_id: &UserId,
		password: Option<HashedPassword>,
		email: Option<Address>,
		client: Option<&IpAddr>,
		device_name: Option<&str>,
	) -> Result<()> {
		self.create_shadow_account(user_id).await?;

		if let Some(password) = password {
			self.convert_to_local_account(user_id, password).await?;
		}

		if let Some(client) = client {
			let notice = if let Some(device_name) = device_name {
				format!(
					"New user \"{user_id}\" registered on this server from IP {client} and \
					 device display name \"{device_name}\".",
				)
			} else {
				format!("New user \"{user_id}\" registered on this server from IP {client}.")
			};
			info!("{notice}");

			if self.services.config.admin_room_notices {
				self.services.admin.notice(&notice).await;
			}
		}

		// Set an initial display name
		{
			let mut displayname = user_id.localpart().to_owned();

			let suffix = &self.services.config.new_user_displayname_suffix;
			if !suffix.is_empty() {
				displayname.push(' ');
				displayname.push_str(suffix);
			}

			self.set_profile_field(
				user_id,
				ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)),
				PropagateTo::None,
			)
			.await
			.expect("should be able to set display name");
		};

		// Set default push rules
		self.services
			.account_data
			.update(
				None,
				user_id,
				GlobalAccountDataEventType::PushRules.to_string().into(),
				&serde_json::to_value(PushRulesEvent::new(
					Ruleset::server_default(user_id).into(),
				))
				.expect("should be able to serialize push rules"),
			)
			.await
			.expect("should be able to update account data");

		// If the user registered with an email, associate it with their account.
		if let Some(email) = email {
			// This may fail if the email is already in use, but we should have already
			// checked that when we sent the validation email, so ignoring the error is
			// acceptable here in the rare case that an email is sniped by another user
			// between the validation email being sent and the account being created.
			let _ = self
				.services
				.threepid
				.associate_localpart_email(user_id.localpart(), &email)
				.await;
		}

		// Attempt to empower the first user and disable first-run mode.
		let was_first_user = self.services.firstrun.empower_first_user(user_id).await;

		// If the registering user was not the first and we're suspending users on
		// register, suspend them.
		if !was_first_user && self.services.config.suspend_on_register {
			// Note that we can still do auto joins for suspended users
			self.suspend_account(user_id, &self.services.globals.server_user)
				.await;

			// And send an @room notice to the admin room, to prompt admins to review the
			// new user and ideally unsuspend them if deemed appropriate.
			if self.services.config.admin_room_notices {
				self.services
					.admin
					.send_loud_message(RoomMessageEventContent::text_plain(format!(
						"User {user_id} has been suspended as they are not the first user on \
						 this server. Please review and unsuspend them if appropriate."
					)))
					.await
					.ok();
			}
		}

		// Autojoin the user to the configured autojoin rooms
		for room in &self.services.config.auto_join_rooms {
			let Ok(room_id) = self.services.alias.resolve(room).await else {
				error!(
					"Failed to resolve room alias to room ID when attempting to auto join \
					 {room}, skipping"
				);
				continue;
			};

			if !self
				.services
				.state_cache
				.server_in_room(self.services.globals.server_name(), &room_id)
				.await
			{
				warn!(
					"Skipping room {room} to automatically join as we have never joined before."
				);
				continue;
			}

			if let Some(room_server_name) = room.server_name() {
				match self
					.services
					.membership
					.join_room(
						user_id,
						&room_id,
						Some("Automatically joining this room upon registration".to_owned()),
						&[
							self.services.globals.server_name().to_owned(),
							room_server_name.to_owned(),
						],
					)
					.boxed()
					.await
				{
					| Err(e) => {
						// don't return this error so we don't fail registrations
						error!(
							"Failed to automatically join room {room} for user {user_id}: {e}"
						);
					},
					| _ => {
						info!("Automatically joined room {room} for user {user_id}");
					},
				}
			}
		}

		info!("Created new user account for {user_id}");

		Ok(())
	}

	pub async fn determine_registration_user_id(
		&self,
		supplied_username: Option<String>,
		email: Option<&Address>,
		appservice_info: Option<&RegistrationInfo>,
	) -> Result<OwnedUserId> {
		const RANDOM_USER_ID_LENGTH: usize = 10;

		let emergency_mode_enabled = self.services.config.emergency_password.is_some();

		let supplied_username = supplied_username.or_else(|| {
			// If the user didn't supply a username but did supply an email, use
			// the email's user part to avoid falling back to a random username
			email.map(|address| address.user().to_owned())
		});

		if let Some(supplied_username) = supplied_username {
			// The user gets to pick their username. Do some validation to make sure it's
			// acceptable.

			// Don't allow registration with forbidden usernames.
			if self
				.services
				.globals
				.forbidden_usernames()
				.is_match(&supplied_username)
				&& !emergency_mode_enabled
			{
				return Err!(Request(Forbidden("Username is forbidden")));
			}

			// Create and validate the user ID
			let user_id = match UserId::parse_with_server_name(
				&supplied_username,
				self.services.globals.server_name(),
			) {
				| Ok(user_id) => user_id,
				| Err(e) => {
					return Err!(Request(InvalidUsername(debug_warn!(
						"Username {supplied_username} is not valid: {e}"
					))));
				},
			};

			if let Err(e) = user_id.validate_strict() {
				// Unless we are in emergency mode, we should follow synapse's behaviour
				// on not allowing things like spaces and UTF-8 characters in
				// usernames
				if !emergency_mode_enabled {
					return Err!(Request(InvalidUsername(debug_warn!(
						"Username {supplied_username} contains disallowed characters or spaces: \
						 {e}"
					))));
				}
			}

			// Don't allow registration with user IDs that aren't local
			if !self.services.globals.user_is_local(&user_id) {
				return Err!(Request(InvalidUsername(
					"Username {supplied_username} is not local to this server"
				)));
			}

			if self.status(&user_id).await.is_found() {
				return Err!(Request(UserInUse("Username is not available.")));
			}

			// Check that the user ID is/is not in an appservice's namespace
			if let Some(appservice_info) = appservice_info {
				if !appservice_info.is_user_match(&user_id) && !emergency_mode_enabled {
					return Err!(Request(Exclusive(
						"Username is not in this appservice's namespace."
					)));
				}
			} else if self
				.services
				.appservice
				.is_exclusive_user_id(&user_id)
				.await && !emergency_mode_enabled
			{
				return Err!(Request(Exclusive("Username is reserved by an appservice.")));
			}

			Ok(user_id)
		} else {
			// The user didn't specify a username. Generate a username for
			// them.

			loop {
				let user_id = UserId::parse_with_server_name(
					utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
					self.services.globals.server_name(),
				)
				.unwrap();

				if !self.status(&user_id).await.is_found() {
					break Ok(user_id);
				}
			}
		}
	}

	/// Deactivates an account, removing all of their device IDs.
	pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
		self.status(user_id).await.ensure_active()?;

		// Remove all associated devices
		self.all_device_ids(user_id)
			.for_each(async |device_id| self.remove_device(user_id, &device_id).await)
			.await;

		// Mark user as deactivated
		self.db.userid_deactivated.insert(user_id, "");

		// TODO: Unhook 3PID
		Ok(())
	}

	/// Suspend account, placing it in a read-only state
	pub async fn suspend_account(&self, user_id: &UserId, suspending_user: &UserId) {
		self.db.userid_suspension.raw_put(
			user_id,
			Json(UserSuspension {
				suspended: true,
				suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
				suspended_by: suspending_user.to_string(),
			}),
		);
	}

	/// Unsuspend account, placing it in a read-write state
	pub async fn unsuspend_account(&self, user_id: &UserId) {
		self.db.userid_suspension.remove(user_id);
	}

	/// Locks an account, preventing it being used until it is unlocked.
	pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) {
		// NOTE: Locking is basically just suspension with a more severe effect,
		// so we'll just re-use the suspension data structure to store the lock state.
		let suspension = self
			.db
			.userid_lock
			.get(user_id)
			.await
			.deserialized::<UserSuspension>()
			.unwrap_or_else(|_| UserSuspension {
				suspended: true,
				suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
				suspended_by: locking_user.to_string(),
			});

		self.db.userid_lock.raw_put(user_id, Json(suspension));
	}

	/// Unlocks an account, allowing the user to log in and use it again.
	pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); }

	/// Check if account is suspended. Returns false if the user does not exist.
	pub async fn is_suspended(&self, user_id: &UserId) -> Result<bool> {
		match self
			.db
			.userid_suspension
			.get(user_id)
			.await
			.deserialized::<UserSuspension>()
		{
			| Ok(s) => Ok(s.suspended),
			| Err(e) =>
				if e.is_not_found() {
					Ok(false)
				} else {
					Err(e)
				},
		}
	}

	/// Returns true if the user is locked. Returns false if the user does not
	/// exist or is not locked.
	pub async fn is_locked(&self, user_id: &UserId) -> Result<bool> {
		match self
			.db
			.userid_lock
			.get(user_id)
			.await
			.deserialized::<UserSuspension>()
		{
			| Ok(s) => Ok(s.suspended),
			| Err(e) =>
				if e.is_not_found() {
					Ok(false)
				} else {
					Err(e)
				},
		}
	}

	/// Disables login for a user, preventing them from creating new devices,
	/// but allows them to continue using their existing sessions unimpeded.
	pub fn disable_login(&self, user_id: &UserId) {
		self.db.userid_logindisabled.insert(user_id, "");
	}

	/// Re-enables login for a user, allowing them to create new devices again.
	pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }

	/// Returns true if the target user's login is disabled.
	pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
		self.db
			.userid_logindisabled
			.exists(user_id.as_str())
			.await
			.is_ok()
	}

	/// Checks the activation status of the provided user ID's account.
	pub async fn status(&self, user_id: &UserId) -> AccountStatus {
		if !self.services.globals.user_is_local(user_id) {
			AccountStatus::NotFound
		} else if self.db.userid_password.exists(user_id).await.is_ok() {
			if self.db.userid_deactivated.exists(user_id).await.is_ok() {
				AccountStatus::Deactivated
			} else {
				AccountStatus::Active
			}
		} else {
			AccountStatus::NotFound
		}
	}

	/// Checks if a user is a shadow.
	pub async fn is_shadow(&self, user_id: &UserId) -> bool {
		self.db
			.userid_password
			.get(user_id)
			.await
			.deserialized::<String>()
			.is_ok_and(|hash| hash.is_empty())
	}

	/// Returns the number of users registered on this server, including
	/// deactivated and shadow users.
	#[inline]
	pub async fn count(&self) -> usize { self.db.userid_password.count().await }

	/// Find out which user an access token belongs to. Will panic if the access
	/// token is empty.
	pub async fn find_from_token(
		&self,
		token: &str,
	) -> Option<(OwnedUserId, OwnedDeviceId, AccessTokenStatus)> {
		assert!(!token.is_empty(), "Empty access token");

		let (user_id, device_id) = self
			.db
			.token_userdeviceid
			.get(token)
			.await
			.deserialized()
			.ok()?;

		// Check if the token has expired
		if let Some(expires) = self
			.db
			.userdeviceid_tokenexpires
			.qry(&(&user_id, &device_id))
			.await
			.deserialized::<u64>()
			.ok()
			.map(Duration::from_secs)
		{
			let expires_at = SystemTime::UNIX_EPOCH
				.checked_add(expires)
				.expect("expiry time should not overflow SystemTime");

			if SystemTime::now() > expires_at {
				return Some((user_id, device_id, AccessTokenStatus::Expired));
			}
		}

		Some((user_id, device_id, AccessTokenStatus::Valid))
	}

	/// Returns an iterator over all local users on this homeserver.
	pub fn stream_local_users(&self) -> impl Stream<Item = OwnedUserId> + Send {
		self.db.userid_password.keys().ignore_err()
	}

	/// Set a user's password.
	pub async fn set_password(&self, user_id: &UserId, password: HashedPassword) -> Result {
		self.status(user_id).await.ensure_active()?;

		self.db.userid_password.insert(user_id, password.0);

		Ok(())
	}

	/// Convert an existing user to an active local account. This will turn
	/// shadow accounts into normal accounts and will clear the deactivation
	/// flag on the user if it is set.
	pub async fn convert_to_local_account(
		&self,
		user_id: &UserId,
		password: HashedPassword,
	) -> Result {
		if !self.status(user_id).await.is_found() {
			return Err!(Request(NotFound("This account does not exist.")));
		}

		self.db.userid_deactivated.remove(user_id);
		self.db.userid_password.insert(user_id, password.0);

		Ok(())
	}

	/// Convert an existing user to a shadow account. This will clear their
	/// password and reactivate them if they are deactivated.
	pub async fn convert_to_shadow_account(&self, user_id: &UserId) -> Result {
		if !self.status(user_id).await.is_found() {
			return Err!(Request(NotFound("This account does not exist.")));
		}

		self.db.userid_deactivated.remove(user_id);
		self.db.userid_password.insert(user_id, "");

		Ok(())
	}

	/// Check a user's password.
	pub async fn check_password(&self, user_id: &UserId, password: &str) -> Result<OwnedUserId> {
		self.status(user_id).await.ensure_active()?;

		let (hash, user_id): (String, OwnedUserId) =
			if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() {
				(hash, user_id.to_owned())
			} else {
				// We also check the lowercased version of the user ID to handle legacy user IDs
				// better
				let lowercase_user_id = UserId::parse(user_id.as_str().to_lowercase()).unwrap();

				let hash = self
					.db
					.userid_password
					.get(lowercase_user_id.as_str())
					.await
					.deserialized()
					.expect("user should exist");

				(hash, lowercase_user_id)
			};

		if hash.is_empty() {
			// Cannot log into shadow users
			return Err!(Request(Forbidden("This user cannot log in with a password.")));
		}

		utils::hash::verify_password(password, &hash)
			.inspect_err(|e| debug_error!("{e}"))
			.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;

		Ok(user_id)
	}

	/// Creates an OpenID token, which can be used to prove that a user has
	/// access to an account (primarily for integrations)
	pub fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result<u64> {
		use std::num::Saturating as Sat;

		let expires_in = self.services.config.openid_token_ttl;
		let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in) * Sat(1000);

		let mut value = expires_at.0.to_be_bytes().to_vec();
		value.extend_from_slice(user_id.as_bytes());

		self.db
			.openidtoken_expiresatuserid
			.insert(token.as_bytes(), value.as_slice());

		Ok(expires_in)
	}

	/// Find out which user an OpenID access token belongs to.
	pub async fn find_from_openid_token(&self, token: &str) -> Result<OwnedUserId> {
		let Ok(value) = self.db.openidtoken_expiresatuserid.get(token).await else {
			return Err!(Request(Unauthorized("OpenID token is unrecognised")));
		};

		let (expires_at_bytes, user_bytes) = value.split_at(0_u64.to_be_bytes().len());
		let expires_at =
			u64::from_be_bytes(expires_at_bytes.try_into().map_err(|e| {
				err!(Database("expires_at in openid_userid is invalid u64. {e}"))
			})?);

		if expires_at < utils::millis_since_unix_epoch() {
			debug_warn!("OpenID token is expired, removing");
			self.db.openidtoken_expiresatuserid.remove(token.as_bytes());

			return Err!(Request(Unauthorized("OpenID token is expired")));
		}

		let user_string = utils::string_from_bytes(user_bytes)
			.map_err(|e| err!(Database("User ID in openid_userid is invalid unicode. {e}")))?;

		OwnedUserId::try_from(user_string)
			.map_err(|e| err!(Database("User ID in openid_userid is invalid. {e}")))
	}

	/// Creates a short-lived login token, which can be used to log in using the
	/// `m.login.token` mechanism.
	pub fn create_login_token(&self, user_id: &UserId, token: &str) -> u64 {
		use std::num::Saturating as Sat;

		let expires_in = self.services.config.login_token_ttl;
		let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in);

		let value = (expires_at.0, user_id);
		self.db.logintoken_expiresatuserid.raw_put(token, value);

		expires_in
	}

	/// Find out which user a login token belongs to.
	/// Removes the token to prevent double-use attacks.
	pub async fn find_from_login_token(&self, token: &str) -> Result<OwnedUserId> {
		let Ok(value) = self.db.logintoken_expiresatuserid.get(token).await else {
			return Err!(Request(Forbidden("Login token is unrecognised")));
		};
		let (expires_at, user_id): (u64, OwnedUserId) = value.deserialized()?;

		if expires_at < utils::millis_since_unix_epoch() {
			trace!(%user_id, ?token, "Removing expired login token");

			self.db.logintoken_expiresatuserid.remove(token);

			return Err!(Request(Forbidden("Login token is expired")));
		}

		self.db.logintoken_expiresatuserid.remove(token);

		Ok(user_id)
	}
}
