use std::{env, fmt, mem, sync::Arc};
use anyhow::Context as _;
use super::{ConfigSource, Flat};
use crate::{
    testing::MOCK_ENV_VARS,
    utils::JsonObject,
    value::{FileFormat, Map, Value, ValueOrigin, WithOrigin},
    Json,
};
#[derive(Debug, Clone)]
pub struct Environment {
    origin: Arc<ValueOrigin>,
    map: Map,
}
impl Default for Environment {
    fn default() -> Self {
        Self {
            origin: Arc::new(ValueOrigin::EnvVars),
            map: Map::new(),
        }
    }
}
impl Environment {
    pub fn prefixed(prefix: &str) -> Self {
        MOCK_ENV_VARS.with_borrow(|mock_vars| {
            let mock_vars = mock_vars
                .iter()
                .map(|(key, value)| (key.clone(), value.clone()));
            Self::from_iter(prefix, env::vars().chain(mock_vars))
        })
    }
    pub fn from_iter<K, V>(prefix: &str, env: impl IntoIterator<Item = (K, V)>) -> Self
    where
        K: AsRef<str> + Into<String>,
        V: Into<String>,
    {
        let origin = Arc::new(ValueOrigin::EnvVars);
        let map = env.into_iter().filter_map(|(name, value)| {
            let retained_name = name.as_ref().strip_prefix(prefix)?.to_lowercase();
            Some((
                retained_name,
                WithOrigin {
                    inner: Value::from(value.into()),
                    origin: Arc::new(ValueOrigin::Path {
                        source: origin.clone(),
                        path: name.into(),
                    }),
                },
            ))
        });
        let map = map.collect();
        Self { origin, map }
    }
    #[must_use]
    pub fn with_vars(mut self, var_names: &[&str]) -> Self {
        let origin = Arc::new(ValueOrigin::EnvVars);
        let defined_vars = var_names.iter().filter_map(|&name| {
            let value = env::var_os(name)?.into_string().ok()?;
            Some((
                name.to_owned(),
                WithOrigin {
                    inner: Value::from(value),
                    origin: Arc::new(ValueOrigin::Path {
                        source: origin.clone(),
                        path: name.to_owned(),
                    }),
                },
            ))
        });
        self.map.extend(defined_vars);
        self
    }
    #[doc(hidden)] pub fn from_dotenv(filename: &str, contents: &str) -> anyhow::Result<Self> {
        let origin = Arc::new(ValueOrigin::File {
            name: filename.to_owned(),
            format: FileFormat::Dotenv,
        });
        let mut map = Map::default();
        for line in contents.lines().map(str::trim) {
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            let (name, variable_value) = line.split_once('=').with_context(|| {
                format!("Incorrect line for setting environment variable: {line}")
            })?;
            let variable_value = variable_value.trim_matches('"');
            map.insert(
                name.to_lowercase(),
                WithOrigin {
                    inner: Value::from(variable_value.to_owned()),
                    origin: Arc::new(ValueOrigin::Path {
                        source: origin.clone(),
                        path: name.into(),
                    }),
                },
            );
        }
        Ok(Self { origin, map })
    }
    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &WithOrigin)> + '_ {
        self.map.iter().map(|(name, value)| (name.as_str(), value))
    }
    #[must_use]
    pub fn strip_prefix(self, prefix: &str) -> Self {
        let prefix = prefix.to_lowercase();
        let filtered = self
            .map
            .into_iter()
            .filter_map(|(name, value)| Some((name.strip_prefix(&prefix)?.to_owned(), value)));
        Self {
            origin: self.origin,
            map: filtered.collect(),
        }
    }
    pub fn coerce_json(&mut self) -> anyhow::Result<()> {
        let mut coerced_values = vec![];
        let mut errors = vec![];
        for (key, value) in &self.map {
            let stripped_key = key
                .strip_suffix("__json")
                .or_else(|| key.strip_suffix(":json"));
            let Some(stripped_key) = stripped_key else {
                continue;
            };
            let Some(value_str) = value.inner.as_plain_str() else {
                continue;
            };
            let val = match serde_json::from_str::<serde_json::Value>(value_str) {
                Ok(val) => val,
                Err(err) => {
                    mem::take(&mut coerced_values);
                    errors.push((value.origin.clone(), err));
                    continue;
                }
            };
            if !errors.is_empty() {
                continue; }
            let root_origin = Arc::new(ValueOrigin::Synthetic {
                source: value.origin.clone(),
                transform: "parsed JSON string".into(),
            });
            let coerced_value = Json::map_value(val, &root_origin, String::new());
            coerced_values.push((key.to_owned(), stripped_key.to_owned(), coerced_value));
        }
        for (key, stripped_key, coerced_value) in coerced_values {
            self.map.remove(&key);
            self.map.insert(stripped_key, coerced_value);
        }
        if errors.is_empty() {
            Ok(())
        } else {
            Err(JsonCoercionErrors(errors).into())
        }
    }
    pub fn convert_flat_params(flat_params: &JsonObject, prefix: &str) -> JsonObject {
        let vars = flat_params.iter().map(|(path, value)| {
            let mut var_name = path.replace('.', "_").to_uppercase();
            var_name.insert_str(0, prefix);
            let value: serde_json::Value = match value {
                serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
                    var_name.push_str("__JSON");
                    value.to_string().into()
                }
                simple => simple.clone(),
            };
            (var_name, value)
        });
        vars.collect()
    }
}
#[derive(Debug)]
struct JsonCoercionErrors(Vec<(Arc<ValueOrigin>, serde_json::Error)>);
impl fmt::Display for JsonCoercionErrors {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(
            formatter,
            "failed coercing flat configuration params to JSON:"
        )?;
        for (i, (key, err)) in self.0.iter().enumerate() {
            writeln!(formatter, "{}. {key}: {err}", i + 1)?;
        }
        Ok(())
    }
}
impl std::error::Error for JsonCoercionErrors {}
impl ConfigSource for Environment {
    type Kind = Flat;
    fn into_contents(self) -> WithOrigin<Map> {
        WithOrigin::new(self.map, self.origin)
    }
}
#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use super::*;
    #[test]
    fn parsing_dotenv_contents() {
        let env = Environment::from_dotenv(
            "test.env",
            r#"
            APP_TEST=what
            APP_OTHER="test string"
            # Overwriting vars should be supported
            APP_TEST=42
            "#,
        )
        .unwrap();
        assert_eq!(env.map.len(), 2, "{:?}", env.map);
        assert_eq!(env.map["app_test"].inner.as_plain_str(), Some("42"));
        let origin = &env.map["app_test"].origin;
        let ValueOrigin::Path { path, source } = origin.as_ref() else {
            panic!("unexpected origin: {origin:?}");
        };
        assert_eq!(path, "APP_TEST");
        assert_matches!(
            source.as_ref(),
            ValueOrigin::File { name, format: FileFormat::Dotenv } if name == "test.env"
        );
        assert_eq!(
            env.map["app_other"].inner.as_plain_str(),
            Some("test string")
        );
        let env = env.strip_prefix("app_");
        assert_eq!(env.map.len(), 2, "{:?}", env.map);
        assert_eq!(env.map["test"].inner.as_plain_str(), Some("42"));
        assert_matches!(env.map["test"].origin.as_ref(), ValueOrigin::Path { path, .. } if path == "APP_TEST");
        assert_eq!(env.map["other"].inner.as_plain_str(), Some("test string"));
    }
    #[test]
    fn converting_flat_params() {
        let params = serde_json::json!({
            "value": 23,
            "flag": true,
            "nested.option": null,
            "nested.renamed": "first",
            "nested.set": ["first", "second"],
            "nested.map": { "call": 42 },
        });
        let params = params.as_object().unwrap();
        let converted = Environment::convert_flat_params(params, "APP_");
        assert_eq!(
            serde_json::Value::from(converted),
            serde_json::json!({
                "APP_VALUE": 23,
                "APP_FLAG": true,
                "APP_NESTED_OPTION": null,
                "APP_NESTED_RENAMED": "first",
                "APP_NESTED_SET__JSON": r#"["first","second"]"#,
                "APP_NESTED_MAP__JSON": r#"{"call":42}"#,
            })
        );
    }
}