pub mod v211;
pub mod v221;

mod build;
mod leaf;

#[cfg(test)]
mod tests;

use std::collections::BTreeSet;

use crate::{
    json,
    warning::{self, IntoCaveat as _},
    Caveat,
};

/// Describes the expected structure of a JSON value.
#[derive(Clone, Copy)]
enum Schema {
    /// A scalar value of a known JSON kind (see [`Scalar`]).
    Scalar(Scalar),
    /// A JSON object with a known set of fields.
    Object(&'static Object),
    /// A homogeneous JSON array; each element validated against `item`, with a
    /// minimum element count given by `cardinality`.
    Array {
        item: &'static Schema,
        cardinality: Cardinality,
    },
}

/// The minimum number of elements an array must contain.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cardinality {
    /// An array with zero or more element is expected. An empty array is valid.
    ZeroOrMore,
    /// An array with one or more elements is expected. An empty array is a violation.
    OneOrMore,
}

impl std::fmt::Display for Cardinality {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Cardinality::ZeroOrMore => f.write_str("zero or more"),
            Cardinality::OneOrMore => f.write_str("one or more"),
        }
    }
}

/// The expected JSON kind of a scalar field.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Scalar {
    /// A JSON string with no length bound. Covers OCPI `DateTime`, `date`, and
    /// `time` (which are format-constrained, not length-constrained) and any
    /// string the spec defines without a declared length. Strings the spec
    /// declares as `string(n)` / `CiString(n)` use [`Scalar::StringMax`]; enum
    /// types use [`Scalar::Enum`].
    String,
    /// A JSON string with a maximum character length, per the OCPI `string(n)`
    /// or `CiString(n)` declaration. The value is checked to be a string and
    /// then its decoded character count is compared against the length bound.
    StringMax(usize),
    /// A JSON string constrained to a fixed set of enum variants. Every OCPI
    /// enum serializes as a string; the slice holds the permitted spellings as
    /// written in the spec (uppercase). The value is matched case-insensitively.
    Enum(&'static [&'static str]),
    /// A JSON number. Covers OCPI `number`, `int`, and `decimal`.
    Number,
    /// A JSON boolean.
    Boolean,
    /// Any value; the JSON kind is not constrained. Used for fields whose value
    /// is a nested object or array this schema layer deliberately does not
    /// model (e.g. `BusinessDetails`, `Hours`).
    Any,
}

/// The integrity of a field. Building an IR value is infallible.
/// Every field ends in one of these states rather than aborting the build.
/// The detail behind `Err` (the kind mismatch, the invalid value) is recorded
/// in the accompanying [`warning::Set`].
///
/// A field the OCPI spec defines as optional is typed `Integrity<Option<T>>`: an
/// absent optional field is `Ok(None)`, not [`Integrity::Missing`].
/// [`Integrity::Missing`] therefore only ever describes an absent (or `null`)
/// *required* field, which is also reported as a [`Warning::MissingField`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Integrity<T> {
    /// The field was present and built successfully.
    Ok(T),
    /// A required field was absent or `null`. This is also reported as a
    /// [`Warning::MissingField`].
    Missing,
    /// The field was present but could not be built (wrong JSON kind, or an
    /// otherwise invalid value).
    Err,
}

// A manual impl, rather than `#[derive(Default)]`, so that `Integrity<T>::default()`
// holds for any `T`. The derive would add an unwanted `T: Default` bound (the IR structs
// store fields like `Integrity<Str>`, where `Str` is not `Default`).
#[allow(
    clippy::derivable_impls,
    reason = "derive would add an unwanted T: Default bound"
)]
impl<T> Default for Integrity<T> {
    fn default() -> Self {
        Self::Missing
    }
}

impl<T> Integrity<T> {
    /// Map the contained value, leaving `Missing`/`Err` unchanged.
    pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Integrity<U> {
        match self {
            Integrity::Ok(value) => Integrity::Ok(op(value)),
            Integrity::Missing => Integrity::Missing,
            Integrity::Err => Integrity::Err,
        }
    }

    /// Borrow the contained value.
    pub fn as_ref(&self) -> Integrity<&T> {
        match self {
            Integrity::Ok(value) => Integrity::Ok(value),
            Integrity::Missing => Integrity::Missing,
            Integrity::Err => Integrity::Err,
        }
    }

    /// The contained value, if `Ok`.
    pub fn ok(self) -> Option<T> {
        match self {
            Integrity::Ok(value) => Some(value),
            Integrity::Missing | Integrity::Err => None,
        }
    }
}

/// Identifies which schema intermediate-representation (IR) value an [`Object`]
/// should be mapped to during the [`walk`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BuilderKind {
    /// The object is validated for warnings but not built into any IR value.
    Ignore,
    V221Tariff,
    V221Element,
    V221PriceComponent,
    V221Restrictions,
    V221Price,
    V221Cdr,
    V221ChargingPeriod,
    V221CdrDimension,
    V211Tariff,
    V211Element,
    V211PriceComponent,
    V211Restrictions,
    V211Cdr,
    V211ChargingPeriod,
    V211CdrDimension,
}

/// The expected fields of a JSON object.
#[derive(Clone, Copy)]
struct Object {
    fields: &'static [Field],
    /// The IR value this object is built into during the [`walk`].
    kind: BuilderKind,
}

/// One field expected in a JSON object.
#[derive(Clone, Copy)]
struct Field {
    /// JSON key name.
    ///
    /// This value is hardcoded and will never contain escapes.
    name: &'static str,
    /// Whether the field must be present.
    presence: Presence,
    /// Expected substructure of the field value.
    schema: Schema,
}

impl Field {
    /// Define a required scalar of the given JSON kind.
    const fn required(name: &'static str, scalar: Scalar) -> Self {
        Self {
            name,
            presence: Presence::Required,
            schema: Schema::Scalar(scalar),
        }
    }

    /// Define a required array (OCPI `+`: present and nonempty).
    const fn required_array(name: &'static str, item: &'static Schema) -> Self {
        Self {
            name,
            presence: Presence::Required,
            schema: Schema::Array {
                item,
                cardinality: Cardinality::OneOrMore,
            },
        }
    }

    /// Define a required object.
    const fn required_object(name: &'static str, schema: &'static Object) -> Self {
        Self {
            name,
            presence: Presence::Required,
            schema: Schema::Object(schema),
        }
    }

    /// Define an optional scalar of the given JSON kind.
    const fn optional(name: &'static str, scalar: Scalar) -> Self {
        Self {
            name,
            presence: Presence::Optional,
            schema: Schema::Scalar(scalar),
        }
    }

    /// Define an optional array (OCPI `*`: may be absent or empty).
    const fn optional_array(name: &'static str, item: &'static Schema) -> Self {
        Self {
            name,
            presence: Presence::Optional,
            schema: Schema::Array {
                item,
                cardinality: Cardinality::ZeroOrMore,
            },
        }
    }

    /// Define an optional object.
    const fn optional_object(name: &'static str, schema: &'static Object) -> Self {
        Self {
            name,
            presence: Presence::Optional,
            schema: Schema::Object(schema),
        }
    }
}

/// Whether a field must be present in its containing object.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Presence {
    /// The schema requires the field. Its absence is a violation (also reported as
    /// [`Warning::MissingField`]).
    Required,
    /// The schema permits the field to be absent.
    Optional,
}

/// A structural problem found while validating a JSON document against a [`Schema`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Warning {
    /// A field present in the JSON that the schema does not list.
    UnexpectedField,
    /// A required field absent from its containing object.
    MissingField {
        /// The field name the schema expected.
        name: &'static str,
    },
    /// A field whose value is JSON `null`. `null` fields can simply be omitted.
    NullField,
    /// A value whose JSON kind does not match the schema.
    TypeMismatch {
        /// The JSON kind the schema expects.
        expected: json::ValueKind,
        /// The JSON kind encountered.
        actual: json::ValueKind,
    },
    /// A string longer than the maximum length the schema permits.
    StringTooLong {
        /// The maximum character length the schema allows.
        max: usize,
        /// The character length actually encountered.
        len: usize,
    },
    /// A string value that is not one of an enum field's permitted variants.
    FieldInvalidValue {
        /// The permitted variant spellings, uppercase as written in the spec.
        expected: &'static [&'static str],
        /// The value encountered, as written in the JSON (escapes not decoded).
        actual: String,
    },
    /// An array holding fewer elements than its declared [`Cardinality`] requires.
    Cardinality {
        /// The cardinality the schema requires.
        expected: Cardinality,
        /// The number of elements actually present.
        len: usize,
    },
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::UnexpectedField => warning::Id::from_static("unexpected_field"),
            Self::MissingField { name } => {
                warning::Id::from_string(format!("missing_field({name})"))
            }
            Self::NullField => warning::Id::from_static("null_field"),
            Self::TypeMismatch { actual, .. } => {
                warning::Id::from_string(format!("invalid_type({actual})"))
            }
            Self::StringTooLong { .. } => warning::Id::from_static("string_too_long"),
            Self::FieldInvalidValue { actual, .. } => {
                warning::Id::from_string(format!("field_invalid_value({actual})"))
            }
            Self::Cardinality { expected, .. } => {
                warning::Id::from_string(format!("cardinality({expected})"))
            }
        }
    }
}

impl std::fmt::Display for Warning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnexpectedField => f.write_str("field is not part of the schema"),
            Self::MissingField { name } => write!(f, "required field `{name}` is missing"),
            Self::NullField => f.write_str(
                "field is `null`. `null` fields have no semantic meaning for OCPI objects",
            ),
            Self::TypeMismatch { expected, actual } => {
                write!(f, "expected {expected} found {actual}")
            }
            Self::StringTooLong { max, len } => {
                write!(
                    f,
                    "string is `{len}` characters, but the maximum allowed is `{max}`"
                )
            }
            Self::FieldInvalidValue { expected, actual } => {
                write!(
                    f,
                    "value `{actual}` is not one of the permitted values: {}",
                    expected.join(", ")
                )
            }
            Self::Cardinality { expected, len } => {
                write!(f, "expected {expected} elements, found {len}")
            }
        }
    }
}

impl warning::Set<Warning> {
    /// Collect the field paths of all [`Warning::UnexpectedField`] warnings into a set of `json::Path`s.
    pub fn unexpected_fields(&self) -> json::PathSet<'_> {
        let mut paths = BTreeSet::new();

        for group in self {
            let (element, group_warnings) = group.to_parts();

            let has_unexpected_field = group_warnings
                .iter()
                .any(|warning| matches!(warning, Warning::UnexpectedField));

            if has_unexpected_field {
                paths.insert(&element.path);
            }
        }

        json::PathSet::new(paths)
    }

    /// Collect the field paths of all [`Warning::MissingField`] warnings into a set of `json::Path`s.
    pub fn missing_fields(&self) -> json::PathSet<'_> {
        let mut paths = BTreeSet::new();

        for group in self {
            let (element, group_warnings) = group.to_parts();

            let has_missing_field = group_warnings
                .iter()
                .any(|warning| matches!(warning, Warning::MissingField { .. }));

            if has_missing_field {
                paths.insert(&element.path);
            }
        }

        json::PathSet::new(paths)
    }

    /// Remove all [`Warning::UnexpectedField`] warnings from the set.
    pub fn remove_unexpected_fields(&mut self) {
        self.retain(|warning| !matches!(warning, Warning::UnexpectedField));
    }

    /// Remove all [`Warning::MissingField`] warnings from the set.
    pub fn remove_missing_fields(&mut self) {
        self.retain(|warning| !matches!(warning, Warning::MissingField { .. }));
    }

    /// Remove all [`Warning::TypeMismatch`] warnings from the set.
    pub fn remove_type_mismatches(&mut self) {
        self.retain(|warning| !matches!(warning, Warning::TypeMismatch { .. }));
    }

    /// Remove all [`Warning::NullField`] warnings from the set.
    pub fn remove_null_fields(&mut self) {
        self.retain(|warning| !matches!(warning, Warning::NullField));
    }

    /// Remove all [`Warning::Cardinality`] warnings from the set.
    pub fn remove_cardinalities(&mut self) {
        self.retain(|warning| !matches!(warning, Warning::Cardinality { .. }));
    }

    /// Remove all [`Warning::StringTooLong`] warnings from the set.
    pub fn remove_string_too_longs(&mut self) {
        self.retain(|warning| !matches!(warning, Warning::StringTooLong { .. }));
    }
}

/// Opaque-subtree marker: a value the schema does not model. The subtree is still
/// walked so nested `null`s are reported.
static ANY: Schema = Schema::Scalar(Scalar::Any);

/// A step in the [`walk`]'s work stack.
enum Step<'a, 'buf> {
    /// Visit a node: record its warnings and build its leaf, or open its
    /// object/array builder.
    Visit {
        elem: &'a json::Element<'buf>,
        schema: &'a Schema,
        slot: Slot,
    },
    /// Finalize the builder on top of the builder stack and route it to its parent.
    Close { slot: Slot },
}

/// Where a built [`build::Node`] attaches within its parent.
#[derive(Clone, Copy)]
enum Slot {
    /// The root value of the walk.
    Root,
    /// A named field of the parent object.
    Field { name: &'static str },
    /// An item of the parent array.
    Item,
    /// A value that is discarded (an unmodeled [`Scalar::Any`] subtree).
    Ignore,
}

/// Validate `doc` against `schema` and build its intermediate representation (IR) in a
/// single pass.
///
/// The returned [`build::Node`] is the value of the root [`Object`]'s [`BuilderKind`].
/// When used through the public API the returned object will be one of the CDR or tariffs
/// root types.
///
/// Building is infallible. Problems with a field emit a [`Warning`] and are stored as
/// an [`Integrity::Err`] or [`Integrity::Missing`] on the IR object's field.
///
/// NOTE: A value whose type is invalid (a type mismatch) or whose key is
/// unexpected is recorded but not descended into. Its substructure cannot
/// be compared to the schema. Opaque [`Scalar::Any`] values are still
/// walked, so nested `null`s are still reported for the inner JSON.
fn walk<'a, 'buf>(
    doc: &'a json::Document<'buf>,
    schema: &'a Schema,
) -> Caveat<build::Node<'buf>, Warning> {
    let mut warnings = warning::Set::new();
    let mut builders: Vec<build::Node<'buf>> = Vec::new();
    let mut root = build::Node::Ignore;

    // Iteration order: an object's own problems are recorded before its descendants'
    // because its fields are scanned (emitting unexpected/missing warnings) when the
    // object is opened, before the field `Visit`s pushed here are popped.
    let mut stack = vec![Step::Visit {
        elem: doc.root(),
        schema,
        slot: Slot::Root,
    }];

    while let Some(step) = stack.pop() {
        match step {
            Step::Visit { elem, schema, slot } => {
                if let json::Value::Null = elem.value() {
                    warnings.insert(elem, Warning::NullField);
                    root.route_to_parent(&mut builders, slot, Integrity::Missing);
                    continue;
                }
                match schema {
                    // An unmodeled subtree: walk children only to report nested nulls.
                    Schema::Scalar(Scalar::Any) => {
                        enqueue_all_children(&mut stack, elem);
                        root.route_to_parent(&mut builders, slot, Integrity::Missing);
                    }
                    Schema::Scalar(scalar) => {
                        let built = check_scalar(&mut warnings, elem, *scalar);
                        root.route_to_parent(&mut builders, slot, built);
                    }
                    Schema::Array { item, cardinality } => {
                        let type_expectation = open_array(
                            &mut stack,
                            &mut builders,
                            &mut warnings,
                            elem,
                            item,
                            *cardinality,
                            slot,
                        );
                        if type_expectation.is_type_invalid() {
                            root.route_to_parent(&mut builders, slot, Integrity::Err);
                        }
                    }
                    Schema::Object(object) => {
                        let type_expectation = open_object(
                            &mut stack,
                            &mut builders,
                            &mut warnings,
                            elem,
                            object,
                            slot,
                        );
                        if type_expectation.is_type_invalid() {
                            root.route_to_parent(&mut builders, slot, Integrity::Err);
                        }
                    }
                }
            }
            Step::Close { slot } => {
                if let Some(node) = builders.pop() {
                    root.route_to_parent(&mut builders, slot, Integrity::Ok(node));
                }
            }
        }
    }

    root.into_caveat(warnings)
}

/// Build the leaf [`build::Node`] for a scalar, recording any kind, length, enum, or
/// string-encoded-number warning. Returns [`Integrity::Err`] for a wrong-kind value.
fn check_scalar<'buf>(
    warnings: &mut warning::Set<Warning>,
    elem: &json::Element<'buf>,
    scalar: Scalar,
) -> Integrity<build::Node<'buf>> {
    let expected = match scalar {
        // Enums serialize as JSON strings; their kind check is the same as a plain
        // string, with the value-membership check applied below.
        Scalar::String | Scalar::StringMax(_) | Scalar::Enum(_) => json::ValueKind::String,
        Scalar::Number => json::ValueKind::Number,
        Scalar::Boolean => json::ValueKind::Bool,
        // `Any` is handled by the caller; never built here.
        Scalar::Any => return Integrity::Missing,
    };

    let actual = elem.value().kind();

    // A `number` may be encoded as a JSON string; that is accepted but flagged below.
    let string_encoded_number =
        expected == json::ValueKind::Number && actual == json::ValueKind::String;
    if actual != expected && !string_encoded_number {
        warnings.insert(elem, Warning::TypeMismatch { expected, actual });
        return Integrity::Err;
    }

    match scalar {
        Scalar::String => Integrity::Ok(build::Node::Str(Str::new(elem.clone()))),
        Scalar::StringMax(max) => {
            if let Some(value) = elem.value().to_raw_str() {
                let len = value.decode_escapes().ignore_warnings().chars().count();
                if len > max {
                    warnings.insert(elem, Warning::StringTooLong { max, len });
                }
            }
            Integrity::Ok(build::Node::Str(Str::new(elem.clone())))
        }
        Scalar::Enum(allowed) => {
            let Some(value) = elem.value().to_raw_str() else {
                return Integrity::Err;
            };
            let canonical = allowed
                .iter()
                .copied()
                .find(|variant| value.eq_any_escape_aware_ignore_ascii_case(&[variant]));
            let Some(canonical) = canonical else {
                warnings.insert(
                    elem,
                    Warning::FieldInvalidValue {
                        expected: allowed,
                        actual: value.as_unescaped_str().to_owned(),
                    },
                );
                return Integrity::Err;
            };
            Integrity::Ok(build::Node::Enum(Enum::new(elem.clone(), canonical)))
        }
        Scalar::Number => {
            // OCPI permits a number to be encoded as a JSON string. The value is accepted
            // either way; the linter can choose to flag the string-encoded form later.
            if string_encoded_number {
                Integrity::Ok(build::Node::Number(Number::StringEncoded(elem.clone())))
            } else {
                Integrity::Ok(build::Node::Number(Number::Number(elem.clone())))
            }
        }
        Scalar::Boolean => Integrity::Ok(build::Node::Bool),
        // Unreachable: handled above.
        Scalar::Any => Integrity::Missing,
    }
}

/// The [`open_array`] and [`open_object`] return whether the type they expected is
/// the type they encountered.
#[derive(Copy, Clone)]
enum TypeExpectation {
    Satisfied,
    Invalid,
}

impl TypeExpectation {
    fn is_type_invalid(self) -> bool {
        matches!(self, Self::Invalid)
    }
}

/// Open an object: push its builder and a [`Step::Close`], then queue its
/// schema-matched fields. Records unexpected and missing-required-field warnings.
/// Returns `false` (and opens nothing) if `elem` is not a JSON object.
fn open_object<'a, 'buf>(
    stack: &mut Vec<Step<'a, 'buf>>,
    builders: &mut Vec<build::Node<'buf>>,
    warnings: &mut warning::Set<Warning>,
    elem: &'a json::Element<'buf>,
    object: &'a Object,
    slot: Slot,
) -> TypeExpectation {
    // `fields` are sorted alphabetically by `Field::name` so the `binary_search_by_key`
    // below is valid; the `debug_assert` guards that against an out-of-order schema.
    debug_assert!(
        object
            .fields
            .windows(2)
            .all(|pair| matches!(pair, [a, b] if a.name <= b.name)),
        "Object::fields must be sorted alphabetically by name"
    );
    let json::Value::Object(fields) = elem.value() else {
        warnings.insert(
            elem,
            Warning::TypeMismatch {
                expected: json::ValueKind::Object,
                actual: elem.value().kind(),
            },
        );
        return TypeExpectation::Invalid;
    };

    builders.push(build::empty(object.kind));
    stack.push(Step::Close { slot });

    // Mark, by schema-field position, which fields the document supplies. Reusing each
    // binary-search hit here lets the missing-field scan below be a single indexed pass
    // instead of a linear `contains` per schema field.
    let mut seen = vec![false; object.fields.len()];
    for field in fields {
        let key = field.key().as_unescaped_str();
        let Ok(idx) = object.fields.binary_search_by_key(&key, |fd| fd.name) else {
            // Not in the schema: record it and do not walk its subtree.
            warnings.insert(field.element(), Warning::UnexpectedField);
            continue;
        };
        if let Some(flag) = seen.get_mut(idx) {
            *flag = true;
        }
        if let Some(fd) = object.fields.get(idx) {
            stack.push(Step::Visit {
                elem: field.element(),
                schema: &fd.schema,
                slot: Slot::Field { name: fd.name },
            });
        }
    }

    // An absent field has no element of its own to `Visit`, so it is recorded here.
    // Every absent field is set to `Integrity::Missing`; the field's extractor then
    // interprets that per the field's optionality (an optional field becomes
    // `Integrity::Ok(None)`, a required field stays `Integrity::Missing`). A required
    // field additionally records a `MissingField` warning against the parent, so its
    // absence is visible in both the IR and the warnings.
    for (field, &present) in object.fields.iter().zip(seen.iter()) {
        if present {
            continue;
        }

        if let Presence::Required = field.presence {
            warnings.insert(elem, Warning::MissingField { name: field.name });
        }
        build::set_top_field(builders, field.name, Integrity::Missing);
    }

    TypeExpectation::Satisfied
}

/// Open an array: push its accumulator builder and a [`Step::Close`], then queue its
/// items in document order. Records a cardinality warning for an empty `OneOrMore`
/// array.
///
/// Returns `false` (and opens nothing) if `elem` is not a JSON array.
fn open_array<'a, 'buf>(
    stack: &mut Vec<Step<'a, 'buf>>,
    builders: &mut Vec<build::Node<'buf>>,
    warnings: &mut warning::Set<Warning>,
    elem: &'a json::Element<'buf>,
    item: &'a Schema,
    cardinality: Cardinality,
    slot: Slot,
) -> TypeExpectation {
    let json::Value::Array(items) = elem.value() else {
        warnings.insert(
            elem,
            Warning::TypeMismatch {
                expected: json::ValueKind::Array,
                actual: elem.value().kind(),
            },
        );
        return TypeExpectation::Invalid;
    };

    if cardinality == Cardinality::OneOrMore && items.is_empty() {
        warnings.insert(
            elem,
            Warning::Cardinality {
                expected: cardinality,
                len: 0,
            },
        );
    }

    builders.push(build::Node::Array(Vec::with_capacity(items.len())));
    stack.push(Step::Close { slot });

    // Push in reverse so items are visited, and accumulated, in document order.
    for child in items.iter().rev() {
        stack.push(Step::Visit {
            elem: child,
            schema: item,
            slot: Slot::Item,
        });
    }

    TypeExpectation::Satisfied
}

/// Queue the children of an opaque [`Scalar::Any`] element so nested `null`s are
/// still reported. Their values are discarded.
fn enqueue_all_children<'a, 'buf>(stack: &mut Vec<Step<'a, 'buf>>, elem: &'a json::Element<'buf>) {
    match elem.value() {
        json::Value::Array(items) => {
            for child in items.iter().rev() {
                stack.push(Step::Visit {
                    elem: child,
                    schema: &ANY,
                    slot: Slot::Ignore,
                });
            }
        }
        json::Value::Object(fields) => {
            for field in fields.iter().rev() {
                stack.push(Step::Visit {
                    elem: field.element(),
                    schema: &ANY,
                    slot: Slot::Ignore,
                });
            }
        }
        json::Value::Null
        | json::Value::True
        | json::Value::False
        | json::Value::String(_)
        | json::Value::Number(_) => {}
    }
}

// Constrained leaf types for the schema intermediate representation (IR).
//
// A leaf wraps a [`json::Element`] that the IR builder has already confirmed to be
// the right JSON kind. Downstream lowering (the `FromSchema` impls) therefore does
// not repeat the kind check; it only does semantic interpretation (parsing a number
// into a `Decimal`, validating an ISO currency code, and so on).
//
// The leaves keep a (cheap, reference-counted) clone of their [`json::Element`] so
// the lowering step can still attach its semantic warnings to the right path.

/// A JSON value the builder confirmed to be a string.
///
/// Length and other lexical checks are applied by the builder when the leaf is
/// constructed; see [`crate::schema::build`].
#[derive(Clone, Debug)]
pub(crate) struct Str<'buf> {
    elem: json::Element<'buf>,
}

impl<'buf> Str<'buf> {
    pub(super) fn new(elem: json::Element<'buf>) -> Self {
        Self { elem }
    }

    /// The element this leaf was built from; the lowering step parses its value and
    /// attaches any semantic warnings to it.
    #[allow(dead_code, reason = "Will be used in FromSchema integration PR")]
    pub fn element(&self) -> &json::Element<'buf> {
        &self.elem
    }
}

/// A JSON value the builder confirmed to be a number, remembering whether it was
/// written as a JSON number or encoded as a JSON string.
///
/// OCPI allows a `number` to be encoded as a string; the [`Number::StringEncoded`]
/// variant records that so the builder can flag it and the lowering step can still
/// read the digits.
#[derive(Clone, Debug)]
pub(crate) enum Number<'buf> {
    /// A JSON number literal.
    Number(json::Element<'buf>),
    /// A number encoded as a JSON string.
    StringEncoded(json::Element<'buf>),
}

#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
impl<'buf> Number<'buf> {
    /// The element this leaf was built from; the lowering step parses its value and
    /// attaches any semantic warnings to it.
    pub fn element(&self) -> &json::Element<'buf> {
        match self {
            Self::Number(elem) | Self::StringEncoded(elem) => elem,
        }
    }
}

/// A JSON string the builder confirmed to be one of an enum's permitted variants.
///
/// The builder stores the matched spelling exactly as the spec writes it (the
/// `canonical` value), so the lowering step maps it to the domain enum without
/// repeating the membership check.
#[derive(Clone, Debug)]
pub(crate) struct Enum<'buf> {
    elem: json::Element<'buf>,
    canonical: &'static str,
}

#[expect(dead_code, reason = "For use in future `FromSchema` trait")]
impl<'buf> Enum<'buf> {
    pub fn new(elem: json::Element<'buf>, canonical: &'static str) -> Self {
        Self { elem, canonical }
    }

    /// The element this leaf was built from; used to attach semantic warnings.
    pub fn element(&self) -> &json::Element<'buf> {
        &self.elem
    }

    /// The permitted spelling (uppercase, as written in the spec) that the value matched.
    pub fn canonical(&self) -> &'static str {
        self.canonical
    }
}
