//! The builder that turns the `validate` walk into a typed IR.
//!
//! A [`Node`] is both the in-progress builder for an object or array and the finished
//! value handed up to its parent. An object node starts empty (all fields `Missing`)
//! and is filled via [`Node::set`] as its children close. An array node accumulates
//! items via [`Node::push`]. [`Node::Ignore`] is used for a subtree the schema
//! validates but doesn't model. [`Node::Ignore`]'s `set`/`push` methods are no-ops.

use super::{v211, v221, BuilderKind, Enum, Integrity, Number, Slot, Str};

/// A node in the schema IR being built.
//
// `Node` is short-lived builder state: a value lives on the builder stack only while
// its object/array is open, then is moved into the (compact) IR via the extractors.
// The object variants are inherently larger than the leaves, so boxing each would add
// indirection to every `set`/extract for no lasting benefit.
#[expect(
    clippy::large_enum_variant,
    reason = "transient builder state; the large variants are moved into the compact IR and dropped"
)]
#[derive(Debug)]
pub(super) enum Node<'buf> {
    /// A subtree validated for warnings but not modeled as IR.
    Ignore,
    Str(Str<'buf>),
    Number(Number<'buf>),
    Enum(Enum<'buf>),
    /// A JSON boolean. No tariff field is a boolean, so the value is not retained.
    Bool,
    /// An array accumulator; each item is itself an [`Integrity`] value.
    Array(Vec<Integrity<Node<'buf>>>),
    Tariff(v221::Tariff<'buf>),
    Cdr(v221::Cdr<'buf>),
    ChargingPeriod(v221::ChargingPeriod<'buf>),
    CdrDimension(v221::Dimension<'buf>),
    Element(v221::Element<'buf>),
    PriceComponent(v221::PriceComponent<'buf>),
    Restrictions(v221::Restrictions<'buf>),
    Price(v221::Price<'buf>),
    V211Tariff(v211::Tariff<'buf>),
    V211Element(v211::Element<'buf>),
    V211PriceComponent(v211::PriceComponent<'buf>),
    V211Restrictions(v211::Restrictions<'buf>),
    V211Cdr(v211::Cdr<'buf>),
    V211ChargingPeriod(v211::ChargingPeriod<'buf>),
    V211CdrDimension(v211::Dimension<'buf>),
}

/// Create the empty in-progress builder for an object of the given [`BuilderKind`].
pub(super) fn empty<'buf>(kind: BuilderKind) -> Node<'buf> {
    match kind {
        BuilderKind::Ignore => Node::Ignore,
        BuilderKind::V221Tariff => Node::Tariff(v221::Tariff::default()),
        BuilderKind::V221Element => Node::Element(v221::Element::default()),
        BuilderKind::V221PriceComponent => Node::PriceComponent(v221::PriceComponent::default()),
        BuilderKind::V221Restrictions => Node::Restrictions(v221::Restrictions::default()),
        BuilderKind::V221Price => Node::Price(v221::Price::default()),
        BuilderKind::V221Cdr => Node::Cdr(v221::Cdr::default()),
        BuilderKind::V221ChargingPeriod => Node::ChargingPeriod(v221::ChargingPeriod::default()),
        BuilderKind::V221CdrDimension => Node::CdrDimension(v221::Dimension::default()),
        BuilderKind::V211Tariff => Node::V211Tariff(v211::Tariff::default()),
        BuilderKind::V211Element => Node::V211Element(v211::Element::default()),
        BuilderKind::V211PriceComponent => {
            Node::V211PriceComponent(v211::PriceComponent::default())
        }
        BuilderKind::V211Restrictions => Node::V211Restrictions(v211::Restrictions::default()),
        BuilderKind::V211Cdr => Node::V211Cdr(v211::Cdr::default()),
        BuilderKind::V211ChargingPeriod => {
            Node::V211ChargingPeriod(v211::ChargingPeriod::default())
        }
        BuilderKind::V211CdrDimension => Node::V211CdrDimension(v211::Dimension::default()),
    }
}

impl<'buf> Node<'buf> {
    /// Route a built value to its parent: a named object field, an array item, the root,
    /// or nowhere (`Ignore`). Every present field is routed here; an absent field is
    /// recorded separately by the walk's missing-field scan.
    pub(super) fn route_to_parent(
        &mut self,
        builders: &mut [Node<'buf>],
        slot: Slot,
        value: Integrity<Node<'buf>>,
    ) {
        match slot {
            Slot::Root => {
                if let Integrity::Ok(node) = value {
                    *self = node;
                }
            }
            Slot::Ignore => {}
            Slot::Field { name } => {
                set_top_field(builders, name, value);
            }
            Slot::Item => {
                if let Some(top) = builders.last_mut() {
                    top.push(value);
                }
            }
        }
    }

    /// Store a built child under the named object field. Unknown fields and non-object
    /// nodes are ignored.
    fn set(&mut self, field: &'static str, child: Integrity<Node<'buf>>) {
        match self {
            Node::Tariff(tariff) => set_tariff(tariff, field, child),
            Node::Cdr(cdr) => set_cdr(cdr, field, child),
            Node::ChargingPeriod(period) => set_charging_period(period, field, child),
            Node::CdrDimension(dimension) => set_dimension(dimension, field, child),
            Node::Element(element) => set_element(element, field, child),
            Node::PriceComponent(component) => set_price_component(component, field, child),
            Node::Restrictions(restrictions) => set_restrictions(restrictions, field, child),
            Node::Price(price) => set_price(price, field, child),
            Node::V211Tariff(tariff) => set_v211_tariff(tariff, field, child),
            Node::V211Element(element) => set_v211_element(element, field, child),
            Node::V211PriceComponent(component) => {
                set_v211_price_component(component, field, child);
            }
            Node::V211Restrictions(restrictions) => {
                set_v211_restrictions(restrictions, field, child);
            }
            Node::V211Cdr(cdr) => set_v211_cdr(cdr, field, child),
            Node::V211ChargingPeriod(period) => set_v211_charging_period(period, field, child),
            Node::V211CdrDimension(dimension) => set_v211_dimension(dimension, field, child),
            Node::Ignore
            | Node::Str(_)
            | Node::Number(_)
            | Node::Enum(_)
            | Node::Bool
            | Node::Array(_) => {}
        }
    }

    /// Append a built item to an array node. Non-array nodes are ignored.
    fn push(&mut self, item: Integrity<Node<'buf>>) {
        if let Node::Array(items) = self {
            items.push(item);
        }
    }
}

/// Store `value` on field `name` of the object builder on top of `builders`. The walk
/// uses this to record an absent field's `Missing` directly, since an absent field has
/// no element to `Visit`. Non-object builders ignore unknown fields.
pub(super) fn set_top_field<'buf>(
    builders: &mut [Node<'buf>],
    name: &'static str,
    value: Integrity<Node<'buf>>,
) {
    if let Some(top) = builders.last_mut() {
        top.set(name, value);
    }
}

// -- Field setters -----------------------------------------------------------

fn set_tariff<'buf>(tariff: &mut v221::Tariff<'buf>, field: &str, child: Integrity<Node<'buf>>) {
    match field {
        "country_code" => tariff.country_code = as_str(child),
        "currency" => tariff.currency = as_str(child),
        "id" => tariff.id = as_str(child),
        "party_id" => tariff.party_id = as_str(child),
        "min_price" => tariff.min_price = as_opt_price(child),
        "max_price" => tariff.max_price = as_opt_price(child),
        "start_date_time" => tariff.start_date_time = as_opt_str(child),
        "end_date_time" => tariff.end_date_time = as_opt_str(child),
        "elements" => tariff.elements = as_array(child, as_element),
        _ => {}
    }
}

fn set_element<'buf>(element: &mut v221::Element<'buf>, field: &str, child: Integrity<Node<'buf>>) {
    match field {
        "price_components" => element.price_components = as_array(child, as_price_component),
        "restrictions" => element.restrictions = as_opt_restrictions(child),
        _ => {}
    }
}

fn set_price_component<'buf>(
    component: &mut v221::PriceComponent<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "type" => component.dimension_type = as_enum(child),
        "vat" => component.vat = as_opt_number(child),
        "price" => component.price = as_number(child),
        "step_size" => component.step_size = as_number(child),
        _ => {}
    }
}

fn set_restrictions<'buf>(
    restrictions: &mut v221::Restrictions<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "start_time" => restrictions.start_time = as_opt_str(child),
        "end_time" => restrictions.end_time = as_opt_str(child),
        "start_date" => restrictions.start_date = as_opt_str(child),
        "end_date" => restrictions.end_date = as_opt_str(child),
        "min_kwh" => restrictions.min_kwh = as_opt_number(child),
        "max_kwh" => restrictions.max_kwh = as_opt_number(child),
        "min_current" => restrictions.min_current = as_opt_number(child),
        "max_current" => restrictions.max_current = as_opt_number(child),
        "min_power" => restrictions.min_power = as_opt_number(child),
        "max_power" => restrictions.max_power = as_opt_number(child),
        "min_duration" => restrictions.min_duration = as_opt_number(child),
        "max_duration" => restrictions.max_duration = as_opt_number(child),
        "day_of_week" => restrictions.day_of_week = as_opt_array(child, as_enum),
        "reservation" => restrictions.reservation = as_opt_enum(child),
        _ => {}
    }
}

fn set_price<'buf>(price: &mut v221::Price<'buf>, field: &str, child: Integrity<Node<'buf>>) {
    match field {
        "excl_vat" => price.excl_vat = as_number(child),
        "incl_vat" => price.incl_vat = as_opt_number(child),
        _ => {}
    }
}

fn set_cdr<'buf>(cdr: &mut v221::Cdr<'buf>, field: &str, child: Integrity<Node<'buf>>) {
    match field {
        "currency" => cdr.currency = as_str(child),
        "start_date_time" => cdr.start_date_time = as_str(child),
        "end_date_time" => cdr.end_date_time = as_str(child),
        "charging_periods" => cdr.charging_periods = as_array(child, as_charging_period),
        "total_cost" => cdr.total_cost = as_price(child),
        "total_energy" => cdr.total_energy = as_number(child),
        "total_time" => cdr.total_time = as_number(child),
        "total_parking_time" => cdr.total_parking_time = as_opt_number(child),
        "total_energy_cost" => cdr.total_energy_cost = as_opt_price(child),
        "total_fixed_cost" => cdr.total_fixed_cost = as_opt_price(child),
        "total_parking_cost" => cdr.total_parking_cost = as_opt_price(child),
        "total_reservation_cost" => cdr.total_reservation_cost = as_opt_price(child),
        "total_time_cost" => cdr.total_time_cost = as_opt_price(child),
        "tariffs" => cdr.tariffs = as_opt_array(child, as_tariff),
        _ => {}
    }
}

fn set_charging_period<'buf>(
    period: &mut v221::ChargingPeriod<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "start_date_time" => period.start_date_time = as_str(child),
        "dimensions" => period.dimensions = as_array(child, as_dimension),
        _ => {}
    }
}

fn set_dimension<'buf>(
    dimension: &mut v221::Dimension<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "type" => dimension.dimension_type = as_enum(child),
        "volume" => dimension.volume = as_number(child),
        _ => {}
    }
}

fn set_v211_tariff<'buf>(
    tariff: &mut v211::Tariff<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "currency" => tariff.currency = as_str(child),
        "id" => tariff.id = as_str(child),
        "elements" => tariff.elements = as_array(child, as_v211_element),
        _ => {}
    }
}

fn set_v211_element<'buf>(
    element: &mut v211::Element<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "price_components" => element.price_components = as_array(child, as_v211_price_component),
        "restrictions" => element.restrictions = as_opt_v211_restrictions(child),
        _ => {}
    }
}

fn set_v211_price_component<'buf>(
    component: &mut v211::PriceComponent<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        // 2.1.1 has no `vat` field.
        "type" => component.dimension_type = as_enum(child),
        "price" => component.price = as_number(child),
        "step_size" => component.step_size = as_number(child),
        _ => {}
    }
}

fn set_v211_restrictions<'buf>(
    restrictions: &mut v211::Restrictions<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        // 2.1.1 has no `min_current`/`max_current` or `reservation` fields.
        "start_time" => restrictions.start_time = as_opt_str(child),
        "end_time" => restrictions.end_time = as_opt_str(child),
        "start_date" => restrictions.start_date = as_opt_str(child),
        "end_date" => restrictions.end_date = as_opt_str(child),
        "min_kwh" => restrictions.min_kwh = as_opt_number(child),
        "max_kwh" => restrictions.max_kwh = as_opt_number(child),
        "min_power" => restrictions.min_power = as_opt_number(child),
        "max_power" => restrictions.max_power = as_opt_number(child),
        "min_duration" => restrictions.min_duration = as_opt_number(child),
        "max_duration" => restrictions.max_duration = as_opt_number(child),
        "day_of_week" => restrictions.day_of_week = as_opt_array(child, as_enum),
        _ => {}
    }
}

fn set_v211_cdr<'buf>(cdr: &mut v211::Cdr<'buf>, field: &str, child: Integrity<Node<'buf>>) {
    match field {
        // 2.1.1 uses `stop_date_time`, `total_cost` is a bare number, and there are no
        // itemized cost totals.
        "currency" => cdr.currency = as_str(child),
        "start_date_time" => cdr.start_date_time = as_str(child),
        "stop_date_time" => cdr.stop_date_time = as_str(child),
        "charging_periods" => cdr.charging_periods = as_array(child, as_v211_charging_period),
        "total_cost" => cdr.total_cost = as_number(child),
        "total_energy" => cdr.total_energy = as_number(child),
        "total_time" => cdr.total_time = as_number(child),
        "total_parking_time" => cdr.total_parking_time = as_opt_number(child),
        "tariffs" => cdr.tariffs = as_opt_array(child, as_v211_tariff),
        _ => {}
    }
}

fn set_v211_charging_period<'buf>(
    period: &mut v211::ChargingPeriod<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "start_date_time" => period.start_date_time = as_str(child),
        "dimensions" => period.dimensions = as_array(child, as_v211_dimension),
        _ => {}
    }
}

fn set_v211_dimension<'buf>(
    dimension: &mut v211::Dimension<'buf>,
    field: &str,
    child: Integrity<Node<'buf>>,
) {
    match field {
        "type" => dimension.dimension_type = as_enum(child),
        "volume" => dimension.volume = as_number(child),
        _ => {}
    }
}

// -- Extraction helpers ------------------------------------------------------
//
// Each converts an `Integrity<Node>` child into the concrete `Integrity<T>` (for a
// required field) or `Integrity<Option<T>>` (for an optional field) the field expects.
// Any unexpected node variant becomes `Err`. For a required field `Missing` propagates;
// for an optional field `Missing` (the absent/`null` case) becomes `Ok(None)`.

/// Define a required-field extractor from a single [`Node`] variant to the type it
/// carries. `Missing` propagates unchanged.
macro_rules! extract {
    ($name:ident, $variant:ident, $ty:ty) => {
        fn $name(child: Integrity<Node<'_>>) -> Integrity<$ty> {
            match child {
                Integrity::Ok(node) => {
                    if let Node::$variant(value) = node {
                        Integrity::Ok(value)
                    } else {
                        Integrity::Err
                    }
                }
                Integrity::Missing => Integrity::Missing,
                Integrity::Err => Integrity::Err,
            }
        }
    };
}

/// Define an optional-field extractor producing `Integrity<Option<T>>`. An absent or
/// `null` field (`Missing`) becomes `Ok(None)`, since the schema permits its absence.
macro_rules! extract_opt {
    ($name:ident, $variant:ident, $ty:ty) => {
        fn $name(child: Integrity<Node<'_>>) -> Integrity<Option<$ty>> {
            match child {
                Integrity::Ok(node) => {
                    if let Node::$variant(value) = node {
                        Integrity::Ok(Some(value))
                    } else {
                        Integrity::Err
                    }
                }
                Integrity::Missing => Integrity::Ok(None),
                Integrity::Err => Integrity::Err,
            }
        }
    };
}

extract!(as_str, Str, Str<'_>);
extract!(as_number, Number, Number<'_>);
extract!(as_enum, Enum, Enum<'_>);
extract!(as_price, Price, v221::Price<'_>);
extract!(as_tariff, Tariff, v221::Tariff<'_>);
extract!(as_charging_period, ChargingPeriod, v221::ChargingPeriod<'_>);
extract!(as_dimension, CdrDimension, v221::Dimension<'_>);
extract!(as_element, Element, v221::Element<'_>);
extract!(as_price_component, PriceComponent, v221::PriceComponent<'_>);
extract!(as_v211_element, V211Element, v211::Element<'_>);
extract!(
    as_v211_price_component,
    V211PriceComponent,
    v211::PriceComponent<'_>
);
extract!(as_v211_tariff, V211Tariff, v211::Tariff<'_>);
extract!(
    as_v211_charging_period,
    V211ChargingPeriod,
    v211::ChargingPeriod<'_>
);
extract!(as_v211_dimension, V211CdrDimension, v211::Dimension<'_>);

extract_opt!(as_opt_str, Str, Str<'_>);
extract_opt!(as_opt_number, Number, Number<'_>);
extract_opt!(as_opt_enum, Enum, Enum<'_>);
extract_opt!(as_opt_price, Price, v221::Price<'_>);
extract_opt!(as_opt_restrictions, Restrictions, v221::Restrictions<'_>);
extract_opt!(
    as_opt_v211_restrictions,
    V211Restrictions,
    v211::Restrictions<'_>
);

/// Convert an array node into `Integrity<Vec<Integrity<T>>>` by mapping each item with
/// `f`. Used for required arrays; `Missing` propagates unchanged.
fn as_array<'buf, T>(
    child: Integrity<Node<'buf>>,
    f: impl Fn(Integrity<Node<'buf>>) -> Integrity<T>,
) -> Integrity<Vec<Integrity<T>>> {
    match child {
        Integrity::Ok(node) => {
            if let Node::Array(items) = node {
                Integrity::Ok(items.into_iter().map(f).collect())
            } else {
                Integrity::Err
            }
        }
        Integrity::Missing => Integrity::Missing,
        Integrity::Err => Integrity::Err,
    }
}

/// Convert an array node into `Integrity<Option<Vec<Integrity<T>>>>` by mapping each item
/// with `f`. Used for optional arrays; an absent array (`Missing`) becomes `Ok(None)`.
fn as_opt_array<'buf, T>(
    child: Integrity<Node<'buf>>,
    f: impl Fn(Integrity<Node<'buf>>) -> Integrity<T>,
) -> Integrity<Option<Vec<Integrity<T>>>> {
    match child {
        Integrity::Ok(node) => {
            if let Node::Array(items) = node {
                Integrity::Ok(Some(items.into_iter().map(f).collect()))
            } else {
                Integrity::Err
            }
        }
        Integrity::Missing => Integrity::Ok(None),
        Integrity::Err => Integrity::Err,
    }
}
