use serde::Deserialize;
use serde_yaml::{Mapping, Value};

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawOperation {
    #[serde(default)]
    pub filter: Option<RawFilter>,
    #[serde(default)]
    pub project: Option<RawProjection>,
    #[serde(default, rename = "addFields")]
    pub add_fields: Option<RawProjection>,
    #[serde(default)]
    pub sort: Option<RawSort>,
    #[serde(default)]
    pub limit: Option<i64>,
    #[serde(default)]
    pub update: Option<RawUpdate>,
}

#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct RawFilter(pub Mapping);

#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct RawProjection(pub Mapping);

#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct RawSort(pub Mapping);

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawUpdate {
    #[serde(rename = "$set", default)]
    pub set: Option<Mapping>,
    #[serde(rename = "$unset", default)]
    pub unset: Option<Mapping>,
}

pub fn parse(yaml: &str) -> Result<RawOperation, serde_yaml::Error> {
    if yaml.trim().is_empty() {
        return Ok(RawOperation::default());
    }
    let value: Value = serde_yaml::from_str(yaml)?;
    if matches!(value, Value::Null) {
        return Ok(RawOperation::default());
    }
    serde_yaml::from_value(value)
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum RawKeyArg {
    Scalar(String),
    Map(RawKeyOpMap),
}

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawKeyOpMap {
    #[serde(rename = "$eq", default)]
    pub eq: Option<String>,
    #[serde(rename = "$ne", default)]
    pub ne: Option<String>,
    #[serde(rename = "$in", default)]
    pub in_: Option<Vec<Value>>,
    #[serde(rename = "$nin", default)]
    pub nin: Option<Vec<Value>>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawRelationalObj {
    #[serde(rename = "match", default)]
    pub match_: Option<Mapping>,
    #[serde(rename = "maxDepth", default)]
    pub max_depth: Option<i64>,
    #[serde(rename = "minDepth", default)]
    pub min_depth: Option<i64>,
    #[serde(rename = "maxDistance", default)]
    pub max_distance: Option<i64>,
    #[serde(rename = "minDistance", default)]
    pub min_distance: Option<i64>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use indoc::indoc;

    fn parse_ok(yaml: &str) -> RawOperation {
        parse(yaml).expect("expected wire parse to succeed")
    }

    fn parse_err(yaml: &str) -> String {
        parse(yaml)
            .expect_err("expected wire parse to fail")
            .to_string()
    }

    #[test]
    fn empty_yaml_parses_to_default() {
        let op = parse_ok("");
        assert!(op.filter.is_none() && op.update.is_none());
    }

    #[test]
    fn full_operation_round_trips() {
        let op = parse_ok(indoc! {"
            filter:
              status: draft
            project:
              title: 1
            sort:
              modified_at: -1
            limit: 10
            update:
              $set:
                reviewed: true
        "});
        assert!(op.filter.is_some());
        assert!(op.project.is_some());
        assert_eq!(op.sort.as_ref().unwrap().0["modified_at"], -1);
        assert_eq!(op.limit, Some(10));
        assert!(op.update.is_some());
    }

    #[test]
    fn unknown_top_level_field_rejected() {
        let err = parse_err("bogus: 1\n");
        assert!(err.contains("bogus"), "{}", err);
    }

    #[test]
    fn scope_field_rejected() {
        let err = parse_err("scope:\n  notes/foo: { self: true }\n");
        assert!(err.contains("scope"), "{}", err);
    }

    #[test]
    fn limit_string_rejected() {
        let err = parse_err("limit: \"20\"\n");
        assert!(
            err.to_lowercase().contains("invalid type") || err.to_lowercase().contains("expected"),
            "{}",
            err
        );
    }

    #[test]
    fn sort_parses_as_raw_mapping() {
        let op = parse_ok("sort:\n  modified_at: -1\n");
        assert!(op.sort.is_some());
    }
}
