use crate::config::Config;

use crossbeam_channel::Receiver;
use log::error;
use matrix_sdk::ruma;
use std::time::{Duration, SystemTime};

pub struct Notification {
	pub topic: String,
}

enum OptionSince<T> {
	Some(T),
	NoneSince(SystemTime),
}

impl<T> OptionSince<T> {
	fn from_result<E, F: FnOnce(E)>(result: Result<T, E>, f: F) -> Self {
		match result {
			Ok(val) => Self::Some(val),
			Err(e) => {
				f(e);
				Self::NoneSince(SystemTime::now())
			}
		}
	}
}

impl<T> From<Option<T>> for OptionSince<T> {
	fn from(opt: Option<T>) -> Self {
		match opt {
			Some(val) => Self::Some(val),
			None => Self::NoneSince(SystemTime::now()),
		}
	}
}

struct Notifier {
	matrix: Option<OptionSince<(matrix_sdk::Client, matrix_sdk::room::Joined)>>,
}

impl Notifier {
	async fn new(config: &Config) -> Self {
		Self {
			matrix: if config.matrix_notify {
				Some(OptionSince::from_result(init_matrix(config).await, |e| {
					error!("Cannot init Matrix: {:?}", e)
				}))
			} else {
				None
			},
		}
	}

	async fn notify(&mut self, config: &Config, notification: Notification) {
		match &self.matrix {
			None => {}
			Some(OptionSince::Some((_client, room))) => {
				let decoded_topic = tera::escape_html(
					&percent_encoding::percent_decode_str(&notification.topic).decode_utf8_lossy(),
				);
				if let Err(e) = room
					.send(
						ruma::events::room::message::RoomMessageEventContent::text_html(
							format!(
								"New comment on topic \"{}\": {}{}t/{}",
								decoded_topic,
								config.public_address,
								config.root_url,
								notification.topic,
							),
							format!(
								"<a href=\"{}{}t/{}\">New comment on topic \"<em>{}</em>\".</a>",
								config.public_address,
								config.root_url,
								notification.topic,
								decoded_topic,
							),
						),
						None,
					)
					.await
				{
					error!("Sending Matrix message: {:?}", e);
				}
			}
			Some(OptionSince::NoneSince(earlier)) => {
				if SystemTime::now().duration_since(*earlier).unwrap()
					> Duration::from_secs(config.matrix_retry_timeout)
				{
					self.matrix = Some(OptionSince::from_result(init_matrix(config).await, |e| {
						error!("Cannot init Matrix: {:?}", e)
					}))
				}
			}
		}
	}
}

pub async fn run_notifier(config: &Config, recv: Receiver<Notification>) {
	let mut notifier = Notifier::new(config).await;
	for notification in recv {
		notifier.notify(config, notification).await;
	}
}

#[derive(Debug)]
enum MatrixError {
	CannotConnect(matrix_sdk::ClientBuildError),
	CannotLogin(matrix_sdk::Error),
	CannotSync(matrix_sdk::Error),
	RoomNotJoined,
	UnknownRoom,
}

async fn init_matrix(
	config: &Config,
) -> Result<(matrix_sdk::Client, matrix_sdk::room::Joined), MatrixError> {
	let user = ruma::UserId::parse(&config.matrix_user)
		.expect("Matrix username should be in format `@user:homeserver`");
	let room_id = <&ruma::RoomId>::try_from(config.matrix_room.as_str())
		.expect("Matrix room should be in format `#roomname:homeserver` or `!roomid:homeserver`");

	let client = matrix_sdk::Client::builder()
		.homeserver_url(&config.matrix_server)
		.user_agent("Webcomment")
		.handle_refresh_tokens()
		.build()
		.await
		.map_err(MatrixError::CannotConnect)?;

	client
		.login_username(&user, &config.matrix_password)
		.send()
		.await
		.map_err(MatrixError::CannotLogin)?;
	client
		.sync_once(matrix_sdk::config::SyncSettings::default())
		.await
		.map_err(MatrixError::CannotSync)?;

	let room = client.get_room(room_id).ok_or(MatrixError::UnknownRoom)?;

	if let matrix_sdk::room::Room::Joined(room) = room {
		Ok((client, room))
	} else {
		Err(MatrixError::RoomNotJoined)
	}
}