//! JSONPath parser for the `json` file format.
//!
//! Delegates parsing to [`jsonpath_rust::parser::parse_json_path`], then
//! validates and lowers the AST into our restricted subset:
//!
//! ```text
//! path     ::= "$" segment*
//! segment  ::= field | wildcard
//! field    ::= "." <name>  |  "[" <string-literal> "]"
//! wildcard ::= "[*]"
//! ```
//!
//! The walker emits one Variant row at each terminal of the path.
//! Concretely:
//!
//! - **Empty / `$`-only path.** One row per top-level JSON document.
//! - **Terminal `[*]`.** One row per array element along the path.
//! - **Terminal field.** One row per top-level document, holding the
//!   value at that path (or zero rows if the path is absent / wrong
//!   type — same permissive contract as every other step).
//!
//! Numeric indexes, slices, filter predicates, recursive descent,
//! unions, and function calls are all rejected with a contextual
//! error.
//!
//! Wildcards: the JSONPath parser collapses `.*` and `[*]` into the
//! same AST node, so we cannot statically distinguish "iterate array
//! elements" (what we support) from "iterate object values" (what the
//! design doc labels "wildcard object keys" and marks unsupported).
//! We therefore treat every accepted wildcard as array-element
//! iteration. At runtime, if the wildcard lands on an object rather
//! than an array, that branch contributes zero rows — consistent with
//! the broader permissive "missing/wrong-type → zero rows" contract
//! for every other step of the walk. Users who intend object-wildcard
//! semantics will see zero rows silently, which is the best we can do
//! without a separate object-wildcard segment in the walker.

use jsonpath_rust::parser::model::{Segment as JpSegment, Selector as JpSelector};
use jsonpath_rust::parser::parse_json_path;

/// A single step along the path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
    /// Descend into the named object field.
    Field(String),
    /// Iterate every element of the current array, emitting one row
    /// per element when this segment is the path's terminal step.
    ArrayElements,
}

/// Errors returned when parsing or validating an `json_path`.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum PathParseError {
    #[error("failed to parse as JSONPath: {0}")]
    Parse(String),
    #[error("numeric array indexes are not supported (got {index})")]
    NumericIndex { index: i64 },
    #[error("array slices are not supported")]
    Slice,
    #[error("filter predicates are not supported")]
    Filter,
    #[error("recursive descent ('..') is not supported")]
    RecursiveDescent,
    #[error("multi-selector unions ('[a,b]') are not supported")]
    Union,
    #[error("invalid escape sequence in bracket-quoted name {literal}: {reason}")]
    InvalidEscape { literal: String, reason: String },
}

/// Parse and validate a `json_path`, returning the lowered segments.
///
/// An empty result is valid — it means "the whole top-level document
/// is the row" (the path is just `$`). A terminal `[*]` means "iterate
/// the array there"; a terminal field name means "extract that field".
pub fn parse(input: &str) -> Result<Vec<Segment>, PathParseError> {
    let jp = parse_json_path(input).map_err(|e| PathParseError::Parse(e.to_string()))?;

    let mut lowered = Vec::with_capacity(jp.segments.len());
    for seg in &jp.segments {
        lowered.push(lower_segment(seg)?);
    }
    Ok(lowered)
}

fn lower_segment(seg: &JpSegment) -> Result<Segment, PathParseError> {
    match seg {
        JpSegment::Descendant(_) => Err(PathParseError::RecursiveDescent),
        JpSegment::Selectors(_) => Err(PathParseError::Union),
        JpSegment::Selector(sel) => lower_selector(sel),
    }
}

fn lower_selector(sel: &JpSelector) -> Result<Segment, PathParseError> {
    match sel {
        JpSelector::Name(name) => Ok(Segment::Field(unquote_name(name)?)),
        // Every wildcard (`.*` or `[*]`) lowers to array-elements — the
        // JSONPath parser collapses them to the same AST. At runtime, a
        // wildcard that lands on an object contributes zero rows (same
        // as any other descent onto the wrong shape). Paths that mean
        // "iterate object values" are effectively unsupported: users
        // get zero rows rather than a static error. See the module-level
        // docstring.
        JpSelector::Wildcard => Ok(Segment::ArrayElements),
        JpSelector::Index(i) => Err(PathParseError::NumericIndex { index: *i }),
        JpSelector::Slice(..) => Err(PathParseError::Slice),
        JpSelector::Filter(_) => Err(PathParseError::Filter),
    }
}

/// Decode a bracket-form name into the raw key it addresses.
///
/// jsonpath-rust preserves the bracket-form string literally — quotes
/// still attached, escape sequences intact. For a path like
/// `$["a\"b"]` the `Name` is the raw six-byte string `"a\"b"`, and the
/// real JSON key is the three-byte string `a"b`. We have to JSON-decode
/// it, not just strip the surrounding quotes.
///
/// Handles both double-quoted (`["..."]`) and single-quoted (`['...']`)
/// forms. Dot-shorthand names (`.name`) bypass this function — they are
/// already raw.
fn unquote_name(name: &str) -> Result<String, PathParseError> {
    let bytes = name.as_bytes();
    if bytes.len() < 2 || bytes[0] != bytes[bytes.len() - 1] {
        // Not a quoted string — pass through unchanged. This path is
        // reached by dot-shorthand names, which jsonpath-rust emits
        // without quotes.
        return Ok(name.to_string());
    }

    match bytes[0] {
        b'"' => {
            // Double-quoted: valid JSON string literal as-is. Feed to
            // serde_json to decode all RFC 8259 escape sequences
            // (\", \\, \/, \b, \f, \n, \r, \t, \uXXXX, surrogate pairs).
            serde_json::from_str::<String>(name).map_err(|e| PathParseError::InvalidEscape {
                literal: name.to_string(),
                reason: e.to_string(),
            })
        }
        b'\'' => {
            // Single-quoted JSONPath string. RFC 9535 lets `\'` stand
            // for a literal apostrophe inside `'...'` (where JSON uses
            // `\"` for the quote character). Translate to a JSON
            // double-quoted literal by:
            //   - replacing `\'` with `'` (unescape the apostrophe)
            //   - escaping any bare `"` as `\"` (required inside JSON
            //     `"..."`)
            // All other JSONPath escape sequences are identical to
            // JSON, so pass them through; serde_json decodes everything
            // (\n, \uXXXX, surrogate pairs, etc.).
            let inner = &name[1..name.len() - 1];
            let json_literal = format!("\"{}\"", inner.replace("\\'", "'").replace('"', "\\\""),);
            serde_json::from_str::<String>(&json_literal).map_err(|e| {
                PathParseError::InvalidEscape {
                    literal: name.to_string(),
                    reason: e.to_string(),
                }
            })
        }
        _ => Ok(name.to_string()),
    }
}

impl std::fmt::Display for Segment {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Segment::Field(name) => write!(f, ".{}", name),
            Segment::ArrayElements => write!(f, "[*]"),
        }
    }
}

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

    fn field(name: &str) -> Segment {
        Segment::Field(name.to_string())
    }

    #[test]
    fn parses_cloudtrail_shape() {
        let segs = parse("$.Records[*]").unwrap();
        assert_eq!(segs, vec![field("Records"), Segment::ArrayElements]);
    }

    #[test]
    fn parses_otlp_nested_shape() {
        let segs = parse("$.resourceLogs[*].scopeLogs[*].logRecords[*]").unwrap();
        assert_eq!(
            segs,
            vec![
                field("resourceLogs"),
                Segment::ArrayElements,
                field("scopeLogs"),
                Segment::ArrayElements,
                field("logRecords"),
                Segment::ArrayElements,
            ]
        );
    }

    #[test]
    fn parses_bracketed_quoted_field() {
        let segs = parse(r#"$["foo.bar"]["weird key"][*]"#).unwrap();
        assert_eq!(
            segs,
            vec![field("foo.bar"), field("weird key"), Segment::ArrayElements,]
        );
        let segs = parse(r#"$['foo']['bar'][*]"#).unwrap();
        assert_eq!(
            segs,
            vec![field("foo"), field("bar"), Segment::ArrayElements]
        );
        // Bracket-quoted names without a terminal `[*]` are valid now —
        // they represent field-extraction (one Variant per top-level
        // document, value at that path).
        let segs = parse(r#"$["foo.bar"]["k"]"#).unwrap();
        assert_eq!(segs, vec![field("foo.bar"), field("k")]);
    }

    #[test]
    fn decodes_json_escape_sequences_in_bracket_quoted_names() {
        // `["a\"b"]` addresses the raw key `a"b`.
        let segs = parse(r#"$["a\"b"][*]"#).unwrap();
        assert_eq!(segs, vec![field("a\"b"), Segment::ArrayElements]);

        // Backslash, newline, tab all decode to their character values.
        let segs = parse(r#"$["a\\b"]["c\nd"]["e\tf"][*]"#).unwrap();
        assert_eq!(
            segs,
            vec![
                field("a\\b"),
                field("c\nd"),
                field("e\tf"),
                Segment::ArrayElements,
            ]
        );

        // Unicode escape.
        let segs = parse(r#"$["café"][*]"#).unwrap();
        assert_eq!(segs, vec![field("café"), Segment::ArrayElements]);
    }

    #[test]
    fn decodes_json_escape_sequences_in_single_quoted_names() {
        // In single-quoted form, `\'` stands for a literal apostrophe.
        // A bare `"` is valid (JSON would require it escaped, but our
        // converter rewrites to a JSON literal first).
        let segs = parse(r#"$['it\'s']['"quoted"'][*]"#).unwrap();
        assert_eq!(
            segs,
            vec![field("it's"), field("\"quoted\""), Segment::ArrayElements]
        );
    }

    /// Escaped backslash immediately followed by an escaped apostrophe.
    /// Inside `'...'` the bytes `\\` are an escaped backslash and `\'`
    /// is an escaped apostrophe; the decoded key is the two-char string
    /// `\'` (literal backslash + literal apostrophe). The naive
    /// `replace("\\'", "'")` is safe here because `'` can only appear in
    /// `inner` as the right half of `\'` (a bare `'` would have closed
    /// the JSONPath string), so the replace never eats a backslash that
    /// belongs to an escaped-backslash sequence.
    #[test]
    fn decodes_escaped_backslash_then_escaped_apostrophe() {
        let segs = parse(r#"$['\\\''][*]"#).unwrap();
        assert_eq!(segs, vec![field("\\'"), Segment::ArrayElements]);

        // Two escaped backslashes followed by an escaped apostrophe.
        let segs = parse(r#"$['\\\\\''][*]"#).unwrap();
        assert_eq!(segs, vec![field("\\\\'"), Segment::ArrayElements]);
    }

    #[test]
    fn rejects_missing_root() {
        assert!(matches!(parse("Records[*]"), Err(PathParseError::Parse(_))));
    }

    #[test]
    fn rejects_numeric_index() {
        assert!(matches!(
            parse("$.Records[0]"),
            Err(PathParseError::NumericIndex { index: 0 })
        ));
    }

    #[test]
    fn rejects_slice() {
        assert!(matches!(
            parse("$.Records[0:5]"),
            Err(PathParseError::Slice)
        ));
    }

    #[test]
    fn rejects_filter_predicate() {
        assert!(matches!(
            parse("$.Records[?@.x > 0]"),
            Err(PathParseError::Filter)
        ));
    }

    #[test]
    fn rejects_recursive_descent() {
        assert!(matches!(
            parse("$..Records[*]"),
            Err(PathParseError::RecursiveDescent)
        ));
    }

    #[test]
    fn wildcard_lowers_to_array_elements_regardless_of_position() {
        // The JSONPath parser collapses `.*` and `[*]` to the same AST
        // node, so both uniformly lower to ArrayElements. `$.*[*]`
        // therefore becomes [ArrayElements, ArrayElements]. At runtime,
        // any wildcard that lands on a non-array value contributes zero
        // rows — matching the walker's permissive missing/wrong-type
        // contract. See the module-level docstring for the tradeoff.
        let segs = parse("$.*[*]").unwrap();
        assert_eq!(segs, vec![Segment::ArrayElements, Segment::ArrayElements]);
    }

    #[test]
    fn rejects_union() {
        assert!(matches!(
            parse(r#"$["a","b"][*]"#),
            Err(PathParseError::Union)
        ));
    }

    #[test]
    fn parses_empty_path_as_no_segments() {
        // Just `$` — emit one Variant per top-level document.
        let segs = parse("$").unwrap();
        assert!(segs.is_empty());
    }

    #[test]
    fn parses_field_only_no_terminal_wildcard() {
        // `$.event` — extract a field, one Variant per document.
        let segs = parse("$.event").unwrap();
        assert_eq!(segs, vec![field("event")]);
    }
}
