use std::{
    collections::{HashMap, HashSet},
    sync::mpsc::Sender,
};

use ql_core::{
    GenericProgress, Instance, IntoIoError, Loader, do_jobs, download,
    json::{InstanceConfigJson, VersionDetails},
    pt,
};
use serde::Deserialize;
use tokio::sync::Mutex;

use crate::store::{
    CurseforgeNotAllowed, DirStructure, ModConfig, ModFile, ModId, ModIndex, QueryType,
    StoreBackendType,
    curseforge::{self, CFSearchResult, CurseforgeFileQuery, ModQuery, get_query_type},
};

use super::PackError;

#[derive(Deserialize)]
pub struct PackIndex {
    minecraft: PackMinecraft,
    name: String,
    files: Vec<PackFile>,
    pub overrides: String,
}

#[derive(Deserialize)]
#[allow(non_snake_case)]
pub struct PackMinecraft {
    version: String,
    modLoaders: Vec<PackLoader>,
    // No one asked for your recommendation bro:
    // pub recommendedRam: usize
}

#[derive(Deserialize)]
pub struct PackLoader {
    id: String,
    // pub primary: bool,
}

#[derive(Deserialize)]
#[allow(non_snake_case)]
pub struct PackFile {
    projectID: i32,
    fileID: usize,
    required: bool,
}

impl PackFile {
    async fn download(
        &self,
        not_allowed: &Mutex<HashSet<CurseforgeNotAllowed>>,
        dirs: &DirStructure,
        sender: Option<&Sender<GenericProgress>>,
        (i, len): (&Mutex<usize>, usize),
        cache: &HashMap<i32, curseforge::Mod>,
        index: &Mutex<ModIndex>,
    ) -> Result<(), PackError> {
        if !self.required {
            return Ok(());
        }

        let mod_info = if let Some(n) = cache.get(&self.projectID) {
            n.clone()
        } else {
            ModQuery::load(self.projectID).await?.data
        };

        let query = CurseforgeFileQuery::load(&self.projectID, self.fileID as i32).await?;
        let query_type = get_query_type(mod_info.class_id).await?;
        let Some(url) = query.data.downloadUrl.clone() else {
            self.add_to_not_allowed(not_allowed, mod_info, query, query_type)
                .await;
            return Ok(());
        };

        let path = dirs.get(query_type)?.join(&query.data.fileName);
        if path.is_file() {
            let metadata = tokio::fs::metadata(&path).await.path(&path)?;
            let got_len = metadata.len();
            if query.data.fileLength == got_len {
                pt!("Already installed {}, skipping", mod_info.name);
                return Ok(());
            }
        }

        download(&url).user_agent_ql().path(&path).await?;
        add_to_index(index, self.projectID.to_string(), &mod_info, query, url).await;

        send_progress(sender, i, len, &mod_info).await;
        Ok(())
    }

    async fn add_to_not_allowed(
        &self,
        not_allowed: &Mutex<HashSet<CurseforgeNotAllowed>>,
        mod_info: curseforge::Mod,
        query: CurseforgeFileQuery,
        query_type: QueryType,
    ) {
        not_allowed.lock().await.insert(CurseforgeNotAllowed {
            name: mod_info.name,
            slug: mod_info.slug,
            file_id: self.fileID,
            project_type: query_type.to_curseforge_str().to_owned(),
            filename: query.data.fileName,
        });
    }
}

async fn add_to_index(
    index: &Mutex<ModIndex>,
    project_id: String,
    mod_info: &curseforge::Mod,
    query: CurseforgeFileQuery,
    url: String,
) {
    let mut index = index.lock().await;
    let project_id = ModId::Curseforge(project_id);
    if !index.mods.contains_key(&project_id) {
        index.mods.insert(
            project_id.clone(),
            ModConfig {
                name: mod_info.name.clone(),
                manually_installed: true,
                installed_version: query.data.displayName.clone(),
                version_release_time: query.data.fileDate.clone(),
                enabled: true,
                description: mod_info.summary.clone(),
                icon_url: mod_info.logo.clone().map(|n| n.url),
                project_source: StoreBackendType::Curseforge,
                project_id,
                files: vec![ModFile {
                    url,
                    filename: query.data.fileName,
                    primary: true,
                }],
                supported_versions: query
                    .data
                    .gameVersions
                    .iter()
                    .filter(|n| n.contains('.'))
                    .cloned()
                    .collect(),
                dependencies: HashSet::new(),
                dependents: HashSet::new(),
            },
        );
    }
}

async fn send_progress(
    sender: Option<&Sender<GenericProgress>>,
    i: &Mutex<usize>,
    len: usize,
    mod_info: &curseforge::Mod,
) {
    if let Some(sender) = sender {
        let mut i = i.lock().await;
        _ = sender.send(GenericProgress {
            done: *i,
            total: len,
            message: Some(format!(
                "Modpack: Installed mod (curseforge) ({i}/{len}):\n{}",
                mod_info.name,
                i = *i + 1,
            )),
            has_finished: false,
        });
        pt!(
            "Installed mod (curseforge) ({i}/{len}): {}",
            mod_info.name,
            i = *i + 1,
        );
        *i += 1;
    }
}

pub async fn install(
    instance: &Instance,
    config: &InstanceConfigJson,
    json: &VersionDetails,
    index: &PackIndex,
    sender: Option<&Sender<GenericProgress>>,
) -> Result<HashSet<CurseforgeNotAllowed>, PackError> {
    if json.get_id() != index.minecraft.version {
        return Err(PackError::GameVersion {
            expect: index.minecraft.version.clone(),
            got: json.get_id().to_owned(),
        });
    }

    pt!("CurseForge Modpack: {}", index.name);

    let loader = match config.mod_type {
        Loader::Forge => "forge",
        Loader::Fabric => "fabric",
        Loader::Quilt => "quilt",
        Loader::Neoforge => "neoforge",
        _ => {
            return Err(expect_got_curseforge(index, config));
        }
    };

    if !index
        .minecraft
        .modLoaders
        .iter()
        .any(|n| n.id.starts_with(loader))
    {
        return Err(expect_got_curseforge(index, config));
    }

    let not_allowed = Mutex::new(HashSet::new());
    let len = index.files.len();

    let i = Mutex::new(0);
    let mod_index = Mutex::new(ModIndex::load(instance).await?);
    let dirs = DirStructure::new(instance, json).await?;

    let cache: HashMap<i32, curseforge::Mod> = {
        let project_ids: Vec<String> = index
            .files
            .iter()
            .map(|n| n.projectID.to_string())
            .collect();
        CFSearchResult::get_from_ids(&project_ids)
            .await?
            .data
            .into_iter()
            .map(|n| (n.id, n))
            .collect()
    };

    do_jobs::<(), PackError>(
        index
            .files
            .iter()
            .map(|file| file.download(&not_allowed, &dirs, sender, (&i, len), &cache, &mod_index)),
    )
    .await?;

    mod_index.lock().await.save(instance).await?;

    let not_allowed = not_allowed.lock().await;
    Ok(not_allowed.clone())
}

fn expect_got_curseforge(index: &PackIndex, config: &InstanceConfigJson) -> PackError {
    PackError::Loader {
        expect: index
            .minecraft
            .modLoaders
            .iter()
            .map(|l| l.id.split('-').next().unwrap_or(&l.id))
            .collect::<Vec<&str>>()
            .join(", "),
        got: config.mod_type,
    }
}
