//! subset of microcosm.blue/RecordPath

use serde_json::Value;

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Modifier {
    /// `[]`
    Elements,
    /// `[nsid]`
    UnionElements(String),
    /// `{nsid}`
    Union(String),
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Segment {
    field: String,
    modifier: Option<Modifier>,
}

pub(crate) fn parse_record_path(path: &str) -> Result<Vec<Segment>, String> {
    let chars: Vec<char> = path.chars().collect();
    let mut segments = Vec::new();
    let mut field = String::new();
    let mut i = 0;
    let mut saw_any = false;

    let unescape = |chars: &[char], i: &mut usize| -> Result<char, String> {
        *i += 1;
        match chars.get(*i) {
            Some(&c @ ('.' | '[' | ']' | '{' | '}' | '!')) => Ok(c),
            Some(c) => Err(format!("invalid escape !{c}")),
            None => Err("trailing ! escape".to_owned()),
        }
    };

    while i < chars.len() {
        let c = chars[i];
        match c {
            '.' => {
                if !saw_any {
                    return Err("empty path segment".to_owned());
                }
                segments.push(Segment {
                    field: std::mem::take(&mut field),
                    modifier: None,
                });
                saw_any = false;
                i += 1;
            }
            '!' => {
                field.push(unescape(&chars, &mut i)?);
                saw_any = true;
                i += 1;
            }
            '[' | '{' => {
                let (close, is_union_brace) = if c == '[' { (']', false) } else { ('}', true) };
                let mut inner = String::new();
                i += 1;
                loop {
                    match chars.get(i) {
                        None => return Err(format!("unclosed {c}")),
                        Some(&close_c) if close_c == close => break,
                        Some(&'!') => inner.push(unescape(&chars, &mut i)?),
                        Some(&ch) => inner.push(ch),
                    }
                    i += 1;
                }
                i += 1; // consume closer
                let modifier = match (is_union_brace, inner.is_empty()) {
                    (false, true) => Modifier::Elements,
                    (false, false) => Modifier::UnionElements(inner),
                    (true, true) => return Err("empty {} union ref".to_owned()),
                    (true, false) => Modifier::Union(inner),
                };
                if field.is_empty() && !saw_any {
                    return Err("modifier without a field".to_owned());
                }
                segments.push(Segment {
                    field: std::mem::take(&mut field),
                    modifier: Some(modifier),
                });
                saw_any = false;
                // a modified segment must end the path or be followed by '.'
                match chars.get(i) {
                    None => break,
                    Some('.') => {
                        i += 1;
                    }
                    Some(other) => {
                        return Err(format!("expected '.' after {c}{close}, got {other:?}"));
                    }
                }
            }
            _ => {
                field.push(c);
                saw_any = true;
                i += 1;
            }
        }
    }
    if saw_any {
        segments.push(Segment {
            field,
            modifier: None,
        });
    }
    if segments.is_empty() {
        return Err("empty path".to_owned());
    }
    Ok(segments)
}

pub(crate) fn type_tag(value: &Value) -> Option<&str> {
    value.get("$type").and_then(Value::as_str)
}

pub(crate) fn walk_path<'a>(
    segments: &[Segment],
    roots: impl IntoIterator<Item = &'a Value>,
) -> Vec<&'a Value> {
    let mut nodes: Vec<&Value> = roots.into_iter().collect();
    for segment in segments {
        let mut next: Vec<&Value> = nodes
            .into_iter()
            .filter_map(|node| node.get(&segment.field))
            .collect();
        if let Some(modifier) = &segment.modifier {
            next = match modifier {
                Modifier::Elements => next
                    .into_iter()
                    .flat_map(|node| node.as_array().into_iter().flatten())
                    .collect(),
                Modifier::UnionElements(nsid) => next
                    .into_iter()
                    .flat_map(|node| node.as_array().into_iter().flatten())
                    .filter(|item| type_tag(item) == Some(nsid.as_str()))
                    .collect(),
                Modifier::Union(nsid) => next
                    .into_iter()
                    .filter(|node| type_tag(node) == Some(nsid.as_str()))
                    .collect(),
            };
        }
        nodes = next;
    }
    nodes
}

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

    fn parse_ok(path: &str) -> Vec<Segment> {
        parse_record_path(path).unwrap_or_else(|e| panic!("{path}: {e}"))
    }

    #[test]
    fn parses_field_paths() {
        assert_eq!(
            parse_ok("subject.uri"),
            vec![
                Segment {
                    field: "subject".into(),
                    modifier: None
                },
                Segment {
                    field: "uri".into(),
                    modifier: None
                },
            ]
        );
    }

    #[test]
    fn parses_array_descents() {
        assert_eq!(
            parse_ok("repos[]"),
            vec![Segment {
                field: "repos".into(),
                modifier: Some(Modifier::Elements)
            }]
        );
        assert_eq!(
            parse_ok("facets[].features[app.bsky.richtext.facet#mention].did"),
            vec![
                Segment {
                    field: "facets".into(),
                    modifier: Some(Modifier::Elements)
                },
                Segment {
                    field: "features".into(),
                    modifier: Some(Modifier::UnionElements(
                        "app.bsky.richtext.facet#mention".into()
                    ))
                },
                Segment {
                    field: "did".into(),
                    modifier: None
                },
            ]
        );
        assert_eq!(
            parse_ok("embed{app.bsky.embed.record}.record.uri"),
            vec![
                Segment {
                    field: "embed".into(),
                    modifier: Some(Modifier::Union("app.bsky.embed.record".into()))
                },
                Segment {
                    field: "record".into(),
                    modifier: None
                },
                Segment {
                    field: "uri".into(),
                    modifier: None
                },
            ]
        );
    }

    #[test]
    fn parses_escaped_field_names() {
        assert_eq!(
            parse_ok("meta.dot!.name"),
            vec![Segment {
                field: "meta".into(),
                modifier: None
            }]
            .into_iter()
            .chain([Segment {
                field: "dot.name".into(),
                modifier: None
            }])
            .collect::<Vec<_>>()
        );
        assert_eq!(
            parse_ok("meta.a!!b"),
            vec![
                Segment {
                    field: "meta".into(),
                    modifier: None
                },
                Segment {
                    field: "a!b".into(),
                    modifier: None
                },
            ]
        );
        assert_eq!(
            parse_ok("meta.$unknown"),
            vec![
                Segment {
                    field: "meta".into(),
                    modifier: None
                },
                Segment {
                    field: "$unknown".into(),
                    modifier: None
                },
            ]
        );
    }

    #[test]
    fn rejects_malformed_paths() {
        for bad in [
            "", ".", "a..b", "a!", "a!x", "a[", "a[]b", "a{}", "[did]", "a[nsid]b",
        ] {
            assert!(parse_record_path(bad).is_err(), "{bad} should fail");
        }
    }

    #[test]
    fn walks_vector_matches() {
        let doc = json!({
            "repos": [
                {"uri": "at://did:plc:a/sh.tangled.repo/x"},
                {"uri": "at://did:plc:b/sh.tangled.repo/y"},
            ],
            "owner": "did:plc:z",
        });
        let segs = parse_ok("repos[].uri");
        let found = walk_path(&segs, [&doc]);
        assert_eq!(found.len(), 2);
    }

    #[test]
    fn walks_union_filters() {
        let doc = json!({
            "items": [
                {"$type": "sh.tangled.repo", "uri": "at://did:plc:a/sh.tangled.repo/x"},
                {"$type": "sh.tangled.actor.profile", "did": "did:plc:b"},
            ]
        });
        let segs = parse_ok("items[sh.tangled.repo].uri");
        let found = walk_path(&segs, [&doc]);
        assert_eq!(found, vec![&json!("at://did:plc:a/sh.tangled.repo/x")]);
    }
}
