Documentation
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use anyhow::{Ok, Result, bail};
use futures::future::try_join_all;
use providers::Provider;

mod providers;
mod resolve;

pub use resolve::{resolve, resolve_one};

type Hydration = HashMap<String, String>;

struct Hydrater {
    providers: Vec<Box<dyn Provider + Send>>,
}

impl Hydrater {
    fn new() -> Self {
        Self {
            providers: providers::providers(),
        }
    }

    fn add(&mut self, value: String) -> Result<()> {
        for provider in self.providers.iter_mut() {
            if provider.add(value.clone()).is_ok() {
                return Ok(());
            }
        }
        bail!("No provider found")
    }

    async fn resolve(&self, cwd: &Path, extra_env: &HashMap<String, String>) -> Result<Hydration> {
        Ok(
            try_join_all(self.providers.iter().map(|p| p.resolve(cwd, extra_env)))
                .await?
                .into_iter()
                .flatten()
                .collect(),
        )
    }
}

pub async fn hydrate(
    env: HashMap<String, String>,
    cwd: PathBuf,
    extra_env: HashMap<String, String>,
) -> Result<HashMap<String, String>> {
    let mut hydrater = Hydrater::new();
    for value_or_uri in env.values() {
        hydrater.add(value_or_uri.clone())?
    }

    let hydration = hydrater.resolve(&cwd, &extra_env).await?;

    let mut ret: HashMap<String, String> = HashMap::default();
    for (key, value_or_uri) in env.iter() {
        ret.insert(
            key.clone(),
            hydration
                .get(value_or_uri)
                .unwrap_or_else(|| {
                    panic!(
                        "Cannot find {} in {}",
                        value_or_uri,
                        hydration
                            .keys()
                            .cloned()
                            .collect::<Vec<String>>()
                            .join(", ")
                    )
                })
                .clone(),
        );
    }

    Ok(ret)
}

pub async fn hydrate_one(
    value: String,
    cwd: &Path,
    extra_env: &HashMap<String, String>,
) -> Result<String> {
    let mut hydrater = Hydrater::new();
    hydrater.add(value.clone())?;
    let hydration = hydrater.resolve(cwd, extra_env).await?;
    Ok(hydration.get(&value).unwrap().to_owned())
}

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

    #[tokio::test]
    async fn test_hydrate_raw_values_with_empty_extra_env() {
        let env = HashMap::from([
            ("KEY1".to_string(), "value1".to_string()),
            ("KEY2".to_string(), "!value2".to_string()),
        ]);
        let result = hydrate(env, PathBuf::from("."), HashMap::new())
            .await
            .unwrap();
        assert_eq!(result.get("KEY1").unwrap(), "value1");
        assert_eq!(result.get("KEY2").unwrap(), "value2");
    }

    #[tokio::test]
    async fn test_hydrate_raw_values_with_extra_env_ignored_by_raw_provider() {
        let env = HashMap::from([("KEY".to_string(), "rawval".to_string())]);
        let extra = HashMap::from([("INJECTED".to_string(), "token123".to_string())]);
        let result = hydrate(env, PathBuf::from("."), extra).await.unwrap();
        assert_eq!(result.get("KEY").unwrap(), "rawval");
    }

    #[tokio::test]
    async fn test_hydrate_one_raw_with_empty_extra_env() {
        let result = hydrate_one("mytoken".to_string(), &PathBuf::from("."), &HashMap::new())
            .await
            .unwrap();
        assert_eq!(result, "mytoken");
    }

    #[tokio::test]
    async fn test_hydrate_one_raw_bang_escape() {
        let result = hydrate_one("!escaped".to_string(), &PathBuf::from("."), &HashMap::new())
            .await
            .unwrap();
        assert_eq!(result, "escaped");
    }
}