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

use anyhow::{Ok, Result};
use providers::Providers;
use rustc_hash::FxHashMap;

mod providers;
mod resolve;

pub use resolve::{resolve, resolve_one};

type Hydration = FxHashMap<String, String>;

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

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

    Ok(env
        .into_iter()
        .map(|(key, value_or_uri)| {
            let value = hydration.get(&value_or_uri).cloned().unwrap_or_else(|| {
                panic!(
                    "Cannot find {} in {}",
                    value_or_uri,
                    hydration
                        .keys()
                        .cloned()
                        .collect::<Vec<String>>()
                        .join(", ")
                )
            });
            (key, value)
        })
        .collect())
}

pub async fn hydrate_one(
    value: String,
    cwd: &Path,
    extra_env: &HashMap<String, String>,
) -> Result<String> {
    let mut providers = Providers::new();
    providers.add(value.clone())?;
    let hydration = providers.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_duplicate_raw_values() {
        let env = HashMap::from([
            ("KEY1".to_string(), "a".to_string()),
            ("KEY2".to_string(), "a".to_string()),
        ]);
        let result = hydrate(env, PathBuf::from("."), HashMap::new())
            .await
            .unwrap();
        assert_eq!(result.get("KEY1").unwrap(), "a");
        assert_eq!(result.get("KEY2").unwrap(), "a");
    }

    #[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");
    }
}
