use futures::StreamExt as _;
use futures::TryStreamExt as _;

pub struct Global {
	pub config: crate::Config,
	pub http: reqwest::Client,
	pub now: std::time::SystemTime,
	pub qbt: url::Url,
}

impl Global {
	pub async fn is_pinned(
		&self,
		t: &crate::Torrent,
	) -> anyhow::Result<Option<crate::PinReason>> {
		let mut need_slow = false;

		let last_activity = self.now.duration_since(t.last_activity)
			.unwrap_or(std::time::Duration::ZERO);

		let mut categories_allowed = crate::Requirement::new(
			|c| c.contains(&t.category),
			&self.config.categories_allowed);
		let mut last_activity_min = crate::Requirement::new(
			|&l| last_activity >= l,
			&self.config.last_activity_min);
		let mut seed_time_min = crate::Requirement::new(
			|&c| t.seeding_time > c,
			&self.config.seed_time_min);
		let mut seeder_count_min = crate::Requirement::new(
			|&s| t.seeders >= s,
			&self.config.seeder_count_min);
		let mut state_allowed = crate::Requirement::new(
			|s| s.is_empty() || s.contains(&t.state),
			&self.config.state_allowed);

		let mut checked_pin = false;

		for rule in self.config.rules.iter().rev() {
			if (rule.pin.is_none() || checked_pin)
				&& !categories_allowed.is_relevant(&rule.categories_allowed)
				&& !last_activity_min.is_relevant(&rule.last_activity_min)
				&& !seed_time_min.is_relevant(&rule.seed_time_min)
				&& !seeder_count_min.is_relevant(&rule.seeder_count_min)
				&& !state_allowed.is_relevant(&rule.state_allowed)
			{
				continue
			}

			let Some(r) = rule.match_.match_fast(self.now, &t) else {
				checked_pin |= rule.pin.is_some();
				categories_allowed.mark_checked_if(&rule.categories_allowed);
				last_activity_min.mark_checked_if(&rule.last_activity_min);
				seed_time_min.mark_checked_if(&rule.seed_time_min);
				seeder_count_min.mark_checked_if(&rule.seeder_count_min);
				state_allowed.mark_checked_if(&rule.state_allowed);

				need_slow = true;
				continue;
			};

			if !r { continue }

			if let Some(p) = rule.pin && !checked_pin {
				return Ok(p.then_some(crate::PinReason::Explicit))
			}

			categories_allowed.apply(&rule.categories_allowed);
			last_activity_min.apply(&rule.last_activity_min);
			seed_time_min.apply(&rule.seed_time_min);
			seeder_count_min.apply(&rule.seeder_count_min);
			state_allowed.apply(&rule.state_allowed);
		}

		if !checked_pin {
			if !categories_allowed.is_ok() {
				return Ok(Some(crate::PinReason::Category(t.category.clone())))
			}
			if !state_allowed.is_ok() {
				return Ok(Some(crate::PinReason::State(t.state)))
			}
			if !seed_time_min.is_ok() {
				return Ok(Some(crate::PinReason::SeedTime(t.seeding_time)))
			}
			if !last_activity_min.is_ok() {
				return Ok(Some(crate::PinReason::Activity(last_activity)))
			}
			if !seeder_count_min.is_ok() {
				return Ok(Some(crate::PinReason::Seeders(t.seeders)))
			}
		}

		if need_slow {
			categories_allowed.reset();
			last_activity_min.reset();
			seed_time_min.reset();
			seeder_count_min.reset();
			state_allowed.reset();

			let trackers = self.http.get(self.qbt.join("/api/v2/torrents/trackers")?)
				.query(&[
					("hash", t.hash),
				])
				.send().await?
				.error_for_status()?
				.json::<Vec<crate::Tracker>>().await?;

			for rule in self.config.rules.iter().rev() {
				if rule.pin.is_none()
					&& !categories_allowed.is_relevant(&rule.categories_allowed)
					&& !last_activity_min.is_relevant(&rule.last_activity_min)
					&& !seed_time_min.is_relevant(&rule.seed_time_min)
					&& !seeder_count_min.is_relevant(&rule.seeder_count_min)
					&& !state_allowed.is_relevant(&rule.state_allowed)
				{
					continue
				}

				if let Some(r) = rule.match_.match_fast(self.now, &t) {
					if !r {
						continue
					}
				} else if !rule.match_.match_slow(&t, &trackers) {
					continue
				}

				if let Some(p) = rule.pin {
					return Ok(p.then_some(crate::PinReason::Explicit))
				}

				categories_allowed.apply(&rule.categories_allowed);
				last_activity_min.apply(&rule.last_activity_min);
				seed_time_min.apply(&rule.seed_time_min);
				seeder_count_min.apply(&rule.seeder_count_min);
				state_allowed.apply(&rule.state_allowed);
			}
		}

		if !categories_allowed.is_ok() {
			return Ok(Some(crate::PinReason::Category(t.category.clone())))
		}
		if !state_allowed.is_ok() {
			return Ok(Some(crate::PinReason::State(t.state)))
		}
		if !seed_time_min.is_ok() {
			return Ok(Some(crate::PinReason::SeedTime(t.seeding_time)))
		}
		if !last_activity_min.is_ok() {
			return Ok(Some(crate::PinReason::Activity(last_activity)))
		}
		if !seeder_count_min.is_ok() {
			return Ok(Some(crate::PinReason::Seeders(t.seeders)))
		}

		Ok(None)
	}

	pub async fn group(
		&self,
		filter_name: Option<&regex::Regex>,
	) -> anyhow::Result<impl Iterator<Item=crate::Group>> {
		let mut torrents = self.http.get(self.qbt.join("/api/v2/torrents/info")?)
			.send().await?
			.error_for_status()?
			.json::<Vec<crate::Torrent>>().await?;

		torrents.retain(|t| {
			let Some(filter_name) = filter_name else { return true };
			filter_name.is_match(&t.name)
		});

		let groups = std::sync::Mutex::<
			std::collections::HashMap::<
				u64,
				std::sync::Arc<
					tokio::sync::Mutex<crate::Group>>>>::default();

		#[derive(Debug,serde::Deserialize)]
		struct File {
			size: u64,
		}

		let () = futures::stream::iter(torrents)
			.map(async |t| -> anyhow::Result<()> {
				if t.state.is_downloading_metadata() {
					return Ok(())
				}

				let files = self.http.get(self.qbt.join("/api/v2/torrents/files")?)
					.query(&[
						("hash", t.hash),
					])
					.send().await?
					.error_for_status()?
					.json::<Vec<File>>().await?;

				let total_size = u64::try_from(t.total_size)
					.map_err(|_| anyhow::format_err!(
						"Torrent {} {:?} has negative size {}.",
						t.hash,
						t.name,
						t.total_size))?;

				let average_size = total_size / u64::try_from(files.len())?;
				let cutoff = average_size / 2;

				let mut counted_size = 0;
				for f in files {
					if f.size >= cutoff {
						counted_size += f.size;
					}
				}

				let group = {
					groups
						.lock().unwrap()
						.entry(counted_size)
						.or_insert_with(||
							std::sync::Arc::new(
								crate::Group::new(counted_size).into()))
						.clone()
				};
				let mut group = group.lock().await;

				if group.pinned.is_none() {
					group.pinned = self.is_pinned(&t).await?;
				}

				group.torrents.push(t);

				Ok(())
			})
			.buffer_unordered(16)
			.try_collect().await?;

		Ok(groups.into_inner()?
			.into_iter()
			.map(|(_, g)| {
				std::sync::Arc::into_inner(g).unwrap()
					.into_inner()
			}))
	}
}
