use std::path::{Path, PathBuf};

use crate::backend::Backend;
use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
use crate::http::{HTTP, HTTP_FETCH};
use crate::install_context::InstallContext;
use crate::plugins::VERSION_REGEX;
use crate::toolset::ToolVersion;
use crate::ui::progress_report::SingleReport;
use crate::{file, plugins};
use eyre::Result;
use itertools::Itertools;
use versions::Versioning;
use xx::regex;

#[derive(Debug)]
pub struct ElixirPlugin {
    ba: BackendArg,
}

impl ElixirPlugin {
    pub fn new() -> Self {
        Self {
            ba: plugins::core::new_backend_arg("elixir"),
        }
    }

    fn elixir_bin(&self, tv: &ToolVersion) -> PathBuf {
        tv.install_path().join("bin").join("elixir")
    }

    fn test_elixir(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
        ctx.pr.set_message("elixir --version".into());
        CmdLineRunner::new(self.elixir_bin(tv))
            .with_pr(&ctx.pr)
            .envs(self.dependency_env()?)
            .arg("--version")
            .execute()
    }

    fn download(&self, tv: &ToolVersion, pr: &Box<dyn SingleReport>) -> Result<PathBuf> {
        let version = &tv.version;
        let url = format!("https://builds.hex.pm/builds/elixir/v{version}.zip");

        let filename = url.split('/').last().unwrap();
        let tarball_path = tv.download_path().join(filename);

        pr.set_message(format!("download {filename}"));
        if !tarball_path.exists() {
            HTTP.download_file(&url, &tarball_path, Some(pr))?;
        }

        Ok(tarball_path)
    }

    fn install(&self, ctx: &InstallContext, tv: &ToolVersion, tarball_path: &Path) -> Result<()> {
        let filename = tarball_path.file_name().unwrap().to_string_lossy();
        ctx.pr.set_message(format!("extract {filename}"));
        file::remove_all(tv.install_path())?;
        file::unzip(tarball_path, &tv.install_path())?;

        Ok(())
    }

    fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
        self.test_elixir(ctx, tv)
    }
}

impl Backend for ElixirPlugin {
    fn ba(&self) -> &BackendArg {
        &self.ba
    }

    fn _list_remote_versions(&self) -> Result<Vec<String>> {
        let versions: Vec<String> = HTTP_FETCH
            .get_text("https://builds.hex.pm/builds/elixir/builds.txt")?
            .lines()
            .unique()
            .filter_map(|s| s.split_once(' ').map(|(v, _)| v.trim_start_matches('v')))
            .filter(|s| regex!(r"^[0-9]+\.[0-9]+\.[0-9]").is_match(s))
            .sorted_by_cached_key(|s| {
                (
                    Versioning::new(s.split_once('-').map(|(v, _)| v).unwrap_or(s)),
                    !VERSION_REGEX.is_match(s),
                    s.contains("-otp-"),
                    Versioning::new(s),
                    s.to_string(),
                )
            })
            .map(|s| s.to_string())
            .collect();
        Ok(versions)
    }

    fn get_dependencies(&self) -> Result<Vec<&str>> {
        Ok(vec!["erlang"])
    }

    fn install_version_(&self, ctx: &InstallContext, mut tv: ToolVersion) -> Result<ToolVersion> {
        let tarball_path = self.download(&tv, &ctx.pr)?;
        self.verify_checksum(ctx, &mut tv, &tarball_path)?;
        self.install(ctx, &tv, &tarball_path)?;
        self.verify(ctx, &tv)?;
        Ok(tv)
    }

    fn list_bin_paths(&self, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
        Ok(["bin", ".mix/escripts"]
            .iter()
            .map(|p| tv.install_path().join(p))
            .collect())
    }
}
