#[cfg(test)]
mod test;

pub(crate) mod v211;
pub(crate) mod v221;
pub mod v2x;

use std::{borrow::Cow, fmt};

use crate::{
    country, currency, datetime, from_warning_all, json, money, number, string, tariff, warning,
    Versioned as _,
};

/// Lint the given tariff and return a report of any [`Warning`]s found.
///
/// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#131-tariff-object>)
/// * See: [OCPI spec 2.1.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md#31-tariff-object>)
pub(crate) fn lint(tariff: &tariff::Versioned<'_>) -> Report {
    let mut warnings = warning::Set::new();

    {
        // Walk all elements taking note of which elements have a null value.
        // There is no reason to use `null` in the OCPI spec.
        // Instead, you should just leave out the field in question.
        let walker = json::walk::DepthFirst::from_doc(tariff.as_doc());

        for elem in walker.filter(|&elem| elem.value().is_null()) {
            warnings.insert(elem, Warning::NeedlessNullField);
        }
    }

    match tariff.version() {
        crate::Version::V221 => v221::lint(tariff.as_element(), warnings),
        crate::Version::V211 => v211::lint(tariff.as_element(), warnings),
    }
}

/// A tariff linting report.
#[derive(Debug)]
pub struct Report {
    /// Any `Warning`s found while linting the tariff.
    pub warnings: warning::Set<Warning>,
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
    Country(country::Warning),

    /// Both the CDR and tariff have a `country_code` that should be an alpha-2.
    CpoCountryCodeShouldBeAlpha2,

    Currency(currency::Warning),

    DateTime(datetime::Warning),

    Elements(v2x::elements::Warning),

    /// The given field is required.
    FieldRequired {
        field_name: Cow<'static, str>,
    },

    /// The `min_price` is greater than `max_price`.
    MinPriceIsGreaterThanMax,

    Money(money::Warning),

    /// The field is null. It can simply be removed.
    NeedlessNullField,

    Number(number::Warning),

    String(string::Warning),

    /// The `start_date_time` is after the `end_date_time`.
    StartDateTimeIsAfterEndDateTime,
}

from_warning_all!(
    country::Warning => Warning::Country,
    currency::Warning => Warning::Currency,
    datetime::Warning => Warning::DateTime,
    number::Warning => Warning::Number,
    money::Warning => Warning::Money,
    string::Warning => Warning::String,
    v2x::elements::Warning => Warning::Elements
);

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::CpoCountryCodeShouldBeAlpha2 => {
                warning::Id::from_static("cpo_country_code_should_be_alpha2")
            }
            Self::Country(kind) => kind.id(),
            Self::Currency(kind) => kind.id(),
            Self::DateTime(kind) => kind.id(),
            Self::Elements(kind) => kind.id(),
            Self::FieldRequired { field_name } => {
                warning::Id::from_string(format!("field_required({field_name})"))
            }
            Self::MinPriceIsGreaterThanMax => {
                warning::Id::from_static("min_price_is_greater_than_max")
            }
            Self::Money(kind) => kind.id(),
            Self::NeedlessNullField => warning::Id::from_static("needless_null_field"),
            Self::Number(kind) => kind.id(),
            Self::StartDateTimeIsAfterEndDateTime => {
                warning::Id::from_static("start_date_time_is_after_end_date_time")
            }
            Self::String(kind) => kind.id(),
        }
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::CpoCountryCodeShouldBeAlpha2 => {
                f.write_str("The value should be an alpha-2 ISO 3166-1 country code")
            }
            Self::Country(kind) => fmt::Display::fmt(kind, f),
            Self::Currency(kind) => fmt::Display::fmt(kind, f),
            Self::DateTime(kind) => fmt::Display::fmt(kind, f),
            Self::Elements(kind) => fmt::Display::fmt(kind, f),
            Self::FieldRequired { field_name } => {
                write!(f, "Field is required: `{field_name}`")
            }
            Self::MinPriceIsGreaterThanMax => {
                f.write_str("The `min_price` is greater than `max_price`.")
            }
            Self::Money(kind) => fmt::Display::fmt(kind, f),
            Self::NeedlessNullField => write!(f, "Null field: the field can simply be removed."),
            Self::Number(kind) => fmt::Display::fmt(kind, f),
            Self::StartDateTimeIsAfterEndDateTime => {
                f.write_str("The `start_date_time` is after the `end_date_time`.")
            }
            Self::String(kind) => fmt::Display::fmt(kind, f),
        }
    }
}
