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, 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")?,
		);

		if Self::emoji_match(&reaction.emoji, emoji) {
			let msg = reaction.message(ctx).await?;
			for mr in &msg.reactions {
				if Self::emoji_match(&mr.reaction_type, 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: mr.count,
						msg,
						emoji,
						name,
						image,
					};

					handler.process_match(ctx).await?;
					break;
				}
			}
		}

		Ok(())
	}

	async fn process_match(&self, ctx: &Context) -> anyhow::Result<()> {
		if self.reaction_count >= self.server_settings.starboard_threshold as u64 {
			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")?;

			if repost_id.is_none() {
				// 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)
	}

	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 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(&self, ctx: &Context, reaction: &Reaction) {
		if let Err(e) = self.handle_reaction(ctx, reaction).await {
			eprintln!("Error in starboard: {:?}", e);
		}
	}
}