use crate::backend::asdf::AsdfBackend;
use crate::cache::{CacheManager, CacheManagerBuilder};
use crate::config::Config;
use crate::env;
use crate::env_diff::EnvMap;
use crate::hash::hash_to_str;
use crate::tera::{BASE_CONTEXT, contains_template_syntax, get_tera, render_str};
use crate::toolset::{ToolRequest, ToolVersion};
use dashmap::DashMap;
use eyre::{WrapErr, eyre};
use std::sync::Arc;

#[derive(Debug, Default)]
pub struct ExternalPluginCache {
    list_bin_paths: DashMap<ToolRequest, CacheManager<Vec<String>>>,
    exec_env: DashMap<ToolRequest, CacheManager<EnvMap>>,
}

impl ExternalPluginCache {
    pub async fn list_bin_paths<F, Fut>(
        &self,
        config: &Arc<Config>,
        plugin: &AsdfBackend,
        tv: &ToolVersion,
        fetch: F,
    ) -> eyre::Result<Vec<String>>
    where
        Fut: Future<Output = eyre::Result<Vec<String>>>,
        F: FnOnce() -> Fut,
    {
        let cm = self
            .list_bin_paths
            .entry(tv.request.clone())
            .or_insert_with(|| {
                let list_bin_paths_filename = match &plugin.toml.list_bin_paths.cache_key {
                    Some(key) => {
                        let key = render_cache_key(config, tv, key);
                        let filename = format!("{key}.msgpack.z");
                        tv.cache_path().join("list_bin_paths").join(filename)
                    }
                    None => tv.cache_path().join("list_bin_paths.msgpack.z"),
                };
                CacheManagerBuilder::new(list_bin_paths_filename)
                    .with_fresh_file(plugin.plugin_path.clone())
                    .with_fresh_file(tv.install_path())
                    .build()
            });
        let start = std::time::Instant::now();
        let res = cm.get_or_try_init_async(fetch).await.cloned();
        trace!(
            "external_plugin_cache.list_bin_paths for {} took {}ms",
            plugin.name,
            start.elapsed().as_millis()
        );
        res
    }

    pub async fn exec_env<F, Fut>(
        &self,
        config: &Config,
        plugin: &AsdfBackend,
        tv: &ToolVersion,
        fetch: F,
    ) -> eyre::Result<EnvMap>
    where
        Fut: Future<Output = eyre::Result<EnvMap>>,
        F: FnOnce() -> Fut,
    {
        let cm = self.exec_env.entry(tv.request.clone()).or_insert_with(|| {
            let exec_env_filename = match &plugin.toml.exec_env.cache_key {
                Some(key) => {
                    let key = render_cache_key(config, tv, key);
                    let filename = format!("{key}.msgpack.z");
                    tv.cache_path().join("exec_env").join(filename)
                }
                None => tv.cache_path().join("exec_env.msgpack.z"),
            };
            CacheManagerBuilder::new(exec_env_filename)
                .with_fresh_file(plugin.plugin_path.clone())
                .with_fresh_file(tv.install_path())
                .build()
        });
        let start = std::time::Instant::now();
        let res = cm.get_or_try_init_async(fetch).await.cloned();
        trace!(
            "external_plugin_cache.exec_env for {} took {}ms",
            plugin.name,
            start.elapsed().as_millis()
        );
        res
    }
}

fn render_cache_key(config: &Config, tv: &ToolVersion, cache_key: &[String]) -> String {
    let elements = cache_key
        .iter()
        .map(|tmpl| {
            let s = parse_template(config, tv, tmpl).unwrap();
            let s = s.trim().to_string();
            trace!("cache key element: {} -> {}", tmpl.trim(), s);
            let mut s = hash_to_str(&s);
            s = s.chars().take(10).collect();
            s
        })
        .collect::<Vec<String>>();
    elements.join("-")
}

fn parse_template(config: &Config, tv: &ToolVersion, tmpl: &str) -> eyre::Result<String> {
    if !contains_template_syntax(tmpl) {
        return Ok(tmpl.to_string());
    }

    let mut ctx = BASE_CONTEXT.clone();
    ctx.insert("project_root", &config.project_root);
    ctx.insert("opts", &tv.request.options().opts_as_strings());
    let mut tera = get_tera(
        config
            .project_root
            .as_ref()
            .or(env::current_dir().as_ref().ok())
            .map(|p| p.as_path()),
    );
    render_str(&mut tera, tmpl, &ctx).wrap_err_with(|| eyre!("failed to parse template: {tmpl}"))
}
