//! JSON parsing and typed decoding for the OCPI CDR pricing/generating and CDR/Tariff linting pipeline.
//!
//! # Parsing vs decoding
//!
//! Parsing and decoding are intentionally separated so the linter can emit
//! actionable warnings rather than hard parse errors.
//!
//! **Parsing** ([`parser`], [`parse`]) converts a raw JSON `&str` into an
//! [`Element`] tree. The parser is deliberately lenient about string content:
//! it only verifies structural correctness (balanced delimiters, valid
//! top-level values) and leaves escape sequences and control characters
//! untouched inside [`RawStr`].
//!
//! **Decoding** ([`decode`]) interprets the raw JSON String as a `&str`.
//! Calling [`RawStr::decode_escapes`] validates escape sequences and
//! rejects control characters, returning [`decode::Warning`]
//! values instead of hard errors. This lets the linter pinpoint the exact
//! field, report what is wrong, and suggest a corrected encoding.
//! The `price` and `generate` mods can choose to hard fail on specific `Warning`s.
//!
pub mod decode;
mod parser;
pub(crate) mod walk;
pub mod write;

#[cfg(test)]
pub(crate) mod test;

#[cfg(test)]
mod test_line_col;

#[cfg(test)]
mod test_path;

#[cfg(test)]
mod test_path_matches_glob;

#[cfg(test)]
mod test_source_json;

use std::{
    borrow::{Borrow, Cow},
    collections::{btree_set, BTreeMap, BTreeSet},
    fmt::{self, Write as _},
    rc::Rc,
};

use crate::warning::{Caveat, IntoCaveat, Set, Verdict};
use crate::{
    string,
    warning::{self, CaveatDeferred},
};

pub(crate) use parser::parse;
pub use parser::{Error, ErrorKind as ParseErrorKind};

/// Parse a raw JSON `&str` into a [`Document`] and require the root value to be a JSON object.
///
/// The input size is gated by [`string::ReasonableLen`]; oversized input returns
/// [`ParseError::SizeExceedsMax`].
pub fn parse_object(json: &str) -> Result<Document<'_>, ParseError> {
    let json = string::ReasonableLen::new(json).map_err(|_e| ParseError::SizeExceedsMax)?;
    let doc = parse(json).map_err(ParseError::Json)?;

    if !doc.root().is_object() {
        return Err(ParseError::ShouldBeAnObject);
    }

    Ok(doc)
}

#[derive(Debug)]
pub enum ParseError {
    /// The JSON parser was unable to parse the JSON str.
    Json(Error),

    /// The OCPI object should be a JSON object.
    ShouldBeAnObject,

    /// The size of the input `str` exceeds the maximum deemed reasonable.
    SizeExceedsMax,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Json(error) => write!(f, "{error}"),
            Self::ShouldBeAnObject => f.write_str("The CDR should be an object."),
            Self::SizeExceedsMax => write!(
                f,
                "The input `&str` exceeds the reasonable maximum `{} MB`.",
                string::ReasonableLen::FACTOR
            ),
        }
    }
}

impl std::error::Error for ParseError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self {
            ParseError::Json(err) => Some(err),
            ParseError::ShouldBeAnObject | ParseError::SizeExceedsMax => None,
        }
    }
}

/// Return the `json::Element` If the given field exists(`Some`).
/// Otherwise, add a `Warning::FieldRequired` to the set of `Warning`s and return them as an `Err`.
#[doc(hidden)]
#[macro_export]
macro_rules! required_field_or_bail {
    ($elem:expr, $fields:expr, $field_name:literal, $warnings:expr) => {
        match $fields.get($field_name) {
            Some(field_elem) => field_elem,
            None => {
                return $warnings.bail(
                    $elem,
                    Warning::FieldRequired {
                        field_name: $field_name.into(),
                    },
                );
            }
        }
    };
}

/// Return the `json::Element` If the given field exists(`Some`).
/// Otherwise, add a `Warning::FieldRequired` to the set of `Warning`s and return them as an `Err`.
#[doc(hidden)]
#[macro_export]
macro_rules! required_field {
    ($elem:expr, $fields:expr, $field_name:literal, $warnings:expr) => {{
        let field = $fields.get($field_name);

        if field.is_none() {
            $warnings.insert(
                $elem,
                Warning::FieldRequired {
                    field_name: $field_name.into(),
                },
            );
        }

        field
    }};
}

/// Return the `Field`s of the given `json::Element` if it's a JSON object.
/// Otherwise, add a `Warning::FieldInvalidType` to the set of `Warning`s and return then as an `Err`.
#[doc(hidden)]
#[macro_export]
macro_rules! expect_object_or_bail {
    ($elem:expr, $warnings:expr) => {
        match $elem.as_object_fields() {
            Some(fields) => fields,
            None => {
                return $warnings.bail(
                    $elem,
                    Warning::FieldInvalidType {
                        expected_type: json::ValueKind::Object,
                    },
                );
            }
        }
    };
}

/// Return the `json::Element`s of the given `json::Element` if it's a JSON array.
/// Otherwise, add a `Warning::FieldInvalidType` to the set of `Warning`s and return then as an `Err`.
#[doc(hidden)]
#[macro_export]
macro_rules! expect_array_or_bail {
    ($elem:expr, $warnings:expr) => {
        match $elem.as_array() {
            Some(fields) => fields,
            None => {
                return $warnings.bail(
                    $elem,
                    Warning::FieldInvalidType {
                        expected_type: json::ValueKind::Array,
                    },
                );
            }
        }
    };
}

/// Get a field by name and fail if it doesn't exist.
///
/// Convert the value of the field to the `$target` type.
#[doc(hidden)]
#[macro_export]
macro_rules! parse_required_or_bail {
    ($elem:expr, $fields:expr, $elem_name:literal, $target:ty, $warnings:expr) => {{
        #[expect(clippy::allow_attributes, reason = "The allow attribute is needed here as the callers scope determines if the imports are used or not")]
        #[allow(
            unused,
            reason = "The macro uses the import but maybe the outside scope does too."
        )]
        use $crate::json::FromJson;
        use $crate::warning::GatherWarnings as _;

        let elem = $crate::required_field_or_bail!($elem, $fields, $elem_name, $warnings);
        <$target as FromJson>::from_json(elem)?.gather_warnings_into(&mut $warnings)
    }};
}

/// Get a field by name and return `None` if it doesn't exist.
///
/// Convert the value of the field to the `$target` type.
#[doc(hidden)]
#[macro_export]
macro_rules! parse_required {
    ($elem:expr, $fields:expr, $elem_name:literal, $target:ty, $warnings:expr) => {{
        #[expect(
            clippy::allow_attributes,
            reason = "The allow attribute is needed here as the callers scope determines if the imports are used or not"
        )]
        #[allow(
            unused,
            reason = "The macro uses the import but maybe the outside scope does too."
        )]
        use $crate::json::FromJson;
        use $crate::warning::GatherWarnings as _;

        let elem = $crate::required_field!($elem, $fields, $elem_name, $warnings);

        if let Some(elem) = elem {
            let value =
                <$target as FromJson>::from_json(elem)?.gather_warnings_into(&mut $warnings);
            Some(value)
        } else {
            None
        }
    }};
}

/// Get an optional field by name and convert the field value to the `$target` type using the
/// blanket impl `Option<$target>` that can handle `null` JSON values.
#[doc(hidden)]
#[macro_export]
macro_rules! parse_nullable_or_bail {
    ($fields:expr, $elem_name:literal, $target:ty, $warnings:expr) => {{
        #[expect(
            clippy::allow_attributes,
            reason = "The allow attribute is needed here as the callers scope determines if the imports are used or not"
        )]
        #[allow(
            unused,
            reason = "The macro uses the import but maybe the outside scope does too."
        )]
        use $crate::json::FromJson as _;
        use $crate::warning::GatherWarnings as _;

        match $fields.get($elem_name) {
            Some(elem) => Option::<$target>::from_json(elem)?.gather_warnings_into(&mut $warnings),
            None => None,
        }
    }};
}

/// The output of [`parse`]: the element tree with path resolution embedded in each element.
#[derive(Clone, Debug)]
pub struct Document<'buf> {
    /// Shared inner state; also held by every element in the tree.
    inner: Rc<DocumentInner<'buf>>,
    /// Root element of the parsed tree.
    root: Element<'buf>,
}

impl<'buf> Document<'buf> {
    /// Returns the source JSON string this document was parsed from.
    pub fn source(&self) -> &'buf str {
        self.inner.source
    }

    /// Returns the root element of this document.
    pub fn root(&self) -> &Element<'buf> {
        &self.root
    }
}

/// A JSON [`Element`] with identity, source span, and value.
///
/// Each element carries a shared reference to the document it was parsed from,
/// so [`Element::path()`] can resolve its path.
#[derive(Clone, Debug)]
pub struct Element<'buf> {
    /// Shared document state.
    doc: Rc<DocumentInner<'buf>>,
    /// Unique identifier within the document; sequentially assigned depth-first.
    id: ElemId,
    /// Byte range of the value only; use for replacement edits.
    span: Span,
    /// End of the value plus any trailing comma and whitespace; use for removal edits.
    /// Equal to `span.end` when there is no trailing comma (root element, or last sibling).
    full_span_end: u32,
    /// Parsed value, borrowing from the source `&str`.
    value: Value<'buf>,
}

impl PartialEq for Element<'_> {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
            && self.span == other.span
            && self.full_span_end == other.full_span_end
            && self.value == other.value
    }
}

impl Eq for Element<'_> {}

impl<'buf> Element<'buf> {
    pub fn id(&self) -> ElemId {
        self.id
    }

    pub fn span(&self) -> Span {
        self.span
    }

    /// Returns the span covering the value plus any trailing comma and whitespace.
    ///
    /// Choose the removal span based on the element's position in its parent:
    ///
    /// | Case | Span to erase |
    /// |---|---|
    /// | Replace value | [`Element::span`] |
    /// | Remove non-last item | `element.full_span()` |
    /// | Remove last item (siblings exist) | `siblings[i-1].span().end .. element.span().end` |
    /// | Remove only item | `element.span()` |
    ///
    /// When there is no trailing comma (root element or last sibling),
    /// `full_span() == span()`. Erasing `full_span` of a last item leaves a
    /// dangling comma on the previous sibling; use the predecessor's `span().end`
    /// as the start of the removal range instead.
    pub fn full_span(&self) -> Span {
        Span {
            start: self.span.start,
            end: self.full_span_end,
        }
    }

    pub fn value(&self) -> &Value<'buf> {
        &self.value
    }

    /// Returns the RFC 9535 path to this element.
    ///
    /// NOTE: The `Path` is constructed anew every time this functions is called.
    pub fn path(&self) -> Path {
        self.doc.paths.path_of(self)
    }

    /// Returns the slice of the source JSON that this element spans.
    #[expect(
        clippy::string_slice,
        reason = "spans are produced by the parser from the same source, so slices are always valid"
    )]
    #[expect(
        clippy::as_conversions,
        reason = "The index is guaranteed within bounds by the parser"
    )]
    pub fn source_json_value(&self) -> &'buf str {
        &self.doc.source[self.span.start as usize..self.span.end as usize]
    }

    /// The full source string; all element spans are relative to this.
    pub fn source(&self) -> &'buf str {
        self.doc.source
    }

    /// Return the location that this element begins at.
    #[expect(
        clippy::string_slice,
        reason = "spans are produced by the parser from the same source, so slices are always valid"
    )]
    #[expect(
        clippy::as_conversions,
        reason = "The index is guaranteed within bounds by the parser"
    )]
    pub fn location(&self) -> Location {
        let source = self.doc.source;

        // Slice up to the start of the span to calculate line and col numbers.
        let lead_in = &source[..self.span.start as usize];
        line_col(lead_in)
    }

    /// Return the inner `Value` by ref.
    pub fn as_value(&self) -> &Value<'buf> {
        &self.value
    }

    /// Return `Some(&str)` if the `Value` is a `String`.
    pub fn to_raw_str(&self) -> Option<RawStr<'buf>> {
        self.value.to_raw_str()
    }

    /// Return `Some(&[Field])` if the `Value` is a `Object`.
    pub fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
        self.value.as_object_fields()
    }

    pub fn as_array(&self) -> Option<&[Element<'buf>]> {
        self.value.as_array()
    }

    pub fn as_number_str(&self) -> Option<&str> {
        self.value.as_number()
    }

    /// Return true if the `Element`s `Value` is null.
    pub fn is_null(&self) -> bool {
        self.value.is_null()
    }

    /// Return true if the `Element`s `Value` is an object.
    pub fn is_object(&self) -> bool {
        self.value.is_object()
    }

    /// Return true if the `Element`s `Value` is an array.
    pub fn is_array(&self) -> bool {
        self.value.is_array()
    }
}

/// A JSON value that borrows its content from the source JSON `&str`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Value<'buf> {
    /// JSON `null` literal.
    Null,
    /// JSON `true` literal.
    True,
    /// JSON `false` literal.
    False,
    /// String content with quotes removed; escape sequences are not decoded.
    String(RawStr<'buf>),
    /// Raw number text; not guaranteed to fit any specific numeric type.
    Number(&'buf str),
    /// Ordered list of child elements.
    Array(Vec<Element<'buf>>),
    /// Ordered list of key-value fields.
    Object(Vec<Field<'buf>>),
}

impl fmt::Display for Value<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Null => write!(f, "null"),
            Self::True => write!(f, "true"),
            Self::False => write!(f, "false"),
            Self::String(s) => write!(f, "{}", s.as_unescaped_str()),
            Self::Number(s) => write!(f, "{s}"),
            Self::Array(..) => f.write_str("[...]"),
            Self::Object(..) => f.write_str("{...}"),
        }
    }
}

/// Byte range of a JSON token within the source string.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
pub struct Span {
    /// Byte offset of the first byte of the token.
    pub start: u32,
    /// Byte offset one past the last byte of the token.
    pub end: u32,
}

impl Span {
    fn new(start: u32, end: u32) -> Self {
        Self { start, end }
    }
}

/// A file location expressed as line and column.
#[derive(Clone, Debug)]
pub struct Location {
    /// The line index is 0 based.
    pub line: u32,

    /// The col index is 0 based.
    pub col: u32,
}

impl From<(u32, u32)> for Location {
    fn from(value: (u32, u32)) -> Self {
        Self {
            line: value.0,
            col: value.1,
        }
    }
}

impl From<Location> for (u32, u32) {
    fn from(value: Location) -> Self {
        (value.line, value.col)
    }
}

impl PartialEq<(u32, u32)> for Location {
    fn eq(&self, other: &(u32, u32)) -> bool {
        self.line == other.0 && self.col == other.1
    }
}

impl fmt::Display for Location {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}:{}", self.line, self.col)
    }
}

/// Return the line and column indices of the end of the slice.
///
/// The line and column indices are zero based.
pub fn line_col(s: &str) -> Location {
    let mut chars = s.chars().rev();
    let mut line = 0_u32;
    let mut col = 0_u32;

    // The col only needs to be calculated on the final line so we iterate from the last char
    // back to the start of the line and then only continue to count the lines after that.
    //
    // This is less work than continuously counting chars from the front of the slice.
    for c in chars.by_ref() {
        // If the `&str` is multiline, we count the line and stop accumulating the col count too.
        if c == '\n' {
            let Some(n) = line.checked_add(1) else {
                break;
            };
            line = n;
            break;
        }
        let Some(n) = col.checked_add(1) else {
            break;
        };
        col = n;
    }

    // The col is now known, continue to the start of the str counting newlines as we go.
    for c in chars {
        if c == '\n' {
            let Some(n) = line.checked_add(1) else {
                break;
            };
            line = n;
        }
    }

    Location { line, col }
}

/// Unique sequential index of a JSON [`Element`] within a document.
///
/// Assigned depth-first by the parser. `Parser::alloc_id` uses `checked_add`
/// so the counter never wraps silently — overflow becomes `ParseError::TooLarge`.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub struct ElemId(usize);

/// Records how one element was reached from its parent.
#[derive(Debug)]
enum PathEntry<'buf> {
    /// The root element; has no parent.
    Root,
    /// An object field; the element's path ends with a key segment.
    Field {
        /// Id of the parent object element.
        parent: ElemId,
        /// Key text borrowed from the source JSON, without surrounding quotes.
        key: RawStr<'buf>,
    },
    /// An array item; the element's path ends with an index segment.
    Item {
        /// Id of the parent array element.
        parent: ElemId,
        /// Zero-based position within the parent array.
        index: u32,
    },
}

/// Shared state carried by every [`Element`] produced from the same parse.
///
/// Wrapped in [`Rc`] so that each element can resolve its own path without
/// holding a live reference to the original [`Document`].
#[derive(Debug)]
struct DocumentInner<'buf> {
    /// The full source string; all element spans are relative to this.
    source: &'buf str,
    /// Parent-pointer table used to reconstruct element paths.
    paths: PathTable<'buf>,
}

/// A table recording the parentage of every [`Element`] produced by a parse.
#[derive(Debug, Default)]
struct PathTable<'buf> {
    /// The `entries` `Vec` is indexed using `ElemId`.
    entries: Vec<PathEntry<'buf>>,
}

impl<'buf> PathTable<'buf> {
    fn push(&mut self, entry: PathEntry<'buf>) {
        self.entries.push(entry);
    }

    /// Build the full RFC 9535 path to `element` by walking the parent-pointer chain.
    ///
    /// Cost is O(depth) per path construction.
    ///
    /// NOTE: The `'buf` lifetime shared by `element` and `self` prevents cross-document
    /// misuse at compile time for documents with distinct source lifetimes.
    ///
    /// # Panics
    ///
    /// Panics if `element` was not produced by the same parse that created this table.
    fn path_of(&self, element: &Element<'buf>) -> Path {
        let mut entries: Vec<&PathEntry<'buf>> = Vec::new();
        let mut elem_id = element.id;

        // Walk back up the path chain.
        loop {
            let entry = self
                .entries
                .get(elem_id.0)
                .expect("ElemId always refers to a valid PathEntry");

            match entry {
                PathEntry::Root => {
                    entries.push(entry);
                    break;
                }
                PathEntry::Field { parent, key: _ } | PathEntry::Item { parent, index: _ } => {
                    entries.push(entry);
                    elem_id = *parent;
                }
            }
        }

        // Reverse the elements so we can walk forward along the chain.
        entries.reverse();

        let mut out = String::with_capacity(30);

        for entry in entries {
            let res = match entry {
                PathEntry::Root => write!(out, "$"),
                PathEntry::Field { parent: _, key } => {
                    write!(out, ".{}", key.as_unescaped_str())
                }
                PathEntry::Item { parent: _, index } => {
                    // Array indices use bracket notation per RFC 9535, e.g.
                    // `$.elements[0]`, rather than a dotted `.0` segment.
                    write!(out, "[{index}]")
                }
            };

            res.expect("Writing to a String can only fail if the system runs out of heap memory");
        }

        Path(out)
    }
}

#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
pub struct Path(String);

impl Path {
    pub fn into_string(self) -> String {
        self.0
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Iterate the [`Component`]s of this path in order, skipping the `$` root.
    ///
    /// For example `$.elements[0].id` yields `Member("elements")`,
    /// `Index("0")`, `Member("id")`. The root path `$` yields nothing.
    pub fn components(&self) -> Components<'_> {
        Components::over(&self.0)
    }
}

/// A single [`Path`] component: an object member or an array index.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Component<'a> {
    /// An object member, the `name` in a `.name` segment.
    Member(&'a str),
    /// An array index, the decimal digits in a `[n]` segment.
    Index(&'a str),
}

/// Iterator over the [`Component`]s of a [`Path`]; see [`Path::components`].
#[derive(Clone, Debug)]
pub struct Components<'a> {
    rest: &'a str,
}

impl<'a> Components<'a> {
    /// Iterate the components of a raw `JSONPath` string, skipping a leading `$`.
    pub(crate) fn over(path: &'a str) -> Self {
        Self {
            rest: path.strip_prefix('$').unwrap_or(path),
        }
    }
}

impl<'a> Iterator for Components<'a> {
    type Item = Component<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(after) = self.rest.strip_prefix('.') {
            // `.name`: read up to the next segment delimiter.
            let end = after.find(['.', '[']).unwrap_or(after.len());
            let (name, tail) = after.split_at(end);
            self.rest = tail;
            Some(Component::Member(name))
        } else if let Some(after) = self.rest.strip_prefix('[') {
            // `[index]`: read up to the closing bracket.
            let end = after.find(']').unwrap_or(after.len());
            let (index, tail) = after.split_at(end);
            self.rest = tail.strip_prefix(']').unwrap_or(tail);
            Some(Component::Index(index))
        } else {
            // Empty (root) or malformed input: stop iterating.
            None
        }
    }
}

impl PartialEq<str> for Path {
    fn eq(&self, other: &str) -> bool {
        self.0 == other
    }
}

impl PartialEq<&str> for Path {
    fn eq(&self, other: &&str) -> bool {
        self.0 == *other
    }
}

impl fmt::Debug for Path {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl fmt::Display for Path {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

/// Set of path with a common issue.
#[derive(Debug)]
pub struct PathSet<'set>(BTreeSet<&'set Path>);

impl<'set> PathSet<'set> {
    pub(crate) fn new(paths: BTreeSet<&'set Path>) -> Self {
        Self(paths)
    }

    /// Return the field paths as a `Vec` of `String`s.
    pub fn to_strings(&self) -> Vec<String> {
        self.0.iter().map(ToString::to_string).collect()
    }

    /// Return the field paths as a `Vec` of `String`s.
    pub fn into_strings(self) -> Vec<String> {
        self.0.into_iter().map(ToString::to_string).collect()
    }

    /// Return true if the list of unexpected fields is empty.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Return the number of unexpected fields.
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Return an Iterator over the unexpected fields.
    pub fn iter(&self) -> btree_set::Iter<'_, &Path> {
        self.0.iter()
    }
}

impl<'set> IntoIterator for PathSet<'set> {
    type Item = &'set Path;

    type IntoIter = btree_set::IntoIter<&'set Path>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl<'a, 'set> IntoIterator for &'a PathSet<'set> {
    type Item = &'a &'set Path;

    type IntoIter = btree_set::Iter<'a, &'set Path>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ValueKind {
    Null,
    Bool,
    Number,
    String,
    Array,
    Object,
}

impl fmt::Display for ValueKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ValueKind::Null => write!(f, "null"),
            ValueKind::Bool => write!(f, "bool"),
            ValueKind::Number => write!(f, "number"),
            ValueKind::String => write!(f, "string"),
            ValueKind::Array => write!(f, "array"),
            ValueKind::Object => write!(f, "object"),
        }
    }
}

impl<'buf> Value<'buf> {
    pub fn kind(&self) -> ValueKind {
        match self {
            Value::Null => ValueKind::Null,
            Value::True | Value::False => ValueKind::Bool,
            Value::String(_) => ValueKind::String,
            Value::Number(_) => ValueKind::Number,
            Value::Array(_) => ValueKind::Array,
            Value::Object(_) => ValueKind::Object,
        }
    }

    pub fn is_null(&self) -> bool {
        matches!(self, Value::Null)
    }

    /// Return true if the `Value` is an array.
    pub fn is_array(&self) -> bool {
        matches!(self, Value::Array(..))
    }

    /// Return true if the `Value` is an object.
    pub fn is_object(&self) -> bool {
        matches!(self, Value::Object(..))
    }

    /// Return true if the `Value` can't contain child elements.
    pub fn is_scalar(&self) -> bool {
        matches!(
            self,
            Value::Null | Value::True | Value::False | Value::String(_) | Value::Number(_)
        )
    }

    pub fn as_array(&self) -> Option<&[Element<'buf>]> {
        if let Value::Array(elems) = self {
            Some(elems)
        } else {
            None
        }
    }

    pub fn as_number(&self) -> Option<&str> {
        if let Value::Number(s) = self {
            Some(s)
        } else {
            None
        }
    }

    /// Return `Some(&str)` if the `Value` is a `String`.
    pub fn to_raw_str(&self) -> Option<RawStr<'buf>> {
        if let Value::String(s) = self {
            Some(*s)
        } else {
            None
        }
    }

    /// Return `Some(&[Field])` if the `Value` is a `Object`.
    pub fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
        if let Value::Object(fields) = self {
            Some(fields)
        } else {
            None
        }
    }
}

/// An object field; upholds the invariant that the inner [`Element`]'s path ends with a key.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Field<'buf> {
    /// Span of the key token, including surrounding `"` delimiters.
    key_span: Span,
    /// The value element; its path ends with the key from `key_span`.
    element: Element<'buf>,
}

impl<'buf> Field<'buf> {
    /// Consume the `Field` and return the inner `Element`.
    pub fn into_element(self) -> Element<'buf> {
        self.element
    }

    /// Return the inner `Element`.
    pub fn element(&self) -> &Element<'buf> {
        &self.element
    }

    pub fn key_span(&self) -> Span {
        self.key_span
    }

    /// Returns the span covering `"key": value` plus any trailing comma and whitespace.
    ///
    /// Choose the removal span based on the field's position in its parent:
    ///
    /// | Case | Span to erase |
    /// |---|---|
    /// | Remove non-last field | `field.full_span()` |
    /// | Remove last field (siblings exist) | `fields[i-1].element().span().end .. field.element().span().end` |
    /// | Remove only field | `field.element().span()` |
    ///
    /// When there is no trailing comma (last field), `full_span()` covers
    /// `"key": value` only. Erasing it leaves a dangling comma on the previous
    /// field; use the predecessor's `element().span().end` as the start instead.
    pub fn full_span(&self) -> Span {
        Span {
            start: self.key_span.start,
            end: self.element.full_span_end,
        }
    }

    /// Returns the key text without surrounding `"` delimiters.
    #[expect(
        clippy::arithmetic_side_effects,
        reason = "key_span always spans a quoted string, so +1/-1 to strip the surrounding quote bytes is safe"
    )]
    #[expect(
        clippy::string_slice,
        reason = "key_span is produced by the parser from the same source; +1/-1 strips the ASCII quote bytes"
    )]
    #[expect(
        clippy::as_conversions,
        reason = "The index is guaranteed within bounds by the parser"
    )]
    pub fn key(&self) -> RawStr<'buf> {
        let src = self.element.source();
        let s = &src[self.key_span.start as usize + 1..self.key_span.end as usize - 1];
        RawStr::from_str(s)
    }

    /// Returns the slice of the source JSON spanning `"key": value`.
    #[expect(
        clippy::string_slice,
        reason = "spans are produced by the parser from the same source, so slices are always valid"
    )]
    #[expect(
        clippy::as_conversions,
        reason = "The index is guaranteed within bounds by the parser"
    )]
    pub fn source_json(&self) -> &'buf str {
        let src = self.element.source();
        &src[self.key_span.start as usize..self.element.span.end as usize]
    }
}

pub type RawMap<'buf> = BTreeMap<RawStr<'buf>, Element<'buf>>;
pub type RawRefMap<'a, 'buf> = BTreeMap<RawStr<'buf>, &'a Element<'buf>>;

#[expect(dead_code, reason = "pending use in `tariff::lint`")]
pub(crate) trait FieldsIntoExt<'buf> {
    fn into_map(self) -> RawMap<'buf>;
}

pub(crate) trait FieldsAsExt<'buf> {
    fn as_raw_map(&self) -> RawRefMap<'_, 'buf>;
    fn find_field(&self, key: &str) -> Option<&Field<'buf>>;
}

impl<'buf> FieldsIntoExt<'buf> for Vec<Field<'buf>> {
    fn into_map(self) -> RawMap<'buf> {
        self.into_iter()
            .map(|field| (field.key(), field.into_element()))
            .collect()
    }
}

impl<'buf> FieldsAsExt<'buf> for Vec<Field<'buf>> {
    fn as_raw_map(&self) -> RawRefMap<'_, 'buf> {
        self.iter()
            .map(|field| (field.key(), field.element()))
            .collect()
    }

    fn find_field(&self, key: &str) -> Option<&Field<'buf>> {
        self.iter()
            .find(|field| field.key().eq_escape_aware(key).ok().unwrap_or(false))
    }
}

impl<'buf> FieldsAsExt<'buf> for [Field<'buf>] {
    fn as_raw_map(&self) -> RawRefMap<'_, 'buf> {
        self.iter()
            .map(|field| (field.key(), field.element()))
            .collect()
    }

    fn find_field(&self, key: &str) -> Option<&Field<'buf>> {
        self.iter()
            .find(|field| field.key().eq_escape_aware(key).ok().unwrap_or(false))
    }
}

/// A `&str` with surrounding quotes removed; escape sequences are not decoded.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct RawStr<'buf>(&'buf str);

/// Impl `Borrow` so `RawStr` plays well with hashed collections.
impl Borrow<str> for RawStr<'_> {
    fn borrow(&self) -> &str {
        self.0
    }
}

/// Impl `Borrow` so `RawStr` plays well with hashed collections.
impl Borrow<str> for &RawStr<'_> {
    fn borrow(&self) -> &str {
        self.0
    }
}

impl<'buf> RawStr<'buf> {
    fn from_str(source: &'buf str) -> Self {
        Self(source)
    }

    /// Compare `other` against this raw `&str`, decoding any JSON escape
    /// sequences in the `&str` on the fly without allocating.
    ///
    /// Returns `Ok(true)`/`Ok(false)` for the comparison, or `Err` if the key
    /// contains a decoding problem (an invalid escape or a control character) at
    /// or before the first differing character.
    pub fn eq_escape_aware(&self, other: &str) -> Result<bool, decode::Warning> {
        decode::eq(self.0, other)
    }

    /// Compare this raw `&str` against a list of `&str`s, decoding any JSON escape
    /// sequences in the `&str` on the fly without allocating.
    ///
    /// Returns true if any of the `other` `&str`s match self.
    pub fn eq_any_escape_aware(&self, other: &[&str]) -> bool {
        other
            .iter()
            .any(|s| decode::eq(self.0, s).ok().unwrap_or(false))
    }

    /// Like [`RawStr::eq_any_escape_aware`], but compares ASCII letters case-insensitively.
    pub fn eq_any_escape_aware_ignore_ascii_case(&self, other: &[&str]) -> bool {
        other.iter().any(|s| {
            decode::eq_ignore_ascii_case(self.0, s)
                .ok()
                .unwrap_or(false)
        })
    }

    /// Return the raw unescaped `&str`.
    pub fn as_unescaped_str(&self) -> &'buf str {
        self.0
    }

    /// Return the `&str` with all escapes decoded.
    pub fn decode_escapes(&self) -> CaveatDeferred<Cow<'_, str>, decode::Warning> {
        decode::from_raw(self.0)
    }

    /// Return a `&str` marked as either having escapes or not.
    pub fn has_escapes(&self, elem: &Element<'buf>) -> Caveat<PendingStr<'buf>, decode::Warning> {
        decode::analyze(self.0, elem)
    }
}

/// Marks a `&str` as having escapes or not.
pub enum PendingStr<'buf> {
    /// The `&str` has no escapes and can be used as is.
    NoEscapes(&'buf str),

    /// The `&str` has escape chars and needs to be unescaped before trying to parse into another form.
    HasEscapes(EscapeStr<'buf>),
}

/// A `&str` with escape chars.
pub struct EscapeStr<'buf>(&'buf str);

impl<'buf> EscapeStr<'buf> {
    pub fn decode_escapes(&self) -> CaveatDeferred<Cow<'buf, str>, decode::Warning> {
        decode::from_raw(self.0)
    }

    /// Consume the `EscapeStr` and return the raw bytes as a str.
    pub fn into_raw(self) -> &'buf str {
        self.0
    }
}

/// A type that can be created from a parsed [`Element`].
///
/// Implementors return a [`Verdict`]: either the converted value with zero or
/// more nonfatal warnings, or a fatal [`warning::ErrorSet`] that aborted creation.
pub(crate) trait FromJson<'buf>: Sized {
    /// Warning type emitted when creation issues are detected.
    type Warning: warning::Warning;

    /// Convert `elem` to `Self`, collecting any nonfatal issues as warnings.
    fn from_json(elem: &Element<'buf>) -> Verdict<Self, Self::Warning>;
}

/// Implement a `null` aware variant of all types that implement `FromJson`.
///
/// If the value of the `json::Element` is `null` then return a `None`.
impl<'buf, T> FromJson<'buf> for Option<T>
where
    T: FromJson<'buf> + IntoCaveat,
{
    type Warning = T::Warning;

    fn from_json(elem: &Element<'buf>) -> Verdict<Self, Self::Warning> {
        let value = elem.as_value();

        if value.is_null() {
            Ok(None.into_caveat(Set::new()))
        } else {
            let v = T::from_json(elem)?;
            Ok(v.map(Some))
        }
    }
}
