use std::borrow::Cow;

use ruma::http_headers::{ContentDisposition, ContentDispositionType};

use crate::debug_info;

/// as defined by MSC2702
const ALLOWED_INLINE_CONTENT_TYPES: [&str; 26] = [
	// keep sorted
	"application/json",
	"application/ld+json",
	"audio/aac",
	"audio/flac",
	"audio/mp4",
	"audio/mpeg",
	"audio/ogg",
	"audio/wav",
	"audio/wave",
	"audio/webm",
	"audio/x-flac",
	"audio/x-pn-wav",
	"audio/x-wav",
	"image/apng",
	"image/avif",
	"image/gif",
	"image/jpeg",
	"image/png",
	"image/webp",
	"text/css",
	"text/csv",
	"text/plain",
	"video/mp4",
	"video/ogg",
	"video/quicktime",
	"video/webm",
];

/// Returns a Content-Disposition of `attachment` or `inline`, depending on the
/// Content-Type against MSC2702 list of safe inline Content-Types
/// (`ALLOWED_INLINE_CONTENT_TYPES`)
#[must_use]
pub fn content_disposition_type(content_type: Option<&str>) -> ContentDispositionType {
	let Some(content_type) = content_type else {
		debug_info!("No Content-Type was given, assuming attachment for Content-Disposition");
		return ContentDispositionType::Attachment;
	};

	debug_assert!(
		ALLOWED_INLINE_CONTENT_TYPES.is_sorted(),
		"ALLOWED_INLINE_CONTENT_TYPES is not sorted"
	);

	let content_type: Cow<'_, str> = content_type
		.split(';')
		.next()
		.unwrap_or(content_type)
		.to_ascii_lowercase()
		.into();

	if ALLOWED_INLINE_CONTENT_TYPES
		.binary_search(&content_type.as_ref())
		.is_ok()
	{
		ContentDispositionType::Inline
	} else {
		ContentDispositionType::Attachment
	}
}

/// sanitises the file name for the Content-Disposition using
/// `sanitize_filename` crate
#[tracing::instrument(level = "debug")]
pub fn sanitise_filename(filename: &str) -> String {
	sanitize_filename::sanitize_with_options(filename, sanitize_filename::Options {
		truncate: false,
		..Default::default()
	})
}

/// creates the final Content-Disposition based on whether the filename exists
/// or not, or if a requested filename was specified (media download with
/// filename)
///
/// if filename exists:
/// `Content-Disposition: attachment/inline; filename=filename.ext`
///
/// else: `Content-Disposition: attachment/inline`
pub fn make_content_disposition(
	content_disposition: Option<&ContentDisposition>,
	content_type: Option<&str>,
	filename: Option<&str>,
) -> ContentDisposition {
	ContentDisposition::new(content_disposition_type(content_type)).with_filename(
		filename
			.or_else(|| {
				content_disposition
					.and_then(|content_disposition| content_disposition.filename.as_deref())
			})
			.map(sanitise_filename),
	)
}

#[cfg(test)]
mod tests {
	#[test]
	fn string_sanitisation() {
		const SAMPLE: &str = "🏳️‍⚧️this\\r\\n įs \r\\n ä \\r\nstrïng 🥴that\n\r \
		                      ../../../../../../../may be\r\n malicious🏳️‍⚧️";
		const SANITISED: &str = "🏳️‍⚧️thisrn įs n ä rstrïng 🥴that ..............may be malicious🏳️‍⚧️";

		let options = sanitize_filename::Options {
			windows: true,
			truncate: true,
			replacement: "",
		};

		// cargo test -- --nocapture
		println!("{SAMPLE}");
		println!("{}", sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
		println!("{SAMPLE:?}");
		println!("{:?}", sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));

		assert_eq!(SANITISED, sanitize_filename::sanitize_with_options(SAMPLE, options.clone()));
	}

	#[test]
	fn empty_sanitisation() {
		use crate::utils::string::EMPTY;

		let result =
			sanitize_filename::sanitize_with_options(EMPTY, sanitize_filename::Options {
				windows: true,
				truncate: true,
				replacement: "",
			});

		assert_eq!(EMPTY, result);
	}
}