//! Parse a tariff and lint the result.

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

#[cfg(test)]
mod test_real_world;

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

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

use crate::{
    country, currency, datetime, duration, enumeration, from_warning_all, guess, json, lint, money,
    number, string, warning, ObjectType, ParseError, ReasonableStr, Version,
};

#[derive(Debug)]
pub enum Warning {
    /// The CDR location is not a valid ISO 3166-1 alpha-3 code.
    Country(country::Warning),
    Currency(currency::Warning),
    DateTime(datetime::Warning),
    Decode(json::decode::Warning),
    Duration(duration::Warning),
    Enum(enumeration::Warning),

    /// A field in the tariff doesn't have the expected type.
    FieldInvalidType {
        /// The type that the given field should have according to the schema.
        expected_type: json::ValueKind,
    },

    /// A field in the tariff doesn't have the expected value.
    FieldInvalidValue {
        /// The value encountered.
        value: String,

        /// A message about what values are expected for this field.
        message: Cow<'static, str>,
    },

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

    Money(money::Warning),

    /// The given tariff has a `min_price` set and the `total_cost` fell below it.
    ///
    /// * See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#131-tariff-object>
    TotalCostClampedToMin,

    /// The given tariff has a `max_price` set and the `total_cost` exceeded it.
    ///
    /// * See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#131-tariff-object>
    TotalCostClampedToMax,

    /// The tariff has no `Element`s.
    NoElements,

    /// The tariff is not active during the `Cdr::start_date_time`.
    NotActive,
    Number(number::Warning),

    String(string::Warning),
}

impl Warning {
    /// Create a new `Warning::FieldInvalidValue` where the field is built from the given `json::Element`.
    fn field_invalid_value(
        value: impl Into<String>,
        message: impl Into<Cow<'static, str>>,
    ) -> Self {
        Warning::FieldInvalidValue {
            value: value.into(),
            message: message.into(),
        }
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::String(warning_kind) => write!(f, "{warning_kind}"),
            Self::Country(warning_kind) => write!(f, "{warning_kind}"),
            Self::Currency(warning_kind) => write!(f, "{warning_kind}"),
            Self::DateTime(warning_kind) => write!(f, "{warning_kind}"),
            Self::Decode(warning_kind) => write!(f, "{warning_kind}"),
            Self::Duration(warning_kind) => write!(f, "{warning_kind}"),
            Self::Enum(warning_kind) => write!(f, "{warning_kind}"),
            Self::FieldInvalidType { expected_type } => {
                write!(f, "Field has invalid type. Expected type `{expected_type}`")
            }
            Self::FieldInvalidValue { value, message } => {
                write!(f, "Field has invalid value `{value}`: {message}")
            }
            Self::FieldRequired { field_name } => {
                write!(f, "Field is required: `{field_name}`")
            }
            Self::Money(warning_kind) => write!(f, "{warning_kind}"),
            Self::NoElements => f.write_str("The tariff has no `elements`"),
            Self::NotActive => f.write_str("The tariff is not active for `Cdr::start_date_time`"),
            Self::Number(warning_kind) => write!(f, "{warning_kind}"),
            Self::TotalCostClampedToMin => write!(
                f,
                "The given tariff has a `min_price` set and the `total_cost` fell below it."
            ),
            Self::TotalCostClampedToMax => write!(
                f,
                "The given tariff has a `max_price` set and the `total_cost` exceeded it."
            ),
        }
    }
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::String(warning) => warning.id(),
            Self::Country(warning) => warning.id(),
            Self::Currency(warning) => warning.id(),
            Self::DateTime(warning) => warning.id(),
            Self::Decode(warning) => warning.id(),
            Self::Duration(warning) => warning.id(),
            Self::Enum(warning) => warning.id(),
            Self::FieldInvalidType { expected_type } => {
                warning::Id::from_string(format!("field_invalid_type({expected_type})"))
            }
            Self::FieldInvalidValue { value, .. } => {
                warning::Id::from_string(format!("field_invalid_value({value})"))
            }
            Self::FieldRequired { field_name } => {
                warning::Id::from_string(format!("field_required({field_name})"))
            }
            Self::Money(warning) => warning.id(),
            Self::NoElements => warning::Id::from_static("no_elements"),
            Self::NotActive => warning::Id::from_static("not_active"),
            Self::Number(warning) => warning.id(),
            Self::TotalCostClampedToMin => warning::Id::from_static("total_cost_clamped_to_min"),
            Self::TotalCostClampedToMax => warning::Id::from_static("total_cost_clamped_to_max"),
        }
    }
}

from_warning_all!(
    country::Warning => Warning::Country,
    currency::Warning => Warning::Currency,
    datetime::Warning => Warning::DateTime,
    duration::Warning => Warning::Duration,
    enumeration::Warning => Warning::Enum,
    json::decode::Warning => Warning::Decode,
    money::Warning => Warning::Money,
    number::Warning => Warning::Number,
    string::Warning => Warning::String
);

/// The five character ID of the CPO.
///
/// The first two characters are the ISO-3166 alpha-2 country code of the CPO.
/// The remaining three characters are the ISO-15118 ID of the CPO.
#[derive(Clone, Debug)]
pub(crate) struct CpoId<'buf> {
    /// The ISO-3166 alpha-2 country code.
    pub country_code: country::Code,

    /// The ISO-15118 ID.
    pub id: string::CiExactLen<'buf, 3>,
}

/// Parse a `&str` into a [`Versioned`] tariff using a schema for the given [`Version`][^spec-v211][^spec-v221] to check for
/// any unexpected fields.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{tariff, Version, ParseError};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let report = tariff::parse_with_version(TARIFF_JSON, Version::V211)?;
/// let tariff::ParseReport {
///     tariff,
///     unexpected_fields,
/// } = report;
///
/// if !unexpected_fields.is_empty() {
///     eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
///
///     for path in &unexpected_fields {
///         eprintln!("{path}");
///     }
/// }
///
/// # Ok::<(), ParseError>(())
/// ```
///
/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md>
/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>
pub fn parse_with_version(
    tariff_json: &str,
    version: Version,
) -> Result<ParseReport<'_>, ParseError> {
    let tariff_json =
        ReasonableStr::new(tariff_json).map_err(ParseError::from_kind(ObjectType::Tariff))?;
    match version {
        Version::V221 => {
            let schema = &*crate::v221::TARIFF_SCHEMA;
            let report =
                json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_cdr_err)?;
            let json::ParseReport {
                element,
                unexpected_fields,
            } = report;
            Ok(ParseReport {
                tariff: Versioned::new(tariff_json.into_inner(), element, Version::V221),
                unexpected_fields,
            })
        }
        Version::V211 => {
            let schema = &*crate::v211::TARIFF_SCHEMA;
            let report =
                json::parse_with_schema(tariff_json, schema).map_err(ParseError::from_cdr_err)?;
            let json::ParseReport {
                element,
                unexpected_fields,
            } = report;
            Ok(ParseReport {
                tariff: Versioned::new(tariff_json.into_inner(), element, Version::V211),
                unexpected_fields,
            })
        }
    }
}

/// Parse the JSON and try to guess the [`Version`] based on fields defined in the
/// OCPI v2.1.1[^spec-v211] and v2.2.1[^spec-v221] tariff spec.
///
/// The parser is forgiving and will not complain if the tariff JSON is missing required fields.
/// The parser will also not complain if unexpected fields are present in the JSON.
/// The [`Version`] guess is based on fields that exist.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{tariff, guess, ParseError, Version, Versioned as _};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
/// let tariff = tariff::parse(TARIFF_JSON)?;
///
/// match tariff {
///     guess::Version::Certain(tariff) => {
///         println!("The tariff version is `{}`", tariff.version());
///     },
///     guess::Version::Uncertain(_tariff) => {
///         eprintln!("Unable to guess the version of given tariff JSON.");
///     }
/// }
///
/// # Ok::<(), ParseError>(())
/// ```
///
/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md>
/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>
pub fn parse(tariff_json: &str) -> Result<guess::TariffVersion<'_>, ParseError> {
    let tariff_json =
        ReasonableStr::new(tariff_json).map_err(ParseError::from_kind(ObjectType::Tariff))?;
    guess::tariff_version(tariff_json)
}

/// Guess the [`Version`][^spec-v211][^spec-v221] of the given tariff JSON and report on any unexpected fields.
///
/// The parser is forgiving and will not complain if the tariff JSON is missing required fields.
/// The parser will also not complain if unexpected fields are present in the JSON.
/// The [`Version`] guess is based on fields that exist.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{guess, tariff, warning};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let report = tariff::parse_and_report(TARIFF_JSON)?;
/// let guess::Report {
///     unexpected_fields,
///     version,
/// } = report;
///
/// if !unexpected_fields.is_empty() {
///     eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
///
///     for path in &unexpected_fields {
///         eprintln!("  * {path}");
///     }
///
///     eprintln!();
/// }
///
/// let guess::Version::Certain(tariff) = version else {
///     return Err("Unable to guess the version of given CDR JSON.".into());
/// };
///
/// let report = tariff::lint(&tariff);
///
/// eprintln!("`{}` lint warnings found", report.warnings.len_warnings());
///
/// for group in report.warnings {
///     let (element, warnings) = group.to_parts();
///     eprintln!(
///         "Warnings reported for `json::Element` at path: `{}`",
///         element.path()
///     );
///
///     for warning in warnings {
///         eprintln!("  * {warning}");
///     }
///
///     eprintln!();
/// }
///
/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
/// ```
///
/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md>
/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>
pub fn parse_and_report(tariff_json: &str) -> Result<guess::TariffReport<'_>, ParseError> {
    let tariff_json =
        ReasonableStr::new(tariff_json).map_err(ParseError::from_kind(ObjectType::Tariff))?;
    guess::tariff_version_with_report(tariff_json)
}

/// A [`Versioned`] tariff along with a set of unexpected fields.
#[derive(Debug)]
pub struct ParseReport<'buf> {
    /// The root JSON `Element`.
    pub tariff: Versioned<'buf>,

    /// A list of fields that were not expected: The schema did not define them.
    pub unexpected_fields: json::UnexpectedFields<'buf>,
}

/// A `json::Element` that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
/// and has been identified as being a certain [`Version`].
#[derive(Clone)]
pub struct Versioned<'buf> {
    /// The source JSON as string.
    source: &'buf str,

    /// The parsed JSON as structured [`Element`](crate::json::Element)s.
    element: json::Element<'buf>,

    /// The `Version` of the tariff, determined during parsing.
    version: Version,
}

impl fmt::Debug for Versioned<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            fmt::Debug::fmt(&self.element, f)
        } else {
            match self.version {
                Version::V211 => f.write_str("V211"),
                Version::V221 => f.write_str("V221"),
            }
        }
    }
}

impl crate::Versioned for Versioned<'_> {
    fn version(&self) -> Version {
        self.version
    }
}

impl<'buf> Versioned<'buf> {
    /// Create a new `Versioned` object.
    pub(crate) fn new(source: &'buf str, element: json::Element<'buf>, version: Version) -> Self {
        Self {
            source,
            element,
            version,
        }
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn into_element(self) -> json::Element<'buf> {
        self.element
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn as_element(&self) -> &json::Element<'buf> {
        &self.element
    }

    /// Return the inner JSON `str` and discard the version info.
    pub fn as_json_str(&self) -> &'buf str {
        self.source
    }
}

/// A [`json::Element`] that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
/// and was determined to not be one of the supported [`Version`]s.
#[derive(Debug)]
pub struct Unversioned<'buf> {
    /// The source JSON as string.
    source: &'buf str,

    /// A list of fields that were not expected: The schema did not define them.
    element: json::Element<'buf>,
}

impl<'buf> Unversioned<'buf> {
    /// Create an unversioned [`json::Element`].
    pub(crate) fn new(source: &'buf str, elem: json::Element<'buf>) -> Self {
        Self {
            source,
            element: elem,
        }
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn into_element(self) -> json::Element<'buf> {
        self.element
    }

    /// Return the inner [`json::Element`] and discard the version info.
    pub fn as_element(&self) -> &json::Element<'buf> {
        &self.element
    }

    /// Return the inner JSON `&str` and discard the version info.
    pub fn as_json_str(&self) -> &'buf str {
        self.source
    }
}

impl<'buf> crate::Unversioned for Unversioned<'buf> {
    type Versioned = Versioned<'buf>;

    fn force_into_versioned(self, version: Version) -> Versioned<'buf> {
        let Self { source, element } = self;
        Versioned {
            source,
            element,
            version,
        }
    }
}

/// Lint the given tariff and return a [`lint::tariff::Report`] of any `Warning`s found.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{guess, tariff, warning};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let report = tariff::parse_and_report(TARIFF_JSON)?;
/// let guess::Report {
///     unexpected_fields,
///     version,
/// } = report;
///
/// if !unexpected_fields.is_empty() {
///     eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
///
///     for path in &unexpected_fields {
///         eprintln!("  * {path}");
///     }
///
///     eprintln!();
/// }
///
/// let guess::Version::Certain(tariff) = version else {
///     return Err("Unable to guess the version of given CDR JSON.".into());
/// };
///
/// let report = tariff::lint(&tariff);
///
/// eprintln!("`{}` lint warnings found", report.warnings.len_warnings());
///
/// for group in report.warnings {
///     let (element, warnings) = group.to_parts();
///     eprintln!(
///         "Warnings reported for `json::Element` at path: `{}`",
///         element.path()
///     );
///
///     for warning in warnings {
///         eprintln!("  * {warning}");
///     }
///
///     eprintln!();
/// }
///
/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
/// ```
pub fn lint(tariff: &Versioned<'_>) -> lint::tariff::Report {
    lint::tariff(tariff)
}
