use crate::handlers::ReactionHandler;
use crate::models::ServerSetting;
use crate::schema;
use crate::schema::{server_settings, starboard_mappings};

use anyhow::Context as AnyhowContext;
use bigdecimal::{BigDecimal, FromPrimitive, ToPrimitive};
use diesel::{ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl};
use serenity::async_trait;
use serenity::builder::CreateEmbed;
use serenity::model::prelude::{
	ChannelId, Emoji, EmojiId, Message, MessageReaction, Reaction, ReactionType,
};
use serenity::prelude::*;
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Clone)]
pub struct SingleMessageHandler {
	conn: Arc<Mutex<PgConnection>>,
	server_settings: ServerSetting,
	reaction_count: u64,
	msg: Message,
	emoji: Emoji,
	name: String,
	image: Option<String>,
}

impl SingleMessageHandler {
	pub async fn handle_reaction(
		conn: Arc<Mutex<PgConnection>>,
		ctx: &Context,
		reaction: &Reaction,
	) -> anyhow::Result<()> {
		let guild = match reaction.channel_id.to_channel(ctx).await?.guild() {
			Some(x) => x,
			None => return Ok(()),
		};

		// get corresponding guild settings
		let mut gs: Vec<ServerSetting> = server_settings::dsl::server_settings
			.filter(
				schema::server_settings::columns::guild_id
					.eq(BigDecimal::from_u64(guild.guild_id.0).unwrap()),
			)
			.limit(1)
			.get_results(&*conn.lock().await)?;
		let gs = match gs.pop() {
			Some(x) => x,
			None => return Ok(()),
		};
		let emoji = EmojiId(
			gs.starboard_emoji_id
				.to_u64()
				.context("Could not convert emoji id to u64")?,
		);

		let msg = reaction.message(ctx).await?;

		// reaction from event handler must match
		// otherwise we'll update the repost (and add an "edited"
		// to the message)
		// whenever someone reacts with any reaction
		if !Self::emoji_match(&reaction.emoji, emoji) {
			return Ok(());
		}

		let reaction_count = Self::find_emoji_match(msg.reactions.iter(), emoji);
		let guild_id = guild.guild_id;

		let emoji = guild_id
			.emoji(ctx, emoji)
			.await
			.context("Could not get emoji from guild")?;

		let name = msg
			.author
			.nick_in(ctx, guild_id)
			.await
			.unwrap_or_else(|| msg.author.tag());

		let image = msg
			.attachments
			.iter()
			.filter(|a| a.width.is_some())
			.map(|a| &a.url)
			.next()
			.cloned();

		let handler = Self {
			conn,
			server_settings: gs,
			reaction_count,
			msg,
			emoji,
			name,
			image,
		};

		handler.process_match(ctx).await?;

		Ok(())
	}

	async fn process_match(&self, ctx: &Context) -> anyhow::Result<()> {
		let original_id = BigDecimal::from(self.msg.id.0);
		diesel::insert_into(starboard_mappings::dsl::starboard_mappings)
			.values(starboard_mappings::columns::original_id.eq(&original_id))
			.returning(starboard_mappings::columns::repost_id)
			.on_conflict_do_nothing()
			.execute(&*self.conn.lock().await)?;

		let repost_id = starboard_mappings::dsl::starboard_mappings
			.filter(starboard_mappings::columns::original_id.eq(&original_id))
			.select(starboard_mappings::columns::repost_id)
			.limit(1)
			.get_results(&*self.conn.lock().await)?;

		let repost_id: &Option<BigDecimal> =
			repost_id.get(0).context("Insert of mapping failed")?;

		match repost_id {
			Some(id) => {
				self.edit_existing_starboard_message(
					ctx,
					id.to_u64()
						.context("Could not convert repost message id to a u64")?,
				)
				.await?;
			}
			None => {
				if self.reaction_count >= self.server_settings.starboard_threshold as u64 {
					// post to repost
					let repost = self.post_new_starboard_message(ctx).await?;

					// update the DB
					let repost_id = BigDecimal::from_u64(repost);
					diesel::update(
						starboard_mappings::dsl::starboard_mappings
							.filter(starboard_mappings::columns::original_id.eq(original_id)),
					)
					.set(starboard_mappings::columns::repost_id.eq(repost_id))
					.execute(&*self.conn.lock().await)?;
				}
			}
		}

		Ok(())
	}

	async fn post_new_starboard_message(&self, ctx: &Context) -> anyhow::Result<u64> {
		let repost = ChannelId(
			self.server_settings
				.starboard_channel
				.to_u64()
				.context("Could not convert starboard channel to a u64")?,
		)
		.send_message(ctx, |m| m.embed(|e| self.clone().starboard_message(e)))
		.await?;

		Ok(repost.id.0)
	}

	async fn edit_existing_starboard_message(
		&self,
		ctx: &Context,
		message_id: u64,
	) -> anyhow::Result<()> {
		let channel_id = ChannelId(
			self.server_settings
				.starboard_channel
				.to_u64()
				.context("Could not convert starboard channel to a u64")?,
		);

		let mut msg = channel_id.message(ctx, message_id).await?;
		msg.edit(ctx, |m| m.embed(|e| self.clone().starboard_message(e)))
			.await?;

		Ok(())
	}

	fn starboard_message(self, e: &mut CreateEmbed) -> &mut CreateEmbed {
		let mut e = e
			.description(format!(
				"[Jump to source]({})\n{}",
				self.msg.link(),
				self.msg.content
			))
			.title(format!("{} {}", self.reaction_count, self.emoji))
			.author(|a| a.name(&self.name).icon_url(self.msg.author.face()))
			.timestamp(self.msg.timestamp);

		if let Some(url) = self.image {
			e = e.image(url);
		}

		e
	}

	fn find_emoji_match<'a, I: Iterator<Item = &'a MessageReaction>>(iter: I, em: EmojiId) -> u64 {
		for mr in iter {
			if Self::emoji_match(&mr.reaction_type, em) {
				return mr.count;
			}
		}

		0
	}

	fn emoji_match(rt: &ReactionType, em: EmojiId) -> bool {
		matches!(rt, ReactionType::Custom { id, .. } if *id == em)
	}
}

pub struct StarboardHandler {
	conn: Arc<Mutex<PgConnection>>,
}

impl StarboardHandler {
	pub fn new(conn: Arc<Mutex<PgConnection>>) -> Self {
		Self { conn }
	}

	async fn handle_reaction(&self, ctx: &Context, reaction: &Reaction) -> anyhow::Result<()> {
		SingleMessageHandler::handle_reaction(self.conn.clone(), ctx, reaction).await
	}
}

#[async_trait]
impl ReactionHandler for StarboardHandler {
	async fn reaction_add(&self, ctx: &Context, reaction: &Reaction) {
		if let Err(e) = self.handle_reaction(ctx, reaction).await {
			eprintln!("Error in starboard: {:?}", e);
		}
	}

	async fn reaction_del(&self, ctx: &Context, reaction: &Reaction) {
		if let Err(e) = self.handle_reaction(ctx, reaction).await {
			eprintln!("Error in starboard: {:?}", e);
		}
	}
}