use serde_json::Value;

pub fn redact_paths(value: &mut Value, paths: &[String]) {
    for path in paths {
        let segments = parse_path(path);
        if segments.is_empty() {
            continue;
        }

        redact_path(value, &segments);
    }
}

pub fn truncate_item_fields(value: &mut Value, max_len: usize) {
    match value {
        Value::String(text) => {
            if text != "[REDACTED]" && text.chars().count() > max_len {
                let truncated = text.chars().take(max_len).collect::<String>();
                *text = format!("{truncated}...");
            }
        }
        Value::Array(items) => {
            for item in items {
                truncate_item_fields(item, max_len);
            }
        }
        Value::Object(map) => {
            for value in map.values_mut() {
                truncate_item_fields(value, max_len);
            }
        }
        Value::Null | Value::Bool(_) | Value::Number(_) => {}
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum PathSegment {
    Field(String),
    Wildcard,
}

fn parse_path(path: &str) -> Vec<PathSegment> {
    let path = path.strip_prefix("$.").or_else(|| path.strip_prefix('$'));
    let Some(path) = path else {
        return Vec::new();
    };

    path.split('.')
        .flat_map(|segment| {
            if let Some(field) = segment.strip_suffix("[*]") {
                vec![PathSegment::Field(field.to_owned()), PathSegment::Wildcard]
            } else if segment == "*" {
                vec![PathSegment::Wildcard]
            } else {
                vec![PathSegment::Field(segment.to_owned())]
            }
        })
        .collect()
}

fn redact_path(value: &mut Value, segments: &[PathSegment]) {
    let Some((current, rest)) = segments.split_first() else {
        *value = Value::String("[REDACTED]".to_owned());
        return;
    };

    match current {
        PathSegment::Field(field) => {
            if let Some(next) = value
                .as_object_mut()
                .and_then(|object| object.get_mut(field))
            {
                redact_path(next, rest);
            }
        }
        PathSegment::Wildcard => {
            if let Some(items) = value.as_array_mut() {
                for item in items {
                    redact_path(item, rest);
                }
            }
        }
    }
}

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

    use super::{redact_paths, truncate_item_fields};

    #[test]
    fn redacts_simple_and_wildcard_paths() {
        let mut value = json!({
            "summary": { "token": "secret" },
            "items": [
                { "token": "a", "message": "keep" },
                { "token": "b", "message": "keep" }
            ]
        });

        redact_paths(
            &mut value,
            &["$.summary.token".to_owned(), "$.items[*].token".to_owned()],
        );

        assert_eq!(value["summary"]["token"], json!("[REDACTED]"));
        assert_eq!(value["items"][0]["token"], json!("[REDACTED]"));
        assert_eq!(value["items"][1]["token"], json!("[REDACTED]"));
    }

    #[test]
    fn truncates_long_strings_recursively() {
        let mut value = json!({
            "message": "abcdefghijklmnop",
            "nested": [{ "message": "qrstuvwxyz" }]
        });

        truncate_item_fields(&mut value, 4);

        assert_eq!(value["message"], json!("abcd..."));
        assert_eq!(value["nested"][0]["message"], json!("qrst..."));
    }
}
