//! Extract nested properties from JSON values using dot-separated paths.
//!
//! Supports:
//! - Dot-separated keys: `"a.b.c"`
//! - Numeric array indices: `"items.0.name"`
//! - Wildcard expansion: `"items.*.name"` (expands all array elements)
//!
//! # Limitations
//!
//! At most one `*` wildcard is permitted per path. Escaped dots (`\.`) in keys
//! are supported.

use serde_json::Value;

use crate::error::{DynoError, Result};

/// Extract a value from `subject` at the given dot-separated `path`.
///
/// # Errors
///
/// - [`DynoError::EmptyPath`] if `path` is empty.
/// - [`DynoError::TooManyWildcards`] if the path contains more than one `*`.
/// - [`DynoError::PropertyOfNonObject`] if the subject is not an object or array.
/// - [`DynoError::WildcardOnNonArray`] if `*` targets a non-array value.
pub fn get_property(subject: &Value, path: &str) -> Result<Option<Value>> {
    validate_path(path)?;
    validate_subject(subject)?;

    let parts = split_path(path);
    Ok(resolve(subject, &parts))
}

/// Validate that the path is non-empty and contains at most one wildcard.
fn validate_path(path: &str) -> Result<()> {
    if path.is_empty() {
        return Err(DynoError::EmptyPath);
    }
    let star_count = path.matches('*').count();
    if star_count > 1 {
        return Err(DynoError::TooManyWildcards);
    }
    Ok(())
}

/// Validate that the subject is an object or array.
fn validate_subject(subject: &Value) -> Result<()> {
    match subject {
        Value::Object(_) | Value::Array(_) => Ok(()),
        _ => Err(DynoError::PropertyOfNonObject),
    }
}

/// Split a path by unescaped dots.
fn split_path(path: &str) -> Vec<String> {
    let mut parts = Vec::new();
    let mut current = String::new();
    let mut chars = path.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '\\' {
            if let Some(&next) = chars.peek() {
                if next == '.' {
                    current.push('.');
                    chars.next();
                    continue;
                }
            }
            current.push(ch);
        } else if ch == '.' {
            parts.push(std::mem::take(&mut current));
        } else {
            current.push(ch);
        }
    }
    parts.push(current);
    parts
}

/// Recursively resolve a path through a JSON value tree.
fn resolve(subject: &Value, parts: &[String]) -> Option<Value> {
    if parts.is_empty() {
        return Some(subject.clone());
    }

    let (part, rest) = parts.split_first()?;

    // Wildcard expansion on arrays
    if part == "*" {
        let arr = subject.as_array()?;
        let results: Vec<Value> = arr.iter().filter_map(|item| resolve(item, rest)).collect();
        return Some(Value::Array(results));
    }

    match subject {
        Value::Array(arr) => {
            // Try numeric index access
            let idx: usize = part.parse().ok()?;
            let item = arr.get(idx)?;
            resolve(item, rest)
        }
        Value::Object(obj) => {
            let val = obj.get(part.as_str())?;
            resolve(val, rest)
        }
        _ => None,
    }
}

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

    // ── Path validation ────────────────────────────────────────────────

    #[test]
    fn empty_path_returns_error() {
        let err = get_property(&json!({}), "").unwrap_err();
        assert!(matches!(err, DynoError::EmptyPath));
    }

    #[test]
    fn too_many_wildcards_returns_error() {
        let err = get_property(&json!({}), "a.*.b.*").unwrap_err();
        assert!(matches!(err, DynoError::TooManyWildcards));
    }

    #[test]
    fn non_object_subject_returns_error() {
        let err = get_property(&json!("string"), "a.b").unwrap_err();
        assert!(matches!(err, DynoError::PropertyOfNonObject));

        let err = get_property(&json!(42), "a.b").unwrap_err();
        assert!(matches!(err, DynoError::PropertyOfNonObject));

        let err = get_property(&json!(null), "a.b").unwrap_err();
        assert!(matches!(err, DynoError::PropertyOfNonObject));
    }

    #[test]
    fn arrays_and_objects_are_valid_subjects() {
        assert!(get_property(&json!([]), "a.b").is_ok());
        assert!(get_property(&json!({}), "a.b").is_ok());
    }

    // ── Property not found ─────────────────────────────────────────────

    #[test]
    fn missing_property_returns_none() {
        assert_eq!(
            get_property(&json!({"a": {"c": "b"}}), "a.c.b").unwrap(),
            None
        );
        assert_eq!(
            get_property(&json!({"a": {"b": {}}}), "a.b.c").unwrap(),
            None
        );
    }

    #[test]
    fn wildcard_on_missing_returns_empty_array() {
        let result = get_property(&json!([{"a": 1}, {"a": 2}]), "*.a.b").unwrap();
        assert_eq!(result, Some(json!([])));
    }

    #[test]
    fn numeric_index_on_missing_returns_none() {
        assert_eq!(
            get_property(&json!([{"a": 1}, {"a": 2}]), "0.b").unwrap(),
            None
        );
    }

    // ── Successful property extraction ─────────────────────────────────

    #[test]
    fn simple_nested_path() {
        assert_eq!(
            get_property(&json!({"a": {"b": "c"}}), "a.b").unwrap(),
            Some(json!("c"))
        );
        assert_eq!(
            get_property(&json!({"a": {"b": "c"}}), "a").unwrap(),
            Some(json!({"b": "c"}))
        );
    }

    #[test]
    fn numeric_key_in_object() {
        assert_eq!(
            get_property(&json!({"1": {"b": "c"}}), "1.b").unwrap(),
            Some(json!("c"))
        );
    }

    #[test]
    fn array_index_access() {
        let subject = json!([{"a": {"b": {"c": "d"}}}]);
        assert_eq!(
            get_property(&subject, "0.a.b").unwrap(),
            Some(json!({"c": "d"}))
        );
    }

    #[test]
    fn wildcard_maps_through_array() {
        let subject = json!([
            {"a": 1, "b": {"c": 1}},
            {"a": 2, "b": {"c": 2}},
            {"a": 3, "b": {"d": 3}},
            {"a": 4}
        ]);
        assert_eq!(
            get_property(&subject, "*.b").unwrap(),
            Some(json!([{"c": 1}, {"c": 2}, {"d": 3}]))
        );
        assert_eq!(
            get_property(&subject, "*.b.c").unwrap(),
            Some(json!([1, 2]))
        );
    }

    #[test]
    fn wildcard_in_nested_path() {
        let subject = json!({
            "a": [
                {"c": {"d": 1}},
                {"c": {"d": 2}},
                {"c": {"d": 3}}
            ]
        });
        assert_eq!(
            get_property(&subject, "a.*.c").unwrap(),
            Some(json!([{"d": 1}, {"d": 2}, {"d": 3}]))
        );
    }

    #[test]
    fn index_then_wildcard() {
        let subject = json!([
            {"a": [10, 20, {"c": {"d": 3}}]},
            {"a": [11, 21, {"c": {"d": 4}}]},
            {"a": [12, 22, {"c": {"d": 5}}]},
            {"a": [12, 22, {}]}
        ]);
        assert_eq!(
            get_property(&subject, "2.a.*").unwrap(),
            Some(json!([12, 22, {"c": {"d": 5}}]))
        );
    }

    #[test]
    fn index_and_wildcard_deep() {
        let subject = json!({
            "a": [
                {"c": {"d": [{"e": 1}, {"e": 2}, {"f": 3}]}},
                {"c": {"d": [{"e": 4}, {"g": 5}, {"e": 6}]}}
            ]
        });
        assert_eq!(
            get_property(&subject, "a.1.c.d.*.e").unwrap(),
            Some(json!([4, 6]))
        );
    }

    #[test]
    fn falsy_property_object() {
        let subject = json!({"a": {"g": {}}});
        assert_eq!(get_property(&subject, "a.g").unwrap(), Some(json!({})));
    }

    // ── Path splitting ─────────────────────────────────────────────────

    #[test]
    fn split_path_basic() {
        assert_eq!(split_path("a.b.c"), vec!["a", "b", "c"]);
    }

    #[test]
    fn split_path_escaped_dot() {
        assert_eq!(split_path(r"a\.b.c"), vec!["a.b", "c"]);
    }

    #[test]
    fn split_path_single() {
        assert_eq!(split_path("a"), vec!["a"]);
    }
}
