pub async fn cmd_clean<Q: crate::Qbt>(
	global: &crate::Global<Q>,
	dry_run: bool,
) -> anyhow::Result<()> {
	let server_state = global.qbt.server_state().await?;

	let Some(to_free) = global.config.target_free.checked_sub(server_state.free_space_on_disk)
		else {
			eprintln!(
				"Have {} free which satisfies target_free of {}, nothing to do.",
				crate::FmtBytes(server_state.free_space_on_disk),
				crate::FmtBytes(global.config.target_free));
			return Ok(())
		};

	eprintln!(
		"Going to free {} to hit target of {} free space.",
		crate::FmtBytes(to_free),
		crate::FmtBytes(global.config.target_free));

	let now = std::time::SystemTime::now();

	let mut groups = global.group(None).await?
		.filter(|g| g.pinned.is_none())
		.collect::<Vec<_>>();
	groups.sort_unstable_by_key(|g| {
		std::cmp::Reverse(crate::F64Ord(g.rank(global)))
	});
	let mut groups = groups.into_iter();

	let mut removed_bytes = 0u64;
	let mut removed_groups = 0usize;
	let mut removed_torrents = 0u64;
	let mut stop_reason = None;

	let mut hashes = Vec::new();
	for g in &mut groups {
		removed_bytes += g.content_size();
		removed_groups += 1;

		eprintln!(
			"--- group {}, torrents: {} ---",
			crate::FmtBytes(g.group_id()),
			g.torrents.len());

		hashes.clear();
		for t in g.torrents {
			let last_active = now.duration_since(t.torrent.last_activity)
				.unwrap_or(std::time::Duration::ZERO);

			eprintln!(
				"DELETE {} {:?} category: {}, tracker: {:?}, seeders: {}, active: {} ratio: {:.02}{}",
				t.torrent.hash,
				t.torrent.name,
				t.torrent.category,
				t.torrent.tracker_host().as_deref().unwrap_or(&t.torrent.tracker),
				t.torrent.seeders,
				crate::FmtDuration(last_active),
				t.torrent.ratio,
				crate::FmtOptional(t.pin_reason.as_deref().map(|r| format!(", reason: {:?}", r))));

			hashes.push(t.torrent.hash);
			removed_torrents += 1;
		}

		if !dry_run {
			global.qbt.delete_torrents(&hashes).await?;
		}

		if removed_bytes >= to_free {
			stop_reason = Some("Met target-free");
			break
		}
		if global.config.delete_limit_data.is_some_and(|l| removed_bytes >= l) {
			stop_reason = Some("Hit delete-limit-data");
			break
		}
		if global.config.delete_limit_groups.is_some_and(|l| removed_groups >= l.get()) {
			stop_reason = Some("Hit delete-limit-groups");
			break
		}
	}

	eprintln!(
		"Deleted {} torrents in {} groups. Approx {} freed.",
		removed_torrents,
		removed_groups,
		crate::FmtBytes(removed_bytes));

	match stop_reason {
		Some(reason) => {
			let remaining = groups.map(|g| g.content_size()).sum();
			eprintln!("{}. {} still available to clean.", reason, crate::FmtBytes(remaining));
		}
		None => eprintln!("No more eligible torrents to reach free space target."),
	}

	Ok(())
}
