use axum::extract::State;
use conduwuit::{Err, Result, debug, err};
use futures::StreamExt;
use ruma::{
	MilliSecondsSinceUnixEpoch,
	api::client::device::{
		self, delete_device, delete_devices, get_device, get_devices, update_device,
	},
};

use crate::{Ruma, client_ip::ClientIp};

/// # `GET /_matrix/client/r0/devices`
///
/// Get metadata on all devices of the sender user.
pub(crate) async fn get_devices_route(
	State(services): State<crate::State>,
	body: Ruma<get_devices::v3::Request>,
) -> Result<get_devices::v3::Response> {
	let devices: Vec<device::Device> = services
		.users
		.all_devices_metadata(body.identity.expect_sender_user()?)
		.collect()
		.await;

	Ok(get_devices::v3::Response::new(devices))
}

/// # `GET /_matrix/client/r0/devices/{deviceId}`
///
/// Get metadata on a single device of the sender user.
pub(crate) async fn get_device_route(
	State(services): State<crate::State>,
	body: Ruma<get_device::v3::Request>,
) -> Result<get_device::v3::Response> {
	let device = services
		.users
		.get_device_metadata(body.identity.expect_sender_user()?, &body.body.device_id)
		.await
		.map_err(|_| err!(Request(NotFound("Device not found."))))?;

	Ok(get_device::v3::Response::new(device))
}

/// # `PUT /_matrix/client/r0/devices/{deviceId}`
///
/// Updates the metadata on a given device of the sender user.
#[tracing::instrument(skip_all, name = "update_device", level = "debug")]
pub(crate) async fn update_device_route(
	State(services): State<crate::State>,
	ClientIp(client): ClientIp, // NOTE: Required for updating device metadata
	body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> {
	let sender_user = body.identity.expect_sender_user()?;
	let appservice = body.identity.appservice_info();

	match services
		.users
		.get_device_metadata(sender_user, &body.device_id)
		.await
	{
		| Ok(mut device) => {
			device.display_name.clone_from(&body.display_name);
			device.last_seen_ip.clone_from(&Some(client.to_string()));
			device
				.last_seen_ts
				.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));

			services
				.users
				.update_device_metadata(sender_user, &body.device_id, &device)
				.await?;

			Ok(update_device::v3::Response::new())
		},
		| Err(_) => {
			let Some(appservice) = appservice else {
				return Err!(Request(NotFound("Device not found.")));
			};

			debug!(
				"Creating new device for {sender_user} from appservice {} as device ID does not \
				 exist",
				appservice.registration.id
			);

			services
				.users
				.create_device(
					sender_user,
					Some(body.device_id.clone()),
					None,
					body.display_name.clone(),
					Some(client.to_string()),
				)
				.await?;

			return Ok(update_device::v3::Response::new());
		},
	}
}

/// # `DELETE /_matrix/client/r0/devices/{deviceId}`
///
/// Deletes the given device.
///
/// - Requires UIAA to verify user password
/// - Invalidates access token
/// - Deletes device metadata (device id, device display name, last seen ip,
///   last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
pub(crate) async fn delete_device_route(
	State(services): State<crate::State>,
	body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> {
	let sender_user = body.identity.expect_sender_user()?;

	// Appservices get to skip UIAA for this endpoint
	if !body.identity.is_appservice() {
		let sender_device = body.identity.expect_sender_device()?;

		// Prompt the user to confirm with their password using UIAA
		let _ = services
			.uiaa
			.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
			.await?;
	}

	services
		.users
		.remove_device(sender_user, &body.device_id)
		.await;

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

/// # `POST /_matrix/client/v3/delete_devices`
///
/// Deletes the given list of devices.
///
/// - Requires UIAA to verify user password.
///
/// For each device:
/// - Invalidates access token
/// - Deletes device metadata (device id, device display name, last seen ip,
///   last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
pub(crate) async fn delete_devices_route(
	State(services): State<crate::State>,
	body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> {
	let sender_user = body.identity.expect_sender_user()?;

	// Appservices get to skip UIAA for this endpoint
	if let Some(sender_device) = body.identity.sender_device() {
		// Prompt the user to confirm with their password using UIAA
		let _ = services
			.uiaa
			.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
			.await?;
	}

	for device_id in &body.devices {
		services.users.remove_device(sender_user, device_id).await;
	}

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