use std::collections::BTreeMap;

use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu, utils::to_canonical_object};
use conduwuit_service::Services;
use futures::StreamExt;
use ruma::{
	UserId,
	api::{
		client::profile::{
			delete_profile_field, get_profile, get_profile_field, set_profile_field,
		},
		federation,
	},
	assign,
	events::room::member::{MembershipState, RoomMemberEventContent},
	presence::PresenceState,
	profile::{ProfileFieldName, ProfileFieldValue},
};
use serde_json::{Value, to_value};

use crate::Ruma;

/// # `GET /_matrix/client/v3/profile/{userId}`
///
/// Returns the user's profile information.
///
/// - If user is on another server and we do not have a local copy already,
///   fetch profile over federation.
pub(crate) async fn get_profile_route(
	State(services): State<crate::State>,
	body: Ruma<get_profile::v3::Request>,
) -> Result<get_profile::v3::Response> {
	let Some(profile) = fetch_full_profile(&services, &body.user_id).await else {
		return Err!(Request(NotFound("This user's profile could not be fetched.")));
	};

	Ok(get_profile::v3::Response::from_iter(profile))
}

pub(crate) async fn get_profile_field_route(
	State(services): State<crate::State>,
	body: Ruma<get_profile_field::v3::Request>,
) -> Result<get_profile_field::v3::Response> {
	let value = fetch_profile_field(&services, &body.user_id, body.field.clone()).await?;

	Ok(assign!(get_profile_field::v3::Response::default(), { value }))
}

pub(crate) async fn set_profile_field_route(
	State(services): State<crate::State>,
	body: Ruma<set_profile_field::v3::Request>,
) -> Result<set_profile_field::v3::Response> {
	if body.user_id != body.sender_user()
		&& !(body.appservice_info.is_some()
			|| services.admin.user_is_admin(body.sender_user()).await)
	{
		return Err!(Request(Forbidden("You may not change other users' profile data.")));
	}

	if !services.globals.user_is_local(&body.user_id) {
		return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
	}

	set_profile_field(&services, &body.user_id, ProfileFieldChange::Set(body.value.clone()))
		.await?;

	Ok(set_profile_field::v3::Response::new())
}

pub(crate) async fn delete_profile_field_route(
	State(services): State<crate::State>,
	body: Ruma<delete_profile_field::v3::Request>,
) -> Result<delete_profile_field::v3::Response> {
	if body.user_id != body.sender_user()
		&& !(body.appservice_info.is_some()
			|| services.admin.user_is_admin(body.sender_user()).await)
	{
		return Err!(Request(Forbidden("You may not change other users' profile data.")));
	}

	if !services.globals.user_is_local(&body.user_id) {
		return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
	}

	set_profile_field(&services, &body.user_id, ProfileFieldChange::Delete(body.field.clone()))
		.await?;

	Ok(delete_profile_field::v3::Response::new())
}

async fn fetch_full_profile(
	services: &Services,
	user_id: &UserId,
) -> Option<BTreeMap<String, Value>> {
	// If the user exists locally, fetch their local profile
	if services.users.exists(user_id).await {
		return Some(get_local_profile(services, user_id).await);
	}

	// Otherwise ask their homeserver
	let Ok(response) = services
		.sending
		.send_federation_request(
			user_id.server_name(),
			federation::query::get_profile_information::v1::Request::new(user_id.to_owned()),
		)
		.await
	else {
		return None;
	};

	// Update our local copies of their profile fields
	services.users.clear_profile(user_id).await;

	for (field, value) in response.iter() {
		let Ok(value) = ProfileFieldValue::new(field, value.to_owned()) else {
			// Skip malformed fields
			continue;
		};

		let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value)).await;
	}

	Some(BTreeMap::from_iter(response))
}

async fn fetch_profile_field(
	services: &Services,
	user_id: &UserId,
	field: ProfileFieldName,
) -> Result<Option<ProfileFieldValue>> {
	// If the user exists locally, fetch their local profile field
	if services.globals.user_is_local(user_id) {
		return Ok(get_local_profile_field(services, user_id, field).await);
	}

	// Otherwise ask their homeserver
	let Ok(response) = services
		.sending
		.send_federation_request(
			user_id.server_name(),
			assign!(federation::query::get_profile_information::v1::Request::new(user_id.to_owned()), {
				field: Some(field.clone())
			}),
		)
		.await
	else {
		return Err!(Request(NotFound(
			"User's homeserver could not provide this profile field."
		)));
	};

	if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
		if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
			let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value.clone()))
				.await;

			Ok(Some(value))
		} else {
			Err!(Request(Unknown(
				"User's homeserver returned malformed data for this profile field."
			)))
		}
	} else {
		let _ = set_profile_field(services, user_id, ProfileFieldChange::Delete(field)).await;

		Ok(None)
	}
}

pub(crate) async fn get_local_profile(
	services: &Services,
	user_id: &UserId,
) -> BTreeMap<String, Value> {
	let mut profile = BTreeMap::new();

	// Get displayname and avatar_url independently because `all_profile_keys`
	// doesn't include them
	for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] {
		let key = field.as_str().to_owned();

		if let Some(value) = get_local_profile_field(services, user_id, field).await {
			profile.insert(key, value.value().into_owned());
		}
	}

	// Insert all other profile fields
	let mut all_fields = services.users.all_profile_keys(user_id);

	while let Some((key, value)) = all_fields.next().await {
		profile.insert(key, value);
	}

	profile
}

pub(crate) async fn get_local_profile_field(
	services: &Services,
	user_id: &UserId,
	field: ProfileFieldName,
) -> Option<ProfileFieldValue> {
	let value = match field.clone() {
		| ProfileFieldName::AvatarUrl => services
			.users
			.avatar_url(user_id)
			.await
			.ok()
			.map(to_value)
			.transpose()
			.expect("converting avatar url to value should succeed"),
		| ProfileFieldName::DisplayName => services
			.users
			.displayname(user_id)
			.await
			.ok()
			.map(to_value)
			.transpose()
			.expect("converting displayname to value should succeed"),
		| other => services
			.users
			.profile_key(user_id, other.as_str())
			.await
			.ok(),
	}?;

	Some(
		ProfileFieldValue::new(field.as_str(), value)
			.expect("local profile field should be valid"),
	)
}

enum ProfileFieldChange {
	Set(ProfileFieldValue),
	Delete(ProfileFieldName),
}

impl ProfileFieldChange {
	fn field_name(&self) -> ProfileFieldName {
		match self {
			| &Self::Delete(ref name) => name.clone(),
			| &Self::Set(ref value) => value.field_name(),
		}
	}

	fn value(&self) -> Option<Value> {
		if let Self::Set(value) = self {
			Some(value.value().into_owned())
		} else {
			None
		}
	}
}

async fn set_profile_field(
	services: &Services,
	user_id: &UserId,
	change: ProfileFieldChange,
) -> Result<()> {
	const MAX_KEY_LENGTH_BYTES: usize = 255;
	const MAX_PROFILE_LENGTH_BYTES: usize = 65536;

	let field_name = change.field_name();

	// TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE,
	// M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list
	// of error codes and Ruma doesn't have them. Should we return those, or is
	// M_TOO_LARGE okay?
	if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES {
		return Err!(Request(TooLarge(
			"Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length."
		)));
	}

	// Serialize the entire profile as canonical JSON, including the new change,
	// to check if it exceeds 64 KiB
	{
		let mut full_profile = get_local_profile(services, user_id).await;

		match &change {
			| ProfileFieldChange::Set(value) => {
				full_profile.insert(
					value.field_name().as_str().to_owned(),
					value.value().clone().into_owned(),
				);
			},
			| ProfileFieldChange::Delete(key) => {
				full_profile.remove(key.as_str());
			},
		}

		if let Ok(canonical_profile) = to_canonical_object(full_profile) {
			if serde_json::to_string(&canonical_profile)
				.expect("should be able to serialize to string")
				.len() > MAX_PROFILE_LENGTH_BYTES
			{
				return Err!(
					"Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in length."
				);
			}
		} else {
			return Err!(Request(BadJson("Failed to canonicalize profile.")));
		}
	}

	match change {
		| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
			services
				.users
				.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty()));
		},
		| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
			services
				.users
				.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid()));
		},
		| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
			services.users.set_displayname(user_id, None);
		},
		| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
			services.users.set_avatar_url(user_id, None);
		},
		| other =>
			services
				.users
				.set_profile_key(user_id, other.field_name().as_str(), other.value()),
	}

	// If the user is local and changed their displayname or avatar_url, update it
	// in all their joined rooms
	if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
		&& services.globals.user_is_local(user_id)
	{
		let displayname = services.users.displayname(user_id).await.ok();
		let avatar_url = services.users.avatar_url(user_id).await.ok();
		let membership_content = assign!(
			RoomMemberEventContent::new(MembershipState::Join), { displayname, avatar_url }
		);

		let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);

		while let Some(room_id) = all_joined_rooms.next().await {
			let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;

			let _ = services
				.rooms
				.timeline
				.build_and_append_pdu(
					PartialPdu::state(user_id.to_string(), &membership_content),
					user_id,
					Some(&room_id),
					&state_lock,
				)
				.await;
		}

		if services.config.allow_local_presence {
			// Send a presence EDU to indicate the profile changed
			let _ = services
				.presence
				.ping_presence(user_id, &PresenceState::Online)
				.await;
		}
	}

	Ok(())
}
