use super::client::*;
use super::diff::*;
use super::install::*;
use super::types::*;
use super::*;
use async_walkdir::WalkDir;
use async_zip::{Compression, ZipEntryBuilder};
use futures::StreamExt;
use std::collections::BTreeMap;

#[tracing::instrument]
pub async fn unpublish_shared_instance(instance_id: &str) -> crate::Result<()> {
    let state = State::get().await?;
    let metadata = crate::state::get_instance(instance_id, &state.pool)
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::InputError("Unknown instance".to_string())
        })?;
    let Some(attachment) = metadata.shared_instance.clone() else {
        return Ok(());
    };
    ensure_owner(&attachment)?;

    delete_remote_instance(&attachment.id, &state).await?;
    detach_local_shared_instance(instance_id, metadata.link, &state).await?;
    emit_instance(instance_id, InstancePayloadType::Edited).await?;

    Ok(())
}

#[tracing::instrument]
pub async fn unlink_shared_instance(instance_id: &str) -> crate::Result<()> {
    let state = State::get().await?;
    let metadata = crate::state::get_instance(instance_id, &state.pool)
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::InputError("Unknown instance".to_string())
        })?;
    let Some(attachment) = metadata.shared_instance.clone() else {
        return Ok(());
    };
    ensure_member(&attachment)?;

    detach_local_shared_instance(instance_id, metadata.link, &state).await?;
    emit_instance(instance_id, InstancePayloadType::Edited).await?;

    Ok(())
}

pub(super) async fn detach_local_shared_instance(
    instance_id: &str,
    link: InstanceLink,
    state: &State,
) -> crate::Result<()> {
    let link_patch = match link {
        InstanceLink::SharedInstance {
            modpack_project_id: Some(project_id),
            modpack_version_id: Some(version_id),
        } => Some((
            InstanceLink::ModrinthModpack {
                project_id,
                version_id,
            },
            ContentSourceKind::ModrinthModpack,
        )),
        InstanceLink::SharedInstance { .. } => {
            Some((InstanceLink::Unmanaged, ContentSourceKind::Local))
        }
        _ => None,
    };

    if let Some((link, source_kind)) = link_patch {
        crate::state::edit_instance(
            instance_id,
            EditInstance {
                link: Some(link),
                content_set_patch: Some(AppliedContentSetPatch {
                    source_kind: Some(source_kind),
                    ..Default::default()
                }),
                ..Default::default()
            },
            &state.pool,
        )
        .await?;
    }

    crate::state::clear_shared_instance(instance_id, &state.pool).await?;

    Ok(())
}

#[tracing::instrument]
pub async fn publish_shared_instance(
    instance_id: &str,
    config_paths: Vec<String>,
) -> crate::Result<SharedInstanceAttachment> {
    let state = State::get().await?;
    publish_shared_instance_inner(instance_id, &config_paths, &state).await?;
    emit_instance(instance_id, InstancePayloadType::Edited).await?;

    shared_attachment(instance_id, &state)
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::InputError(
                "Shared instance attachment was not persisted".to_string(),
            )
            .into()
        })
}

#[tracing::instrument]
pub async fn get_shared_instance_publish_preview(
    instance_id: &str,
) -> crate::Result<Option<SharedInstancePublishPreview>> {
    let state = State::get().await?;
    let metadata = crate::state::get_instance(instance_id, &state.pool)
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::InputError("Unknown instance".to_string())
        })?;
    let Some(attachment) = metadata.shared_instance.clone() else {
        return Ok(None);
    };
    ensure_owner(&attachment)?;

    let (version, snapshot) = tokio::try_join!(
        get_latest_remote_version_optional_unavailable(&attachment.id, &state,),
        collect_publish_snapshot(&metadata, &state),
    )?;
    let version = match version {
        SharedInstanceRemoteResponse::Available(version) => version,
        SharedInstanceRemoteResponse::Unavailable(reason) => {
            handle_unavailable_shared_instance_if_current_user(
                instance_id,
                &attachment,
                reason,
                &state,
            )
            .await?;
            return Err(shared_instance_unavailable_error(reason));
        }
    };
    let diffs =
        shared_instance_publish_diffs(&metadata, &version, &snapshot, &state)
            .await?;
    set_shared_instance_publish_status(
        instance_id,
        &attachment,
        version.version,
        !diffs.is_empty(),
        &state,
    )
    .await?;
    emit_instance(instance_id, InstancePayloadType::Edited).await?;

    Ok(Some(SharedInstancePublishPreview {
        shared_instance_id: attachment.id,
        latest_version: version.version,
        diffs,
        config_files: snapshot
            .config_files
            .into_iter()
            .map(|file| file.path)
            .collect(),
    }))
}

pub(super) async fn remote_publish_content(
    version: &InstanceVersionResponse,
    include_modpack_dependencies: bool,
    state: &State,
) -> crate::Result<(Vec<String>, HashSet<String>)> {
    let mut version_ids = version.modrinth_ids.clone();
    if let Some(modpack_id) =
        version.modpack_id.as_deref().filter(|id| !id.is_empty())
    {
        version_ids.retain(|id| id != modpack_id);

        if include_modpack_dependencies {
            version_ids.extend(
                modpack_dependency_version_ids(modpack_id, state).await?,
            );
        }
    }
    dedupe_strings(&mut version_ids);

    Ok((
        version_ids,
        version
            .external_files
            .iter()
            .filter(|file| file.file_type != CONFIG_BUNDLE_FILE_TYPE)
            .map(|file| file.file_name.clone())
            .collect(),
    ))
}

pub(super) async fn modpack_dependency_version_ids(
    modpack_id: &str,
    state: &State,
) -> crate::Result<Vec<String>> {
    let modpack_version = CachedEntry::get_version(
        modpack_id,
        Some(CacheBehaviour::Bypass),
        &state.pool,
        &state.api_semaphore,
    )
    .await?
    .ok_or_else(|| {
        crate::ErrorKind::InputError(
            "Shared instance modpack version was not found".to_string(),
        )
    })?;

    Ok(modpack_version
        .dependencies
        .into_iter()
        .filter_map(|dependency| dependency.version_id)
        .collect())
}

pub(super) async fn shared_instance_install_modpack(
    version: &InstanceVersionResponse,
    state: &State,
) -> crate::Result<Option<crate::install::SharedInstanceInstallModpack>> {
    let Some(modpack_id) =
        version.modpack_id.as_deref().filter(|id| !id.is_empty())
    else {
        return Ok(None);
    };
    let modpack_version = CachedEntry::get_version(
        modpack_id,
        Some(CacheBehaviour::MustRevalidate),
        &state.pool,
        &state.api_semaphore,
    )
    .await?
    .ok_or_else(|| {
        crate::ErrorKind::InputError(
            "Shared instance modpack version was not found".to_string(),
        )
    })?;
    let project = CachedEntry::get_project(
        &modpack_version.project_id,
        Some(CacheBehaviour::MustRevalidate),
        &state.pool,
        &state.api_semaphore,
    )
    .await?;

    Ok(Some(crate::install::SharedInstanceInstallModpack {
        project_id: modpack_version.project_id,
        version_id: modpack_version.id,
        dependency_count: modpack_version.dependencies.len(),
        title: project
            .as_ref()
            .map(|project| project.title.clone())
            .unwrap_or(modpack_version.name),
        icon_url: project.and_then(|project| project.icon_url),
    }))
}

pub(super) fn shared_instance_loader_version(
    loader_version: String,
) -> Option<String> {
    (!loader_version.is_empty()).then_some(loader_version)
}

pub(super) async fn current_shared_content(
    metadata: &crate::state::InstanceMetadata,
    include_linked_modpack_content: bool,
    state: &State,
) -> crate::Result<(Vec<String>, HashSet<String>)> {
    let entries =
        crate::state::instances::adapters::sqlite::content_rows::get_content_entries(
            &metadata.applied_content_set.id,
            &state.pool,
        )
        .await?;
    let files = crate::state::instances::adapters::sqlite::content_rows::get_instance_files(
        &metadata.instance.id,
        &state.pool,
    )
    .await?
    .into_iter()
    .map(|file| (file.id.clone(), file))
    .collect::<HashMap<_, _>>();
    let mut version_ids = Vec::new();
    let mut external_files = HashSet::new();

    for entry in entries {
        let include_entry = entry.source_kind
            == crate::state::ContentSourceKind::SharedInstance
            || (include_linked_modpack_content
                && entry.source_kind
                    == crate::state::ContentSourceKind::ModrinthModpack);
        if !include_entry {
            continue;
        }

        if let Some(version_id) = entry.version_id {
            version_ids.push(version_id);
            continue;
        }

        let Some(file_id) = entry.file_id else {
            continue;
        };
        if let Some(file) = files.get(&file_id) {
            external_files.insert(file.file_name.clone());
        }
    }
    if include_linked_modpack_content
        && let Some(modpack_id) = shared_modpack_id(&metadata.link)
    {
        version_ids
            .extend(modpack_dependency_version_ids(&modpack_id, state).await?);
    }
    dedupe_strings(&mut version_ids);

    Ok((version_ids, external_files))
}

pub(super) struct CurrentPublishSnapshot {
    pub(super) version_ids: Vec<String>,
    pub(super) external_files: Vec<ExternalFileCandidate>,
    pub(super) disabled_project_ids: HashSet<String>,
    pub(super) disabled_version_ids: Vec<String>,
    pub(super) disabled_external_files: HashSet<String>,
    pub(super) config_files: Vec<ConfigFile>,
}

pub(super) async fn collect_publish_snapshot(
    metadata: &crate::state::InstanceMetadata,
    state: &State,
) -> crate::Result<CurrentPublishSnapshot> {
    let instance_path = state
        .directories
        .instances_dir()
        .join(&metadata.instance.path);
    let (items, config_files) = if CONFIG_SYNC_ENABLED {
        tokio::try_join!(
            crate::state::list_content(
                &metadata.instance.id,
                None,
                None,
                state,
            ),
            collect_config_files(&instance_path),
        )?
    } else {
        (
            crate::state::list_content(
                &metadata.instance.id,
                None,
                None,
                state,
            )
            .await?,
            Vec::new(),
        )
    };
    let modpack_id = shared_modpack_id(&metadata.link);
    let mut version_ids = Vec::new();
    let mut seen_version_ids = HashSet::new();
    let mut external_files = Vec::new();
    let mut seen_external_files = HashSet::new();
    let mut disabled_project_ids = HashSet::new();
    let mut disabled_version_ids = Vec::new();
    let mut seen_disabled_version_ids = HashSet::new();
    let mut disabled_external_files = HashSet::new();

    for item in items {
        if item.enabled {
            if let Some(version) = item.version {
                if seen_version_ids.insert(version.id.clone()) {
                    version_ids.push(version.id);
                }
                continue;
            }

            if item.file_path.is_empty() {
                continue;
            }

            let file_type = file_type(item.project_type);
            let external_key = format!("{}:{file_type}", item.file_path);
            if seen_external_files.insert(external_key) {
                external_files.push(ExternalFileCandidate {
                    file_name: item.file_name,
                    file_type,
                    source: ExternalFileSource::InstanceFile(item.file_path),
                });
            }
            continue;
        }

        let is_modpack = item.version.as_ref().is_some_and(|version| {
            modpack_id.as_deref() == Some(version.id.as_str())
        });
        if is_modpack {
            continue;
        }

        if let Some(project) = item.project.as_ref() {
            disabled_project_ids.insert(project.id.clone());
        }

        if let Some(version) = item.version {
            if seen_disabled_version_ids.insert(version.id.clone()) {
                disabled_version_ids.push(version.id);
            }
            continue;
        }

        if item.file_path.is_empty() {
            continue;
        }

        disabled_external_files.insert(enabled_file_name(&item.file_name));
    }

    Ok(CurrentPublishSnapshot {
        version_ids,
        external_files,
        disabled_project_ids,
        disabled_version_ids,
        disabled_external_files,
        config_files,
    })
}

pub(super) async fn shared_versions_by_project(
    version_ids: &[String],
    state: &State,
) -> crate::Result<HashMap<String, crate::state::Version>> {
    let version_id_refs =
        version_ids.iter().map(String::as_str).collect::<Vec<_>>();
    let versions = CachedEntry::get_version_many(
        &version_id_refs,
        Some(CacheBehaviour::Bypass),
        &state.pool,
        &state.api_semaphore,
    )
    .await?;

    Ok(versions
        .into_iter()
        .map(|version| (version.project_id.clone(), version))
        .collect())
}

pub(super) async fn shared_project_names(
    project_ids: &HashSet<String>,
    state: &State,
) -> crate::Result<HashMap<String, String>> {
    let project_id_refs =
        project_ids.iter().map(String::as_str).collect::<Vec<_>>();
    let projects = CachedEntry::get_project_many(
        &project_id_refs,
        Some(CacheBehaviour::Bypass),
        &state.pool,
        &state.api_semaphore,
    )
    .await?;

    Ok(projects
        .into_iter()
        .map(|project| (project.id, project.title))
        .collect())
}

pub(super) fn dedupe_strings(values: &mut Vec<String>) {
    let mut seen = HashSet::new();
    values.retain(|value| seen.insert(value.clone()));
}

pub(super) fn enabled_file_name(file_name: &str) -> String {
    file_name
        .strip_suffix(".disabled")
        .unwrap_or(file_name)
        .to_string()
}

pub(super) fn shared_instance_name(name: String) -> String {
    match name.trim() {
        "" => "Shared instance".to_string(),
        name => name.to_string(),
    }
}

pub(super) async fn set_shared_instance_publish_status(
    instance_id: &str,
    attachment: &SharedInstanceAttachment,
    latest_version: i32,
    has_changes: bool,
    state: &State,
) -> crate::Result<()> {
    let (status, applied_version) = if has_changes {
        (ContentSetSyncStatus::Stale, attachment.applied_version)
    } else {
        (ContentSetSyncStatus::UpToDate, Some(latest_version))
    };

    crate::state::set_shared_instance_sync_status(
        instance_id,
        status,
        applied_version,
        Some(latest_version),
        &state.pool,
    )
    .await
}

pub(super) async fn publish_shared_instance_inner(
    instance_id: &str,
    config_paths: &[String],
    state: &State,
) -> crate::Result<()> {
    let attachment =
        shared_attachment(instance_id, state)
            .await?
            .ok_or_else(|| {
                crate::ErrorKind::InputError(
                    "Instance is not attached to a shared instance".to_string(),
                )
            })?;
    ensure_owner(&attachment)?;
    tracing::info!(
        instance_id,
        shared_instance_id = %attachment.id,
        applied_version = attachment.applied_version,
        latest_version = attachment.latest_version,
        "Publishing shared instance content"
    );

    crate::state::set_shared_instance_sync_status(
        instance_id,
        ContentSetSyncStatus::Applying,
        attachment.applied_version,
        attachment.latest_version,
        &state.pool,
    )
    .await?;

    let result = publish_current_content(
        instance_id,
        &attachment.id,
        config_paths,
        state,
    )
    .await;

    match result {
        Ok(version) => {
            tracing::info!(
                instance_id,
                shared_instance_id = %attachment.id,
                version,
                "Published shared instance content"
            );
            crate::state::set_shared_instance_sync_status(
                instance_id,
                ContentSetSyncStatus::UpToDate,
                Some(version),
                Some(version),
                &state.pool,
            )
            .await?;
            Ok(())
        }
        Err(error) => {
            tracing::warn!(
                instance_id,
                shared_instance_id = %attachment.id,
                error = %error,
                "Failed to publish shared instance content"
            );
            crate::state::set_shared_instance_sync_status(
                instance_id,
                ContentSetSyncStatus::Error,
                attachment.applied_version,
                attachment.latest_version,
                &state.pool,
            )
            .await?;
            Err(error)
        }
    }
}

pub(super) async fn publish_current_content(
    instance_id: &str,
    shared_instance_id: &str,
    config_paths: &[String],
    state: &State,
) -> crate::Result<i32> {
    let metadata = crate::state::get_instance(instance_id, &state.pool)
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::InputError("Unknown instance".to_string())
        })?;
    ensure_shareable_link(&metadata.link)?;
    update_remote_instance(
        shared_instance_id,
        shared_instance_name(metadata.instance.name.clone()),
        state,
    )
    .await?;
    let modpack_id = shared_modpack_id(&metadata.link);
    let snapshot = collect_publish_snapshot(&metadata, state).await?;
    let previous_version = if CONFIG_SYNC_ENABLED
        && metadata
            .shared_instance
            .as_ref()
            .and_then(|attachment| attachment.applied_version)
            .is_some()
    {
        match get_latest_remote_version_optional_unavailable(
            shared_instance_id,
            state,
        )
        .await?
        {
            SharedInstanceRemoteResponse::Available(version) => Some(version),
            SharedInstanceRemoteResponse::Unavailable(_) => None,
        }
    } else {
        None
    };
    let config_bundle = if CONFIG_SYNC_ENABLED {
        build_config_bundle_candidate(
            &metadata.instance.path,
            &snapshot.config_files,
            config_paths,
            previous_version.as_ref(),
            state,
        )
        .await?
    } else {
        None
    };
    let modrinth_ids = snapshot.version_ids;
    let mut external_files = snapshot.external_files;
    if let Some(config_bundle) = config_bundle {
        external_files.push(config_bundle);
    }
    tracing::debug!(
        instance_id,
        shared_instance_id,
        modpack_id = modpack_id.as_deref().unwrap_or("none"),
        modrinth_id_count = modrinth_ids.len(),
        external_file_count = external_files.len(),
        "Creating shared instance version"
    );
    let external_file_data = external_files
        .iter()
        .map(|file| ExternalFileData {
            file_name: file.file_name.clone(),
            file_type: file.file_type.clone(),
        })
        .collect::<Vec<_>>();
    let response = request_json_optional_unavailable::<InstanceVersionResponse>(
        "create_instance_version",
        Method::POST,
        &format!("/instances/{shared_instance_id}/versions"),
        Some(json!({
            "modrinth_ids": modrinth_ids,
            "external_files": external_file_data,
            "modpack_id": modpack_id,
            "game_version": metadata.applied_content_set.game_version.clone(),
            "loader": metadata.applied_content_set.loader.as_str(),
            "loader_version": metadata
                .applied_content_set
                .loader_version
                .clone()
                .unwrap_or_default(),
        })),
        state,
        SharedInstancesRequestAuth::ModrinthSession,
    )
    .await?;
    let response = match response {
        SharedInstanceRemoteResponse::Available(response) => response,
        SharedInstanceRemoteResponse::Unavailable(reason) => {
            if let Some(attachment) = metadata.shared_instance.as_ref() {
                handle_unavailable_shared_instance_if_current_user(
                    instance_id,
                    attachment,
                    reason,
                    state,
                )
                .await?;
            }

            return Err(shared_instance_unavailable_error(reason));
        }
    };

    if !response.external_files.is_empty() {
        upload_external_files(
            &metadata.instance.path,
            &external_files,
            &response.external_files,
            state,
        )
        .await?;
    } else if !response.ready {
        tracing::debug!(
            "Shared instance version {} was not ready but had no external files",
            response.version
        );
    }

    Ok(response.version)
}

pub(super) async fn collect_config_files(
    instance_path: &std::path::Path,
) -> crate::Result<Vec<ConfigFile>> {
    let config_path = instance_path.join(CONFIG_DIRECTORY);
    crate::util::io::create_dir_all(&config_path).await?;
    let mut files = Vec::new();
    let mut walker = WalkDir::new(&config_path);

    while let Some(entry) = walker.next().await {
        let entry = entry.map_err(|error| {
            crate::ErrorKind::OtherError(format!(
                "Failed to read config directory: {error}"
            ))
        })?;
        if !entry.file_type().await?.is_file()
            || !is_supported_config_file(&entry.path())
        {
            continue;
        }

        let entry_path = entry.path();
        let relative_path = entry_path.strip_prefix(&config_path)?;
        let path = relative_path.to_string_lossy().replace('\\', "/");
        files.push(ConfigFile { path });
    }

    files.sort_by(|left, right| left.path.cmp(&right.path));
    Ok(files)
}

fn is_supported_config_file(path: &std::path::Path) -> bool {
    path.extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| {
            CONFIG_FILE_EXTENSIONS
                .iter()
                .any(|candidate| extension.eq_ignore_ascii_case(candidate))
        })
}

async fn build_config_bundle_candidate(
    instance_path: &str,
    local_files: &[ConfigFile],
    selected_paths: &[String],
    previous_version: Option<&InstanceVersionResponse>,
    state: &State,
) -> crate::Result<Option<ExternalFileCandidate>> {
    let selected_paths = selected_paths
        .iter()
        .map(String::as_str)
        .collect::<HashSet<_>>();
    let local_files_by_path = local_files
        .iter()
        .map(|file| (file.path.as_str(), file))
        .collect::<HashMap<_, _>>();
    for selected_path in &selected_paths {
        if !local_files_by_path.contains_key(selected_path) {
            return Err(crate::ErrorKind::InputError(format!(
                "Config file is unavailable for sharing: {selected_path}"
            ))
            .into());
        }
    }

    let previous_bundle = previous_version.and_then(|version| {
        version
            .external_files
            .iter()
            .find(|file| file.file_type == CONFIG_BUNDLE_FILE_TYPE)
    });
    if previous_bundle.is_none() && selected_paths.is_empty() {
        return Ok(None);
    }

    let mut entries = BTreeMap::new();
    if let (Some(_), Some(previous_bundle)) =
        (previous_version, previous_bundle)
    {
        let response = REQWEST_CLIENT.get(&previous_bundle.url).send().await?;
        if !response.status().is_success() {
            return Err(crate::ErrorKind::OtherError(format!(
                "Previous config bundle download failed with status {}",
                response.status()
            ))
            .into());
        }
        let bytes = response.bytes().await?;
        let archived_entries = tokio::task::spawn_blocking(move || {
            read_config_bundle(bytes.as_ref())
        })
        .await??;
        entries.extend(archived_entries);
    }

    let config_path = state
        .directories
        .instances_dir()
        .join(instance_path)
        .join(CONFIG_DIRECTORY);
    for selected_path in selected_paths {
        let file = local_files_by_path
            .get(selected_path)
            .expect("selected config paths were validated");
        let bytes = crate::util::io::read(config_path.join(&file.path)).await?;
        entries.insert(file.path.clone(), bytes);
    }

    let bytes = config_bundle_bytes(&entries).await?;

    Ok(Some(ExternalFileCandidate {
        file_name: CONFIG_BUNDLE_FILE_NAME.to_string(),
        file_type: CONFIG_BUNDLE_FILE_TYPE.to_string(),
        source: ExternalFileSource::ConfigBundle(bytes),
    }))
}

async fn config_bundle_bytes(
    entries: &BTreeMap<String, Vec<u8>>,
) -> crate::Result<Vec<u8>> {
    if entries.len() > MAX_CONFIG_BUNDLE_ENTRIES {
        let mut folder_entry_counts = HashMap::new();
        for path in entries.keys() {
            if let Some((folder, _)) = path.split_once('/') {
                *folder_entry_counts.entry(folder).or_insert(0_usize) += 1;
            }
        }

        if let Some((folder, count)) = folder_entry_counts
            .into_iter()
            .filter(|(_, count)| *count > MAX_CONFIG_BUNDLE_ENTRIES)
            .max_by_key(|(_, count)| *count)
        {
            return Err(crate::ErrorKind::InputError(format!(
				"The \"{folder}\" config folder has too many files to share ({count}; maximum {MAX_CONFIG_BUNDLE_ENTRIES}). Select fewer files from this folder."
			))
			.into());
        }

        return Err(crate::ErrorKind::InputError(format!(
			"Too many config files were selected to share ({}; maximum {MAX_CONFIG_BUNDLE_ENTRIES}). Select fewer files.",
			entries.len()
		))
		.into());
    }
    let mut total_size = 0_u64;
    for bytes in entries.values() {
        let size = bytes.len() as u64;
        if size > MAX_CONFIG_BUNDLE_FILE_SIZE {
            return Err(crate::ErrorKind::InputError(
                "Shared instance config bundle contains a file that is too large"
                    .to_string(),
            )
            .into());
        }
        total_size = total_size.checked_add(size).ok_or_else(|| {
            crate::ErrorKind::InputError(
                "Shared instance config bundle size overflowed".to_string(),
            )
        })?;
        if total_size > MAX_CONFIG_BUNDLE_TOTAL_SIZE {
            return Err(crate::ErrorKind::InputError(
                "Shared instance config bundle exceeds the uncompressed size limit"
                    .to_string(),
            )
            .into());
        }
    }

    let mut writer = async_zip::base::write::ZipFileWriter::new(Vec::new());
    for (path, bytes) in entries {
        writer
            .write_entry_whole(
                ZipEntryBuilder::new(path.clone().into(), Compression::Deflate),
                bytes,
            )
            .await?;
    }

    Ok(writer.close().await?)
}

fn read_config_bundle(
    bytes: &[u8],
) -> crate::Result<BTreeMap<String, Vec<u8>>> {
    let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))
        .map_err(|error| {
            crate::ErrorKind::InputError(format!(
                "Invalid shared instance config bundle: {error}"
            ))
        })?;
    if archive.len() > MAX_CONFIG_BUNDLE_ENTRIES {
        return Err(crate::ErrorKind::InputError(
            "Shared instance config bundle contains too many entries"
                .to_string(),
        )
        .into());
    }
    let mut entries = BTreeMap::new();
    let mut total_size = 0;

    for index in 0..archive.len() {
        let file = archive.by_index(index).map_err(|error| {
            crate::ErrorKind::InputError(format!(
                "Invalid shared instance config bundle entry: {error}"
            ))
        })?;
        if file.is_dir() {
            continue;
        }
        let path = file.enclosed_name().ok_or_else(|| {
            crate::ErrorKind::InputError(
                "Shared instance config bundle contains an unsafe path"
                    .to_string(),
            )
        })?;
        if !is_supported_config_file(&path) {
            return Err(crate::ErrorKind::InputError(format!(
                "Shared instance config bundle contains unsupported file {}",
                path.display()
            ))
            .into());
        }
        let path = path.to_string_lossy().replace('\\', "/");
        let declared_size = file.size();
        let bytes = read_bounded_config_bundle_entry(
            file,
            declared_size,
            &mut total_size,
        )?;
        if entries.insert(path.clone(), bytes).is_some() {
            return Err(crate::ErrorKind::InputError(format!(
                "Shared instance config bundle contains duplicate file {path}"
            ))
            .into());
        }
    }

    Ok(entries)
}

pub(super) fn shared_modpack_id(link: &InstanceLink) -> Option<String> {
    match link {
        InstanceLink::ModrinthModpack { version_id, .. } => {
            Some(version_id.clone())
        }
        InstanceLink::ServerProjectModpack {
            content_version_id, ..
        } => Some(content_version_id.clone()),
        InstanceLink::SharedInstance {
            modpack_version_id: Some(version_id),
            ..
        } => Some(version_id.clone()),
        _ => None,
    }
}

pub(super) fn ensure_shareable_link(link: &InstanceLink) -> crate::Result<()> {
    if matches!(link, InstanceLink::ImportedModpack { .. }) {
        return Err(crate::ErrorKind::InputError(
            "You must unlink this modpack to share your instance".to_string(),
        )
        .into());
    }

    Ok(())
}

pub(super) async fn upload_external_files(
    instance_path: &str,
    candidates: &[ExternalFileCandidate],
    uploads: &[ExternalFileResponse],
    state: &State,
) -> crate::Result<()> {
    for upload in uploads {
        let candidate = candidates
            .iter()
            .find(|candidate| {
                candidate.file_name == upload.file_name
                    && candidate.file_type == upload.file_type
            })
            .ok_or_else(|| {
                crate::ErrorKind::InputError(format!(
                    "Shared instance service requested unknown external file {}",
                    upload.file_name
                ))
            })?;
        let bytes = match &candidate.source {
            ExternalFileSource::InstanceFile(file_path) => {
                let path = state
                    .directories
                    .instances_dir()
                    .join(instance_path)
                    .join(file_path);
                crate::util::io::read(path).await?
            }
            ExternalFileSource::ConfigBundle(bytes) => bytes.clone(),
        };
        let upload_url = url::Url::parse(&upload.url).map_err(|error| {
            crate::ErrorKind::OtherError(format!(
                "Invalid shared instance external file upload URL: {error}"
            ))
        })?;
        let response = send_bytes_request_to_url(
            "upload_external_file",
            Method::PUT,
            upload_url.path(),
            &upload.url,
            bytes,
            state,
        )
        .await?;

        if !response.status().is_success() {
            return shared_instances_request_error(
                "upload_external_file",
                Method::PUT,
                upload_url.path(),
                response,
            )
            .await;
        }
    }

    Ok(())
}

pub(super) async fn shared_attachment(
    instance_id: &str,
    state: &State,
) -> crate::Result<Option<SharedInstanceAttachment>> {
    Ok(crate::state::get_instance(instance_id, &state.pool)
        .await?
        .and_then(|metadata| metadata.shared_instance))
}

pub(crate) async fn sync_shared_instance_icon(
    instance_id: &str,
    icon_path: Option<&str>,
    state: &State,
) -> crate::Result<()> {
    let Some(attachment) = shared_attachment(instance_id, state).await? else {
        return Ok(());
    };
    if attachment.role != SharedInstanceRole::Owner {
        return Ok(());
    }

    update_remote_instance_icon(&attachment.id, icon_path, state).await
}

pub(super) async fn shared_instance_for_invites(
    instance_id: &str,
    user_count: usize,
    state: &State,
) -> crate::Result<(crate::state::InstanceMetadata, SharedInstanceAttachment)> {
    let metadata = crate::state::get_instance(instance_id, &state.pool)
        .await?
        .ok_or_else(|| {
            crate::ErrorKind::InputError("Unknown instance".to_string())
        })?;
    let attachment = match metadata.shared_instance.clone() {
        Some(attachment) => {
            tracing::debug!(
                instance_id,
                shared_instance_id = %attachment.id,
                role = attachment.role.as_str(),
                user_count,
                "Using existing shared instance attachment for invite"
            );
            attachment
        }
        None => {
            ensure_shareable_link(&metadata.link)?;
            tracing::info!(
                instance_id,
                user_count,
                "Creating shared instance before first invite"
            );
            let remote = create_remote_instance(
                shared_instance_name(metadata.instance.name.clone()),
                state,
            )
            .await?;
            let linked_user_id = linked_modrinth_user_id(state).await?;
            tracing::info!(
                instance_id,
                shared_instance_id = %remote.id,
                "Created remote shared instance"
            );
            crate::state::attach_shared_instance(
                instance_id,
                crate::state::SharedInstanceAttachmentInput {
                    id: remote.id.clone(),
                    role: SharedInstanceRole::Owner,
                    manager_id: None,
                    server_manager_name: None,
                    server_manager_icon_url: None,
                    linked_user_id,
                    status: ContentSetSyncStatus::Unknown,
                    applied_version: None,
                    latest_version: None,
                },
                &state.pool,
            )
            .await?;
            tracing::debug!(
                instance_id,
                shared_instance_id = %remote.id,
                "Attached local instance as shared instance owner"
            );
            publish_shared_instance_inner(instance_id, &[], state).await?;
            shared_attachment(instance_id, state)
                .await?
                .ok_or_else(|| {
                    crate::ErrorKind::InputError(
                        "Shared instance attachment was not persisted"
                            .to_string(),
                    )
                })?
        }
    };

    ensure_owner(&attachment)?;
    update_remote_instance_icon(
        &attachment.id,
        metadata.instance.icon_path.as_deref(),
        state,
    )
    .await?;

    Ok((metadata, attachment))
}

pub(super) fn ensure_owner(
    attachment: &SharedInstanceAttachment,
) -> crate::Result<()> {
    if attachment.role == SharedInstanceRole::Owner {
        return Ok(());
    }

    Err(crate::ErrorKind::InputError(
        "Only the owner instance can manage shared instance users".to_string(),
    )
    .into())
}

pub(super) fn ensure_member(
    attachment: &SharedInstanceAttachment,
) -> crate::Result<()> {
    if attachment.role.is_member() {
        return Ok(());
    }

    Err(crate::ErrorKind::InputError(
        "Only shared instance members can unlink from shared instances"
            .to_string(),
    )
    .into())
}

pub(super) fn file_type(project_type: ProjectType) -> String {
    project_type.get_name().to_string()
}
