use crate::{config::*, db::*, helpers, queries::*, templates::*};

use argon2::{Argon2, PasswordHash, PasswordVerifier};
use crossbeam_channel::Sender;
use log::{error, warn};
use std::sync::Arc;
use tera::Context;

pub async fn run_server(config: Arc<Config>, dbs: Dbs, templates: Arc<Templates>) {
	tide::log::start();

	let (notify_send, notify_recv) = crossbeam_channel::bounded(10);
	tokio::spawn(crate::notify::run_notifier(config.clone(), notify_recv));

	let mut app = tide::new();
	app.at(&format!("{}t/:topic", config.root_url)).get({
		let config = config.clone();
		let templates = templates.clone();
		let dbs = dbs.clone();
		move |req: tide::Request<()>| {
			serve_comments(
				req,
				config.clone(),
				templates.clone(),
				dbs.clone(),
				Context::new(),
				200,
			)
		}
	});
	app.at(&format!("{}t/:topic", config.root_url)).post({
		let config = config.clone();
		let templates = templates.clone();
		let dbs = dbs.clone();
		move |req: tide::Request<()>| {
			handle_post_comments(
				req,
				config.clone(),
				templates.clone(),
				dbs.clone(),
				notify_send.clone(),
			)
		}
	});
	app.at(&format!("{}admin", config.root_url)).get({
		let config = config.clone();
		let templates = templates.clone();
		move |req: tide::Request<()>| serve_admin_login(req, config.clone(), templates.clone())
	});
	app.at(&format!("{}admin", config.root_url)).post({
		let config = config.clone();
		let templates = templates.clone();
		let dbs = dbs.clone();
		move |req: tide::Request<()>| {
			handle_post_admin(req, config.clone(), templates.clone(), dbs.clone())
		}
	});
	app.listen(config.listen).await.unwrap();
}

async fn serve_comments<'a>(
	req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
	mut context: Context,
	status_code: u16,
) -> tide::Result<tide::Response> {
	let Ok(topic) = req.param("topic") else {
		return Err(tide::Error::from_str(404, "No topic"))
	};

	let admin = req.cookie("admin").map_or(false, |psw| {
		check_admin_password_hash(&config, &String::from(psw.value()))
	});

	let topic_hash = TopicHash::from_topic(topic);

	context.insert("config", &config);
	context.insert("admin", &admin);

	if admin {
		if let Ok(query) = req.query::<ApproveQuery>() {
			if let Ok(comment_id) = CommentId::from_base64(&query.approve) {
				helpers::approve_comment(comment_id, &dbs)
					.map_err(|e| error!("Approving comment: {:?}", e))
					.ok();
			}
		}
		if let Ok(query) = req.query::<RemoveQuery>() {
			if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
				helpers::remove_comment(comment_id, &dbs)
					.map_err(|e| error!("Removing comment: {:?}", e))
					.ok();
			}
		}
		if let Ok(query) = req.query::<EditQuery>() {
			if let Ok(comment_id) = CommentId::from_base64(&query.edit) {
				if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
					context.insert("edit_comment", &comment_id.to_base64());
					context.insert("edit_comment_author", &comment.author);
					context.insert("edit_comment_email", &comment.email);
					context.insert("edit_comment_text", &comment.text);
				}
			}
		}

		context.insert(
			"comments_pending",
			&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
				.map(|(comment_id, comment)| CommentWithId {
					author: comment.author,
					editable: admin,
					id: comment_id.to_base64(),
					needs_approval: true,
					post_time: comment.post_time,
					text: comment.text,
				})
				.collect::<Vec<CommentWithId>>(),
		);
	}

	context.insert(
		"comments",
		&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
			.map(|(comment_id, comment)| CommentWithId {
				author: comment.author,
				editable: admin,
				id: comment_id.to_base64(),
				needs_approval: false,
				post_time: comment.post_time,
				text: comment.text,
			})
			.collect::<Vec<CommentWithId>>(),
	);

	Ok(tide::Response::builder(status_code)
		.content_type(tide::http::mime::HTML)
		.body(templates.tera.render("comments.html", &context)?)
		.build())
}

async fn serve_admin<'a>(
	_req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
) -> tide::Result<tide::Response> {
	let mut context = Context::new();
	context.insert("config", &config);
	context.insert("admin", &true);

	context.insert(
		"comments",
		&dbs.comment_pending
			.iter()
			.filter_map(|entry| {
				let ((_topic_hash, _time, comment_id), ()) = entry
					.map_err(|e| error!("Reading comment_pending: {:?}", e))
					.ok()?;
				let comment = dbs
					.comment
					.get(&comment_id)
					.map_err(|e| error!("Reading comment: {:?}", e))
					.ok()?
					.or_else(|| {
						error!("Comment not found");
						None
					})?;
				Some(CommentWithId {
					author: comment.author,
					editable: true,
					id: comment_id.to_base64(),
					needs_approval: true,
					post_time: comment.post_time,
					text: comment.text,
				})
			})
			.collect::<Vec<CommentWithId>>(),
	);

	Ok(tide::Response::builder(200)
		.content_type(tide::http::mime::HTML)
		.body(templates.tera.render("comments.html", &context)?)
		.build())
}

async fn serve_admin_login(
	_req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
) -> tide::Result<tide::Response> {
	let mut context = Context::new();
	context.insert("config", &config);

	Ok(tide::Response::builder(200)
		.content_type(tide::http::mime::HTML)
		.body(templates.tera.render("admin_login.html", &context)?)
		.build())
}

async fn handle_post_comments(
	mut req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
	notify_send: Sender<()>,
) -> tide::Result<tide::Response> {
	let admin = req.cookie("admin").map_or(false, |psw| {
		check_admin_password_hash(&config, &String::from(psw.value()))
	});

	let client_addr = if !admin && config.antispam_enable {
		match helpers::get_client_addr(&config, &req) {
			Some(Ok(addr)) => {
				if config.antispam_whitelist.contains(&addr) {
					None
				} else {
					Some(addr)
				}
			}
			Some(Err(e)) => {
				warn!("Unable to parse client addr: {}", e);
				None
			}
			None => {
				warn!("No client addr");
				None
			}
		}
	} else {
		None
	};

	let mut errors = Vec::new();
	let mut context = Context::new();

	match req.body_form::<CommentQuery>().await? {
		CommentQuery::NewComment(query) => {
			let Ok(topic) = req.param("topic") else {
				return Err(tide::Error::from_str(404, "No topic"))
			};

			helpers::check_comment(&config, &query.comment, &mut errors);

			if let Some(client_addr) = &client_addr {
				if let Some(antispam_timeout) =
					helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap()
				{
					errors.push(format!(
						"The edition quota from your IP is reached. You will be unblocked in {}s.",
						antispam_timeout
					));
				}
			}

			if errors.is_empty() {
				if let Some(client_addr) = &client_addr {
					helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
				}

				let topic_hash = TopicHash::from_topic(topic);

				let time = std::time::SystemTime::now()
					.duration_since(std::time::UNIX_EPOCH)
					.unwrap()
					.as_secs();

				let comment = Comment {
					topic_hash,
					author: query.comment.author,
					email: if query.comment.email.is_empty() {
						None
					} else {
						Some(query.comment.email)
					},
					last_edit_time: None,
					post_time: time,
					text: query.comment.text,
				};
				helpers::new_pending_comment(&comment, &dbs)
					.map_err(|e| error!("Adding pending comment: {:?}", e))
					.ok();
				notify_send.send(()).ok();
			} else {
				context.insert("new_comment_author", &query.comment.author);
				context.insert("new_comment_email", &query.comment.email);
				context.insert("new_comment_text", &query.comment.text);
			}
			context.insert("new_comment_errors", &errors);
		}
		CommentQuery::EditComment(query) => {
			if !admin {
				return Err(tide::Error::from_str(403, "Forbidden"));
			}

			helpers::check_comment(&config, &query.comment, &mut errors);

			let comment_id = if let Ok(comment_id) = CommentId::from_base64(&query.id) {
				comment_id
			} else {
				return Err(tide::Error::from_str(400, "Invalid comment id"));
			};

			let mut comment = if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
				comment
			} else {
				return Err(tide::Error::from_str(404, "Not found"));
			};

			if let Some(client_addr) = &client_addr {
				if let Some(antispam_timeout) =
					helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap()
				{
					errors.push(format!(
						"The edition quota from your IP is reached. You will be unblocked in {}s.",
						antispam_timeout
					));
				}
			}

			if errors.is_empty() {
				if let Some(client_addr) = &client_addr {
					helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
				}

				let time = std::time::SystemTime::now()
					.duration_since(std::time::UNIX_EPOCH)
					.unwrap()
					.as_secs();

				comment.author = query.comment.author;
				comment.email = if query.comment.email.is_empty() {
					None
				} else {
					Some(query.comment.email)
				};
				comment.text = query.comment.text;
				comment.last_edit_time = Some(time);

				dbs.comment.insert(&comment_id, &comment).unwrap();
			} else {
				context.insert("edit_comment", &comment_id.to_base64());
				context.insert("edit_comment_author", &query.comment.author);
				context.insert("edit_comment_email", &query.comment.email);
				context.insert("edit_comment_text", &query.comment.text);
			}
			context.insert("edit_comment_errors", &errors);
		}
	}
	serve_comments(
		req,
		config,
		templates,
		dbs,
		context,
		if errors.is_empty() { 200 } else { 400 },
	)
	.await
}

async fn handle_post_admin(
	mut req: tide::Request<()>,
	config: Arc<Config>,
	templates: Arc<Templates>,
	dbs: Dbs,
) -> tide::Result<tide::Response> {
	if let Some(psw) = req.cookie("admin") {
		if check_admin_password(&config, &String::from(psw.value())).is_some() {
			#[allow(clippy::match_single_binding)]
			match req.body_form::<AdminQuery>().await? {
				_ => serve_admin(req, config, templates, dbs).await,
			}
		} else {
			serve_admin_login(req, config, templates).await
		}
	} else if let AdminQuery::Login(query) = req.body_form::<AdminQuery>().await? {
		if let Some(password_hash) = check_admin_password(&config, &query.psw) {
			serve_admin(req, config.clone(), templates, dbs)
				.await
				.map(|mut r| {
					let mut cookie = tide::http::Cookie::new("admin", password_hash);
					cookie.set_http_only(Some(true));
					cookie.set_path(config.root_url.clone());
					if let Some(domain) = &config.cookies_domain {
						cookie.set_domain(domain.clone());
					}
					if config.cookies_https_only {
						cookie.set_secure(Some(true));
					}
					r.insert_cookie(cookie);
					r
				})
		} else {
			serve_admin_login(req, config, templates).await
		}
	} else {
		serve_admin_login(req, config, templates).await
	}
}

fn check_admin_password(config: &Config, password: &str) -> Option<String> {
	let argon2 = Argon2::default();
	config
		.admin_passwords
		.iter()
		.filter_map(|admin_password| PasswordHash::new(admin_password).ok())
		.find(|admin_password| {
			argon2
				.verify_password(password.as_bytes(), admin_password)
				.is_ok()
		})
		.map(|password_hash| password_hash.to_string())
}

fn check_admin_password_hash(config: &Config, password_hash: &str) -> bool {
	config.admin_passwords.iter().any(|h| h == password_hash)
}