use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
    sync::Arc,
};

use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
use crate::http::{HTTP, HTTP_FETCH};
use crate::install_context::InstallContext;
use crate::lockfile::PlatformInfo;
use crate::plugins::VERSION_REGEX;
use crate::toolset::{ToolVersion, Toolset};
use crate::ui::progress_report::SingleReport;
use crate::{
    backend::Backend, backend::VersionInfo, backend::platform_target::PlatformTarget,
    config::Config,
};
use crate::{env, file, plugins};
use async_trait::async_trait;
use eyre::Result;
use itertools::Itertools;
use versions::Versioning;
use xx::regex;

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

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

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

    async 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.as_ref())
            .envs(self.dependency_env(&ctx.config).await?)
            .envs(tv.install_env())
            .arg("--version")
            .execute()
    }

    fn version_tag(version: &str) -> String {
        if regex!(r"^[0-9]").is_match(version) {
            format!("v{version}")
        } else {
            version.to_string()
        }
    }

    fn download_url(version: &str) -> String {
        format!(
            "https://builds.hex.pm/builds/elixir/{}.zip",
            Self::version_tag(version)
        )
    }

    async fn checksum(version: &str) -> Result<Option<String>> {
        let version_tag = Self::version_tag(version);
        let builds = HTTP_FETCH
            .get_text_cached("https://builds.hex.pm/builds/elixir/builds.txt")
            .await?;
        Ok(builds.lines().find_map(|line| {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 4 && parts[0] == version_tag {
                Some(format!("sha256:{}", parts[3]))
            } else {
                None
            }
        }))
    }

    fn lockfile_url(&self, tv: &ToolVersion) -> Option<String> {
        tv.lock_platforms
            .get(&self.get_platform_key())
            .and_then(|p| p.url.clone())
    }

    fn set_lockfile_url(&self, tv: &mut ToolVersion, url: &str) {
        tv.lock_platforms
            .entry(self.get_platform_key())
            .or_default()
            .url = Some(url.to_string());
    }

    async fn download(&self, tv: &mut ToolVersion, pr: &dyn SingleReport) -> Result<PathBuf> {
        let url = self
            .lockfile_url(tv)
            .unwrap_or_else(|| Self::download_url(&tv.version));

        let filename = url.split('/').next_back().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)).await?;
        }
        self.set_lockfile_url(tv, &url);

        Ok(tarball_path)
    }

    async 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(), &Default::default())?;

        Ok(())
    }

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

#[async_trait]
impl Backend for ElixirPlugin {
    fn ba(&self) -> &Arc<BackendArg> {
        &self.ba
    }

    async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<VersionInfo>> {
        // Format: "version hash timestamp checksum"
        // Example: "v1.17.3 abc123 2024-12-01T00:00:00Z def456"
        let versions: Vec<VersionInfo> = HTTP_FETCH
            .get_text_cached("https://builds.hex.pm/builds/elixir/builds.txt")
            .await?
            .lines()
            .unique()
            .filter_map(|s| {
                let parts: Vec<&str> = s.split_whitespace().collect();
                if parts.len() >= 3 {
                    let version = parts[0].trim_start_matches('v');
                    let timestamp = parts[2]; // Third field is the timestamp
                    Some((version.to_string(), timestamp.to_string()))
                } else {
                    None
                }
            })
            .filter(|(v, _)| regex!(r"^[0-9]+\.[0-9]+\.[0-9]").is_match(v))
            .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(|(version, created_at)| VersionInfo {
                version,
                created_at: Some(created_at),
                ..Default::default()
            })
            .collect();
        Ok(versions)
    }

    async fn _idiomatic_filenames(&self) -> eyre::Result<Vec<String>> {
        Ok(vec![".exenv-version".into()])
    }

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

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

    async fn resolve_lock_info(
        &self,
        tv: &ToolVersion,
        _target: &PlatformTarget,
    ) -> Result<PlatformInfo> {
        Ok(PlatformInfo {
            url: Some(Self::download_url(&tv.version)),
            checksum: Self::checksum(&tv.version).await?,
            ..Default::default()
        })
    }

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

    async fn exec_env(
        &self,
        config: &Arc<Config>,
        _ts: &Toolset,
        tv: &ToolVersion,
    ) -> eyre::Result<BTreeMap<String, String>> {
        let mut map = BTreeMap::new();
        let mut set = |k: &str, v: PathBuf| {
            map.insert(k.to_string(), v.to_string_lossy().to_string());
        };
        let config_env = config.env().await?;
        if !env::PRISTINE_ENV.contains_key("MIX_HOME") && !config_env.contains_key("MIX_HOME") {
            set("MIX_HOME", tv.install_path().join(".mix"));
        }
        if !env::PRISTINE_ENV.contains_key("MIX_ARCHIVES")
            && !config_env.contains_key("MIX_ARCHIVES")
        {
            set(
                "MIX_ARCHIVES",
                tv.install_path().join(".mix").join("archives"),
            );
        }
        Ok(map)
    }
}

fn elixir_bin_name() -> &'static str {
    if cfg!(windows) {
        "elixir.bat"
    } else {
        "elixir"
    }
}
