use std::str::FromStr;

use num::traits::Euclid;
use poise::serenity_prelude::{
	Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
};
use sqlx::{query_as, SqlitePool};

use crate::arcaea::chart::{Chart, Song};
use crate::context::{Error, UserContext};
use crate::user::User;

use super::score::Score;

// {{{ Create play
#[derive(Debug, Clone)]
pub struct CreatePlay {
	chart_id: u32,
	user_id: u32,
	discord_attachment_id: Option<AttachmentId>,

	// Actual score data
	score: Score,
	zeta_score: Score,

	// Optional score details
	max_recall: Option<u32>,
	far_notes: Option<u32>,

	// Creation data
	creation_ptt: Option<u32>,
	creation_zeta_ptt: Option<u32>,
}

impl CreatePlay {
	#[inline]
	pub fn new(score: Score, chart: &Chart, user: &User) -> Self {
		Self {
			chart_id: chart.id,
			user_id: user.id,
			discord_attachment_id: None,
			score,
			zeta_score: score.to_zeta(chart.note_count as u32),
			max_recall: None,
			far_notes: None,
			// TODO: populate these
			creation_ptt: None,
			creation_zeta_ptt: None,
		}
	}

	#[inline]
	pub fn with_attachment(mut self, attachment: &Attachment) -> Self {
		self.discord_attachment_id = Some(attachment.id);
		self
	}

	#[inline]
	pub fn with_fars(mut self, far_count: Option<u32>) -> Self {
		self.far_notes = far_count;
		self
	}

	#[inline]
	pub fn with_max_recall(mut self, max_recall: Option<u32>) -> Self {
		self.max_recall = max_recall;
		self
	}

	// {{{ Save
	pub async fn save(self, ctx: &UserContext) -> Result<Play, Error> {
		let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64);
		let play = sqlx::query!(
			"
        INSERT INTO plays(
        user_id,chart_id,discord_attachment_id,
        score,zeta_score,max_recall,far_notes
        )
        VALUES(?,?,?,?,?,?,?)
        RETURNING id, created_at
      ",
			self.user_id,
			self.chart_id,
			attachment_id,
			self.score.0,
			self.zeta_score.0,
			self.max_recall,
			self.far_notes
		)
		.fetch_one(&ctx.db)
		.await?;

		Ok(Play {
			id: play.id as u32,
			created_at: play.created_at,
			chart_id: self.chart_id,
			user_id: self.user_id,
			discord_attachment_id: self.discord_attachment_id,
			score: self.score,
			zeta_score: self.zeta_score,
			max_recall: self.max_recall,
			far_notes: self.far_notes,
			creation_ptt: self.creation_ptt,
			creation_zeta_ptt: self.creation_zeta_ptt,
		})
	}
	// }}}
}
// }}}
// {{{ DbPlay
/// Version of `Play` matching the format sqlx expects
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct DbPlay {
	pub id: i64,
	pub chart_id: i64,
	pub user_id: i64,
	pub discord_attachment_id: Option<String>,
	pub score: i64,
	pub zeta_score: i64,
	pub max_recall: Option<i64>,
	pub far_notes: Option<i64>,
	pub created_at: chrono::NaiveDateTime,
	pub creation_ptt: Option<i64>,
	pub creation_zeta_ptt: Option<i64>,
}

impl DbPlay {
	#[inline]
	pub fn to_play(self) -> Play {
		Play {
			id: self.id as u32,
			chart_id: self.chart_id as u32,
			user_id: self.user_id as u32,
			score: Score(self.score as u32),
			zeta_score: Score(self.zeta_score as u32),
			max_recall: self.max_recall.map(|r| r as u32),
			far_notes: self.far_notes.map(|r| r as u32),
			created_at: self.created_at,
			discord_attachment_id: self
				.discord_attachment_id
				.and_then(|s| AttachmentId::from_str(&s).ok()),
			creation_ptt: self.creation_ptt.map(|r| r as u32),
			creation_zeta_ptt: self.creation_zeta_ptt.map(|r| r as u32),
		}
	}
}
// }}}
// {{{ Play
#[derive(Debug, Clone)]
pub struct Play {
	pub id: u32,
	pub chart_id: u32,
	pub user_id: u32,

	#[allow(unused)]
	pub discord_attachment_id: Option<AttachmentId>,

	// Actual score data
	pub score: Score,
	pub zeta_score: Score,

	// Optional score details
	pub max_recall: Option<u32>,
	pub far_notes: Option<u32>,

	// Creation data
	pub created_at: chrono::NaiveDateTime,

	#[allow(unused)]
	pub creation_ptt: Option<u32>,

	#[allow(unused)]
	pub creation_zeta_ptt: Option<u32>,
}

impl Play {
	// {{{ Play => distribution
	pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
		if let Some(fars) = self.far_notes {
			let (_, shinies, units) = self.score.analyse(note_count);
			let (pures, rem) = units.checked_sub(fars)?.div_rem_euclid(&2);
			if rem == 1 {
				println!("The impossible happened: got an invalid amount of far notes!");
				return None;
			}

			let lost = note_count.checked_sub(fars + pures)?;
			let non_max_pures = pures.checked_sub(shinies)?;
			Some((shinies, non_max_pures, fars, lost))
		} else {
			None
		}
	}
	// }}}
	// {{{ Play => status
	#[inline]
	pub fn status(&self, chart: &Chart) -> Option<String> {
		let score = self.score.0;
		if score >= 10_000_000 {
			if score > chart.note_count + 10_000_000 {
				return None;
			}

			let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
			if non_max_pures == 0 {
				Some("MPM".to_string())
			} else {
				Some(format!("PM (-{})", non_max_pures))
			}
		} else if let Some(distribution) = self.distribution(chart.note_count) {
			// if no lost notes...
			if distribution.3 == 0 {
				Some(format!("FR (-{}/-{})", distribution.1, distribution.2))
			} else {
				Some(format!(
					"C (-{}/-{}/-{})",
					distribution.1, distribution.2, distribution.3
				))
			}
		} else {
			None
		}
	}

	#[inline]
	pub fn short_status(&self, chart: &Chart) -> Option<char> {
		let score = self.score.0;
		if score >= 10_000_000 {
			let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
			if non_max_pures == 0 {
				Some('M')
			} else {
				Some('P')
			}
		} else if let Some(distribution) = self.distribution(chart.note_count)
			&& distribution.3 == 0
		{
			Some('F')
		} else {
			Some('C')
		}
	}
	// }}}
	// {{{ Play to embed
	/// Creates a discord embed for this play.
	///
	/// The `index` variable is only used to create distinct filenames.
	pub async fn to_embed(
		&self,
		db: &SqlitePool,
		user: &User,
		song: &Song,
		chart: &Chart,
		index: usize,
		author: Option<&poise::serenity_prelude::User>,
	) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
		// {{{ Get previously best score
		let previously_best = query_as!(
			DbPlay,
			"
        SELECT * FROM plays
        WHERE user_id=?
        AND chart_id=?
        AND created_at<?
        ORDER BY score DESC
    ",
			user.id,
			chart.id,
			self.created_at
		)
		.fetch_optional(db)
		.await
		.map_err(|_| {
			format!(
				"Could not find any scores for {} [{:?}]",
				song.title, chart.difficulty
			)
		})?
		.map(|p| p.to_play());
		// }}}

		let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
		let icon_attachement = match chart.cached_jacket.as_ref() {
			Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)),
			None => None,
		};

		let mut embed = CreateEmbed::default()
			.title(format!(
				"{} [{:?} {}]",
				&song.title, chart.difficulty, chart.level
			))
			.field("Score", format!("{} (+?)", self.score), true)
			.field(
				"Rating",
				format!(
					"{:.2} (+?)",
					self.score.play_rating_f32(chart.chart_constant)
				),
				true,
			)
			.field("Grade", format!("{}", self.score.grade()), true)
			.field("ξ-Score", format!("{} (+?)", self.zeta_score), true)
			// {{{ ξ-Rating
			.field(
				"ξ-Rating",
				{
					let play_rating = self.zeta_score.play_rating_f32(chart.chart_constant);
					if let Some(previous) = previously_best {
						let previous_play_rating =
							previous.zeta_score.play_rating_f32(chart.chart_constant);

						if play_rating >= previous_play_rating {
							format!(
								"{:.2} (+{})",
								play_rating,
								play_rating - previous_play_rating
							)
						} else {
							format!(
								"{:.2} (-{})",
								play_rating,
								play_rating - previous_play_rating
							)
						}
					} else {
						format!("{:.2}", play_rating)
					}
				},
				true,
			)
			// }}}
			.field("ξ-Grade", format!("{}", self.zeta_score.grade()), true)
			.field(
				"Status",
				self.status(chart).unwrap_or("-".to_string()),
				true,
			)
			.field(
				"Max recall",
				if let Some(max_recall) = self.max_recall {
					format!("{}", max_recall)
				} else {
					format!("-")
				},
				true,
			)
			.field("ID", format!("{}", self.id), true);

		if icon_attachement.is_some() {
			embed = embed.thumbnail(format!("attachment://{}", &attachement_name));
		}

		if let Some(user) = author {
			let mut embed_author = CreateEmbedAuthor::new(&user.name);
			if let Some(url) = user.avatar_url() {
				embed_author = embed_author.icon_url(url);
			}

			embed = embed
				.timestamp(Timestamp::from_millis(
					self.created_at.and_utc().timestamp_millis(),
				)?)
				.author(embed_author);
		}

		Ok((embed, icon_attachement))
	}
	// }}}
}
// }}}