use chrono::DateTime;
use ini::Ini;
use std::{
    path::Path,
    sync::{Arc, Mutex, mpsc::Sender},
};

use crate::{InstancePackageError, import::OUT_OF, import::pipe_progress};
use ql_core::{
    GenericProgress, Instance, IntoIoError, IntoJsonError, LAUNCHER_DIR, ListEntry, Loader,
    do_jobs, download, err,
    file_utils::{self, exists},
    info,
    jarmod::{JarMod, JarMods},
    json::{
        FabricJSON, InstanceConfigJson, Manifest, V_1_12_2, V_OFFICIAL_FABRIC_SUPPORT,
        VersionDetails,
    },
    pt,
};
use ql_mod_manager::loaders::fabric::{self, get_list_of_versions_from_backend};
use serde::{Deserialize, Serialize};
use tokio::fs;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MmcPack {
    components: Vec<MmcPackComponent>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct MmcPackComponent {
    cachedName: String,
    cachedVersion: Option<String>,
    uid: String,
}

#[derive(Debug, Clone)]
pub struct InstanceRecipe {
    is_lwjgl3: bool,
    mc_version: String,
    loader: Option<Loader>,
    loader_version: Option<String>,
    force_vanilla_launch: bool,
    jarmods: Vec<String>,
}

impl InstanceRecipe {
    async fn setup_lwjgl3(&mut self) -> Result<(), InstancePackageError> {
        async fn adjust_for_lwjgl3(mc_version: &str) -> Result<bool, InstancePackageError> {
            let manifest = Manifest::download().await?;
            if let Some(version) = manifest.find_name(mc_version) {
                if let (Ok(look), Ok(expect)) = (
                    DateTime::parse_from_rfc3339(&version.releaseTime),
                    DateTime::parse_from_rfc3339(V_1_12_2),
                ) {
                    if look <= expect {
                        return Ok(true);
                    }
                }
            }
            Ok(false)
        }

        if self.is_lwjgl3 && adjust_for_lwjgl3(&self.mc_version).await? {
            self.mc_version.push_str("-lwjgl3");
        }
        Ok(())
    }
}

pub async fn import(
    download_assets: bool,
    temp_dir: &Path,
    mmc_pack: &str,
    sender: Option<Arc<Sender<GenericProgress>>>,
) -> Result<Instance, InstancePackageError> {
    info!("Importing MultiMC instance...");
    let mmc_pack: MmcPack = serde_json::from_str(mmc_pack).json(mmc_pack.to_owned())?;

    let ini = read_config_ini(temp_dir).await?;
    let (instance, instance_recipe) =
        tokio::try_join!(get_instance(&ini), get_instance_recipe(&mmc_pack))?;

    create_minecraft_instance(
        download_assets,
        sender.clone(),
        instance.get_name(),
        instance_recipe.mc_version.clone(),
    )
    .await?;

    install_loader(sender.as_deref(), &instance, &instance_recipe).await?;

    copy_files(temp_dir, sender, &instance).await?;

    tokio::try_join!(
        setup_details(&instance),
        async {
            let mut config = InstanceConfigJson::read(&instance).await?;
            setup_config(&ini, &instance_recipe, &mut config);
            config.save(&instance).await?;
            Ok(())
        },
        // Instance notes
        async {
            let notes = general_get(&ini, "notes").unwrap_or_default();
            if !notes.is_empty() {
                ql_instances::notes::write(instance.clone(), notes.to_owned()).await?;
            }
            Ok(())
        },
        async {
            let mut jarmods = JarMods::read(&instance).await?;
            jarmods
                .mods
                .extend(instance_recipe.jarmods.iter().map(|n| JarMod {
                    filename: n.clone(),
                    enabled: true,
                }));
            jarmods.save(&instance).await?;
            Ok(())
        }
    )?;

    info!("Finished importing MultiMC instance");
    Ok(instance)
}

async fn setup_details(instance: &Instance) -> Result<(), InstancePackageError> {
    if exists(&instance.get_instance_path().join("patches/org.lwjgl.json")).await {
        let mut details = VersionDetails::load(instance).await?;
        details.libraries.retain(|lib| {
            if let Some(name) = &lib.name {
                if name.starts_with("org.mcphackers:legacy-lwjgl3:") {
                    pt!("Removing legacy-lwjgl3 for compatibility reasons...");
                    return false;
                }
            }
            true
        });
        details.save(instance).await?;
    }
    Ok(())
}

fn setup_config(ini: &Ini, instance_recipe: &InstanceRecipe, config: &mut InstanceConfigJson) {
    if instance_recipe.force_vanilla_launch {
        config.main_class_override = Some("net.minecraft.client.Minecraft".to_owned());
    }
    // TODO: `LaunchMaximized: bool`

    if let Ok(win_height) = general_get(ini, "MinecraftWinHeight") {
        if let Ok(height) = win_height.parse::<u32>() {
            config.c_global_settings().window_height = Some(height);
        }
    }
    if let Ok(win_width) = general_get(ini, "MinecraftWinWidth") {
        if let Ok(width) = win_width.parse::<u32>() {
            config.c_global_settings().window_width = Some(width);
        }
    }

    if let Ok(jvmargs) = general_get(ini, "JvmArgs") {
        config
            .java_args
            .get_or_insert_default()
            .extend(jvmargs.split_whitespace().map(str::to_owned));
    }

    if let Ok(prefix) = general_get(ini, "WrapperCommand") {
        config.c_global_settings().pre_launch_prefix = Some(
            prefix
                .split_whitespace()
                .filter(|n| !n.is_empty())
                .map(str::to_owned)
                .collect(),
        );
    }
}

fn general_get<'a>(ini: &'a Ini, key: &str) -> Result<&'a str, InstancePackageError> {
    ini.get_from(Some("General"), key)
        .or(ini.get_from(None::<String>, key))
        .ok_or_else(|| InstancePackageError::IniFieldMissing("General".to_owned(), key.to_owned()))
}

async fn get_instance(ini: &Ini) -> Result<Instance, InstancePackageError> {
    let mut instance_name = general_get(ini, "name")?.to_owned();

    // If `MyInstance` exists, try `MyInstance (1)`, `(2)`...
    let instance_dir = LAUNCHER_DIR.join("instances");
    let mut path = instance_dir.join(&instance_name);

    if fs::try_exists(&path).await.path(&path)? {
        let mut name_i = 1;
        let mut name = String::new();
        while fs::try_exists(&path).await.path(&path)? {
            name = format!("{instance_name} ({name_i})");
            path = instance_dir.join(&name);
            name_i += 1;
        }
        instance_name = name;
    }

    Ok(Instance::client(&instance_name))
}

async fn read_config_ini(temp_dir: &Path) -> Result<Ini, InstancePackageError> {
    let ini_path = temp_dir.join("instance.cfg");
    let ini = fs::read_to_string(&ini_path).await.path(ini_path)?;
    Ok(Ini::load_from_str(&filter_bytearray(&ini))?)
}

async fn get_instance_recipe(mmc_pack: &MmcPack) -> Result<InstanceRecipe, InstancePackageError> {
    let mut recipe = InstanceRecipe {
        is_lwjgl3: false,
        mc_version: "(MultiMC) Couldn't find minecraft version".to_owned(),
        loader: None,
        loader_version: None,
        force_vanilla_launch: false,
        jarmods: Vec::new(),
    };

    for component in &mmc_pack.components {
        if component.uid.starts_with("custom.jarmod.") {
            recipe.force_vanilla_launch = true; // ?
        }
        let version = component.cachedVersion.clone().unwrap_or_default();

        match component.cachedName.as_str() {
            "Minecraft" => recipe.mc_version.clone_from(&version),

            "Forge" => {
                recipe.loader = Some(Loader::Forge);
                recipe.loader_version = Some(version);
            }
            "NeoForge" => {
                recipe.loader = Some(Loader::Neoforge);
                recipe.loader_version = Some(version);
            }
            "Fabric Loader" => {
                recipe.loader = Some(Loader::Fabric);
                recipe.loader_version = Some(version);
            }
            "Quilt Loader" => {
                recipe.loader = Some(Loader::Quilt);
                recipe.loader_version = Some(version);
            }

            "LWJGL 3" => recipe.is_lwjgl3 = true,

            "LWJGL 2" | "Intermediary Mappings" => {}
            name if name.contains("(jar mod)") => {
                if let Some(jarmod_filename) = component.uid.split('.').next_back() {
                    recipe.jarmods.push(format!("{jarmod_filename}.jar"));
                }
            }
            name => err!("Unknown MultiMC Component: {name}"),
        }
    }

    recipe.setup_lwjgl3().await?;
    Ok(recipe)
}

async fn install_loader(
    sender: Option<&Sender<GenericProgress>>,
    instance: &Instance,
    instance_recipe: &InstanceRecipe,
) -> Result<(), InstancePackageError> {
    if let Some(loader) = instance_recipe.loader {
        match loader {
            n @ (Loader::Fabric | Loader::Quilt) => {
                install_fabric(
                    sender,
                    instance,
                    instance_recipe.loader_version.clone(),
                    matches!(n, Loader::Quilt),
                )
                .await?;
            }
            n @ (Loader::Forge | Loader::Neoforge) => {
                mmc_forge(
                    sender,
                    instance,
                    instance_recipe.loader_version.clone(),
                    matches!(n, Loader::Neoforge),
                )
                .await?;
            }
            loader => {
                err!("Unimplemented MultiMC Component: {loader:?}");
            }
        }
    }
    Ok(())
}

async fn install_fabric(
    sender: Option<&Sender<GenericProgress>>,
    instance_selection: &Instance,
    version: Option<String>,
    is_quilt: bool,
) -> Result<(), InstancePackageError> {
    let backend = if is_quilt {
        fabric::BackendType::Quilt
    } else {
        fabric::BackendType::Fabric
    };

    let version_json = VersionDetails::load(instance_selection).await?;
    if !version_json.is_before_or_eq(V_OFFICIAL_FABRIC_SUPPORT) {
        fabric::install(version, instance_selection.clone(), sender, backend).await?;
        return Ok(());
    }

    // Hack for versions below 1.14
    let url = format!(
        "https://{}/versions/loader/1.14.4/{}/profile/json",
        if is_quilt {
            "meta.quiltmc.org/v3"
        } else {
            "meta.fabricmc.net/v2"
        },
        if let Some(version) = version.clone() {
            version
        } else {
            // Using 1.14.4 just to get the overall list of versions.
            get_list_of_versions_from_backend("1.14.4", backend, ql_core::InstanceKind::Client)
                .await?
                .first()
                .map_or_else(
                    || " No versions found! ".to_owned(),
                    |n| n.loader.version.clone(),
                )
        }
    );
    let fabric_json_text = file_utils::download_file_to_string(&url, false).await?;
    let fabric_json: FabricJSON =
        serde_json::from_str(&fabric_json_text).json(fabric_json_text.clone())?;

    let instance_path = instance_selection.get_instance_path();
    let libraries_dir = instance_path.join("libraries");

    info!("Custom fabric implementation, installing libraries:");
    let i = Mutex::new(0);
    let len = fabric_json.libraries.len();
    do_jobs(fabric_json.libraries.iter().map(|library| async {
        if library.name.starts_with("net.fabricmc:intermediary") {
            return Ok::<_, InstancePackageError>(());
        }
        let path_str = library.get_path();
        let Some(url) = library.get_url() else {
            return Ok::<_, InstancePackageError>(());
        };
        let path = libraries_dir.join(&path_str);

        let parent_dir = path
            .parent()
            .ok_or(InstancePackageError::PathBufParent(path.clone()))?;
        tokio::fs::create_dir_all(parent_dir)
            .await
            .path(parent_dir)?;
        download(&url).path(&path).await?;

        {
            let mut i = i.lock().unwrap();
            *i += 1;
            pt!(
                "({i}/{len}) {}\n    Path: {path_str}\n    Url: {url}",
                library.name
            );
            if let Some(sender) = sender {
                _ = sender.send(GenericProgress {
                    done: *i,
                    total: len,
                    message: Some(format!("Installing fabric: library {}", library.name)),
                    has_finished: false,
                });
            }
        }

        Ok(())
    }))
    .await?;

    let mut config = InstanceConfigJson::read(instance_selection).await?;
    config.main_class_override = Some(fabric_json.mainClass.clone());
    config.mod_type = Loader::Fabric;
    config.save(instance_selection).await?;

    let fabric_json_path = instance_path.join("fabric.json");
    tokio::fs::write(&fabric_json_path, &fabric_json_text)
        .await
        .path(&fabric_json_path)?;
    Ok(())
}

async fn copy_files(
    temp_dir: &Path,
    sender: Option<Arc<Sender<GenericProgress>>>,
    instance_selection: &Instance,
) -> Result<(), InstancePackageError> {
    let src = temp_dir.join("minecraft");
    if src.is_dir() {
        let dst = instance_selection.get_dot_minecraft_path();
        if let Some(sender) = sender.as_deref() {
            _ = sender.send(GenericProgress {
                done: 2,
                total: OUT_OF,
                message: Some("Copying files...".to_owned()),
                has_finished: false,
            });
        }
        file_utils::copy_dir_recursive(&src, &dst).await?;
    }

    copy_folder_over(temp_dir, instance_selection, "jarmods").await?;
    copy_folder_over(temp_dir, instance_selection, "patches").await?;

    Ok(())
}

async fn copy_folder_over(
    temp_dir: &Path,
    instance_selection: &Instance,
    path: &'static str,
) -> Result<(), InstancePackageError> {
    let src = temp_dir.join(path);
    if src.is_dir() {
        let dst = instance_selection.get_instance_path().join(path);
        file_utils::copy_dir_recursive(&src, &dst).await?;
    }
    Ok(())
}

async fn create_minecraft_instance(
    download_assets: bool,
    sender: Option<Arc<Sender<GenericProgress>>>,
    instance_name: &str,
    version: String,
) -> Result<(), InstancePackageError> {
    let version = ListEntry::new(version);
    let (d_send, d_recv) = std::sync::mpsc::channel();
    if let Some(sender) = sender.clone() {
        std::thread::spawn(move || {
            pipe_progress(d_recv, &sender);
        });
    }
    ql_instances::create_instance(
        instance_name.to_owned(),
        version,
        Some(d_send),
        download_assets,
    )
    .await?;
    Ok(())
}

async fn mmc_forge(
    sender: Option<&Sender<GenericProgress>>,
    instance_selection: &Instance,
    version: Option<String>,
    is_neoforge: bool,
) -> Result<(), InstancePackageError> {
    let (f_send, f_recv) = std::sync::mpsc::channel();
    if let Some(sender) = sender.cloned() {
        std::thread::spawn(move || {
            pipe_progress(f_recv, &sender);
        });
    }
    if is_neoforge {
        ql_mod_manager::loaders::neoforge::install(
            version,
            instance_selection.clone(),
            Some(f_send),
            None, // TODO: Java install progress
        )
        .await?;
    } else {
        ql_mod_manager::loaders::forge::install(
            version,
            instance_selection.clone(),
            Some(f_send),
            None, // TODO: Java install progress
        )
        .await?;
    }
    Ok(())
}

fn filter_bytearray(input: &str) -> String {
    // PrismLauncher puts some weird ByteArray
    // field in the INI config file, that `ini`
    // doesn't understand. So we have to filter it out.
    input
        .lines()
        .filter(|n| !n.contains("\\Columns=@ByteArray"))
        .collect::<Vec<_>>()
        .join("\n")
}
