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

use rustc_hash::FxHashMap;

use anyhow::{Ok, Result};
use async_trait::async_trait;
use futures::future::try_join_all;
use itertools::Itertools;
use log::debug;
use serde::Deserialize;
use url::Url;

use crate::Hydration;

use super::{Provider, Warnings, add_url, deserialize_output, host_with_port, run_cli};

#[derive(Default)]
pub struct Doppler {
    urls: FxHashMap<Url, String>,
}

impl Doppler {
    pub fn new() -> Self {
        Default::default()
    }
}

#[derive(Deserialize)]
struct DopplerExport {
    computed: String,
}

#[async_trait]
impl Provider for Doppler {
    fn add(&mut self, value: String) -> Result<()> {
        add_url(&mut self.urls, value, "doppler")
    }

    fn name(&self) -> &'static str {
        "Doppler"
    }

    fn install_url(&self) -> &'static str {
        "https://docs.doppler.com/docs/install-cli"
    }

    fn has_work(&self) -> bool {
        !self.urls.is_empty()
    }

    async fn resolve(
        &self,
        _: &Path,
        extra_env: &HashMap<String, String>,
        _: &Warnings,
    ) -> Result<Hydration> {
        let extra_env = Arc::new(extra_env.clone());
        let name = self.name();
        let install_url = self.install_url();
        let fetches = self
            .urls
            .iter()
            .into_group_map_by(|(url, _)| host_with_port(url))
            .into_iter()
            .flat_map(|(host, group)| {
                group
                    .into_iter()
                    .into_group_map_by(|(url, _)| {
                        url.path().split('/').nth(1).expect("Missing project")
                    })
                    .into_iter()
                    .flat_map(|(project, group)| {
                        group
                            .into_iter()
                            .into_group_map_by(|(url, _)| {
                                url.path().split('/').nth(2).expect("Missing env")
                            })
                            .into_iter()
                            .map(|(env, group)| {
                                let vars = group
                                    .iter()
                                    .map(|(u, value)| {
                                        (
                                            u.path().split('/').nth(3).expect("Missing variable"),
                                            (*value).clone(),
                                        )
                                    })
                                    .collect::<HashMap<_, _>>();
                                let host = host.clone();
                                let extra_env = Arc::clone(&extra_env);
                                async move {
                                    let cmd = [
                                        "doppler",
                                        "--api-host",
                                        &format!("https://{}", host),
                                        "secrets",
                                        "--project",
                                        project,
                                        "--config",
                                        env,
                                        "--json",
                                    ];
                                    debug!("Lade run: {}", cmd.join(" "));
                                    let child =
                                        run_cli(&cmd, &extra_env, name, install_url, None).await?;
                                    let loaded: HashMap<String, DopplerExport> =
                                        deserialize_output(&child, name)?;
                                    let hydration = vars
                                        .into_iter()
                                        .map(|(key, value)| {
                                            (
                                                value,
                                                loaded
                                                    .get(key)
                                                    .unwrap_or_else(|| {
                                                        panic!(
                                                            "Variable not found in Doppler: {}",
                                                            key
                                                        )
                                                    })
                                                    .computed
                                                    .clone(),
                                            )
                                        })
                                        .collect::<Hydration>();
                                    debug!("hydration: {:?}", hydration);
                                    Ok(hydration)
                                }
                            })
                            .collect::<Vec<_>>()
                    })
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>();

        Ok(try_join_all(fetches).await?.into_iter().flatten().collect())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::providers::fake_cli;
    use std::path::Path;
    use tempfile::tempdir;

    fn path_env(dir: &tempfile::TempDir) -> HashMap<String, String> {
        HashMap::from([(
            "PATH".to_string(),
            dir.path().to_string_lossy().into_owned(),
        )])
    }

    #[test]
    fn test_add_routing() {
        let mut p = Doppler::new();
        assert!(
            p.add("doppler://api.doppler.com/myproject/dev/MY_SECRET".to_string())
                .is_ok()
        );
        assert!(p.add("vault://host/mount/key/field".to_string()).is_err());
        assert!(p.add("plainvalue".to_string()).is_err());
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_single_var() {
        let fake_bin = tempdir().unwrap();
        fake_cli(
            &fake_bin,
            "doppler",
            r#"echo '{"MY_SECRET":{"computed":"doppler_value"}}'"#,
        );
        let mut p = Doppler::new();
        p.add("doppler://api.doppler.com/myproject/dev/MY_SECRET".to_string())
            .unwrap();
        let result = p
            .resolve(Path::new("."), &path_env(&fake_bin), &Warnings::default())
            .await
            .unwrap();
        assert_eq!(
            result
                .get("doppler://api.doppler.com/myproject/dev/MY_SECRET")
                .unwrap(),
            "doppler_value"
        );
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_multiple_vars_same_project() {
        let fake_bin = tempdir().unwrap();
        fake_cli(
            &fake_bin,
            "doppler",
            r#"echo '{"KEY1":{"computed":"val1"},"KEY2":{"computed":"val2"}}'"#,
        );
        let mut p = Doppler::new();
        p.add("doppler://api.doppler.com/myproject/dev/KEY1".to_string())
            .unwrap();
        p.add("doppler://api.doppler.com/myproject/dev/KEY2".to_string())
            .unwrap();
        let result = p
            .resolve(Path::new("."), &path_env(&fake_bin), &Warnings::default())
            .await
            .unwrap();
        assert_eq!(
            result
                .get("doppler://api.doppler.com/myproject/dev/KEY1")
                .unwrap(),
            "val1"
        );
        assert_eq!(
            result
                .get("doppler://api.doppler.com/myproject/dev/KEY2")
                .unwrap(),
            "val2"
        );
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_cli_not_found() {
        let empty_bin = tempdir().unwrap();
        let mut p = Doppler::new();
        p.add("doppler://api.doppler.com/myproject/dev/MY_SECRET".to_string())
            .unwrap();
        let result = p
            .resolve(Path::new("."), &path_env(&empty_bin), &Warnings::default())
            .await;
        assert!(
            result
                .unwrap_err()
                .to_string()
                .contains("Doppler CLI not found")
        );
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_resolve_malformed_json_error() {
        let fake_bin = tempdir().unwrap();
        fake_cli(&fake_bin, "doppler", "echo 'not valid json'");
        let mut p = Doppler::new();
        p.add("doppler://api.doppler.com/myproject/dev/MY_SECRET".to_string())
            .unwrap();
        let result = p
            .resolve(Path::new("."), &path_env(&fake_bin), &Warnings::default())
            .await;
        assert!(result.unwrap_err().to_string().contains("Doppler error"));
    }
}