use std::{
	pin::Pin,
	sync::Arc,
	time::{Duration, Instant},
};

use assetserver::Asset;
use chrono::{DateTime, TimeDelta, Utc};
use maud::{Markup, PreEscaped, html};
use page::{DeviceType, RequestContext, page::Page};
use parking_lot::Mutex;
use serde::Deserialize;
use tracing::{debug, warn};

use crate::{
	components::{
		md::{Markdown, backlinks, meta_from_markdown},
		misc::FarLink,
	},
	pages::page_wrapper,
	routes::assets::Image_Icon,
};

#[derive(Debug, Deserialize)]
struct HandoutEntry {
	title: String,
	group: String,
	handout: String,
	solutions: Option<String>,
}

struct CachedRequestInner<T> {
	last_fetch: DateTime<Utc>,
	last_value: Option<Arc<T>>,
}

pub struct CachedRequest<T> {
	inner: Mutex<CachedRequestInner<T>>,
	ttl: TimeDelta,
	get: Box<dyn Fn() -> Pin<Box<dyn Future<Output = T> + Send + Sync>> + Send + Sync>,
}

impl<T> CachedRequest<T> {
	pub fn new(
		ttl: TimeDelta,
		get: Box<dyn Fn() -> Pin<Box<dyn Future<Output = T> + Send + Sync>> + Send + Sync>,
	) -> Arc<Self> {
		Arc::new(Self {
			get,
			ttl,
			inner: Mutex::new(CachedRequestInner {
				last_fetch: Utc::now(),
				last_value: None,
			}),
		})
	}

	pub async fn get(self: Arc<Self>) -> Arc<T> {
		let now = Utc::now();
		let expires = self.inner.lock().last_fetch + self.ttl;

		if now < expires
			&& let Some(last_value) = self.inner.lock().last_value.clone()
		{
			return last_value;
		}

		let res = Arc::new((self.get)().await);

		let mut inner = self.inner.lock();
		inner.last_fetch = now;
		inner.last_value = Some(res.clone());
		return res;
	}

	pub async fn autoget(self: Arc<Self>, interval: Duration) {
		loop {
			{
				let now = Utc::now();
				let res = Arc::new((self.get)().await);

				let mut inner = self.inner.lock();
				inner.last_fetch = now;
				inner.last_value = Some(res.clone());
			}

			tokio::time::sleep(interval).await;
		}
	}
}

async fn get_index() -> Result<Vec<HandoutEntry>, reqwest::Error> {
	let start = Instant::now();
	let res = reqwest::get(
		"https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json",
	)
	.await;

	let res = match res {
		Ok(x) => x,
		Err(err) => {
			warn!("Error while getting index: {err:?}");
			return Err(err);
		}
	};

	let mut res: Vec<HandoutEntry> = res.json().await?;
	res.sort_by_key(|x| x.title.clone());

	debug!(
		message = "Fetched handout index",
		n_handouts = res.len(),
		time_ms = start.elapsed().as_millis()
	);
	return Ok(res);
}

fn build_list_for_group(
	handouts: &[HandoutEntry],
	group: &str,
	req_ctx: &RequestContext,
) -> Markup {
	let mobile = req_ctx.client_info.device_type == DeviceType::Mobile;

	if mobile {
		html! {
			ul class="handout-ul" {

				@for h in handouts {
					@if h.group ==group {
						li {
							span class="handout-li-title" {
								a href=(h.handout) class="underline-link" {
									strong style="text-decoration: underline;text-underline-offset:1.5pt;color:var(--fgColor);" { (h.title) }
								}
							}

							@if let Some(solutions) = &h.solutions {
								" ["
								a href=(solutions) { "sols" }
								"]"
							}
						}
					}
				}
			}
		}
	} else {
		html! {
			ul class="handout-ul" {

				@for h in handouts {
					@if h.group ==group {
						li {
							span class="handout-li-title" {
								strong { (h.title) }
							}
							" "
							span class="handout-li-links" {
								"[ "

								@if let Some(solutions) = &h.solutions {
									a href=(h.handout) {"handout"}
									" | "
									a href=(solutions) {"solutions"}
								} @else {
									a href=(h.handout) {"handout"}
								}
								" ]"
							}
						}
					}
				}
			}
		}
	}
}

//
// MARK: page
//

pub fn handouts() -> Page {
	let md = Markdown::parse(include_str!("handouts.md"));

	let index = CachedRequest::new(
		TimeDelta::minutes(30),
		Box::new(|| Box::pin(async move { get_index().await })),
	);

	tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20)));

	#[expect(clippy::unwrap_used)]
	let mut meta = meta_from_markdown(&md).unwrap().unwrap();

	if meta.image.is_none() {
		meta.image = Some(Image_Icon::URL.to_owned());
	}

	let html = PreEscaped(md.render());

	Page {
		meta,
		html_ttl: Some(TimeDelta::seconds(300)),

		generate_html: Box::new(move |page, ctx| {
			let html = html.clone(); // TODO: find a way to not clone here
			let index = index.clone();
			Box::pin(async move {
				let handouts = index.get().await;

				let fallback = html! {
					span style="color:var(--yellow)" {
						"Could not load handouts, something broke."
					}
					" "
					(
						FarLink(
							"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
							"Try this direct link."
						)
					)
				};

				let warmups = match &*handouts {
					Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", ctx),
					Err(error) => {
						warn!("Could not load handout index: {error:?}");
						fallback.clone()
					}
				};

				let advanced = match &*handouts {
					Ok(handouts) => build_list_for_group(handouts, "Advanced", ctx),
					Err(_) => fallback,
				};

				let inner = html! {
						@if let Some(backlinks) = backlinks(page, ctx) {
							(backlinks)
						}

						(html)

						(Markdown(concat!(
							"## Warm-Ups",
							"\n\n",
							"Students never show up on time. Some come early, some come late. Warm-ups ",
							"are my solution to this problem: we hand these out as students walk in, ",
							"giving them something to do until we can start the lesson.",
						)))
						(warmups)
						br {}

						(Markdown(concat!(
							"## Advanced",
							"\n\n",
							"The highest level of the ORMC, and the group I spend most of my time with. ",
							"Students in ORMC Advanced are in high school, which means ",
							"they're ~14-18 years old.",
						)))
						(advanced)
						br {}
				};

				page_wrapper(&page.meta, inner).await
			})
		}),
	}
}
