use crate::cli::Juliaup;
use crate::command_completions::write_completion_files;
use crate::config_file::get_read_lock;
use crate::config_file::load_config_db;
use crate::config_file::load_mut_config_db;
use crate::config_file::save_config_db;
use crate::config_file::JuliaupConfig;
use crate::config_file::JuliaupConfigChannel;
use crate::config_file::JuliaupConfigVersion;
use crate::get_bundled_dbversion;
use crate::get_bundled_julia_version;
use crate::get_juliaup_target;
use crate::global_paths::GlobalPaths;
use crate::jsonstructs_versionsdb::JuliaupVersionDB;
use crate::utils::check_server_supports_nightlies;
use crate::utils::get_bin_dir;
use crate::utils::get_julianightlies_base_url;
use crate::utils::get_juliaserver_base_url;
use crate::utils::is_valid_julia_path;
use crate::utils::retry_rename;
use crate::utils::{print_juliaup_style, JuliaupMessageType};
use anyhow::{anyhow, bail, Context, Error, Result};
use bstr::ByteSlice;
use bstr::ByteVec;
use console::style;
#[cfg(not(target_os = "freebsd"))]
use flate2::read::GzDecoder;
use indicatif::{ProgressBar, ProgressStyle};
use indoc::formatdoc;
use regex::Regex;
use semver::Version;
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt;

use std::{
    io::{BufReader, Read, Seek, Write},
    path::{Path, PathBuf},
};
#[cfg(not(target_os = "freebsd"))]
use tar::Archive;
use tempfile::Builder;
use tempfile::TempPath;
use url::Url;

// Progress bar prefix with proper indentation to match other messages (12 characters wide, right-aligned)
const DOWNLOADING_PREFIX: &str = " Downloading";

/// Creates an HTTP client with a proper User-Agent header.
/// Some CDNs (like CloudFront) block requests without User-Agent.
#[cfg(not(windows))]
fn http_client() -> Result<reqwest::blocking::Client> {
    let user_agent = format!("juliaup/{}", env!("CARGO_PKG_VERSION"));
    reqwest::blocking::Client::builder()
        .user_agent(user_agent)
        .build()
        .with_context(|| "Failed to create HTTP client")
}

/// Creates a Windows HTTP client with a proper User-Agent header.
/// Some CDNs (like CloudFront) block requests without User-Agent.
#[cfg(windows)]
fn http_client() -> Result<windows::Web::Http::HttpClient> {
    use windows::core::HSTRING;

    let http_client =
        windows::Web::Http::HttpClient::new().with_context(|| "Failed to create HttpClient.")?;

    let user_agent = format!("juliaup/{}", env!("CARGO_PKG_VERSION"));
    http_client
        .DefaultRequestHeaders()
        .with_context(|| "Failed to get default request headers.")?
        .UserAgent()
        .with_context(|| "Failed to get User-Agent header collection.")?
        .TryParseAdd(&HSTRING::from(&user_agent))
        .with_context(|| "Failed to set User-Agent header.")?;

    Ok(http_client)
}

#[cfg(not(target_os = "freebsd"))]
fn unpack_sans_parent<R, P>(src: R, dst: P, levels_to_skip: usize) -> Result<()>
where
    R: Read,
    P: AsRef<Path>,
{
    let dst = dst.as_ref();
    let tar = GzDecoder::new(src);
    let mut archive = Archive::new(tar);
    // Extract to a sibling temp dir (same filesystem as dst) so the final rename
    // is atomic and avoids a cross-device copy. Archive::unpack uses unpack_in
    // internally, which rejects path-traversal components (e.g. `..`).
    let temp_dir =
        tempfile::Builder::new().tempdir_in(dst.parent().unwrap_or_else(|| Path::new("..")))?;
    archive.unpack(temp_dir.path())?;
    // Walk down `levels_to_skip` directory levels to reach the payload.
    let mut source = temp_dir.path().to_path_buf();
    for _ in 0..levels_to_skip {
        source = std::fs::read_dir(&source)?
            .filter_map(|e| e.ok())
            .find(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
            .with_context(|| {
                format!(
                    "Archive contained no subdirectory to skip in '{}'",
                    source.display()
                )
            })?
            .path();
    }
    if dst.exists() {
        // Move the existing installation aside so we can restore it on failure.
        let backup = tempfile::Builder::new()
            .prefix(".juliaup-backup-")
            .tempdir_in(dst.parent().unwrap_or_else(|| Path::new("..")))?;
        let backup_path = backup.path().join("old");
        std::fs::rename(dst, &backup_path).with_context(|| {
            format!(
                "Failed to back up existing directory '{}' before update.",
                dst.display()
            )
        })?;

        if let Err(e) = std::fs::rename(&source, dst) {
            // Restore the previous installation.
            if let Err(restore_err) = std::fs::rename(&backup_path, dst) {
                bail!(
                    "Failed to install to '{}': {:#}. \
                     Additionally, restoring the previous installation failed: {:#}. \
                     A backup may remain at '{}'.",
                    dst.display(),
                    e,
                    restore_err,
                    backup_path.display()
                );
            }
            return Err(e).with_context(|| {
                format!(
                    "Failed to move extracted archive to '{}'. Previous installation has been restored.",
                    dst.display()
                )
            });
        }
        // Success — backup is cleaned up automatically when `backup` is dropped.
    } else {
        std::fs::rename(&source, dst)
            .with_context(|| format!("Failed to move extracted archive to '{}'", dst.display()))?;
    }
    Ok(())
}

// As of this writing, the Rust `tar` library does not fully implement reading the
// POSIX.1-2001 PAX format. This is the default format used by BSD tar, and is thus
// the format of the Julia tarballs on BSD systems. There is a PR to add support for
// PAX to tar.rs: https://github.com/alexcrichton/tar-rs/pull/298, but as of this
// writing, it has not been merged. Thus we'll shell out to command line `tar` on
// FreeBSD in the meantime, and unify the approaches if/when support is available.
#[cfg(target_os = "freebsd")]
fn unpack_sans_parent<R, P>(mut src: R, dst: P, levels_to_skip: usize) -> Result<()>
where
    R: Read,
    P: AsRef<Path>,
{
    std::fs::create_dir_all(dst.as_ref())?;
    let mut tar = std::process::Command::new("tar")
        .arg("-C")
        .arg(dst.as_ref())
        .arg("-x")
        .arg("-z")
        .arg(format!("--strip-components={}", levels_to_skip))
        .arg("-f")
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::null())
        .spawn()
        .expect("Failed to spawn `tar` process");
    let mut stdin = tar
        .stdin
        .take()
        .expect("Failed to get stdin for `tar` process");
    std::io::copy(&mut src, &mut stdin)?;
    Ok(())
}

fn format_progress_bar(state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write) {
    // Mimics Pkg.jl's smooth progress bar implementation:
    // https://github.com/JuliaLang/Pkg.jl/commit/e099a62e572c7f868857374688affc1fa1da0e88
    use console::Style;

    let width = 25; // Progress bar width in characters
    let pos = state.pos();
    let len = state.len().unwrap_or(pos);

    let perc = if len > 0 {
        (pos as f64 / len as f64) * 100.0
    } else {
        0.0
    };

    // Use floor instead of ceil for smoother progress
    let max_progress_width = width;
    let n_filled = ((max_progress_width as f64 * perc / 100.0).floor() as usize).min(width);
    let partial_filled = (max_progress_width as f64 * perc / 100.0) - n_filled as f64;

    let cyan = Style::new().cyan();
    let dim = Style::new().black().bright();

    // Draw filled portion (cyan)
    let filled_str = "━".repeat(n_filled);
    let _ = write!(w, "{}", cyan.apply_to(filled_str));

    // Draw partial character and empty portion to always maintain width
    if n_filled < width {
        let n_left = width - n_filled - 1;

        if partial_filled > 0.5 {
            // More filled, use ╸ in cyan
            let _ = write!(w, "{}", cyan.apply_to("╸"));
        } else {
            // Less filled, use ╺ in dim color
            let _ = write!(w, "{}", dim.apply_to("╺"));
        }

        // Draw empty portion (dim)
        let empty_str = "━".repeat(n_left);
        let _ = write!(w, "{}", dim.apply_to(empty_str));
    }
}

fn bar_style() -> ProgressStyle {
    ProgressStyle::default_bar()
        .template("{prefix:.cyan.bold} {bar} {bytes}/{total_bytes} eta: {eta}")
        .unwrap()
        .with_key("bar", format_progress_bar)
}

#[cfg(target_os = "macos")]
fn show_install_progress(message: &str) {
    eprint!(
        "\r\x1b[2K{} {}",
        style("  Installing").cyan().bold(),
        message
    );
    let _ = std::io::stderr().flush();
}

#[cfg(target_os = "macos")]
pub fn download_extract_dmg(url: &str, target_path: &Path) -> Result<String> {
    use std::fs::File;
    use std::io::Write;

    struct DmgMountGuard {
        mount_point: String,
        armed: bool,
    }

    impl DmgMountGuard {
        fn new(mount_point: String) -> Self {
            Self {
                mount_point,
                armed: true,
            }
        }

        fn detach(&mut self) {
            if !self.armed {
                return;
            }

            match std::process::Command::new("hdiutil")
                .args(["detach", "-force", &self.mount_point])
                .output()
            {
                Ok(output) if !output.status.success() => {
                    log::warn!(
                        "Failed to unmount DMG at {}: {}",
                        self.mount_point,
                        String::from_utf8_lossy(&output.stderr)
                    );
                }
                Err(e) => log::warn!("Failed to execute hdiutil detach: {}", e),
                _ => {}
            }

            self.armed = false;
        }
    }

    impl Drop for DmgMountGuard {
        fn drop(&mut self) {
            self.detach();
        }
    }

    log::debug!("Downloading DMG from url `{}`.", url);
    let response = http_client()?
        .get(url)
        .send()
        .with_context(|| format!("Failed to download from url `{}`.", url))?;

    if !response.status().is_success() {
        bail!("DMG not found at URL (status: {})", response.status());
    }

    let pb = match response.content_length() {
        Some(len) => ProgressBar::new(len),
        None => ProgressBar::new_spinner(),
    };
    pb.set_prefix(DOWNLOADING_PREFIX);
    pb.set_style(bar_style());

    let etag = response
        .headers()
        .get("etag")
        .ok_or_else(|| anyhow!("Failed to get etag from `{}`", url))?
        .to_str()?
        .to_string();

    // Download to temporary DMG file
    let temp_dmg = Builder::new().prefix("julia-").suffix(".dmg").tempfile()?;

    let mut dmg_file = File::create(temp_dmg.path())?;
    std::io::copy(&mut pb.wrap_read(response), &mut dmg_file)?;
    dmg_file.flush()?;
    drop(dmg_file);

    pb.finish_and_clear();

    if std::env::var_os("JULIAUP_TEST_DMG_FAIL").is_some() {
        bail!("Simulated DMG install failure for tests.");
    }

    show_install_progress("Mounting installer...");

    // Mount the DMG
    let mount_output = std::process::Command::new("hdiutil")
        .args(["attach", "-nobrowse", "-plist"])
        .arg(temp_dmg.path())
        .output()?;

    if !mount_output.status.success() {
        bail!(
            "Failed to mount DMG: {}",
            String::from_utf8_lossy(&mount_output.stderr)
        );
    }

    // Parse mount point from plist output
    let mount_output_str = String::from_utf8_lossy(&mount_output.stdout);
    let mount_point = mount_output_str
        .lines()
        .enumerate()
        .find_map(|(i, line)| {
            if line.trim() == "<key>mount-point</key>" {
                mount_output_str.lines().nth(i + 1).and_then(|next_line| {
                    let trimmed = next_line.trim();
                    trimmed
                        .strip_prefix("<string>")
                        .and_then(|s| s.strip_suffix("</string>"))
                        .map(|s| s.to_string())
                })
            } else {
                None
            }
        })
        .ok_or_else(|| {
            anyhow!(
                "Failed to parse mount point from hdiutil output. Output:\n{}",
                mount_output_str
            )
        })?;

    log::debug!("Mounted DMG at: {}", mount_point);
    let mut mount_guard = DmgMountGuard::new(mount_point.clone());

    show_install_progress("Copying application...");

    // Find and copy .app bundle
    let mount_path = Path::new(&mount_point);
    let app_bundle = std::fs::read_dir(mount_path)?
        .filter_map(|e| e.ok())
        .find(|e| e.file_name().to_str().is_some_and(|n| n.ends_with(".app")))
        .ok_or_else(|| anyhow!("No .app bundle found in DMG."))?;

    std::fs::create_dir_all(target_path)?;

    let copy_status = std::process::Command::new("cp")
        .args(["-R"])
        .arg(app_bundle.path())
        .arg(target_path)
        .status()?;

    if !copy_status.success() {
        bail!("Failed to copy .app bundle from DMG.");
    }

    show_install_progress("Unmounting installer...");
    mount_guard.detach();

    eprint!("\r\x1b[2K");
    let _ = std::io::stderr().flush();

    Ok(etag)
}

#[cfg(target_os = "macos")]
fn strip_quarantine_attribute(path: &Path) {
    let _ = std::process::Command::new("xattr")
        .args(["-dr", "com.apple.quarantine"])
        .arg(path)
        .output();
}

#[cfg(target_os = "macos")]
fn try_download_dmg_with_fallback(url: &url::Url, target_path: &Path) -> Result<(String, bool)> {
    let dmg_url = if url.as_str().ends_with(".tar.gz") {
        url.as_str().replace(".tar.gz", ".dmg")
    } else {
        url.to_string()
    };

    if let Ok(dmg_url) = url::Url::parse(&dmg_url) {
        if let Ok(etag) = download_extract_dmg(dmg_url.as_ref(), target_path) {
            strip_quarantine_attribute(target_path);
            return Ok((etag, true));
        }
    }

    let etag = download_extract_sans_parent(url.as_ref(), target_path, 1)?;
    strip_quarantine_attribute(target_path);
    Ok((etag, false))
}

#[cfg(not(windows))]
pub fn download_extract_sans_parent(
    url: &str,
    target_path: &Path,
    levels_to_skip: usize,
) -> Result<String> {
    log::debug!("Downloading from url `{}`.", url);
    let response = http_client()?
        .get(url)
        .send()
        .with_context(|| format!("Failed to download from url `{}`.", url))?;

    let content_length = response.content_length();

    let pb = match content_length {
        Some(content_length) => ProgressBar::new(content_length),
        None => ProgressBar::new_spinner(),
    };

    pb.set_prefix(DOWNLOADING_PREFIX);
    pb.set_style(bar_style());

    // Extract etag if present, otherwise return empty string
    // Empty etag is valid for regular version installs from servers without etag support
    let last_modified = response
        .headers()
        .get("etag")
        .map(|etag| etag.to_str().unwrap_or("").to_string())
        .unwrap_or_default();

    let response_with_pb = pb.wrap_read(response);

    unpack_sans_parent(response_with_pb, target_path, levels_to_skip)
        .with_context(|| format!("Failed to extract downloaded file from url `{}`.", url))?;

    Ok(last_modified)
}

#[cfg(windows)]
struct DataReaderWrap(windows::Storage::Streams::DataReader);

#[cfg(windows)]
impl std::io::Read for DataReaderWrap {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let mut bytes =
            self.0
                .LoadAsync(buf.len() as u32)
                .map_err(|e| std::io::Error::from_raw_os_error(e.code().0))?
                .join()
                .map_err(|e| std::io::Error::from_raw_os_error(e.code().0))? as usize;
        bytes = bytes.min(buf.len());
        self.0
            .ReadBytes(&mut buf[0..bytes])
            .map_err(|e| std::io::Error::from_raw_os_error(e.code().0))
            .map(|_| bytes)
    }
}

#[cfg(windows)]
pub fn download_extract_sans_parent(
    url: &str,
    target_path: &Path,
    levels_to_skip: usize,
) -> Result<String> {
    use windows::core::HSTRING;

    let http_client = http_client()?;

    let request_uri = windows::Foundation::Uri::CreateUri(&HSTRING::from(url))
        .with_context(|| "Failed to convert url string to Uri.")?;

    let http_response = http_client
        .GetAsync(&request_uri)
        .with_context(|| "Failed to initiate download.")?
        .join()
        .with_context(|| "Failed to complete async download operation.")?;

    http_response
        .EnsureSuccessStatusCode()
        .with_context(|| format!("Failed to download from `{}`.", url))?;

    // Extract etag if present, otherwise return empty string
    // Empty etag is valid for regular version installs from servers without etag support
    let last_modified = http_response
        .Headers()
        .ok()
        .and_then(|headers| headers.Lookup(&HSTRING::from("etag")).ok())
        .map(|etag| etag.to_string())
        .unwrap_or_default();

    let http_response_content = http_response
        .Content()
        .with_context(|| "Failed to obtain content from http response.")?;

    let response_stream = http_response_content
        .ReadAsInputStreamAsync()
        .with_context(|| "Failed to initiate get input stream from response")?
        .join()
        .with_context(|| "Failed to obtain input stream from http response")?;

    let reader = windows::Storage::Streams::DataReader::CreateDataReader(&response_stream)
        .with_context(|| "Failed to create DataReader.")?;

    reader
        .SetInputStreamOptions(windows::Storage::Streams::InputStreamOptions::ReadAhead)
        .with_context(|| "Failed to set input stream options.")?;

    let mut content_length: u64 = 0;
    let pb = if http_response_content.TryComputeLength(&mut content_length)? {
        ProgressBar::new(content_length)
    } else {
        ProgressBar::new_spinner()
    };

    pb.set_prefix(DOWNLOADING_PREFIX);
    pb.set_style(bar_style());

    let response_with_pb = pb.wrap_read(DataReaderWrap(reader));

    unpack_sans_parent(response_with_pb, target_path, levels_to_skip)
        .with_context(|| format!("Failed to extract downloaded file from url `{}`.", url))?;

    Ok(last_modified)
}

#[cfg(not(windows))]
pub fn download_juliaup_version(url: &str) -> Result<Version> {
    let response = http_client()?
        .get(url)
        .send()
        .with_context(|| format!("Failed to download from url `{}`.", url))?;

    let status = response.status();
    if !status.is_success() {
        let status_code = status.as_u16();
        let hint = match status_code {
            404 => "Resource not found",
            503 => "Service temporarily unavailable, please try again later",
            _ => "Unexpected server error",
        };
        anyhow::bail!(
            "Failed to download from `{}`: HTTP {} - {}",
            url,
            status,
            hint
        );
    }

    let response_text = response.text()?;
    let trimmed_response = response_text.trim();

    let version = Version::parse(trimmed_response).with_context(|| {
        format!(
            "`download_juliaup_version` failed to parse `{}` as a valid semversion.",
            trimmed_response
        )
    })?;

    Ok(version)
}

#[cfg(not(windows))]
pub fn download_versiondb(url: &str, path: &Path) -> Result<()> {
    let mut response = http_client()?
        .get(url)
        .send()
        .with_context(|| format!("Failed to download from url `{}`.", url))?;

    let status = response.status();
    if !status.is_success() {
        let status_code = status.as_u16();
        let hint = match status_code {
            404 => "Resource not found",
            503 => "Service temporarily unavailable, please try again later",
            _ => "Unexpected server error",
        };
        anyhow::bail!(
            "Failed to download version database from `{}`: HTTP {} - {}",
            url,
            status,
            hint
        );
    }

    let mut file = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(path)
        .with_context(|| {
            format!(
                "Failed to open or create version db file `{}`.",
                path.display()
            )
        })?;
    let mut buf: Vec<u8> = vec![];
    response.copy_to(&mut buf)?;
    file.write_all(buf.as_slice()).with_context(|| {
        format!(
            "Failed to write content into version db file `{}`.",
            path.display()
        )
    })?;

    Ok(())
}

#[cfg(windows)]
pub fn download_juliaup_version(url: &str) -> Result<Version> {
    let http_client = http_client()?;

    let request_uri = windows::Foundation::Uri::CreateUri(&windows::core::HSTRING::from(url))
        .with_context(|| "Failed to convert url string to Uri.")?;

    let async_op = http_client
        .GetStringAsync(&request_uri)
        .with_context(|| "Failed on http_client.GetStringAsync")?;

    let response = async_op
        .join()
        .with_context(|| "Failed on http_client.GetStringAsync.get")?
        .to_string();

    let trimmed_response = response.trim();

    let version = Version::parse(trimmed_response).with_context(|| {
        format!(
            "`download_juliaup_version` failed to parse `{}` as a valid semversion.",
            trimmed_response
        )
    })?;

    Ok(version)
}

#[cfg(windows)]
pub fn download_versiondb(url: &str, path: &Path) -> Result<()> {
    let http_client = http_client()?;

    let request_uri = windows::Foundation::Uri::CreateUri(&windows::core::HSTRING::from(url))
        .with_context(|| "Failed to convert url string to Uri.")?;

    let response = http_client
        .GetStringAsync(&request_uri)
        .with_context(|| "Failed to download version db step 1.")?
        .join()
        .with_context(|| "Failed to download version db step 2.")?
        .to_string();

    let mut file = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(path)
        .with_context(|| {
            format!(
                "Failed to open or create version db file `{}`.",
                path.display()
            )
        })?;

    file.write_all(response.as_bytes()).with_context(|| {
        format!(
            "Failed to write content into version db file `{}`.",
            path.display()
        )
    })?;

    Ok(())
}

/// Computes the relative binary path for storing in the config file.
///
/// Resolves the Julia binary within `target_path` (handling .app bundles on macOS),
/// then returns it as a relative path anchored at `rel_prefix` (which is typically
/// `./julia-{version}` or `./julia-{channel}`).
fn compute_relative_binary_path(
    target_path: &Path,
    rel_prefix: &Path,
    juliauphome: &Path,
) -> Option<String> {
    crate::utils::resolve_julia_binary_path(target_path)
        .ok()
        .map(|p| {
            p.strip_prefix(target_path)
                .map(|suffix| rel_prefix.join(suffix).to_string_lossy().into_owned())
                .unwrap_or_else(|_| {
                    p.strip_prefix(juliauphome)
                        .map(|rel| PathBuf::from(".").join(rel).to_string_lossy().into_owned())
                        .unwrap_or_else(|_| {
                            rel_prefix
                                .join("bin")
                                .join("julia")
                                .to_string_lossy()
                                .into_owned()
                        })
                })
        })
}

pub fn install_version(
    fullversion: &String,
    config_data: &mut JuliaupConfig,
    version_db: &JuliaupVersionDB,
    paths: &GlobalPaths,
) -> Result<()> {
    // Return immediately if the version is already installed.
    if config_data.installed_versions.contains_key(fullversion) {
        return Ok(());
    }

    // TODO At some point we could put this behind a conditional compile, we know
    // that we don't ship a bundled version for some platforms.
    let full_version_string_of_bundled_version = get_bundled_julia_version();
    let my_own_path = std::env::current_exe()?;
    let path_of_bundled_version = my_own_path
        .parent()
        .unwrap() // unwrap OK because we can't get a path that does not have a parent
        .join("BundledJulia");

    let child_target_foldername = format!("julia-{}", fullversion);
    let target_path = paths.juliauphome.join(&child_target_foldername);
    let target_parent = target_path.parent().ok_or_else(|| {
        anyhow!(
            "Target installation path `{}` has no parent directory.",
            target_path.display()
        )
    })?;
    std::fs::create_dir_all(target_parent).with_context(|| {
        format!(
            "Failed to create parent directory `{}` for installation path `{}`.",
            target_parent.display(),
            target_path.display()
        )
    })?;

    if fullversion == full_version_string_of_bundled_version && path_of_bundled_version.exists() {
        let mut options = fs_extra::dir::CopyOptions::new();
        options.overwrite = true;
        options.content_only = true;
        fs_extra::dir::copy(path_of_bundled_version, &target_path, &options)?;
    } else {
        let juliaupserver_base =
            get_juliaserver_base_url().with_context(|| "Failed to get Juliaup server base URL.")?;

        let download_url_path = &version_db
            .available_versions
            .get(fullversion)
            .ok_or_else(|| {
                anyhow!(
                    "Failed to find download url in versions db for '{}'.",
                    fullversion
                )
            })?
            .url_path;

        // On macOS, try_download_dmg_with_fallback handles .tar.gz → .dmg conversion
        // TODO: Replace runtime URL manipulation with database schema v2 that includes
        // multiple ranked download sources. See: https://github.com/JuliaLang/juliaup/pull/1320
        let download_url = juliaupserver_base
            .join(download_url_path)
            .with_context(|| {
                format!(
                    "Failed to construct a valid url from '{}' and '{}'.",
                    juliaupserver_base, download_url_path
                )
            })?;

        print_juliaup_style(
            "Installing",
            &format!("Julia {}", fullversion),
            JuliaupMessageType::Progress,
        );

        #[cfg(target_os = "macos")]
        let used_dmg = {
            let (_, used_dmg) = try_download_dmg_with_fallback(&download_url, &target_path)?;
            used_dmg
        };

        #[cfg(target_os = "macos")]
        if !used_dmg {
            let needs_notarization_check = semver::Version::parse(fullversion)
                .ok()
                .zip(semver::Version::parse("1.11.0-rc1").ok())
                .is_some_and(|(v, threshold)| v > threshold);

            if needs_notarization_check {
                let julia_path = crate::utils::resolve_julia_binary_path(&target_path)?;
                check_stdlib_notarization(&julia_path);
            }
        }

        #[cfg(not(target_os = "macos"))]
        {
            download_extract_sans_parent(download_url.as_ref(), &target_path, 1)?;
        }
    }

    let mut rel_path = PathBuf::new();
    rel_path.push(".");
    rel_path.push(&child_target_foldername);

    let binary_path = compute_relative_binary_path(&target_path, &rel_path, &paths.juliauphome);

    config_data.installed_versions.insert(
        fullversion.clone(),
        JuliaupConfigVersion {
            path: rel_path.to_string_lossy().into_owned(),
            binary_path,
        },
    );

    Ok(())
}

// which arch to default to when simply using the `nightly` or `pr00000` channel
pub fn default_arch() -> Result<String> {
    if cfg!(target_arch = "aarch64") {
        Ok("aarch64".to_string())
    } else if cfg!(target_arch = "x86_64") {
        Ok("x64".to_string())
    } else if cfg!(target_arch = "x86") {
        Ok("x86".to_string())
    } else {
        bail!("Unsupported architecture for nightly channel.")
    }
}

// which archs are compatible with the current system, for `juliaup list` purposes
pub fn compatible_archs() -> Result<Vec<String>> {
    if cfg!(target_os = "macos") {
        if cfg!(target_arch = "x86_64") {
            Ok(vec!["x64".to_string()])
        } else if cfg!(target_arch = "aarch64") {
            // Rosetta 2 can execute x86_64 binaries
            Ok(vec!["aarch64".to_string(), "x64".to_string()])
        } else {
            bail!("Unsupported architecture for nightly channel on macOS.")
        }
    } else if cfg!(target_arch = "x86") {
        Ok(vec!["x86".to_string()])
    } else if cfg!(target_arch = "x86_64") {
        // x86_64 can execute x86 binaries, but we don't produce x86 binaries for FreeBSD
        if cfg!(target_os = "freebsd") {
            Ok(vec!["x64".to_string()])
        } else {
            Ok(vec!["x86".to_string(), "x64".to_string()])
        }
    } else if cfg!(target_arch = "aarch64") {
        Ok(vec!["aarch64".to_string()])
    } else {
        bail!("Unsupported architecture for nightly channel.")
    }
}

// which nightly channels are compatible with the current system
pub fn get_channel_variations(channel: &str) -> Result<Vec<String>> {
    let archs = compatible_archs()?;

    let channels: Vec<String> = std::iter::once(channel.to_string())
        .chain(
            archs
                .into_iter()
                .map(|arch| format!("{}~{}", channel, arch)),
        )
        .collect();
    Ok(channels)
}

// considers the nightly channels as system channels
// XXX: does not account for PR channels
pub fn is_valid_channel(versions_db: &JuliaupVersionDB, channel: &String) -> Result<bool> {
    let regular = versions_db.has_channel(channel);

    let nightly_chans = get_channel_variations("nightly")?;

    let nightly = nightly_chans.contains(channel);
    Ok(regular || nightly)
}

pub fn is_pr_channel(channel: &str) -> bool {
    Regex::new(r"^(pr\d+)(~|$)").unwrap().is_match(channel)
}

fn parse_nightly_channel_or_id(channel: &str) -> Option<String> {
    let nightly_re =
        Regex::new(r"^((?:nightly|latest)|latest|(\d+\.\d+)-(?:nightly|latest))").unwrap();

    let caps = nightly_re.captures(channel)?;
    if let Some(xy_match) = caps.get(2) {
        Some(xy_match.as_str().to_string())
    } else {
        Some("".to_string())
    }
}

// Identify the unversioned name of a nightly (e.g., `latest-macos-x86_64`) for a channel
pub fn channel_to_name(channel: &str) -> Result<String> {
    let mut parts = channel.splitn(2, '~');

    let channel = parts.next().expect("Failed to parse channel name.");

    let version = if let Some(version_prefix) = parse_nightly_channel_or_id(channel) {
        if version_prefix.is_empty() {
            "latest".to_string()
        } else {
            format!("{}-latest", version_prefix)
        }
    } else {
        channel.to_string()
    };
    let arch = match parts.next() {
        Some(arch) => arch.to_string(),
        None => default_arch()?,
    };

    let os_arch_suffix = {
        #[cfg(target_os = "macos")]
        if arch == "x64" {
            "macos-x86_64"
        } else if arch == "aarch64" {
            "macos-aarch64"
        } else {
            bail!("Unsupported architecture for nightly channel on macOS.")
        }

        #[cfg(target_os = "windows")]
        if arch == "x64" {
            "win64"
        } else if arch == "x86" {
            "win32"
        } else {
            bail!("Unsupported architecture for nightly channel on Windows.")
        }

        #[cfg(target_os = "linux")]
        if arch == "x64" {
            "linux-x86_64"
        } else if arch == "x86" {
            "linux-i686"
        } else if arch == "aarch64" {
            "linux-aarch64"
        } else {
            bail!("Unsupported architecture for nightly channel on Linux.")
        }

        #[cfg(target_os = "freebsd")]
        if arch == "x64" {
            "freebsd-x86_64"
        } else {
            bail!("Unsupported architecture for nightly channel on FreeBSD.")
        }
    };

    Ok(version.to_string() + "-" + os_arch_suffix)
}

fn query_julia_version(julia_path: &Path) -> Result<String> {
    let output = std::process::Command::new(julia_path)
        .arg("--startup-file=no")
        .arg("-e")
        .arg("print(VERSION)")
        .output()
        .with_context(|| {
            format!(
                "Failed to execute Julia binary at `{}`.",
                julia_path.display()
            )
        })?;
    Ok(String::from_utf8(output.stdout)?)
}

/// Installs Julia from a URL into the specified path.
/// Returns the config channel and a bool indicating whether a DMG installer was used (macOS only).
pub fn install_from_url(
    url: &Url,
    path: &PathBuf,
    #[cfg_attr(not(target_os = "macos"), allow(unused))] is_pr: bool,
    paths: &GlobalPaths,
) -> Result<(crate::config_file::JuliaupConfigChannel, bool)> {
    // Check if the nightly server supports etag headers (required for nightly/PR channels)
    // Do this BEFORE downloading to avoid wasting bandwidth
    if !check_server_supports_nightlies()
        .context("Failed to check if nightly server supports etag headers")?
    {
        bail!(
            "The configured nightly server does not support etag headers, which are required for nightly and PR channels.\n\
            Nightly and PR channels cannot be installed from this server."
        );
    }

    // Download and extract into a temporary directory
    let temp_dir = Builder::new()
        .prefix("julia-temp-")
        .tempdir_in(&paths.juliauphome)
        .expect("Failed to create temporary directory");

    #[cfg(target_os = "macos")]
    let (server_etag, used_dmg) = try_download_dmg_with_fallback(url, temp_dir.path())?;

    #[cfg(not(target_os = "macos"))]
    let (server_etag, used_dmg) = {
        let download_result = download_extract_sans_parent(url.as_ref(), temp_dir.path(), 1);
        match download_result {
            Ok(last_updated) => (last_updated, false),
            Err(e) => {
                std::fs::remove_dir_all(temp_dir.path())?;
                bail!("Failed to download and extract pr or nightly: {}", e);
            }
        }
    };

    // On macOS, PR tarball builds are unsigned. Gatekeeper blocks unsigned binaries,
    // which would hang the version query below. Prompt the user to codesign.
    // DMG installs come with proper code signatures, so skip codesigning for those.
    #[cfg(target_os = "macos")]
    let did_codesign = if is_pr && !used_dmg {
        prompt_and_codesign_pr_build(temp_dir.path())?
    } else {
        true // DMG installs and official nightlies are already signed
    };

    // Query the actual version
    let julia_path = crate::utils::resolve_julia_binary_path(temp_dir.path())
        .with_context(|| "Failed to resolve Julia binary path after extraction.")?;

    #[cfg(target_os = "macos")]
    let julia_version = if did_codesign {
        let version = query_julia_version(&julia_path)?;
        if !used_dmg {
            check_stdlib_notarization(&julia_path);
        }
        version
    } else {
        String::new()
    };

    #[cfg(not(target_os = "macos"))]
    let julia_version = query_julia_version(&julia_path)?;

    // Move into the final location
    let target_path = paths.juliauphome.join(path);
    if target_path.exists() {
        std::fs::remove_dir_all(&target_path)?;
    }

    // keep() consumes the TempDir and returns the path without cleanup
    // For macOS DMG installs, this preserves the .app bundle structure
    retry_rename(&temp_dir.keep(), &target_path)?;

    let binary_path = compute_relative_binary_path(&target_path, path, &paths.juliauphome);

    Ok((
        JuliaupConfigChannel::DirectDownloadChannel {
            path: path.to_string_lossy().into_owned(),
            url: url.to_string().to_owned(), // TODO Use proper URL
            local_etag: server_etag.clone(), // TODO Use time stamp of HTTPS response
            server_etag,
            version: julia_version,
            binary_path,
        },
        used_dmg,
    ))
}

/// Installs a non-database version (nightly/PR) of Julia.
/// Returns the config channel and a bool indicating whether a DMG installer was used (macOS only).
pub fn install_non_db_version(
    channel: &str,
    name: &String,
    paths: &GlobalPaths,
) -> Result<(crate::config_file::JuliaupConfigChannel, bool)> {
    // Check if the nightly server supports etag headers (required for nightly/PR channels)
    if !check_server_supports_nightlies()
        .context("Failed to check if nightly server supports etag headers")?
    {
        bail!(
            "The configured nightly server does not support etag headers, which are required for nightly and PR channels.\n\
            Nightly and PR channels cannot be installed from this server."
        );
    }

    // Determine the download URL
    let download_url_base = get_julianightlies_base_url()?;

    let mut parts = name.splitn(2, '-');

    let mut id = parts
        .next()
        .expect("Failed to parse channel name.")
        .to_string();
    let mut arch = parts.next().expect("Failed to parse channel name.");

    // Check for the case where name is given as "x.y-latest-...", in which case
    // we peel off the "latest" part of the `arch` and attach it to the `id``.
    if arch.starts_with("latest") {
        let mut parts = arch.splitn(2, '-');
        let nightly = parts.next().expect("Failed to parse channel name.");
        id.push('-');
        id.push_str(nightly);
        arch = parts.next().expect("Failed to parse channel name.");
    }

    let nightly_version = parse_nightly_channel_or_id(&id);

    let download_url_path = if let Some(nightly_version) = nightly_version {
        let nightly_folder = if nightly_version.is_empty() {
            "".to_string() // No version folder
        } else {
            format!("/{}", nightly_version) // Use version as folder
        };
        match arch {
            "macos-x86_64" => Ok(format!(
                "bin/macos/x86_64{}/julia-latest-macos-x86_64.tar.gz",
                nightly_folder
            )),
            "macos-aarch64" => Ok(format!(
                "bin/macos/aarch64{}/julia-latest-macos-aarch64.tar.gz",
                nightly_folder
            )),
            "win64" => Ok(format!(
                "bin/winnt/x64{}/julia-latest-win64.tar.gz",
                nightly_folder
            )),
            "win32" => Ok(format!(
                "bin/winnt/x86{}/julia-latest-win32.tar.gz",
                nightly_folder
            )),
            "linux-x86_64" => Ok(format!(
                "bin/linux/x86_64{}/julia-latest-linux-x86_64.tar.gz",
                nightly_folder
            )),
            "linux-i686" => Ok(format!(
                "bin/linux/i686{}/julia-latest-linux-i686.tar.gz",
                nightly_folder
            )),
            "linux-aarch64" => Ok(format!(
                "bin/linux/aarch64{}/julia-latest-linux-aarch64.tar.gz",
                nightly_folder
            )),
            "freebsd-x86_64" => Ok(format!(
                "bin/freebsd/x86_64{}/julia-latest-freebsd-x86_64.tar.gz",
                nightly_folder
            )),
            _ => Err(anyhow!("Unknown nightly.")),
        }
    } else if id.starts_with("pr") {
        match arch {
            // https://github.com/JuliaLang/juliaup/issues/903#issuecomment-2183206994
            "macos-x86_64" => {
                Ok("bin/macos/x86_64/julia-".to_owned() + &id + "-macos-x86_64.tar.gz")
            }
            "macos-aarch64" => {
                Ok("bin/macos/aarch64/julia-".to_owned() + &id + "-macos-aarch64.tar.gz")
            }
            "win64" => Ok("bin/windows/x86_64/julia-".to_owned() + &id + "-windows-x86_64.tar.gz"),
            "win32" => Ok("bin/windows/x86/julia-".to_owned() + &id + "-windows-x86.tar.gz"),
            "linux-x86_64" => {
                Ok("bin/linux/x86_64/julia-".to_owned() + &id + "-linux-x86_64.tar.gz")
            }
            "linux-aarch64" => {
                Ok("bin/linux/aarch64/julia-".to_owned() + &id + "-linux-aarch64.tar.gz")
            }
            "freebsd-x86_64" => {
                Ok("bin/freebsd/x86_64/julia-".to_owned() + &id + "-freebsd-x86_64.tar.gz")
            }
            _ => Err(anyhow!("Unknown pr.")),
        }
    } else {
        Err(anyhow!("Unknown non-db channel."))
    }?;

    let download_url = download_url_base
        .join(download_url_path.as_str())
        .with_context(|| {
            format!(
                "Failed to construct a valid url from '{}' and '{}'.",
                download_url_base, download_url_path
            )
        })?;

    let child_target_foldername = format!("julia-{}", channel);

    let mut rel_path = PathBuf::new();
    rel_path.push(".");
    rel_path.push(&child_target_foldername);

    print_juliaup_style(
        "Installing",
        &format!("Julia {}", name),
        JuliaupMessageType::Progress,
    );

    let (channel_data, used_dmg) =
        install_from_url(&download_url, &rel_path, is_pr_channel(channel), paths)?;

    Ok((channel_data, used_dmg))
}

pub fn garbage_collect_versions(
    prune_linked: bool,
    config_data: &mut JuliaupConfig,
    paths: &GlobalPaths,
) -> Result<()> {
    let mut versions_to_uninstall: Vec<String> = Vec::new();
    for (installed_version, detail) in &config_data.installed_versions {
        if config_data.installed_channels.iter().all(|j| match &j.1 {
            JuliaupConfigChannel::SystemChannel { version } => version != installed_version,
            _ => true,
        }) {
            let path_to_delete = paths.juliauphome.join(&detail.path).canonicalize()?;
            let display = path_to_delete.display();

            match std::fs::remove_dir_all(&path_to_delete) {
                Ok(_) => versions_to_uninstall.push(installed_version.clone()),
                Err(_) => print_juliaup_style(
                    "WARNING",
                    &format!(
                        "Failed to delete {}. \
                    Make sure to close any old julia version still running.\n\
                    You can try to delete at a later point by running `juliaup gc`.",
                        display
                    ),
                    JuliaupMessageType::Warning,
                ),
            }
        }
    }

    if versions_to_uninstall.is_empty() {
        print_juliaup_style(
            "Tidyup",
            "No unused Julia installations to clean up.",
            JuliaupMessageType::Success,
        );
    } else {
        for i in versions_to_uninstall {
            print_juliaup_style(
                "Tidyup",
                &format!("Removed Julia {}", &i),
                JuliaupMessageType::Success,
            );
            config_data.installed_versions.remove(&i);
        }
    }

    if prune_linked {
        let mut channels_to_uninstall: Vec<String> = Vec::new();
        for (installed_channel, detail) in &config_data.installed_channels {
            if let JuliaupConfigChannel::LinkedChannel {
                command: cmd,
                args: _,
            } = &detail
            {
                if !is_valid_julia_path(&PathBuf::from(cmd)) {
                    channels_to_uninstall.push(installed_channel.clone());
                }
            }
        }
        for channel in channels_to_uninstall {
            remove_symlink(&format!("julia-{}", &channel))?;
            config_data.installed_channels.remove(&channel);
        }
    }

    Ok(())
}

fn _remove_symlink(symlink_path: &Path) -> Result<Option<PathBuf>> {
    let parent = symlink_path.parent().ok_or_else(|| {
        anyhow!(
            "Symlink path `{}` has no parent directory.",
            symlink_path.display()
        )
    })?;

    std::fs::create_dir_all(parent).with_context(|| {
        format!(
            "Failed to create parent directory `{}` for symlink `{}`.",
            parent.display(),
            symlink_path.display()
        )
    })?;

    if symlink_path.exists() {
        let prev_target = std::fs::read_link(symlink_path).with_context(|| {
            format!(
                "Failed to read existing symlink target at `{}`.",
                symlink_path.display()
            )
        })?;
        std::fs::remove_file(symlink_path).with_context(|| {
            format!(
                "Failed to remove existing symlink `{}`.",
                symlink_path.display()
            )
        })?;
        return Ok(Some(prev_target));
    }

    Ok(None)
}

pub fn remove_symlink(symlink_name: &String) -> Result<()> {
    let symlink_path = get_bin_dir()
        .with_context(|| "Failed to retrieve binary directory while trying to remove a symlink.")?
        .join(symlink_name);

    print_juliaup_style(
        "Deleting",
        &format!("symlink {}.", symlink_name),
        JuliaupMessageType::Progress,
    );

    _remove_symlink(&symlink_path)?;

    Ok(())
}

#[cfg(not(windows))]
fn create_system_channel_symlink(
    version: &str,
    symlink_name: &str,
    symlink_path: &Path,
    paths: &GlobalPaths,
    updating: &Option<PathBuf>,
) -> Result<()> {
    let child_target_foldername = format!("julia-{}", version);
    let target_path = paths.juliauphome.join(&child_target_foldername);

    if let Some(ref prev_target) = updating {
        print_juliaup_style(
            "Updating",
            &format!(
                "symlink {} ( {} -> {} )",
                symlink_name,
                prev_target.to_string_lossy(),
                version
            ),
            JuliaupMessageType::Progress,
        );
    } else {
        print_juliaup_style(
            "Creating",
            &format!("symlink {} for Julia {}", symlink_name, version),
            JuliaupMessageType::Progress,
        );
    }

    let binary_path = crate::utils::resolve_julia_binary_path(&target_path)?;
    std::os::unix::fs::symlink(binary_path, symlink_path).with_context(|| {
        format!(
            "failed to create symlink `{}`.",
            symlink_path.to_string_lossy()
        )
    })
}

#[cfg(not(windows))]
fn create_direct_download_symlink(
    path: &str,
    version: &str,
    symlink_name: &str,
    symlink_path: &Path,
    paths: &GlobalPaths,
    updating: &Option<PathBuf>,
) -> Result<()> {
    let target_path = paths.juliauphome.join(path);

    if let Some(ref prev_target) = updating {
        print_juliaup_style(
            "Updating",
            &format!(
                "symlink {} ( {} -> {} )",
                symlink_name,
                prev_target.to_string_lossy(),
                version
            ),
            JuliaupMessageType::Progress,
        );
    } else {
        print_juliaup_style(
            "Creating",
            &format!("symlink {} for Julia {}", symlink_name, version),
            JuliaupMessageType::Progress,
        );
    }

    let binary_path = crate::utils::resolve_julia_binary_path(&target_path)?;
    std::os::unix::fs::symlink(binary_path, symlink_path).with_context(|| {
        format!(
            "failed to create symlink `{}`.",
            symlink_path.to_string_lossy()
        )
    })
}

#[cfg(not(windows))]
fn create_linked_channel_shim(
    command: &str,
    args: &Option<Vec<String>>,
    symlink_name: &str,
    symlink_path: &Path,
    updating: &Option<PathBuf>,
) -> Result<()> {
    let formatted_command = match args {
        Some(x) => format!("{} {}", command, x.join(" ")),
        None => command.to_string(),
    };

    if let Some(ref prev_target) = updating {
        print_juliaup_style(
            "Updating",
            &format!(
                "shim {} ( {} -> {} )",
                symlink_name,
                prev_target.to_string_lossy(),
                formatted_command
            ),
            JuliaupMessageType::Progress,
        );
    } else {
        print_juliaup_style(
            "Creating",
            &format!("shim {} for {}", symlink_name, formatted_command),
            JuliaupMessageType::Progress,
        );
    }

    std::fs::write(
        symlink_path,
        format!(
            r#"#!/bin/sh
{} "$@"
"#,
            formatted_command,
        ),
    )
    .with_context(|| {
        format!(
            "failed to create shim `{}`.",
            symlink_path.to_string_lossy()
        )
    })?;

    // set as executable
    let perms = std::fs::Permissions::from_mode(0o755);
    std::fs::set_permissions(symlink_path, perms).with_context(|| {
        format!(
            "failed to change permissions for shim `{}`.",
            symlink_path.to_string_lossy()
        )
    })
}

#[cfg(not(windows))]
pub fn create_symlink(
    channel: &JuliaupConfigChannel,
    symlink_name: &String,
    paths: &GlobalPaths,
) -> Result<()> {
    let symlink_folder = get_bin_dir()
        .with_context(|| "Failed to retrieve binary directory while trying to create a symlink.")?;

    let symlink_path = symlink_folder.join(symlink_name);
    let updating = _remove_symlink(&symlink_path)?;

    match channel {
        JuliaupConfigChannel::SystemChannel { version } => {
            create_system_channel_symlink(version, symlink_name, &symlink_path, paths, &updating)
        }
        JuliaupConfigChannel::DirectDownloadChannel { path, version, .. } => {
            create_direct_download_symlink(
                path,
                version,
                symlink_name,
                &symlink_path,
                paths,
                &updating,
            )
        }
        JuliaupConfigChannel::LinkedChannel { command, args } => {
            create_linked_channel_shim(command, args, symlink_name, &symlink_path, &updating)
        }
        JuliaupConfigChannel::AliasChannel { .. } => Ok(()), // Aliases have their symlinks resolved at runtime
    }?;

    if updating.is_none() {
        if let Ok(path) = std::env::var("PATH") {
            if !path.split(':').any(|p| Path::new(p) == symlink_folder) {
                eprintln!(
                "Symlink {} added in {}. Add this directory to the system PATH to make the command available in your shell.",
                &symlink_name, symlink_folder.display(),
            );
            }
        }
    }

    Ok(())
}

#[cfg(windows)]
pub fn create_symlink(_: &JuliaupConfigChannel, _: &String, _paths: &GlobalPaths) -> Result<()> {
    Ok(())
}

#[cfg(feature = "selfupdate")]
pub fn install_background_selfupdate(interval: i64) -> Result<()> {
    use itertools::Itertools;
    use std::process::Stdio;

    let own_exe_path = std::env::current_exe()
        .with_context(|| "Could not determine the path of the running exe.")?;

    let my_own_path = own_exe_path.to_str().unwrap();

    match std::env::var("WSL_DISTRO_NAME") {
        // This is the WSL case, where we schedule a Windows task to do the update
        Ok(val) => {
            std::process::Command::new("schtasks.exe")
                .args([
                    "/create",
                    "/sc",
                    "minute",
                    "/mo",
                    &interval.to_string(),
                    "/tn",
                    &format!("Juliaup self update for WSL {} distribution", val),
                    "/f",
                    "/it",
                    "/tr",
                    &format!("wsl --distribution {} {} self update", val, my_own_path),
                ])
                .output()
                .with_context(|| "Failed to create new Windows task for juliaup.")?;
        }
        Err(_e) => {
            let output = std::process::Command::new("crontab")
                .args(["-l"])
                .output()
                .with_context(|| "Failed to retrieve crontab configuration.")?;

            let new_crontab_content = String::from_utf8(output.stdout)?
                .lines()
                .filter(|x| !x.contains("4c79c12db1d34bbbab1f6c6f838f423f"))
                .chain([
                    &format!(
                        "*/{} * * * * {} 4c79c12db1d34bbbab1f6c6f838f423f",
                        interval, my_own_path
                    ),
                    "",
                ])
                .join("\n");

            let mut child = std::process::Command::new("crontab")
                .stdin(Stdio::piped())
                .stdout(Stdio::piped())
                .spawn()?;

            let mut child_stdin = child.stdin.take().unwrap();

            child_stdin.write_all(new_crontab_content.as_bytes())?;

            // Close stdin to finish and avoid indefinite blocking
            drop(child_stdin);

            child.wait_with_output()?;
        }
    };

    Ok(())
}

#[cfg(feature = "selfupdate")]
pub fn uninstall_background_selfupdate() -> Result<()> {
    use itertools::Itertools;
    use std::process::Stdio;

    match std::env::var("WSL_DISTRO_NAME") {
        // This is the WSL case, where we schedule a Windows task to do the update
        Ok(val) => {
            std::process::Command::new("schtasks.exe")
                .args([
                    "/delete",
                    "/tn",
                    &format!("Juliaup self update for WSL {} distribution", val),
                    "/f",
                ])
                .output()
                .with_context(|| "Failed to remove Windows task for juliaup.")?;
        }
        Err(_e) => {
            let output = std::process::Command::new("crontab")
                .args(["-l"])
                .output()
                .with_context(|| "Failed to remove cron task.")?;

            let new_crontab_content = String::from_utf8(output.stdout)?
                .lines()
                .filter(|x| !x.contains("4c79c12db1d34bbbab1f6c6f838f423f"))
                .chain([""])
                .join("\n");

            let mut child = std::process::Command::new("crontab")
                .stdin(Stdio::piped())
                .stdout(Stdio::piped())
                .spawn()?;

            let mut child_stdin = child.stdin.take().unwrap();

            child_stdin.write_all(new_crontab_content.as_bytes())?;

            // Close stdin to finish and avoid indefinite blocking
            drop(child_stdin);

            child.wait_with_output()?;
        }
    };

    Ok(())
}

const S_MARKER: &[u8] = b"# >>> juliaup initialize >>>";
const E_MARKER: &[u8] = b"# <<< juliaup initialize <<<";
const HEADER: &[u8] = b"\n\n# !! Contents within this block are managed by juliaup !!\n\n";

fn get_shell_script_juliaup_content(
    bin_path: &Path,
    juliauphome: &Path,
    path: &Path,
) -> Result<Vec<u8>> {
    let mut result: Vec<u8> = Vec::new();

    let bin_path_str = match bin_path.to_str() {
        Some(s) => s,
        None =>  bail!("Could not create UTF-8 string from passed-in binary application path. Currently only valid UTF-8 paths are supported"),
    };

    let juliauphome_str = match juliauphome.to_str() {
        Some(s) => s,
        None => bail!("Could not create UTF-8 string from juliaup home path."),
    };

    result.extend_from_slice(S_MARKER);
    result.extend_from_slice(HEADER);
    let file_name = path
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or_else(|| anyhow!("Could not determine file name for path: {}", path.display()))?;
    if file_name == ".zshrc" {
        append_zsh_content(&mut result, bin_path_str);
    } else if path.file_name().unwrap() == ".cshrc" || path.file_name().unwrap() == ".tcshrc" {
        append_csh_content(&mut result, bin_path_str);
    } else {
        append_sh_content(&mut result, bin_path_str);
    }
    if file_name == ".zshrc" || file_name.starts_with(".bash") {
        append_completions_content(&mut result, file_name, juliauphome_str);
    }
    result.extend_from_slice(b"\n");
    result.extend_from_slice(E_MARKER);

    Ok(result)
}

fn append_zsh_content(buf: &mut Vec<u8>, path_str: &str) {
    // zsh specific syntax for path extension
    let content = formatdoc!(
        "
            path=('{}' $path)
            export PATH
        ",
        path_str
    );

    buf.extend_from_slice(content.as_bytes());
}

fn append_csh_content(buf: &mut Vec<u8>, path_str: &str) {
    // csh specific syntax for path extension
    let content = formatdoc!(
        "
            set path = ({} $path)
        ",
        path_str
    );

    buf.extend_from_slice(content.as_bytes());
}

fn append_sh_content(buf: &mut Vec<u8>, path_str: &str) {
    // If the variable is already contained in $PATH, do nothing
    // Otherwise prepend it to path
    // ${PATH:+:${PATH}} => Only append :$PATH if $PATH is set
    let content = formatdoc!(
        "
            case \":$PATH:\" in
                *:{0}:*)
                    ;;

                *)
                    export PATH={0}${{PATH:+:${{PATH}}}}
                    ;;
            esac
        ",
        path_str
    );
    buf.extend_from_slice(content.as_bytes());
}

fn append_completions_content(buf: &mut Vec<u8>, file_name: &str, juliauphome: &str) {
    let (shell, ext) = if file_name == ".zshrc" {
        ("zsh", "zsh")
    } else {
        ("bash", "sh")
    };
    let content = formatdoc!(
        r#"
            # Tab completion for juliaup and julia channel selection
            [ -f "{juliauphome}/completions/{shell}.{ext}" ] && source "{juliauphome}/completions/{shell}.{ext}"
        "#,
    );
    buf.extend_from_slice(content.as_bytes());
}

fn match_markers(buffer: &[u8]) -> Result<Option<(usize, usize)>> {
    let start_marker = buffer.find(S_MARKER);
    let end_marker = buffer.find(E_MARKER);

    // This ensures exactly one opening and one closing marker exists
    let (start_marker, end_marker) = match (start_marker, end_marker) {
        (Some(sidx), Some(eidx)) => {
            if sidx != buffer.rfind(S_MARKER).unwrap() || eidx != buffer.rfind(E_MARKER).unwrap() {
                bail!("Found multiple startup script sections from juliaup.");
            }
            (sidx, eidx)
        }
        (None, None) => {
            return Ok(None);
        }
        (_, None) => {
            bail!("Found an opening marker but no end marker of juliaup section.");
        }
        (None, _) => {
            bail!("Found an opening marker but no end marker of juliaup section.");
        }
    };

    Ok(Some((start_marker, end_marker + E_MARKER.len())))
}

fn add_path_to_specific_file(bin_path: &Path, juliauphome: &Path, path: &Path) -> Result<()> {
    let mut file = std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(false)
        .open(path)
        .with_context(|| format!("Failed to open file {}.", path.display()))?;

    let mut buffer: Vec<u8> = Vec::new();

    file.read_to_end(&mut buffer)
        .with_context(|| format!("Failed to read data from file {}.", path.display()))?;

    let existing_code_pos = match_markers(&buffer).with_context(|| {
        format!(
            "Error occured while searching juliaup shell startup script section in {}",
            path.display()
        )
    })?;

    let new_content =
        get_shell_script_juliaup_content(bin_path, juliauphome, path).with_context(|| {
            format!(
                "Error occured while generating juliaup shell startup script section for {}",
                path.display()
            )
        })?;

    match existing_code_pos {
        Some(pos) => {
            buffer.replace_range(pos.0..pos.1, &new_content);
        }
        None => {
            buffer.extend_from_slice(b"\n");
            buffer.extend_from_slice(&new_content);
            buffer.extend_from_slice(b"\n");
        }
    };

    file.rewind()
        .with_context(|| format!("Failed to rewind file {}.", path.display()))?;

    file.set_len(0)
        .with_context(|| format!("Failed to truncate file {}.", path.display()))?;

    file.write_all(&buffer)
        .with_context(|| format!("Failed to write file {}.", path.display()))?;

    file.sync_all()
        .with_context(|| format!("Failed to sync file {}.", path.display()))?;

    Ok(())
}

fn remove_path_from_specific_file(path: &Path) -> Result<()> {
    let mut file = std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open(path)
        .with_context(|| format!("Failed to open file: {}", path.display()))?;

    let mut buffer: Vec<u8> = Vec::new();

    file.read_to_end(&mut buffer)?;

    let existing_code_pos = match_markers(&buffer).with_context(|| {
        format!(
            "Error occured while searching juliaup shell startup script section in {}",
            path.display()
        )
    })?;

    if let Some(pos) = existing_code_pos {
        buffer.replace_range(pos.0..pos.1, "");

        file.rewind().unwrap();

        file.set_len(0).unwrap();

        file.write_all(&buffer).unwrap();

        file.sync_all().unwrap();
    }

    Ok(())
}

pub fn find_shell_scripts_to_be_modified(add_case: bool) -> Result<Vec<PathBuf>> {
    let home_dir = dirs::home_dir().unwrap();

    let paths_to_test: Vec<PathBuf> = vec![
        home_dir.join(".bashrc"),
        home_dir.join(".profile"),
        home_dir.join(".bash_profile"),
        home_dir.join(".bash_login"),
        home_dir.join(".zshrc"),
        home_dir.join(".cshrc"),
        home_dir.join(".tcshrc"),
    ];

    let result = paths_to_test
        .iter()
        .filter(
            |p| {
                p.exists()
                    || (add_case
                        && p.file_name().unwrap() == ".zshrc"
                        && std::env::consts::OS == "macos")
            }, // On MacOS, always edit .zshrc as that is the default shell, but only when we add things
        )
        .cloned()
        .collect();
    Ok(result)
}

pub fn add_binfolder_to_path_in_shell_scripts(bin_path: &Path, juliauphome: &Path) -> Result<()> {
    write_completion_files::<Juliaup>(juliauphome, "juliaup")
        .with_context(|| "Failed to write completion files.")?;

    let paths = find_shell_scripts_to_be_modified(true)?;

    paths.into_iter().for_each(|p| {
        add_path_to_specific_file(bin_path, juliauphome, &p).unwrap();
    });
    Ok(())
}

pub fn remove_binfolder_from_path_in_shell_scripts() -> Result<()> {
    let paths = find_shell_scripts_to_be_modified(false)?;

    paths.into_iter().for_each(|p| {
        remove_path_from_specific_file(&p).unwrap();
    });
    Ok(())
}

/// Re-generate the juliaup init block in any shell rc files that already contain it.
/// This is called during self-update to propagate changes (e.g. new completions)
/// to existing users without requiring them to re-run `juliaup config modifypath true`.
pub fn refresh_existing_shell_init_blocks(bin_path: &Path, juliauphome: &Path) -> Result<()> {
    let paths = find_shell_scripts_to_be_modified(false)?;

    for p in paths {
        let content = std::fs::read(&p).unwrap_or_default();
        if match_markers(&content).unwrap_or(None).is_some() {
            add_path_to_specific_file(bin_path, juliauphome, &p).ok();
        }
    }
    Ok(())
}

pub fn update_version_db(channel: &Option<String>, paths: &GlobalPaths) -> Result<()> {
    print_juliaup_style(
        "Checking",
        "for new Julia versions",
        JuliaupMessageType::Progress,
    );

    let file_lock = get_read_lock(paths)?;

    let mut temp_versiondb_download_path: Option<TempPath> = None;
    let mut delete_old_version_db: bool = false;

    let old_config_file = load_config_db(paths, Some(&file_lock)).with_context(|| {
        "`run_command_update_version_db` command failed to load configuration db."
    })?;

    let local_dbversion = match std::fs::OpenOptions::new()
        .read(true)
        .open(&paths.versiondb)
    {
        Ok(file) => {
            let reader = BufReader::new(&file);

            if let Ok(versiondb) =
                serde_json::from_reader::<BufReader<&std::fs::File>, JuliaupVersionDB>(reader)
            {
                semver::Version::parse(&versiondb.version).ok()
            } else {
                None
            }
        }
        Err(_) => None,
    };

    // This scope makes sure the lock file gets closed after we release the lock
    {
        let (_, res) = file_lock.data_unlock();
        res.with_context(|| {
            format!(
                "Failed to unlock configuration lock file `{}`.",
                paths.lockfile.display()
            )
        })?;
    }

    #[cfg(feature = "selfupdate")]
    let juliaup_channel = match &old_config_file.self_data.juliaup_channel {
        Some(juliaup_channel) => juliaup_channel.to_string(),
        None => "release".to_string(),
    };

    // TODO Figure out how we can learn about the correct Juliaup channel here
    #[cfg(not(feature = "selfupdate"))]
    let juliaup_channel = "release".to_string();

    let juliaupserver_base =
        get_juliaserver_base_url().with_context(|| "Failed to get Juliaup server base URL.")?;

    let dbversion_url_path = match juliaup_channel.as_str() {
        "release" => "juliaup/RELEASECHANNELDBVERSION",
        "releasepreview" => "juliaup/RELEASEPREVIEWCHANNELDBVERSION",
        "dev" => "juliaup/DEVCHANNELDBVERSION",
        _ => bail!(
            "Juliaup is configured to a channel named '{}' that does not exist.",
            &juliaup_channel
        ),
    };

    let dbversion_url = juliaupserver_base
        .join(dbversion_url_path)
        .with_context(|| {
            format!(
                "Failed to construct a valid url from '{}' and '{}'.",
                juliaupserver_base, dbversion_url_path
            )
        })?;

    let online_dbversion = download_juliaup_version(dbversion_url.as_ref())
        .with_context(|| "Failed to download current version db version.")?;

    let bundled_dbversion = get_bundled_dbversion()
        .with_context(|| "Failed to determine the bundled version db version.")?;

    if online_dbversion > bundled_dbversion {
        if local_dbversion.is_none() || online_dbversion > local_dbversion.unwrap() {
            let onlineversiondburl = juliaupserver_base
                .join(&format!(
                    "juliaup/versiondb/versiondb-{}-{}.json",
                    online_dbversion,
                    get_juliaup_target()
                ))
                .with_context(|| "Failed to construct URL for version db download.")?;

            let versiondb_parent = paths.versiondb.parent().ok_or_else(|| {
                anyhow!(
                    "Version db path `{}` has no parent directory.",
                    paths.versiondb.display()
                )
            })?;

            let temp_path = tempfile::NamedTempFile::new_in(versiondb_parent)
                .with_context(|| {
                    format!(
                        "Failed to create temporary version db file in `{}`.",
                        versiondb_parent.display()
                    )
                })?
                .into_temp_path();

            download_versiondb(onlineversiondburl.as_ref(), &temp_path).with_context(|| {
                format!(
                    "Failed to download new version db from {}.",
                    onlineversiondburl
                )
            })?;

            temp_versiondb_download_path = Some(temp_path);
        }
    } else if local_dbversion.is_some() {
        // If the bundled version is up-to-date we can delete any cached version db json file
        delete_old_version_db = true;
    }

    let direct_download_etags = download_direct_download_etags(channel, &old_config_file.data)?;

    let mut new_config_file = load_mut_config_db(paths).with_context(|| {
        "`run_command_update_version_db` command failed to load configuration db."
    })?;

    // This is our optimistic locking check: if someone changed the last modified
    // field since we released the read-lock, we just give up
    if new_config_file.data != old_config_file.data {
        if let Some(temp_versiondb_download_path) = temp_versiondb_download_path {
            let _ = std::fs::remove_file(temp_versiondb_download_path);
        }

        return Ok(());
    }

    for (channel, etag) in direct_download_etags {
        let channel_data = new_config_file
            .data
            .installed_channels
            .get(&channel)
            .unwrap();

        if let JuliaupConfigChannel::DirectDownloadChannel {
            path,
            url,
            local_etag,
            server_etag: _,
            version,
            binary_path,
        } = channel_data
        {
            if let Some(etag) = etag {
                new_config_file.data.installed_channels.insert(
                    channel,
                    JuliaupConfigChannel::DirectDownloadChannel {
                        path: path.clone(),
                        url: url.clone(),
                        local_etag: local_etag.clone(),
                        server_etag: etag,
                        version: version.clone(),
                        binary_path: binary_path.clone(),
                    },
                );
            } else {
                print_juliaup_style(
                    "Failed",
                    &format!(
                        "to update {}. This can happen if a build is no longer available.",
                        channel
                    ),
                    JuliaupMessageType::Error,
                );
            }
        }
    }

    new_config_file.data.last_version_db_update = Some(chrono::Utc::now());

    if let Some(temp_versiondb_download_path) = temp_versiondb_download_path {
        retry_rename(&temp_versiondb_download_path, &paths.versiondb)?;
    } else if delete_old_version_db {
        let _ = std::fs::remove_file(&paths.versiondb);
    }

    save_config_db(&mut new_config_file, paths)
        .with_context(|| "Failed to save configuration file.")?;

    Ok(())
}

// A generic function to run a function with a timeout and a message to inform the user why it is taking so long
fn run_with_slow_message<F, R>(func: F, timeout_secs: u64, message: &str) -> Result<R, Error>
where
    F: FnOnce() -> Result<R, Error> + Send + 'static,
    R: Send + 'static,
{
    use std::sync::mpsc::channel;
    use std::thread;
    use std::time::Duration;

    let (tx, rx) = channel();

    // Run the function in a separate thread
    thread::spawn(move || {
        let result = func();
        tx.send(result).unwrap();
    });

    // Attempt to receive the result with a timeout
    match rx.recv_timeout(Duration::from_secs(timeout_secs)) {
        Ok(result) => result,
        Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
            // Function has not completed within timeout_secs seconds, inform why
            eprintln!("{}", message);

            // Now wait for the function to complete

            rx.recv().unwrap()
        }
        Err(e) => panic!("Error receiving result: {:?}", e),
    }
}

#[cfg(windows)]
fn download_direct_download_etags(
    channel: &Option<String>,
    config_data: &JuliaupConfig,
) -> Result<Vec<(String, Option<String>)>> {
    use windows::core::HSTRING;
    use windows::Foundation::Uri;
    use windows::Web::Http::HttpMethod;
    use windows::Web::Http::HttpRequestMessage;

    // Check if the server supports etag headers (required for nightly/PR updates)
    let server_supports_etag = check_server_supports_nightlies().unwrap_or(false);

    let http_client = http_client()?;

    let mut requests = Vec::new();

    for (channel_name, installed_channel) in &config_data.installed_channels {
        if let Some(chan) = channel {
            // TODO: convert to an if-let chain once stabilized https://github.com/rust-lang/rust/pull/132833
            if chan != channel_name {
                continue;
            }
        }

        if let JuliaupConfigChannel::DirectDownloadChannel { url, .. } = installed_channel {
            // If server doesn't support etag, we can't check for updates on nightly/PR channels
            // Return None gracefully so the update process can continue with other channels
            if !server_supports_etag {
                requests.push((channel_name.clone(), None));
                continue;
            }

            let http_client = http_client.clone();
            let url_clone = url.clone();
            let channel_name_clone = channel_name.clone();
            let message = format!(
                "{} for new version on channel '{}' is taking a while... This can be slow due to server caching",
                style("    Checking").cyan().bold(),
                channel_name
            );

            let etag = run_with_slow_message(
                move || {
                    let request_uri = Uri::CreateUri(&HSTRING::from(&url_clone))
                        .with_context(|| format!("Failed to create URI from {}", &url_clone))?;

                    let request = HttpRequestMessage::Create(&HttpMethod::Head()?, &request_uri)
                        .with_context(|| "Failed to create HttpRequestMessage.")?;

                    let async_op = http_client
                        .SendRequestAsync(&request)
                        .map_err(|e| anyhow!("Failed to send request: {:?}", e))?;

                    let response = async_op
                        .join()
                        .map_err(|e| anyhow!("Failed to get response: {:?}", e))?;

                    if response.IsSuccessStatusCode()? {
                        // Gracefully handle missing etag - return None instead of error
                        let etag = response
                            .Headers()
                            .ok()
                            .and_then(|headers| headers.Lookup(&HSTRING::from("ETag")).ok())
                            .map(|s| s.to_string());

                        Ok::<Option<String>, anyhow::Error>(etag)
                    } else {
                        Ok::<Option<String>, anyhow::Error>(None)
                    }
                },
                3, // Timeout in seconds
                &message,
            )?;

            requests.push((channel_name_clone, etag));
        }
    }

    Ok(requests)
}

#[cfg(not(windows))]
fn download_direct_download_etags(
    channel: &Option<String>,
    config_data: &JuliaupConfig,
) -> Result<Vec<(String, Option<String>)>> {
    use std::sync::Arc;

    // Check if the server supports etag headers (required for nightly/PR updates)
    let server_supports_etag = check_server_supports_nightlies().unwrap_or(false);

    let client = Arc::new(http_client()?);

    let mut requests = Vec::new();

    for (channel_name, installed_channel) in &config_data.installed_channels {
        if let Some(chan) = channel {
            // TODO: convert to an if-let chain once stabilized https://github.com/rust-lang/rust/pull/132833
            if chan != channel_name {
                continue;
            }
        }

        if let JuliaupConfigChannel::DirectDownloadChannel { url, .. } = installed_channel {
            // If server doesn't support etag, we can't check for updates on nightly/PR channels
            // Return None gracefully so the update process can continue with other channels
            if !server_supports_etag {
                requests.push((channel_name.clone(), None));
                continue;
            }

            let client = Arc::clone(&client);
            let url_clone = url.clone();
            let channel_name_clone = channel_name.clone();

            let message = format!(
                "{} for new version on channel '{}' is taking a while... This can be slow due to server caching",
                style("    Checking").cyan().bold(),
                channel_name
            );

            let etag = run_with_slow_message(
                move || {
                    let response = client.head(&url_clone).send().with_context(|| {
                        format!("Failed to send HEAD request to {}", &url_clone)
                    })?;

                    if response.status().is_success() {
                        // Gracefully handle missing etag - return None instead of error
                        let etag = response
                            .headers()
                            .get("etag")
                            .and_then(|h| h.to_str().ok())
                            .map(|s| s.to_string());

                        Ok::<Option<String>, anyhow::Error>(etag)
                    } else {
                        Ok::<Option<String>, anyhow::Error>(None)
                    }
                },
                3, // Timeout in seconds
                &message,
            )?;

            requests.push((channel_name_clone, etag));
        }
    }

    Ok(requests)
}

#[cfg(target_os = "macos")]
fn prompt_and_codesign_pr_build(dir: &Path) -> Result<bool> {
    use std::io::{self, Write};

    eprintln!("\nWARNING: PR builds are not code-signed for macOS.");
    eprintln!("         The Julia binary will fail to run unless you codesign it locally.");
    eprint!("\nWould you like to automatically codesign this PR build now? [Y/n]: ");
    io::stderr().flush()?;

    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let input = input.trim().to_lowercase();

    if input == "n" || input == "no" {
        eprintln!("\nSkipping codesigning. You can manually codesign later with:");
        eprintln!(
            "  find {} -type f -perm +111 -exec codesign --force --sign - {{}} \\;",
            dir.display()
        );
        return Ok(false);
    }

    eprintln!("\nCodesigning all Mach-O binaries...");
    let mut signed_count = 0u32;
    let mut dirs_to_visit = vec![dir.to_path_buf()];
    while let Some(current) = dirs_to_visit.pop() {
        let entries = match std::fs::read_dir(&current) {
            Ok(e) => e,
            Err(_) => continue,
        };
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();
            if path.is_dir() {
                // Skip dSYM bundles and precompiled cache
                if !path.to_string_lossy().ends_with(".dSYM")
                    && !path.ends_with("share/julia/compiled")
                {
                    dirs_to_visit.push(path);
                }
                continue;
            }
            if !path.is_file() {
                continue;
            }
            let is_executable = entry
                .metadata()
                .map(|m| m.permissions().mode() & 0o111 != 0)
                .unwrap_or(false);
            let is_dylib = path.extension().map(|e| e == "dylib").unwrap_or(false);
            if is_executable || is_dylib {
                codesign_file(&path)?;
                signed_count += 1;
            }
        }
    }
    eprintln!("\u{2713} Codesigning completed successfully ({signed_count} files).");
    Ok(true)
}

#[cfg(target_os = "macos")]
fn codesign_file(path: &std::path::Path) -> Result<()> {
    use std::process::Command;

    let status = Command::new("codesign")
        .args(["--force", "--sign", "-"])
        .arg(path)
        .status()
        .with_context(|| format!("Failed to execute codesign on {}", path.display()))?;

    if !status.success() {
        return Err(anyhow!("Failed to codesign {}", path.display()));
    }
    Ok(())
}

#[cfg(target_os = "macos")]
fn check_stdlib_notarization(julia_path: &std::path::Path) {
    eprint!("\nChecking standard library notarization");
    let _ = std::io::stdout().flush();

    match std::process::Command::new(julia_path)
        .env("JULIA_LOAD_PATH", "@stdlib")
        .arg("--startup-file=no")
        .arg("-e")
        .arg("foreach(p -> begin print(stderr, '.'); @eval(import $(Symbol(p))) end, filter!(x -> isfile(joinpath(Sys.STDLIB, x, \"src\", \"$(x).jl\")), readdir(Sys.STDLIB)))")
        .status()
    {
        Ok(exit_status) => {
            if exit_status.success() {
                eprintln!("done.")
            } else {
                eprintln!("failed with {}.", exit_status);
            }
        }
        Err(e) => {
            eprintln!("failed to execute Julia binary.");
            eprintln!("Error: {}", e);
            if e.raw_os_error() == Some(86) {
                eprintln!("This may indicate an architecture mismatch (e.g., trying to run an Intel binary on Apple Silicon or vice versa).");
            }
            eprintln!("Check was skipped.");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "macos")]
    use std::sync::{Mutex, OnceLock};

    #[cfg(target_os = "macos")]
    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
    }

    #[cfg(target_os = "macos")]
    struct EnvVarGuard {
        key: &'static str,
        original: Option<String>,
    }

    #[cfg(target_os = "macos")]
    impl EnvVarGuard {
        fn set(key: &'static str, value: &std::path::Path) -> Self {
            let original = std::env::var(key).ok();
            std::env::set_var(key, value);
            Self { key, original }
        }
    }

    #[cfg(target_os = "macos")]
    impl Drop for EnvVarGuard {
        fn drop(&mut self) {
            if let Some(ref value) = self.original {
                std::env::set_var(self.key, value);
            } else {
                std::env::remove_var(self.key);
            }
        }
    }

    #[test]
    fn match_markers_none_without_markers() {
        let inp: &[u8] = b"Some input\n";
        let res = match_markers(inp);
        assert!(res.is_ok());
        let res = res.unwrap();
        assert!(res.is_none());
    }

    #[test]
    fn match_markers_returns_correct_indices() {
        let mut inp: Vec<u8> = Vec::new();
        let start_bytes = b"Some random bytes.";
        let middle_bytes = b"More bytes.";
        let end_bytes = b"Final bytes.";
        inp.extend_from_slice(start_bytes);
        inp.extend_from_slice(S_MARKER);
        inp.extend_from_slice(middle_bytes);
        inp.extend_from_slice(E_MARKER);
        inp.extend_from_slice(end_bytes);

        // Verify Ok(Some(..)) is returned
        let res = match_markers(&inp);
        assert!(res.is_ok());
        let res = res.unwrap();
        assert!(res.is_some());
        let (sidx, eidx) = res.unwrap();

        // Verify correct positions
        assert_eq!(sidx, start_bytes.len());
        let expected_eidx =
            start_bytes.len() + S_MARKER.len() + middle_bytes.len() + E_MARKER.len();
        assert_eq!(eidx, expected_eidx);
    }

    #[test]
    fn match_markers_returns_err_without_start() {
        let mut inp: Vec<u8> = Vec::new();
        let start_bytes = b"Some random bytes.";
        let middle_bytes = b"More bytes.";
        let end_bytes = b"Final bytes.";
        inp.extend_from_slice(start_bytes);
        inp.extend_from_slice(middle_bytes);
        inp.extend_from_slice(E_MARKER);
        inp.extend_from_slice(end_bytes);

        // Verify Err(..) is returned
        let res = match_markers(&inp);
        assert!(res.is_err());
    }

    #[test]
    fn match_markers_returns_err_without_end() {
        let mut inp: Vec<u8> = Vec::new();
        let start_bytes = b"Some random bytes.";
        let middle_bytes = b"More bytes.";
        let end_bytes = b"Final bytes.";
        inp.extend_from_slice(start_bytes);
        inp.extend_from_slice(S_MARKER);
        inp.extend_from_slice(middle_bytes);
        inp.extend_from_slice(end_bytes);

        // Verify Err(..) is returned
        let res = match_markers(&inp);
        assert!(res.is_err());
    }

    #[test]
    fn match_markers_returns_err_with_multiple_start() {
        let mut inp: Vec<u8> = Vec::new();
        let start_bytes = b"Some random bytes.";
        let middle_bytes = b"More bytes.";
        let end_bytes = b"Final bytes.";
        inp.extend_from_slice(S_MARKER);
        inp.extend_from_slice(start_bytes);
        inp.extend_from_slice(S_MARKER);
        inp.extend_from_slice(middle_bytes);
        inp.extend_from_slice(E_MARKER);
        inp.extend_from_slice(end_bytes);

        // Verify Err(..) is returned
        let res = match_markers(&inp);
        assert!(res.is_err());
    }

    #[test]
    fn match_markers_returns_err_with_multiple_end() {
        let mut inp: Vec<u8> = Vec::new();
        let start_bytes = b"Some random bytes.";
        let middle_bytes = b"More bytes.";
        let end_bytes = b"Final bytes.";
        inp.extend_from_slice(start_bytes);
        inp.extend_from_slice(S_MARKER);
        inp.extend_from_slice(middle_bytes);
        inp.extend_from_slice(E_MARKER);
        inp.extend_from_slice(end_bytes);
        inp.extend_from_slice(E_MARKER);

        // Verify Err(..) is returned
        let res = match_markers(&inp);
        assert!(res.is_err());
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn symlink_uses_app_bundle_for_system_channel() -> Result<()> {
        let _guard = env_lock();
        let depot_dir = tempfile::TempDir::new()?;
        let _env_guard = EnvVarGuard::set("JULIAUP_DEPOT_PATH", depot_dir.path());
        let paths = crate::global_paths::get_paths()?;

        let version = "1.2.3";
        let target_path = paths.juliauphome.join(format!("julia-{}", version));
        let julia_bin = target_path.join("Julia-1.2.app/Contents/Resources/julia/bin/julia");
        std::fs::create_dir_all(julia_bin.parent().unwrap())?;
        std::fs::write(&julia_bin, b"")?;

        let symlink_dir = tempfile::TempDir::new()?;
        let symlink_path = symlink_dir.path().join("julia-1.2");

        create_system_channel_symlink(version, "julia-1.2", &symlink_path, &paths, &None)?;

        let link_target = std::fs::read_link(&symlink_path)?;
        assert_eq!(link_target, julia_bin);
        Ok(())
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn symlink_uses_app_bundle_for_direct_download_channel() -> Result<()> {
        let _guard = env_lock();
        let depot_dir = tempfile::TempDir::new()?;
        let _env_guard = EnvVarGuard::set("JULIAUP_DEPOT_PATH", depot_dir.path());
        let paths = crate::global_paths::get_paths()?;

        let version = "1.2.3";
        let path = "julia-pr123";
        let target_path = paths.juliauphome.join(path);
        let julia_bin = target_path.join("Julia-1.2.app/Contents/Resources/julia/bin/julia");
        std::fs::create_dir_all(julia_bin.parent().unwrap())?;
        std::fs::write(&julia_bin, b"")?;

        let symlink_dir = tempfile::TempDir::new()?;
        let symlink_path = symlink_dir.path().join("julia-pr123");

        create_direct_download_symlink(path, version, "julia-pr123", &symlink_path, &paths, &None)?;

        let link_target = std::fs::read_link(&symlink_path)?;
        assert_eq!(link_target, julia_bin);
        Ok(())
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn dmg_failure_falls_back_to_tarball() -> Result<()> {
        use flate2::write::GzEncoder;
        use flate2::Compression;
        use std::io::{Read, Write};
        use std::net::TcpListener;
        use std::thread;
        use tar::Builder;

        let _guard = env_lock();
        let _env_guard = EnvVarGuard::set("JULIAUP_TEST_DMG_FAIL", std::path::Path::new("1"));

        let tarball_bytes = {
            let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
            {
                let mut builder = Builder::new(&mut encoder);

                let mut dir_header = tar::Header::new_gnu();
                dir_header.set_entry_type(tar::EntryType::Directory);
                dir_header.set_mode(0o755);
                dir_header.set_size(0);
                dir_header.set_cksum();
                builder.append_data(&mut dir_header, "julia-1.2.3/bin", std::io::empty())?;

                let data = b"#!/bin/sh\n";
                let mut header = tar::Header::new_gnu();
                header.set_size(data.len() as u64);
                header.set_mode(0o755);
                header.set_cksum();
                builder.append_data(&mut header, "julia-1.2.3/bin/julia", &data[..])?;

                builder.finish()?;
            }
            encoder.finish()?
        };

        let dmg_bytes = b"not-a-real-dmg".to_vec();

        let listener = TcpListener::bind("127.0.0.1:0")?;
        let addr = listener.local_addr()?;
        let handle = thread::spawn(move || {
            for _ in 0..2 {
                if let Ok((mut stream, _)) = listener.accept() {
                    let mut buf = [0u8; 1024];
                    let n = stream.read(&mut buf).unwrap_or(0);
                    let request = String::from_utf8_lossy(&buf[..n]);
                    let path = request
                        .lines()
                        .next()
                        .and_then(|line| line.split_whitespace().nth(1))
                        .unwrap_or("/");

                    let (body, etag) = if path.ends_with(".dmg") {
                        (dmg_bytes.as_slice(), "\"dmg-etag\"")
                    } else {
                        (tarball_bytes.as_slice(), "\"tar-etag\"")
                    };

                    let response = format!(
                        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nETag: {}\r\nConnection: close\r\n\r\n",
                        body.len(),
                        etag
                    );
                    let _ = stream.write_all(response.as_bytes());
                    let _ = stream.write_all(body);
                }
            }
        });

        let url = url::Url::parse(&format!("http://{}/julia.tar.gz", addr))?;
        let target_dir = tempfile::TempDir::new()?;
        let (etag, used_dmg) = try_download_dmg_with_fallback(&url, target_dir.path())?;

        assert!(!used_dmg);
        assert_eq!(etag, "\"tar-etag\"");
        assert!(target_dir.path().join("bin/julia").exists());

        let _ = handle.join();
        Ok(())
    }

    // unpack_sans_parent is only compiled for non-freebsd targets
    #[cfg(not(target_os = "freebsd"))]
    /// Builds a minimal `.tar.gz` in memory with a single entry whose path is given
    /// as raw bytes, bypassing the `tar::Builder` safety checks so we can place
    /// dangerous components (e.g. `..`) directly in the archive header.
    fn make_tar_gz_with_raw_path(path: &[u8]) -> Vec<u8> {
        use flate2::write::GzEncoder;
        use flate2::Compression;
        use std::io::Write;

        let mut header = [0u8; 512];
        let path_len = path.len().min(100);
        header[..path_len].copy_from_slice(&path[..path_len]);
        header[100..108].copy_from_slice(b"0000644\0"); // mode
        header[108..116].copy_from_slice(b"0000000\0"); // uid
        header[116..124].copy_from_slice(b"0000000\0"); // gid
        header[124..136].copy_from_slice(b"00000000000\0"); // size = 0
        header[136..148].copy_from_slice(b"00000000000\0"); // mtime
        for b in &mut header[148..156] {
            *b = b' '; // checksum placeholder
        }
        header[156] = b'0'; // regular file
        let sum: u32 = header.iter().map(|&b| b as u32).sum();
        let cksum = format!("{:06o}\0 ", sum);
        header[148..156].copy_from_slice(cksum.as_bytes());

        let mut tar_bytes = header.to_vec();
        tar_bytes.extend([0u8; 1024]); // end-of-archive

        let mut enc = GzEncoder::new(Vec::new(), Compression::default());
        enc.write_all(&tar_bytes).unwrap();
        enc.finish().unwrap()
    }

    #[cfg(not(target_os = "freebsd"))]
    #[test]
    fn unpack_rejects_path_traversal_dotdot() -> Result<()> {
        // Use raw tar bytes so the ParentDir component reaches our code rather
        // than being caught by the tar::Builder's own safety check.
        let tarball = make_tar_gz_with_raw_path(b"top/../evil.txt");

        let dst = tempfile::TempDir::new()?;
        let result = unpack_sans_parent(tarball.as_slice(), dst.path(), 1);
        assert!(
            result.is_err(),
            "Expected extraction to fail on path traversal attempt"
        );
        Ok(())
    }

    #[cfg(not(target_os = "freebsd"))]
    #[test]
    fn unpack_accepts_normal_paths() -> Result<()> {
        use flate2::write::GzEncoder;
        use flate2::Compression;
        use tar::Builder;

        let tarball = {
            let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
            {
                let mut archive = Builder::new(&mut encoder);
                // Directory entry must come first so entry.unpack() can create the file
                let mut dir_header = tar::Header::new_gnu();
                dir_header.set_entry_type(tar::EntryType::Directory);
                dir_header.set_mode(0o755);
                dir_header.set_size(0);
                dir_header.set_cksum();
                archive.append_data(&mut dir_header, "top/bin", std::io::empty())?;
                let data = b"hello";
                let mut header = tar::Header::new_gnu();
                header.set_size(data.len() as u64);
                header.set_mode(0o644);
                header.set_cksum();
                archive.append_data(&mut header, "top/bin/julia", &data[..])?;
                archive.finish()?;
            }
            encoder.finish()?
        };

        let dst = tempfile::TempDir::new()?;
        unpack_sans_parent(tarball.as_slice(), dst.path(), 1)?;
        assert!(dst.path().join("bin/julia").exists());
        Ok(())
    }
}
