use std::sync::Arc;
use anyhow::Context;
use super::{ConfigSource, Hierarchical};
use crate::value::{FileFormat, Map, Pointer, Value, ValueOrigin, WithOrigin};
#[derive(Debug, Clone)]
pub struct Yaml {
    origin: Arc<ValueOrigin>,
    inner: Map,
}
impl Yaml {
    pub fn new(filename: &str, object: serde_yaml::Mapping) -> anyhow::Result<Self> {
        let origin = Arc::new(ValueOrigin::File {
            name: filename.to_owned(),
            format: FileFormat::Yaml,
        });
        let inner =
            Self::map_value(serde_yaml::Value::Mapping(object), &origin, String::new())?.inner;
        let Value::Object(inner) = inner else {
            unreachable!();
        };
        Ok(Self { origin, inner })
    }
    fn map_key(key: serde_yaml::Value, parent_path: &str) -> anyhow::Result<String> {
        Ok(match key {
            serde_yaml::Value::String(value) => value,
            serde_yaml::Value::Number(value) => value.to_string(),
            serde_yaml::Value::Bool(value) => value.to_string(),
            serde_yaml::Value::Null => "null".into(),
            _ => anyhow::bail!("unsupported key type at {parent_path:?}: {key:?}; only primitive value types are supported as keys"),
        })
    }
    fn map_number(number: &serde_yaml::Number, path: &str) -> anyhow::Result<serde_json::Number> {
        Ok(if let Some(number) = number.as_u64() {
            number.into()
        } else if let Some(number) = number.as_i64() {
            number.into()
        } else if let Some(number) = number.as_f64() {
            serde_json::Number::from_f64(number)
                .with_context(|| format!("unsupported number at {path:?}: {number:?}"))?
        } else {
            anyhow::bail!("unsupported number at {path:?}: {number:?}")
        })
    }
    fn map_value(
        value: serde_yaml::Value,
        file_origin: &Arc<ValueOrigin>,
        path: String,
    ) -> anyhow::Result<WithOrigin> {
        let inner = match value {
            serde_yaml::Value::Null => Value::Null,
            serde_yaml::Value::Bool(value) => value.into(),
            serde_yaml::Value::Number(value) => Value::Number(Self::map_number(&value, &path)?),
            serde_yaml::Value::String(value) => value.into(),
            serde_yaml::Value::Sequence(items) => Value::Array(
                items
                    .into_iter()
                    .enumerate()
                    .map(|(i, value)| {
                        let child_path = Pointer(&path).join(&i.to_string());
                        Self::map_value(value, file_origin, child_path)
                    })
                    .collect::<anyhow::Result<_>>()?,
            ),
            serde_yaml::Value::Mapping(items) => Value::Object(
                items
                    .into_iter()
                    .map(|(key, value)| {
                        let key = Self::map_key(key, &path)?;
                        let child_path = Pointer(&path).join(&key);
                        anyhow::Ok((key, Self::map_value(value, file_origin, child_path)?))
                    })
                    .collect::<anyhow::Result<_>>()?,
            ),
            serde_yaml::Value::Tagged(tagged) => {
                return Self::map_value(tagged.value, file_origin, path);
            }
        };
        Ok(WithOrigin {
            inner,
            origin: if path.is_empty() {
                file_origin.clone()
            } else {
                Arc::new(ValueOrigin::Path {
                    source: file_origin.clone(),
                    path,
                })
            },
        })
    }
}
impl ConfigSource for Yaml {
    type Kind = Hierarchical;
    fn into_contents(self) -> WithOrigin<Map> {
        WithOrigin::new(self.inner, self.origin)
    }
}
#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use super::*;
    use crate::value::StrValue;
    const YAML_CONFIG: &str = r#"
bool: true
nested:
    int: 123
    string: "what?"
array:
    - test: 23
    "#;
    fn filename(source: &ValueOrigin) -> &str {
        if let ValueOrigin::File {
            name,
            format: FileFormat::Yaml,
        } = source
        {
            name
        } else {
            panic!("unexpected source: {source:?}");
        }
    }
    #[test]
    fn creating_yaml_config() {
        let yaml: serde_yaml::Value = serde_yaml::from_str(YAML_CONFIG).unwrap();
        let serde_yaml::Value::Mapping(yaml) = yaml else {
            unreachable!();
        };
        let yaml = Yaml::new("test.yml", yaml).unwrap();
        assert_matches!(yaml.inner["bool"].inner, Value::Bool(true));
        assert_matches!(
            yaml.inner["bool"].origin.as_ref(),
            ValueOrigin::Path { path, source } if filename(source) == "test.yml" && path == "bool"
        );
        let str = yaml.inner["nested"].get(Pointer("string")).unwrap();
        assert_matches!(&str.inner, Value::String(StrValue::Plain(s)) if s == "what?");
        assert_matches!(
            str.origin.as_ref(),
            ValueOrigin::Path { path, source } if filename(source) == "test.yml" && path == "nested.string"
        );
        let inner_int = yaml.inner["array"].get(Pointer("0.test")).unwrap();
        assert_matches!(&inner_int.inner, Value::Number(num) if *num == 23_u64.into());
    }
    #[test]
    fn unsupported_key() {
        let yaml = r"
array:
    - [12, 34]: bogus
        ";
        let yaml: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
        let serde_yaml::Value::Mapping(yaml) = yaml else {
            unreachable!();
        };
        let err = Yaml::new("test.yml", yaml).unwrap_err().to_string();
        assert!(err.contains("unsupported key type"), "{err}");
        assert!(err.contains("array.0"), "{err}");
    }
}