use std::{
    ffi::OsStr,
    path::Path,
    process::Command,
    sync::{Arc, mpsc::Sender},
};

use ql_core::{
    GenericProgress, IntoIoError, IoError, JsonError, LAUNCHER_VERSION, RequestError, err,
    file_utils::{self, exists},
    impl_3_errs_jri, pt,
};
use serde::Deserialize;
use thiserror::Error;
use tokio::task::JoinError;

#[derive(Debug, Clone)]
pub enum UpdateCheckInfo {
    UpToDate,
    NewVersion { url: String },
}

/// Checks for any launcher updates to be installed.
///
/// Returns `Ok(UpdateCheckInfo::UpToDate)` if the launcher is up to date.
///
/// Returns `Ok(UpdateCheckInfo::NewVersion { url: String })` if there is a new version available.
/// (url pointing to zip file containing new version executable).
///
/// # Errors
/// - current version is ahead of latest version
///   (experimental dev build)
/// - release info couldn't be downloaded
/// - release info couldn't be parsed into JSON
/// - no releases could be found in the GitHub repo
/// - new version's version number is incompatible
///   with semver (even after conversion)
/// - user is on unsupported architecture or OS
/// - no matching download could be found for the platform
pub async fn check() -> Result<UpdateCheckInfo, UpdateError> {
    const URL: &str = "https://api.github.com/repos/Mrmayman/quantum-launcher/releases";

    let json: Vec<GithubRelease> = file_utils::download_file_to_json(URL, true).await?;
    let mut json = json.into_iter();

    let mut version;
    let mut latest;

    loop {
        latest = json.next().ok_or(UpdateError::NoReleases)?;

        version = latest.tag_name.clone();
        // v0.2 -> 0.2
        if version.starts_with('v') {
            version = version[1..version.len()].to_owned();
        }
        // 0.2 -> 0.2.0
        if version.chars().filter(|n| *n == '.').count() == 1 {
            version.push_str(".0");
        }

        // The new update has been disabled/yanked for whatever reason
        // so look for another one.
        // Naming scheme: ends with "-D" followed by (optional) numbers
        if version
            .trim_end_matches(|c: char| c.is_numeric())
            .ends_with("-D")
        {
            continue;
        }
        break;
    }

    let version = semver::Version::parse(&version)?;

    match version.cmp(&LAUNCHER_VERSION) {
        std::cmp::Ordering::Less => Err(UpdateError::AheadOfLatestVersion),
        // ====
        // Comment first line to test out update mechanism
        std::cmp::Ordering::Equal => Ok(UpdateCheckInfo::UpToDate),
        // std::cmp::Ordering::Equal |
        // ====
        std::cmp::Ordering::Greater => {
            let arch = if cfg!(target_arch = "x86_64") {
                "x86_64"
            } else if cfg!(target_arch = "aarch64") {
                "aarch64"
            } else if cfg!(target_arch = "arm") {
                "arm32"
            } else if cfg!(target_arch = "x86") {
                "i686"
            } else {
                err!("Update checking: Unsupported architecture");
                return Err(UpdateError::UnsupportedArchitecture);
            };

            let os = if cfg!(target_os = "windows") {
                "windows"
            } else if cfg!(target_os = "linux") {
                "linux"
            } else if cfg!(target_os = "macos") {
                "macos"
            // Currently not supported,
            // but hook left here for any future plans
            } else if cfg!(target_os = "freebsd") {
                "freebsd"
            } else if cfg!(target_os = "netbsd") {
                "netbsd"
            } else if cfg!(target_os = "solaris") {
                "solaris"
            } else {
                return Err(UpdateError::UnsupportedOS);
            };

            let name = format!("quantum_launcher_{os}_{arch}.");

            let matching_release = latest
                .assets
                .iter()
                .find(|asset| asset.name.replace('-', "_").starts_with(&name))
                .ok_or(UpdateError::NoMatchingDownloadFound)?;

            Ok(UpdateCheckInfo::NewVersion {
                url: matching_release.browser_download_url.clone(),
            })
        }
    }
}

/// Installs a new version of the launcher.
/// The launcher will be backed up, the new version
/// will be downloaded and extracted.
///
/// The new version will be started. You must exit the current process manually.
///
/// # Arguments
/// - `url`: The url to the zip file containing the new version of the launcher.
/// - `progress`: A channel to send progress updates to.
///
/// # Errors
/// ## New version:
/// - Couldn't be downloaded
/// - Couldn't be extracted (invalid zip)
/// - Couldn't be started
/// ## Current executable:
/// - Couldn't be found
/// - Has a name with invalid UNICODE
pub async fn install(url: String, progress: Sender<GenericProgress>) -> Result<(), UpdateError> {
    _ = progress.send(GenericProgress::default());

    let exe_path = std::env::current_exe().map_err(UpdateError::CurrentExeError)?;
    let exe_location = exe_path.parent().ok_or(UpdateError::ExeParentPathError)?;

    let exe_name = exe_path.file_name().ok_or(UpdateError::ExeFileNameError)?;
    let exe_name = exe_name
        .to_str()
        .ok_or(UpdateError::OsStrToStr(exe_name.into()))?;

    let mut backup_idx = 1;
    while exists(&exe_location.join(format!("backup_{backup_idx}_{exe_name}"))).await {
        backup_idx += 1;
    }

    send_progress(&progress, 1, "Backing up existing launcher");
    let backup_path = exe_location.join(format!("backup_{backup_idx}_{exe_name}"));
    tokio::fs::rename(&exe_path, &backup_path)
        .await
        .path(backup_path)?;

    send_progress(&progress, 2, "Downloading new launcher version");
    let download_zip = file_utils::download_file_to_bytes(&url, false).await?;

    send_progress(&progress, 3, "Extracting new launcher");
    let url_cmp = url.to_lowercase();
    if url_cmp.ends_with(".tar.gz") {
        let exe_location = exe_location.to_owned();
        tokio::task::spawn_blocking(move || {
            file_utils::extract_tar_gz(&download_zip, &exe_location)
        })
        .await?
        .map_err(UpdateError::TarGz)?;
    } else if url_cmp.ends_with(".zip") {
        file_utils::extract_zip_archive(std::io::Cursor::new(download_zip), exe_location, true)
            .await?;
    } else {
        return Err(UpdateError::UnknownFileExtension(url));
    }

    clean_file(exe_location, "README.md").await?;
    clean_file(exe_location, "LICENSE").await?;

    let extract_name = if cfg!(target_os = "windows") {
        "quantum_launcher.exe"
    } else {
        "quantum_launcher"
    };

    let new_path = exe_location.join(extract_name);
    _ = Command::new(&new_path).spawn().path(new_path)?;

    Ok(())
}

async fn clean_file(parent: &Path, name: &str) -> Result<(), UpdateError> {
    let p = parent.join(name);
    if exists(&p).await {
        tokio::fs::remove_file(&p).await.path(p)?;
    }
    Ok(())
}

fn send_progress(progress: &Sender<GenericProgress>, done: usize, msg: &str) {
    pt!("({done}/4) {msg}");
    _ = progress.send(GenericProgress {
        done,
        total: 4,
        message: Some(msg.to_owned()),
        has_finished: false,
    });
}

const ERR_PREFIX: &str = "launcher update:\n";

#[derive(Debug, Error)]
pub enum UpdateError {
    #[error("{ERR_PREFIX}{0}")]
    Request(#[from] RequestError),
    #[error("{ERR_PREFIX}{0}")]
    Json(#[from] JsonError),
    #[error("{ERR_PREFIX}{0}")]
    Io(#[from] IoError),

    #[error("{ERR_PREFIX}zip extract error: {0}")]
    Zip(#[from] zip::result::ZipError),
    #[error("{ERR_PREFIX}tar.gz extract error: {0}")]
    TarGz(std::io::Error),
    #[error("{ERR_PREFIX}semver error: {0}")]
    SemverError(#[from] semver::Error),
    #[error("{ERR_PREFIX}tokio task join error: {0}")]
    Join(#[from] JoinError),

    #[error("unsupported OS for launcher update")]
    UnsupportedOS,
    #[error("unsupported architecture for launcher update")]
    UnsupportedArchitecture,

    #[error("{ERR_PREFIX}no Release entries found in launcher github")]
    NoReleases,
    #[error("no matching launcher update download found for your platform")]
    NoMatchingDownloadFound,
    #[error("{ERR_PREFIX}unknown file extension\n{0}")]
    UnknownFileExtension(String),
    #[error("Running dev build... (ahead of latest version)")]
    AheadOfLatestVersion,

    #[error("{ERR_PREFIX}could not get current exe path: {0}")]
    CurrentExeError(std::io::Error),
    #[error("{ERR_PREFIX}could not get current exe parent path")]
    ExeParentPathError,
    #[error("{ERR_PREFIX}could not get current exe file name")]
    ExeFileNameError,
    #[error("{ERR_PREFIX}could not convert OsStr to str: {0:?}")]
    OsStrToStr(Arc<OsStr>),
}

impl_3_errs_jri!(UpdateError, Json, Request, Io);

#[derive(Deserialize)]
struct GithubRelease {
    tag_name: String,
    assets: Vec<GithubAsset>,
    // url: String,
    // assets_url: String,
    // upload_url: String,
    // html_url: String,
    // id: usize,
    // name: String,
    // draft: bool,
    // prerelease: bool,
    // created_at: String,
    // published_at: String,
}

#[derive(Deserialize)]
struct GithubAsset {
    name: String,
    browser_download_url: String,
}
