//! Logic for launching Minecraft
use crate::data::ModLoader;
use crate::event::emit::{emit_instance, emit_loading, init_loading};
use crate::event::{InstancePayloadType, LoadingBarType};
use crate::install::{
    InstallJavaStep, InstallPhaseDetails, InstallPhaseId, InstallProgress,
    InstallProgressReporter,
};
use crate::instance::QuickPlayType;
use crate::launcher::download::download_log_config;
use crate::launcher::io::IOError;
use crate::launcher::quick_play_version::{
    QuickPlayServerVersion, QuickPlayVersion,
};
use crate::server_address::{ServerAddress, parse_server_address};
use crate::state::server_join_log::JoinLogEntry;
use crate::state::{
    Credentials, InstanceInstallStage, InstanceLaunchContext, InstanceLink,
    JavaVersion, MemorySettings, ProcessMetadata, WindowSize,
};
use crate::util::io;
use crate::util::rpc::RpcServerBuilder;
use crate::{State, get_resource_file, process};
use chrono::Utc;
use daedalus as d;
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
use daedalus::modded::{LoaderVersion, Manifest};
use regex::Regex;
use serde::Deserialize;
use std::fmt::Write;
use std::path::PathBuf;
use tokio::process::Command;

mod args;
pub(crate) mod hooks;

pub mod download;
pub mod quick_play_version;

// All nones -> disallowed
// 1+ true -> allowed
// 1+ false -> disallowed
#[tracing::instrument]
pub fn parse_rules(
    rules: &[d::minecraft::Rule],
    java_version: &str,
    quick_play_type: &QuickPlayType,
    minecraft_updated: bool,
) -> bool {
    let mut x = rules
        .iter()
        .map(|x| {
            parse_rule(x, java_version, quick_play_type, minecraft_updated)
        })
        .collect::<Vec<Option<bool>>>();

    if rules
        .iter()
        .all(|x| matches!(x.action, RuleAction::Disallow))
    {
        x.push(Some(true))
    }

    !(x.iter().any(|x| x == &Some(false)) || x.iter().all(|x| x.is_none()))
}

// if anything is disallowed, it should NOT be included
// if anything is not disallowed, it shouldn't factor in final result
// if anything is not allowed, it should NOT be included
// if anything is allowed, it should be included
#[tracing::instrument]
pub fn parse_rule(
    rule: &d::minecraft::Rule,
    java_version: &str,
    quick_play_type: &QuickPlayType,
    minecraft_updated: bool,
) -> Option<bool> {
    use d::minecraft::{Rule, RuleAction};

    let res = match rule {
        Rule { os: Some(os), .. } => {
            crate::util::platform::os_rule(os, java_version, minecraft_updated)
        }
        Rule {
            features: Some(features),
            ..
        } => {
            !features.is_demo_user.unwrap_or(true)
                || features.has_custom_resolution.unwrap_or(false)
                || !features.has_quick_plays_support.unwrap_or(true)
                || (features.is_quick_play_singleplayer.unwrap_or(false)
                    && matches!(
                        quick_play_type,
                        QuickPlayType::Singleplayer(_)
                    ))
                || (features.is_quick_play_multiplayer.unwrap_or(false)
                    && matches!(quick_play_type, QuickPlayType::Server(..)))
                || !features.is_quick_play_realms.unwrap_or(true)
        }
        _ => return Some(true),
    };

    match rule.action {
        RuleAction::Allow => {
            if res {
                Some(true)
            } else {
                Some(false)
            }
        }
        RuleAction::Disallow => {
            if res {
                Some(false)
            } else {
                None
            }
        }
    }
}

macro_rules! processor_rules {
    ($dest:expr; $($name:literal : client => $client:expr, server => $server:expr;)+) => {
        $(std::collections::HashMap::insert(
            $dest,
            String::from($name),
            daedalus::modded::SidedDataEntry {
                client: String::from($client),
                server: String::from($server),
            },
        );)+
    }
}

pub async fn get_java_version_from_launch_context(
    context: &InstanceLaunchContext,
    version_info: &VersionInfo,
) -> crate::Result<Option<JavaVersion>> {
    if let Some(java) = context.launch_overrides.java_path.as_ref() {
        let java =
            crate::api::jre::check_jre(std::path::PathBuf::from(java)).await;

        if let Ok(java) = java {
            return Ok(Some(java));
        }
    }

    let key = version_info
        .java_version
        .as_ref()
        .map_or(8, |it| it.major_version);

    let state = State::get().await?;

    let java_version = JavaVersion::get(key, &state.pool).await?;

    Ok(java_version)
}

pub async fn get_loader_version_from_profile(
    game_version: &str,
    loader: ModLoader,
    loader_version: Option<&str>,
) -> crate::Result<Option<LoaderVersion>> {
    if loader == ModLoader::Vanilla {
        return Ok(None);
    }

    let version = loader_version.unwrap_or("latest");

    let filter = |it: &LoaderVersion| match version {
        "latest" => true,
        "stable" => it.stable,
        id => it.id == *id,
    };

    let versions =
        crate::api::metadata::get_loader_versions(loader.as_meta_str()).await?;

    if let Some(loaders) =
        loader_versions_for_game_version(&versions, game_version)
    {
        let loader_version =
            loaders
                .iter()
                .find(|x| filter(x))
                .or(if version == "stable" {
                    loaders.first()
                } else {
                    None
                });

        Ok(loader_version.cloned())
    } else {
        Ok(None)
    }
}

fn loader_versions_for_game_version<'a>(
    manifest: &'a Manifest,
    game_version: &str,
) -> Option<&'a [LoaderVersion]> {
    let version = manifest.game_versions.iter().find(|x| {
        x.id.replace(daedalus::modded::DUMMY_REPLACE_STRING, game_version)
            == game_version
    })?;

    if let Some(version_group) = &version.version_group {
        manifest
            .version_groups
            .iter()
            .find(|group| group.id == *version_group)
            .map(|group| group.loaders.as_slice())
    } else {
        Some(version.loaders.as_slice())
    }
}

pub(crate) async fn resolve_java_for_launch(
    context: &InstanceLaunchContext,
) -> crate::Result<JavaVersion> {
    let state = State::get().await?;
    let content_set = &context.applied_content_set;
    let (minecraft, version_index) =
        resolve_minecraft_manifest(&content_set.game_version, &state).await?;
    let version = &minecraft.versions[version_index];

    let mut loader_version = get_loader_version_from_profile(
        &content_set.game_version,
        content_set.loader,
        content_set.loader_version.as_deref(),
    )
    .await?;

    if content_set.loader != ModLoader::Vanilla && loader_version.is_none() {
        loader_version = get_loader_version_from_profile(
            &content_set.game_version,
            content_set.loader,
            Some("stable"),
        )
        .await?;
    }

    let version_info = download::download_version_info(
        &state,
        version,
        loader_version.as_ref(),
        None,
        None,
        None,
    )
    .await?;

    let key = version_info
        .java_version
        .as_ref()
        .map_or(8, |it| it.major_version);
    let (java_path, set_java) = if let Some(java_version) =
        get_java_version_from_launch_context(context, &version_info).await?
    {
        (PathBuf::from(java_version.path), false)
    } else {
        (crate::api::jre::auto_install_java(key).await?, true)
    };

    let java_version = crate::api::jre::check_jre(java_path).await?;

    if set_java {
        java_version.upsert(&state.pool).await?;
    }

    Ok(java_version)
}

/// Resolves the Minecraft version manifest and finds the index for the given
/// game version. If the version isn't found in the cache, forces a manifest
/// refresh to pick up newly-released versions.
pub async fn resolve_minecraft_manifest(
    game_version: &str,
    state: &State,
) -> crate::Result<(d::minecraft::VersionManifest, usize)> {
    let minecraft = crate::api::metadata::get_minecraft_versions().await?;

    if let Some(idx) = minecraft
        .versions
        .iter()
        .position(|it| it.id == game_version)
    {
        return Ok((minecraft, idx));
    }

    // Version not found in cache — force a manifest refresh in case it was
    // released after the cache was populated.
    let refreshed = crate::state::CachedEntry::get_minecraft_manifest(
        Some(crate::state::CacheBehaviour::MustRevalidate),
        &state.pool,
        &state.api_semaphore,
    )
    .await?
    .ok_or_else(|| {
        crate::ErrorKind::NoValueFor("minecraft versions".to_string())
    })?;

    let idx = refreshed
        .versions
        .iter()
        .position(|it| it.id == game_version)
        .ok_or(crate::ErrorKind::LauncherError(format!(
            "Invalid game version: {game_version}"
        )))?;

    Ok((refreshed, idx))
}

async fn get_instance_full_path(instance_path: &str) -> crate::Result<PathBuf> {
    let state = State::get().await?;
    let instances_dir = state.directories.instances_dir();
    let full_path = io::canonicalize(instances_dir.join(instance_path))?;
    Ok(full_path)
}

pub async fn install_minecraft_with_reporter(
    context: &InstanceLaunchContext,
    repairing: bool,
    reporter: Option<InstallProgressReporter>,
) -> crate::Result<()> {
    let instance = &context.instance;
    let content_set = &context.applied_content_set;
    let phase_details = InstallPhaseDetails::Minecraft {
        game_version: content_set.game_version.clone(),
        loader: content_set.loader,
    };
    let loading_bar = if reporter.is_none() {
        Some(
            init_loading(
                LoadingBarType::MinecraftDownload {
                    // If we are downloading minecraft for a profile, provide its name and uuid
                    instance_name: instance.name.clone(),
                    instance_id: instance.id.clone(),
                },
                100.0,
                "Downloading Minecraft",
            )
            .await?,
        )
    } else {
        None
    };

    let state = State::get().await?;
    let previous_install_stage = instance.install_stage;

    crate::state::instances::commands::set_instance_install_stage(
        &instance.id,
        InstanceInstallStage::MinecraftInstalling,
        &state.pool,
    )
    .await?;
    emit_instance(&instance.id, InstancePayloadType::Edited).await?;

    let result = async {
    let instance_path = get_instance_full_path(&instance.path).await?;
    if let Some(reporter) = &reporter {
        reporter
            .update(
                InstallPhaseId::ResolvingMinecraft,
                None,
                phase_details.clone(),
            )
            .await?;
    }
    let (minecraft, version_index) =
        resolve_minecraft_manifest(&content_set.game_version, &state).await?;
    let version = &minecraft.versions[version_index];
    let minecraft_updated = version_index
        <= minecraft
            .versions
            .iter()
            .position(|x| x.id == "22w16a")
            .unwrap_or(0);

    if content_set.loader != ModLoader::Vanilla
        && let Some(reporter) = &reporter
    {
        reporter
            .update(
                InstallPhaseId::ResolvingLoader,
                None,
                phase_details.clone(),
            )
            .await?;
    }

    let mut loader_version = get_loader_version_from_profile(
        &content_set.game_version,
        content_set.loader,
        content_set.loader_version.as_deref(),
    )
    .await?;

    // If no loader version is selected, try to select the stable version!
    if content_set.loader != ModLoader::Vanilla && loader_version.is_none() {
        loader_version = get_loader_version_from_profile(
            &content_set.game_version,
            content_set.loader,
            Some("stable"),
        )
        .await?;

        crate::state::instances::commands::set_applied_content_set_loader_version(
            &instance.id,
            loader_version.as_ref().map(|x| x.id.as_str()),
            &state.pool,
        )
        .await?;
    }

    let version_jar =
        loader_version.as_ref().map_or(version.id.clone(), |it| {
            format!("{}-{}", version.id.clone(), it.id.clone())
        });

    // Download version info (5)
    let mut version_info = download::download_version_info(
        &state,
        version,
        loader_version.as_ref(),
        Some(repairing),
        loading_bar.as_ref(),
        reporter.as_ref(),
    )
    .await?;

    let key = version_info
        .java_version
        .as_ref()
        .map_or(8, |it| it.major_version);
    if let Some(reporter) = &reporter {
        reporter
            .update(
                InstallPhaseId::PreparingJava,
                Some(InstallProgress {
                    current: 0,
                    total: 4,
                    secondary: None,
                }),
                InstallPhaseDetails::Java {
                    major_version: key,
                    step: InstallJavaStep::Resolving,
                },
            )
            .await?;
    }
    let (java_version, set_java) = if let Some(java_version) =
        get_java_version_from_launch_context(context, &version_info).await?
    {
        (std::path::PathBuf::from(java_version.path), false)
    } else {
        let path = if let Some(reporter) = &reporter {
            crate::api::jre::auto_install_java_with_reporter(
                key,
                reporter.clone(),
            )
            .await?
        } else {
            crate::api::jre::auto_install_java_with_loading(key, true).await?
        };

        (path, true)
    };

    // Test jre version
    if let Some(reporter) = &reporter {
        reporter
            .update(
                InstallPhaseId::PreparingJava,
                Some(InstallProgress {
                    current: 4,
                    total: 4,
                    secondary: None,
                }),
                InstallPhaseDetails::Java {
                    major_version: key,
                    step: InstallJavaStep::Validating,
                },
            )
            .await?;
    }
    let java_version = crate::api::jre::check_jre(java_version.clone()).await?;

    if set_java {
        java_version.upsert(&state.pool).await?;
    }

    // Download minecraft (5-90)
    if let Some(reporter) = &reporter {
        reporter
            .update(
                InstallPhaseId::DownloadingMinecraft,
                None,
                phase_details.clone(),
            )
            .await?;
    }
    download::download_minecraft(
        &state,
        &version_info,
        loading_bar.as_ref(),
        &java_version.architecture,
        repairing,
        minecraft_updated,
        reporter.clone(),
        phase_details.clone(),
    )
    .await?;

    let client_path = state
        .directories
        .version_dir(&version_jar)
        .join(format!("{version_jar}.jar"));
    if let Some(processors) = &version_info.processors {
        let libraries_dir = state.directories.libraries_dir();

        if let Some(ref mut data) = version_info.data {
            processor_rules! {
                data;
                "SIDE":
                    client => "client",
                    server => "";
                "MINECRAFT_JAR" :
                    client => client_path.to_string_lossy(),
                    server => "";
                "MINECRAFT_VERSION":
                    client => content_set.game_version.clone(),
                    server => "";
                "ROOT":
                    client => instance_path.to_string_lossy(),
                    server => "";
                "LIBRARY_DIR":
                    client => libraries_dir.to_string_lossy(),
                    server => "";
            }

            if let Some(loading_bar) = &loading_bar {
                emit_loading(
                    loading_bar,
                    0.0,
                    Some("Running forge processors"),
                )?;
            }
            let total_length = processors.len();
            if let Some(reporter) = &reporter {
                reporter
                    .update(
                        InstallPhaseId::RunningLoaderProcessors,
                        Some(InstallProgress {
                            current: 0,
                            total: total_length as u64,
                            secondary: None,
                        }),
                        phase_details.clone(),
                    )
                    .await?;
            }

            // Forge processors (90-100)
            for (index, processor) in processors.iter().enumerate() {
                if let Some(sides) = &processor.sides
                    && !sides.contains(&String::from("client"))
                {
                    if let Some(reporter) = &reporter {
                        reporter
                            .update(
                                InstallPhaseId::RunningLoaderProcessors,
                                Some(InstallProgress {
                                    current: (index + 1) as u64,
                                    total: total_length as u64,
                                    secondary: None,
                                }),
                                phase_details.clone(),
                            )
                            .await?;
                    }
                    continue;
                }

                let cp = {
                    let mut cp = processor.classpath.clone();
                    cp.push(processor.jar.clone());
                    cp
                };

                let child = Command::new(&java_version.path)
                    .arg("-cp")
                    .arg(args::get_class_paths_jar(
                        &libraries_dir,
                        &cp,
                        &java_version.architecture,
                    )?)
                    .arg(
                        args::get_processor_main_class(args::get_lib_path(
                            &libraries_dir,
                            &processor.jar,
                            false,
                        )?)
                        .await?
                        .ok_or_else(|| {
                            crate::ErrorKind::LauncherError(format!(
                                "Could not find processor main class for {}",
                                processor.jar
                            ))
                        })?,
                    )
                    .args(args::get_processor_arguments(
                        &libraries_dir,
                        &processor.args,
                        data,
                    )?)
                    .output()
                    .await
                    .map_err(|e| IOError::with_path(e, &java_version.path))
                    .map_err(|err| {
                        crate::ErrorKind::LauncherError(format!(
                            "Error running processor: {err}",
                        ))
                    })?;

                if !child.status.success() {
                    return Err(crate::ErrorKind::LauncherError(format!(
                        "Processor error: {}",
                        String::from_utf8_lossy(&child.stderr)
                    ))
                    .as_error());
                }

                if let Some(loading_bar) = &loading_bar {
                    emit_loading(
                        loading_bar,
                        30.0 / total_length as f64,
                        Some(&format!(
                            "Running forge processor {index}/{total_length}"
                        )),
                    )?;
                }
                if let Some(reporter) = &reporter {
                    reporter
                        .update(
                            InstallPhaseId::RunningLoaderProcessors,
                            Some(InstallProgress {
                                current: (index + 1) as u64,
                                total: total_length as u64,
                                secondary: None,
                            }),
                            phase_details.clone(),
                        )
                        .await?;
                }
            }
        }
    }

    let protocol_version = read_protocol_version_from_jar(client_path).await?;

    crate::state::instances::commands::set_applied_content_set_protocol_version(
        &instance.id,
        protocol_version,
        &state.pool,
    )
    .await?;
	if reporter.is_none() {
		crate::state::instances::commands::set_instance_install_stage(
			&instance.id,
			InstanceInstallStage::Installed,
			&state.pool,
		)
		.await?;
		emit_instance(&instance.id, InstancePayloadType::Edited).await?;
	}
    if let Some(loading_bar) = &loading_bar {
        emit_loading(loading_bar, 1.0, Some("Finished installing"))?;
    }

    Ok::<(), crate::Error>(())
    }
    .await;

    if result.is_err() {
        if let Err(error) =
            crate::state::instances::commands::set_instance_install_stage(
                &instance.id,
                previous_install_stage,
                &state.pool,
            )
            .await
        {
            tracing::error!(
                "Failed to restore install stage for instance {}: {error}",
                instance.id
            );
        } else if let Err(error) =
            emit_instance(&instance.id, InstancePayloadType::Edited).await
        {
            tracing::error!(
                "Failed to emit restored install stage for instance {}: {error}",
                instance.id
            );
        }
    }

    result
}

pub async fn install_minecraft_for_instance_id_with_reporter(
    instance_id: &str,
    repairing: bool,
    reporter: Option<InstallProgressReporter>,
) -> crate::Result<()> {
    let state = State::get().await?;
    let context =
        crate::state::instances::commands::get_instance_launch_context(
            instance_id,
            &state.pool,
        )
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::OtherError(format!(
                "Tried to install a nonexistent or unloaded instance {instance_id}!"
            ))
        })?;

    install_minecraft_with_reporter(&context, repairing, reporter).await
}

pub async fn read_protocol_version_from_jar(
    path: PathBuf,
) -> crate::Result<Option<u32>> {
    let zip = async_zip::tokio::read::fs::ZipFileReader::new(path).await?;
    let Some(entry_index) = zip
        .file()
        .entries()
        .iter()
        .position(|x| matches!(x.filename().as_str(), Ok("version.json")))
    else {
        return Ok(None);
    };

    #[derive(Deserialize, Debug)]
    struct VersionData {
        protocol_version: Option<u32>,
    }

    let mut data = vec![];
    zip.reader_with_entry(entry_index)
        .await?
        .read_to_end_checked(&mut data)
        .await?;
    let data: VersionData = serde_json::from_slice(&data)?;

    Ok(data.protocol_version)
}

fn link_project_and_version(
    link: &InstanceLink,
) -> (Option<&String>, Option<&String>) {
    match link {
        InstanceLink::ModrinthModpack {
            project_id,
            version_id,
        } => (Some(project_id), Some(version_id)),
        InstanceLink::ServerProject { project_id } => (Some(project_id), None),
        InstanceLink::ServerProjectModpack {
            server_project_id,
            content_version_id,
            ..
        } => (Some(server_project_id), Some(content_version_id)),
        InstanceLink::ImportedModpack {
            project_id,
            version_id,
            ..
        } => (project_id.as_ref(), version_id.as_ref()),
        InstanceLink::Unmanaged
        | InstanceLink::ModrinthHosting { .. }
        | InstanceLink::SharedInstance { .. } => (None, None),
    }
}

#[tracing::instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn launch_minecraft(
    java_args: &[String],
    env_args: &[(String, String)],
    mc_set_options: &[(String, String)],
    wrapper: &Option<String>,
    memory: &MemorySettings,
    resolution: &WindowSize,
    credentials: &Credentials,
    post_exit_hook: Option<String>,
    context: &InstanceLaunchContext,
    mut quick_play_type: QuickPlayType,
) -> crate::Result<ProcessMetadata> {
    let instance = &context.instance;
    let content_set = &context.applied_content_set;

    if instance.install_stage == InstanceInstallStage::PackInstalling
        || instance.install_stage == InstanceInstallStage::MinecraftInstalling
    {
        return Err(crate::ErrorKind::LauncherError(
            "Instance is still installing".to_string(),
        )
        .into());
    }

    if instance.install_stage != InstanceInstallStage::Installed {
        return Err(crate::ErrorKind::LauncherError(
            "Instance is not installed; start an install job first".to_string(),
        )
        .into());
    }

    let state = State::get().await?;

    let instance_path = get_instance_full_path(&instance.path).await?;

    let (minecraft, version_index) =
        resolve_minecraft_manifest(&content_set.game_version, &state).await?;
    let version = &minecraft.versions[version_index];
    let minecraft_updated = version_index
        <= minecraft
            .versions
            .iter()
            .position(|x| x.id == "22w16a")
            .unwrap_or(0);

    let loader_version = get_loader_version_from_profile(
        &content_set.game_version,
        content_set.loader,
        content_set.loader_version.as_deref(),
    )
    .await?;

    if content_set.loader != ModLoader::Vanilla && loader_version.is_none() {
        return Err(crate::ErrorKind::LauncherError(format!(
            "No loader version selected for {}",
            content_set.loader.as_str()
        ))
        .into());
    }

    let version_jar =
        loader_version.as_ref().map_or(version.id.clone(), |it| {
            format!("{}-{}", version.id.clone(), it.id.clone())
        });

    let mut version_info = download::download_version_info(
        &state,
        version,
        loader_version.as_ref(),
        None,
        None,
        None,
    )
    .await?;
    if version_info.logging.is_none() {
        let requires_logging_info = version_index
            <= minecraft
                .versions
                .iter()
                .position(|x| x.id == "13w39a")
                .unwrap_or(0);
        if requires_logging_info {
            version_info = download::download_version_info(
                &state,
                version,
                loader_version.as_ref(),
                Some(true),
                None,
                None,
            )
            .await?;
        }
    }

    let _ =
        download_log_config(&state, &version_info, None, false, None).await?;

    let java_version =
        get_java_version_from_launch_context(context, &version_info)
            .await?
            .ok_or_else(|| {
                crate::ErrorKind::LauncherError(
                    "Missing correct java installation".to_string(),
                )
            })?;

    // Test jre version
    let java_version =
        crate::api::jre::check_jre(java_version.path.clone().into()).await?;

    let client_path = state
        .directories
        .version_dir(&version_jar)
        .join(format!("{version_jar}.jar"));

    let args = version_info.arguments.clone().unwrap_or_default();
    let mut command = match wrapper {
        Some(hook) => {
            let mut cmd = shlex::split(hook)
                .ok_or_else(|| {
                    crate::ErrorKind::LauncherError(format!(
                        "Invalid wrapper command: {hook}",
                    ))
                })?
                .into_iter();
            let mut command = Command::new(cmd.next().ok_or(
                crate::ErrorKind::LauncherError(
                    "Empty wrapper command".to_owned(),
                ),
            )?);
            command.args(cmd);
            command.arg(&java_version.path);
            command
        }
        None => Command::new(&java_version.path),
    };

    let env_args = Vec::from(env_args);

    // Check if instance has a running process, and reject running the command if it does
    // Done late so a quick double call doesn't launch two instances
    let existing_processes = process::get_by_instance_id(&instance.id).await?;
    if let Some(process) = existing_processes.first() {
        return Err(crate::ErrorKind::LauncherError(format!(
            "Instance {} is already running as process {}",
            instance.id, process.uuid
        ))
        .as_error());
    }

    let natives_dir = state.directories.version_natives_dir(&version_jar);
    if !natives_dir.exists() {
        io::create_dir_all(&natives_dir).await?;
    }

    let quick_play_version =
        QuickPlayVersion::find_version(version_index, &minecraft.versions);
    tracing::debug!(
        "Found QuickPlayVersion for {}: {quick_play_version:?}",
        content_set.game_version
    );
    if let QuickPlayType::Server(address) = &mut quick_play_type
        && quick_play_version.server >= QuickPlayServerVersion::BuiltinLegacy
    {
        // Record last-played for the original server address immediately so
        // recent-worlds can match without DNS/SRV resolution.
        let original = match address {
            ServerAddress::Unresolved(address) => parse_server_address(address)
                .ok()
                .map(|(h, p)| (h.to_owned(), p)),
            ServerAddress::Resolved {
                original_host,
                original_port,
                ..
            } => Some((original_host.clone(), *original_port)),
        };
        if let Some((host, port)) = original
            && let Err(e) = (JoinLogEntry {
                instance_id: instance.id.clone(),
                host,
                port,
                join_time: Utc::now(),
            })
            .upsert(&state.pool)
            .await
        {
            tracing::warn!("Failed to write server join log entry: {e}");
        }

        address.resolve().await?;
    }

    let (main_class_keep_alive, main_class_path) =
        get_resource_file!(env "JAVA_JARS_DIR" / "theseus.jar")?;

    let rpc_server = RpcServerBuilder::new().launch().await?;

    command.args(
        args::get_jvm_arguments(
            args.get(&d::minecraft::ArgumentType::Jvm)
                .map(|x| x.as_slice()),
            &natives_dir,
            &state.directories.libraries_dir(),
            &state.directories.log_configs_dir(),
            &args::get_class_paths(
                &state.directories.libraries_dir(),
                version_info.libraries.as_slice(),
                &[&main_class_path, &client_path],
                &java_version.architecture,
                minecraft_updated,
            )?,
            &main_class_path,
            &version_jar,
            *memory,
            Vec::from(java_args),
            &java_version.architecture,
            &quick_play_type,
            quick_play_version,
            version_info
                .logging
                .as_ref()
                .and_then(|x| x.get(&LoggingSide::Client)),
            rpc_server.address(),
        )?
        .into_iter(),
    );

    // The java launcher requires access to java.lang.reflect in order to force access in to
    // whatever module the main class is in
    if java_version.parsed_version >= 9 {
        command.arg("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
    }

    // The java launcher code requires internal JDK code in Java 25+ in order to support JEP 512
    if java_version.parsed_version >= 25 {
        command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
    }

    command
        .arg("com.modrinth.theseus.MinecraftLaunch")
        .arg(version_info.main_class.clone())
        .args(
            args::get_minecraft_arguments(
                args.get(&d::minecraft::ArgumentType::Game)
                    .map(|x| x.as_slice()),
                version_info.minecraft_arguments.as_deref(),
                credentials,
                &version.id,
                &version_info.asset_index.id,
                &instance_path,
                &state.directories.assets_dir(),
                &version.type_,
                *resolution,
                &java_version.architecture,
                &quick_play_type,
                quick_play_version,
            )
            .await?
            .into_iter(),
        )
        .current_dir(instance_path.clone());

    // CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground
    #[cfg(target_os = "macos")]
    if std::env::var("CARGO").is_ok() {
        command.env_remove("DYLD_FALLBACK_LIBRARY_PATH");
    }
    // Java options should be set in instance options (the existence of _JAVA_OPTIONS overwrites them)
    command.env_remove("_JAVA_OPTIONS");

    command.envs(env_args.iter().cloned());

    // Overwrites the minecraft options.txt file with the settings from the profile
    // Uses 'a:b' syntax which is not quite yaml
    if !mc_set_options.is_empty() {
        let options_path = instance_path.join("options.txt");

        let (mut options_string, input_encoding) = if options_path.exists() {
            io::read_any_encoding_to_string(&options_path).await?
        } else {
            (String::new(), encoding_rs::UTF_8)
        };

        // UTF-16 encodings may be successfully detected and read, but we cannot encode
        // them back, and it's technically possible that the game client strongly expects
        // such encoding
        if input_encoding != input_encoding.output_encoding() {
            return Err(crate::ErrorKind::LauncherError(format!(
                "The instance options.txt file uses an unsupported encoding: {}. \
                Please either turn off instance options that need to modify this file, \
                or convert the file to an encoding that both the game and this app support, \
                such as UTF-8.",
                input_encoding.name()
            ))
            .into());
        }

        for (key, value) in mc_set_options {
            let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
            // check if the regex exists in the file
            if !re.is_match(&options_string) {
                // The key was not found in the file, so append it
                write!(&mut options_string, "\n{key}:{value}").unwrap();
            } else {
                let replaced_string = re
                    .replace_all(&options_string, &format!("{key}:{value}"))
                    .to_string();
                options_string = replaced_string;
            }
        }

        io::write(&options_path, input_encoding.encode(&options_string).0)
            .await?;
    }

    crate::state::instances::commands::set_instance_last_played(
        &instance.id,
        Utc::now(),
        &state.pool,
    )
    .await?;

    // If in tauri, and the 'minimize on launch' setting is enabled, minimize the window
    #[cfg(feature = "tauri")]
    {
        use crate::EventState;

        let window = EventState::get_main_window().await?;
        if let Some(window) = window {
            let settings = crate::state::Settings::get(&state.pool).await?;
            if settings.hide_on_process_start {
                window.minimize()?;
            }
        }
    }

    let _ = state
        .discord_rpc
        .set_activity(&format!("Playing {}", instance.name), true)
        .await;

    let _ = state
        .friends_socket
        .update_status(Some(instance.name.clone()))
        .await;

    // Create Minecraft child by inserting it into the state
    // This also spawns the process and prepares the subsequent processes
    state
        .process_manager
        .insert_new_process(
            &instance.id,
            &instance.path,
            &instance.name,
            command,
            post_exit_hook,
            env_args,
            state.directories.instance_logs_dir(&instance.path),
            version_info.logging.is_some(),
            main_class_keep_alive,
            rpc_server,
            async |process: &ProcessMetadata, rpc_server| {
                let process_start_time = process.start_time.to_rfc3339();
                let instance_created_time = instance.created.to_rfc3339();
                let instance_modified_time = instance.modified.to_rfc3339();
                let (link_project_id, link_version_id) =
                    link_project_and_version(&context.link);
                let system_properties = [
                    ("modrinth.process.startTime", Some(&process_start_time)),
                    ("modrinth.profile.created", Some(&instance_created_time)),
                    ("modrinth.profile.icon", instance.icon_path.as_ref()),
                    ("modrinth.profile.link.project", link_project_id),
                    ("modrinth.profile.link.version", link_version_id),
                    (
                        "modrinth.profile.modified",
                        Some(&instance_modified_time),
                    ),
                    ("modrinth.profile.name", Some(&instance.name)),
                ];
                for (key, value) in system_properties {
                    let Some(value) = value else {
                        continue;
                    };
                    rpc_server
                        .call_method_2::<()>("set_system_property", key, value)
                        .await?;
                }
                rpc_server.call_method::<()>("launch").await?;
                Ok(())
            },
        )
        .await
}
