use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::Context;
use mcvm_shared::output::{MCVMOutput, MessageContents, MessageLevel};
use mcvm_shared::translate;
use mcvm_shared::versions::VersionPattern;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::{sync::Semaphore, task::JoinSet};

use crate::io::files::{self, paths::Paths};
use crate::io::update::{UpdateManager, UpdateMethodResult};
use crate::io::{json_from_file, json_to_file};
use crate::net::download::{self, get_transfer_limit};
use crate::util::versions::VersionName;

use super::client_meta::ClientMeta;

/// Structure for the assets index
#[derive(Deserialize, Serialize)]
pub struct AssetIndex {
	/// The map of asset resource locations to index entries
	pub objects: HashMap<String, IndexEntry>,
}

/// A single asset in the index
#[derive(Deserialize, Serialize)]
pub struct IndexEntry {
	/// The hash of the index file
	pub hash: String,
	/// The size of the asset in bytes
	pub size: usize,
}

impl IndexEntry {
	/// Get the hash path for this asset, which is used for the relative location on the filesystem
	/// and the remote server where they are downloaded
	pub fn get_hash_path(&self) -> String {
		format!("{}/{}", &self.hash[..2], self.hash)
	}
}

/// Download assets used by the client, such as game resources and icons.
pub async fn get(
	client_meta: &ClientMeta,
	paths: &Paths,
	version: &VersionName,
	version_list: &[String],
	manager: &UpdateManager,
	client: &Client,
	o: &mut impl MCVMOutput,
) -> anyhow::Result<UpdateMethodResult> {
	let mut out = UpdateMethodResult::new();
	let version_string = version.to_string();
	let indexes_dir = paths.assets.join("indexes");
	files::create_dir(&indexes_dir)?;

	let index_path = indexes_dir.join(version_string + ".json");
	let index_url = &client_meta.asset_index.url;

	let (objects_dir, virtual_dir) = create_dirs(paths, version, version_list)
		.await
		.context("Failed to create directories for assets")?;

	let index = match download_index(index_url, &index_path, manager, client, false).await {
		Ok(val) => val,
		Err(err) => {
			o.display(
				MessageContents::Error(translate!(o, AssetIndexFailed)),
				MessageLevel::Important,
			);
			o.display(
				MessageContents::Error(format!("{}", err)),
				MessageLevel::Important,
			);
			o.display(
				MessageContents::StartProcess(translate!(o, Redownloading)),
				MessageLevel::Important,
			);
			download_index(index_url, &index_path, manager, client, true)
				.await
				.context("Failed to obtain asset index")?
		}
	};

	struct AssetData {
		name: String,
		url: String,
		path: PathBuf,
		virtual_path: Option<PathBuf>,
		size: usize,
	}

	let mut assets_to_download = Vec::new();
	for (name, asset) in index.objects {
		let hash_path = asset.get_hash_path();
		let url = format!("https://resources.download.minecraft.net/{hash_path}");

		let path = objects_dir.join(&hash_path);
		let virtual_path = virtual_dir.as_ref().map(|x| x.join(&hash_path));
		if !manager.should_update_file(&path) {
			if let Some(virtual_path) = &virtual_path {
				if !manager.should_update_file(virtual_path) {
					continue;
				}
			} else {
				continue;
			}
		}

		out.files_updated.insert(path.clone());
		files::create_leading_dirs(&path)?;
		if let Some(virtual_path) = &virtual_path {
			files::create_leading_dirs(virtual_path)?;
		}
		let data = AssetData {
			name,
			url,
			path,
			virtual_path,
			size: asset.size,
		};
		assets_to_download.push(data);
	}
	// Sort downloads by biggest first
	assets_to_download.sort_by_key(|x| std::cmp::Reverse(x.size));

	let count = assets_to_download.len();
	if count > 0 {
		o.display(
			MessageContents::StartProcess(translate!(
				o,
				StartDownloadingAssets,
				"count" = &format!("{count}")
			)),
			MessageLevel::Important,
		);

		o.start_process();
	}

	let mut join = JoinSet::new();
	// Used to limit the number of open file descriptors
	let sem = Arc::new(Semaphore::new(get_transfer_limit()));
	for asset in assets_to_download {
		let client = client.clone();
		let sem = sem.clone();
		let fut = async move {
			let _permit = sem.acquire().await;
			let response = download::bytes(asset.url, &client)
				.await
				.context("Failed to download asset")?;

			// Write JSON as minified to save storage space
			if asset.name.ends_with(".json") {
				let json: serde_json::Value = serde_json::from_slice(&response)
					.context("Failed to deserialize JSON of asset")?;
				json_to_file(&asset.path, &json)
					.context("Failed to write minified JSON asset to file")?;
			} else {
				tokio::fs::write(&asset.path, response)
					.await
					.context("Failed to write asset to file")?;
			}

			if let Some(virtual_path) = asset.virtual_path {
				files::update_hardlink_async(&asset.path, &virtual_path)
					.await
					.context("Failed to hardlink virtual asset")?;
			}
			Ok::<String, anyhow::Error>(asset.name)
		};
		join.spawn(fut);
	}

	if count > 0 {
		o.display(
			MessageContents::Associated(
				Box::new(MessageContents::Progress {
					current: 0,
					total: count as u32,
				}),
				Box::new(MessageContents::Simple(String::new())),
			),
			MessageLevel::Important,
		);
	}
	let mut num_done = 0;
	while let Some(asset) = join.join_next().await {
		let name = asset??;
		num_done += 1;
		o.display(
			MessageContents::Associated(
				Box::new(MessageContents::Progress {
					current: num_done,
					total: count as u32,
				}),
				Box::new(MessageContents::Simple(translate!(
					o,
					DownloadedAsset,
					"asset" = &name
				))),
			),
			MessageLevel::Important,
		);
	}

	o.display(
		MessageContents::Success(translate!(o, FinishDownloadingAssets)),
		MessageLevel::Important,
	);
	o.end_process();

	Ok(out)
}

/// Downloads the asset index which contains all of the assets that need to be downloaded
async fn download_index(
	url: &str,
	path: &Path,
	manager: &UpdateManager,
	client: &Client,
	force: bool,
) -> anyhow::Result<AssetIndex> {
	let index = if manager.allow_offline && !force && path.exists() {
		json_from_file(path).context("Failed to read asset index contents from file")?
	} else {
		let index = download::json(url, client)
			.await
			.context("Failed to download asset index")?;

		json_to_file(path, &index).context("Failed to serialize asset index to a file")?;

		index
	};

	Ok(index)
}

/// Create the directories needed to store assets
async fn create_dirs(
	paths: &Paths,
	version: &VersionName,
	version_list: &[String],
) -> anyhow::Result<(PathBuf, Option<PathBuf>)> {
	let objects_dir = paths.assets.join("objects");
	files::create_dir(&objects_dir)?;
	// Apparently this directory name is used for older game versions
	let virtual_dir =
		if VersionPattern::Before("13w48b".into()).matches_single(version, version_list) {
			Some(get_virtual_dir_path(paths))
		} else {
			None
		};
	Ok((objects_dir, virtual_dir))
}

/// Get the virtual assets directory path
pub fn get_virtual_dir_path(paths: &Paths) -> PathBuf {
	paths.assets.join("virtual").join("legacy")
}
