use crate::{config::Config, db::*, locales::Locales, queries::*};

use fluent_bundle::FluentArgs;
use log::error;
use std::{net::IpAddr, str::FromStr};
use unic_langid::LanguageIdentifier;

pub fn new_pending_comment(
	comment: &Comment,
	addr: Option<IpAddr>,
	dbs: &Dbs,
) -> Result<CommentId, sled::Error> {
	let comment_id = CommentId::new();
	dbs.comment
		.insert(&comment_id, &(comment.clone(), CommentStatus::Pending))?;
	dbs.comment_pending.insert(
		&(
			comment.topic_hash.clone(),
			comment.post_time,
			comment_id.clone(),
		),
		&(addr, false),
	)?;
	Ok(comment_id)
}

// TODO when approval disabled
pub fn edit_comment(
	comment_id: CommentId,
	old_comment: Comment,
	comment_status: CommentStatus,
	edited_comment: Comment,
	addr: Option<IpAddr>,
	dbs: &Dbs,
) -> Result<(), sled::Error> {
	match comment_status {
		CommentStatus::Pending => {
			dbs.comment
				.insert(&comment_id, &(edited_comment, CommentStatus::Pending))?;
			// TODO should we update ip address in comment_pending?
		}
		CommentStatus::Approved => {
			dbs.comment_pending.insert(
				&(
					edited_comment.topic_hash.clone(),
					edited_comment.post_time,
					comment_id.clone(),
				),
				&(addr, true),
			)?;
			dbs.comment.insert(
				&comment_id,
				&(old_comment, CommentStatus::ApprovedEdited(edited_comment)),
			)?;
		}
		CommentStatus::ApprovedEdited(_old_edited_comment) => {
			dbs.comment.insert(
				&comment_id,
				&(old_comment, CommentStatus::ApprovedEdited(edited_comment)),
			)?;
			// TODO should we update ip address in comment_pending?
		}
	}
	Ok(())
}

pub fn approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Error> {
	if let Some((comment, CommentStatus::Pending)) = dbs.comment.get(&comment_id)? {
		dbs.comment_pending.remove(&(
			comment.topic_hash.clone(),
			comment.post_time,
			comment_id.clone(),
		))?;
		dbs.comment_approved.insert(
			&(
				comment.topic_hash.clone(),
				comment.post_time,
				comment_id.clone(),
			),
			&(),
		)?;
		dbs.comment
			.insert(&comment_id, &(comment, CommentStatus::Approved))?;
	}
	Ok(())
}

pub fn approve_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
	if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
		dbs.comment.get(&comment_id)?
	{
		dbs.comment_pending.remove(&(
			edited_comment.topic_hash.clone(),
			edited_comment.post_time,
			comment_id.clone(),
		))?;
		dbs.comment
			.insert(&comment_id, &(edited_comment, CommentStatus::Approved))?;
		return Ok(Some(comment));
	}
	Ok(None)
}

pub fn remove_comment(
	comment_id: CommentId,
	dbs: &Dbs,
) -> Result<Option<(Comment, CommentStatus)>, sled::Error> {
	if let Some((comment, edited_comment)) = dbs.comment.remove(&comment_id)? {
		match &edited_comment {
			CommentStatus::Pending => {
				dbs.comment_pending.remove(&(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id,
				))?;
			}
			CommentStatus::Approved => {
				dbs.comment_approved.remove(&(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id,
				))?;
			}
			CommentStatus::ApprovedEdited(edited_comment) => {
				dbs.comment_pending.remove(&(
					edited_comment.topic_hash.clone(),
					edited_comment.post_time,
					comment_id.clone(),
				))?;
				dbs.comment_approved.remove(&(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id,
				))?;
			}
		}
		return Ok(Some((comment, edited_comment)));
	}
	Ok(None)
}

pub fn remove_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
	if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
		dbs.comment.get(&comment_id)?
	{
		dbs.comment_pending.remove(&(
			edited_comment.topic_hash.clone(),
			edited_comment.post_time,
			comment_id.clone(),
		))?;
		dbs.comment
			.insert(&comment_id, &(comment.clone(), CommentStatus::Approved))?;
		return Ok(Some(comment));
	}
	Ok(None)
}

pub fn iter_comments_by_topic<'a, V: typed_sled::KV>(
	topic_hash: TopicHash,
	tree: &'a Tree<(TopicHash, Time, CommentId), V>,
	dbs: &'a Dbs,
) -> impl Iterator<Item = (CommentId, Comment, CommentStatus, V)> + 'a {
	tree.range(
		(topic_hash.clone(), 0, CommentId::zero())..=(topic_hash, Time::MAX, CommentId::max()),
	)
	.filter_map(|entry| {
		let ((_topic_hash, _time, comment_id), val) = entry
			.map_err(|e| error!("Reading comment index: {:?}", e))
			.ok()?;
		let (comment, comment_status) = dbs
			.comment
			.get(&comment_id)
			.map_err(|e| error!("Reading comment: {:?}", e))
			.ok()?
			.or_else(|| {
				error!("Comment not found");
				None
			})?;
		Some((comment_id, comment, comment_status, val))
	})
}

pub fn iter_approved_comments_by_topic(
	topic_hash: TopicHash,
	dbs: &Dbs,
) -> impl Iterator<Item = (CommentId, Comment, CommentStatus)> + '_ {
	iter_comments_by_topic(topic_hash, &dbs.comment_approved, dbs)
		.map(|(comment_id, comment, comment_status, ())| (comment_id, comment, comment_status))
}

pub fn iter_pending_comments_by_topic(
	topic_hash: TopicHash,
	dbs: &Dbs,
) -> impl Iterator<Item = (CommentId, Comment, Option<IpAddr>, CommentStatus)> + '_ {
	iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs).map(
		|(comment_id, comment, comment_status, (addr, _is_edit))| {
			(comment_id, comment, addr, comment_status)
		},
	)
}

/// Returns Some(time_left) if the client is banned.
pub fn antispam_check_client_mutation(
	addr: &IpAddr,
	dbs: &Dbs,
	config: &Config,
) -> Result<Option<Time>, sled::Error> {
	let time = std::time::SystemTime::now()
		.duration_since(std::time::UNIX_EPOCH)
		.unwrap()
		.as_secs();
	Ok(dbs
		.client_mutation
		.get(addr)?
		.and_then(|(last_mutation, mutation_count)| {
			let timeout = last_mutation + config.antispam_duration;
			if timeout > time && mutation_count >= config.antispam_mutation_limit {
				Some(timeout - time)
			} else {
				None
			}
		}))
}

pub fn antispam_update_client_mutation(addr: &IpAddr, dbs: &Dbs) -> Result<(), sled::Error> {
	let time = std::time::SystemTime::now()
		.duration_since(std::time::UNIX_EPOCH)
		.unwrap()
		.as_secs();
	dbs.client_mutation.fetch_and_update(addr, |entry| {
		if let Some((_last_mutation, mutation_count)) = entry {
			Some((time, mutation_count.saturating_add(1)))
		} else {
			Some((time, 1))
		}
	})?;
	Ok(())
}

/*pub fn new_client_mutation(
	addr: &IpAddr,
	dbs: &Dbs,
	config: &Config,
) -> Result<Option<Time>, sled::Error> {
	let time = std::time::SystemTime::now()
		.duration_since(std::time::UNIX_EPOCH)
		.unwrap()
		.as_secs();
	let mut res = None;
	dbs.client_mutation.fetch_and_update(addr, |entry| {
		if let Some((last_mutation, mutation_count)) = entry {
			if last_mutation + config.antispam_duration > time {
				if mutation_count >= config.antispam_mutation_limit {
					res = Some(last_mutation + config.antispam_duration);
					Some((last_mutation, mutation_count))
				} else {
					Some((time, mutation_count.saturating_add(1)))
				}
			} else {
				Some((time, 1))
			}
		} else {
			Some((time, 1))
		}
	})?;
	Ok(res)
}*/

pub fn get_client_addr<State>(
	config: &Config,
	req: &tide::Request<State>,
) -> Option<Result<IpAddr, std::net::AddrParseError>> {
	Some(IpAddr::from_str(
		if config.reverse_proxy {
			req.remote()
		} else {
			req.peer_addr()
		}?
		.rsplit_once(':')?
		.0,
	))
}

pub fn check_comment(
	config: &Config,
	locales: &Locales,
	langs: &[LanguageIdentifier],
	comment: &CommentForm,
	errors: &mut Vec<String>,
) {
	if comment.author.len() > config.comment_author_max_len {
		let mut args = FluentArgs::new();
		args.set("len", comment.author.len());
		args.set("max_len", config.comment_author_max_len);
		errors.push(
			locales
				.tr(langs, "error-comment-author_name_too_long", Some(&args))
				.unwrap()
				.to_string(),
		);
	}
	if comment.email.len() > config.comment_email_max_len {
		let mut args = FluentArgs::new();
		args.set("len", comment.email.len());
		args.set("max_len", config.comment_email_max_len);
		errors.push(
			locales
				.tr(langs, "error-comment-email_too_long", Some(&args))
				.unwrap()
				.to_string(),
		);
	}
	if comment.text.len() > config.comment_text_max_len {
		let mut args = FluentArgs::new();
		args.set("len", comment.text.len());
		args.set("max_len", config.comment_text_max_len);
		errors.push(
			locales
				.tr(langs, "error-comment-text_too_long", Some(&args))
				.unwrap()
				.to_string(),
		);
	}
}

pub fn check_can_edit_comment<'a>(
	config: &Config,
	comment: &Comment,
	mutation_token: &MutationToken,
) -> Result<(), &'a str> {
	if &comment.mutation_token != mutation_token {
		return Err("bad mutation token");
	}

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

	if time
		> comment
			.post_time
			.saturating_add(config.comment_edit_timeout)
	{
		return Err("mutation timeout expired");
	}

	Ok(())
}

#[cfg(test)]
mod test {
	use super::*;

	#[test]
	fn test_comment() {
		// Post a comment

		let comment = Comment {
			topic_hash: TopicHash::from_topic("test"),
			author: String::from("Jerry"),
			email: None,
			last_edit_time: None,
			mutation_token: MutationToken::new(),
			post_time: 42,
			text: String::from("Hello world!"),
		};

		let dbs = load_dbs(None);
		let comment_id = new_pending_comment(&comment, None, &dbs).unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(comment.clone(), CommentStatus::Pending)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				(None, false)
			)))
		);
		assert_eq!(iter.next(), None);

		// Edit the comment

		let comment2 = Comment {
			topic_hash: TopicHash::from_topic("test"),
			author: String::from("Jerry Smith"),
			email: Some(String::from("jerry.smith@example.tld")),
			last_edit_time: Some(137),
			mutation_token: comment.mutation_token.clone(),
			post_time: 42,
			text: String::from("Good bye world!"),
		};

		edit_comment(
			comment_id.clone(),
			comment.clone(),
			CommentStatus::Pending,
			comment2.clone(),
			None,
			&dbs,
		)
		.unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(comment2.clone(), CommentStatus::Pending)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				(None, false)
			)))
		);
		assert_eq!(iter.next(), None);

		// Approve the comment

		approve_comment(comment_id.clone(), &dbs).unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(comment2.clone(), CommentStatus::Approved)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_approved.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				()
			)))
		);
		assert_eq!(iter.next(), None);

		// Edit the approved comment

		let comment3 = Comment {
			topic_hash: TopicHash::from_topic("test"),
			author: String::from("Jerry Smith is back"),
			email: Some(String::from("jerry.smith@example.tld")),
			last_edit_time: Some(666),
			mutation_token: comment.mutation_token.clone(),
			post_time: 42,
			text: String::from("Hello again!"),
		};

		edit_comment(
			comment_id.clone(),
			comment2.clone(),
			CommentStatus::Approved,
			comment3.clone(),
			None,
			&dbs,
		)
		.unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(
					comment2.clone(),
					CommentStatus::ApprovedEdited(comment3.clone())
				)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				(None, true)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_approved.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				()
			)))
		);
		assert_eq!(iter.next(), None);

		// Edit the edited approved comment

		let comment4 = Comment {
			topic_hash: TopicHash::from_topic("test"),
			author: String::from("Jerry Smith is still back"),
			email: Some(String::from("jerry.smith@example.tld")),
			last_edit_time: Some(1337),
			mutation_token: comment.mutation_token.clone(),
			post_time: 42,
			text: String::from("Hello again one more time!"),
		};

		edit_comment(
			comment_id.clone(),
			comment2.clone(),
			CommentStatus::ApprovedEdited(comment3.clone()),
			comment4.clone(),
			None,
			&dbs,
		)
		.unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(comment2, CommentStatus::ApprovedEdited(comment4.clone()))
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				(None, true)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_approved.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				()
			)))
		);
		assert_eq!(iter.next(), None);

		// Approve the edit

		approve_edit(comment_id.clone(), &dbs).unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(comment4.clone(), CommentStatus::Approved)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_approved.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				()
			)))
		);
		assert_eq!(iter.next(), None);

		// Edit and remove the edit

		edit_comment(
			comment_id.clone(),
			comment4.clone(),
			CommentStatus::Approved,
			comment.clone(),
			None,
			&dbs,
		)
		.unwrap();
		remove_edit(comment_id.clone(), &dbs).unwrap();

		let mut iter = dbs.comment.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				comment_id.clone(),
				(comment4.clone(), CommentStatus::Approved)
			)))
		);
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_pending.iter();
		assert_eq!(iter.next(), None);

		let mut iter = dbs.comment_approved.iter();
		assert_eq!(
			iter.next(),
			Some(Ok((
				(
					comment.topic_hash.clone(),
					comment.post_time,
					comment_id.clone()
				),
				()
			)))
		);
		assert_eq!(iter.next(), None);
	}
}