//! # OCPI Tariffs library
//!
//! Calculate the (sub)totals of a [charge session](https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc)
//! using the [`cdr::price`] function and use the generated [`price::Report`] to review and compare the calculated
//! totals versus the sources from the `CDR`.
//!
//! - Use the [`cdr::parse`] and [`tariff::parse`] function to parse and guess which OCPI version of a CDR or tariff you have.
//! - Use the [`cdr::parse_with_version`] and [`tariff::parse_with_version`] functions to parse a CDR of tariff as the given version.
//! - Use the [`tariff::lint`] to lint a tariff: flag common errors, bugs, dangerous constructs and stylistic flaws in the tariff.
//!
//! # Examples
//!
//! ## Price a CDR with embedded tariff
//!
//! If you have a CDR JSON with an embedded tariff you can price the CDR with the following code:
//!
//! ```rust
//! # use ocpi_tariffs::{cdr, price, warning, Version};
//! #
//! # const CDR_JSON: &str = include_str!("cdr.json");
//!
//! let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
//! let cdr::ParseReport {
//!     cdr,
//!     unexpected_fields,
//! } = report;
//!
//! # if !unexpected_fields.is_empty() {
//! #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
//! #
//! #     for path in &unexpected_fields {
//! #         eprintln!("{path}");
//! #     }
//! # }
//!
//! let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
//! let (report, warnings) = report.into_parts();
//!
//! if !warnings.is_empty() {
//!     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
//!
//!     for group in warnings {
//!         let (element, warnings) = group.to_parts();
//!         eprintln!("  {}", element.path());
//!
//!         for warning in warnings {
//!             eprintln!("    - {warning}");
//!         }
//!     }
//! }
//!
//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
//! ```
//!
//! ## Price a CDR using tariff in separate JSON file
//!
//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
//! following code:
//!
//! ```rust
//! # use ocpi_tariffs::{cdr, price, tariff, warning, Version};
//! #
//! # const CDR_JSON: &str = include_str!("cdr.json");
//! # const TARIFF_JSON: &str = include_str!("tariff.json");
//!
//! let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
//! let cdr::ParseReport {
//!     cdr,
//!     unexpected_fields,
//! } = report;
//!
//! # if !unexpected_fields.is_empty() {
//! #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
//! #
//! #     for path in &unexpected_fields {
//! #         eprintln!("{path}");
//! #     }
//! # }
//!
//! let tariff::ParseReport {
//!     tariff,
//!     unexpected_fields,
//! } = tariff::parse_with_version(TARIFF_JSON, Version::V211).unwrap();
//! let report = cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), chrono_tz::Tz::Europe__Amsterdam).unwrap();
//! let (report, warnings) = report.into_parts();
//!
//! if !warnings.is_empty() {
//!     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
//!
//!     for group in warnings {
//!         let (element, warnings) = group.to_parts();
//!         eprintln!("  {}", element.path());
//!
//!         for warning in warnings {
//!             eprintln!("    - {warning}");
//!         }
//!     }
//! }
//!
//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
//! ```
//!
//! ## Lint a tariff
//!
//! ```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>>(())
//! ```

#[cfg(test)]
mod test;

#[cfg(test)]
mod test_reasonable_str;

#[cfg(test)]
mod test_rust_decimal_arbitrary_precision;

pub mod cdr;
pub mod country;
pub mod currency;
pub mod datetime;
pub mod duration;
mod energy;
pub mod enumeration;
pub mod generate;
pub mod guess;
pub mod json;
pub mod lint;
pub mod money;
pub mod number;
pub mod price;
pub mod string;
pub mod tariff;
pub mod timezone;
mod v211;
mod v221;
pub mod warning;
pub mod weekday;

use std::{collections::BTreeSet, fmt};

use warning::IntoCaveat;
use weekday::Weekday;

#[doc(inline)]
pub use duration::{ToDuration, ToHoursDecimal};

#[doc(inline)]
pub use energy::{Ampere, Kw, Kwh};

#[doc(inline)]
use enumeration::{Enum, IntoEnum};

#[doc(inline)]
pub use money::{Cost, Money, Price, Vat};

#[doc(inline)]
pub use warning::{Caveat, Verdict, VerdictExt, Warning};

/// Set of unexpected fields encountered while parsing a CDR or tariff.
pub type UnexpectedFields = BTreeSet<String>;

/// The Id for a tariff used in the pricing of a CDR.
pub type TariffId = String;

/// The OCPI versions supported by this crate.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Version {
    /// OCPI version 2.2.1.
    ///
    /// See: <https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes>.
    V221,

    /// OCPI version 2.1.1.
    ///
    /// See: <https://github.com/ocpi/ocpi/tree/release-2.1.1-bugfixes>.
    V211,
}

impl Versioned for Version {
    fn version(&self) -> Version {
        *self
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Version::V221 => f.write_str("v221"),
            Version::V211 => f.write_str("v211"),
        }
    }
}

/// An object for a specific OCPI [`Version`].
pub trait Versioned: fmt::Debug {
    /// Return the OCPI `Version` of this object.
    fn version(&self) -> Version;
}

/// An object with an uncertain [`Version`].
pub trait Unversioned: fmt::Debug {
    /// The concrete [`Versioned`] type.
    type Versioned: Versioned;

    /// Forced an [`Unversioned`] object to be the given [`Version`].
    ///
    /// This does not change the structure of the OCPI object.
    /// It simply relabels the object as a different OCPI Version.
    ///
    /// Use this with care.
    fn force_into_versioned(self, version: Version) -> Self::Versioned;
}

/// Errors that can happen if a JSON str is parsed.
pub struct ParseError {
    /// The type of object we were trying to deserialize.
    object: ObjectType,

    /// The error that occurred while deserializing.
    kind: ParseErrorKind,
}

/// The kind of Error that occurred.
#[derive(Debug)]
pub enum ParseErrorKind {
    /// Some Error types are erased to avoid leaking dependencies.
    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),

    /// The integrated JSON parser was unable to parse a JSON str.
    Json(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 ParseErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Internal(_) => f.write_str("internal"),
            Self::Json(error) => write!(f, "{error}"),
            Self::ShouldBeAnObject => f.write_str("The element should be an object."),
            Self::SizeExceedsMax => write!(
                f,
                "The input `&str` exceeds the reasonable maximum `{} MB`.",
                ReasonableStr::FACTOR
            ),
        }
    }
}

impl Warning for ParseErrorKind {
    fn id(&self) -> warning::Id {
        match self {
            Self::Internal(_) => warning::Id::from_static("internal"),
            Self::Json(error) => error.id(),
            Self::ShouldBeAnObject => warning::Id::from_static("should_be_an_object"),
            Self::SizeExceedsMax => warning::Id::from_static("size_exceeds_max"),
        }
    }
}

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

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

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "while deserializing {:?}: ", self.object)?;

        match &self.kind {
            ParseErrorKind::Internal(err) => write!(f, "{err}"),
            ParseErrorKind::Json(err) => write!(f, "{err}"),
            ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
            ParseErrorKind::SizeExceedsMax => write!(
                f,
                "The input `&str` exceeds the reasonable maximum `{} MB`.",
                ReasonableStr::MAX_STR_INPUT_LEN
            ),
        }
    }
}

impl ParseError {
    /// Create a [`ParseError`] from a generic std Error for a CDR object.
    fn from_cdr_err(err: json::Error) -> Self {
        Self {
            object: ObjectType::Cdr,
            kind: ParseErrorKind::Json(err),
        }
    }

    /// Create a [`ParseError`] from a generic std Error for a tariff object.
    fn from_tariff_err(err: json::Error) -> Self {
        Self {
            object: ObjectType::Tariff,
            kind: ParseErrorKind::Json(err),
        }
    }

    /// Create a [`ParseError`] for a CDR that isn't a JSON object.
    fn cdr_should_be_object() -> ParseError {
        Self {
            object: ObjectType::Cdr,
            kind: ParseErrorKind::ShouldBeAnObject,
        }
    }

    /// Create a [`ParseError`] for a CDR that isn't a JSON object.
    fn tariff_should_be_object() -> ParseError {
        Self {
            object: ObjectType::Tariff,
            kind: ParseErrorKind::ShouldBeAnObject,
        }
    }

    /// Return a function that creates a [`ParseError`] from a kind.
    fn from_kind(object: ObjectType) -> impl FnOnce(ParseErrorKind) -> Self {
        move |kind| Self { object, kind }
    }

    /// Return the `ParseErrorKind`.
    pub fn kind(&self) -> &ParseErrorKind {
        &self.kind
    }

    /// Deconstruct the error into it's `ObjectType` and error kind.
    pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
        (self.object, self.kind)
    }
}

/// A `str` that is checked for having a reasonable size.
#[derive(Copy, Clone)]
struct ReasonableStr<'buf>(&'buf str);

impl<'buf> ReasonableStr<'buf> {
    /// One million bytes of information.
    const MEGA: usize = 1_000_000;

    /// Limit the string to this many MB.
    const FACTOR: usize = 5;

    /// The maximum allowed size for a `str` given to a parse function.
    ///
    /// If the input `str` exceeds this size, a [`ParseErrorKind::SizeExceedsMax`] is returned.
    ///
    /// NOTE: Currently the largest tariff at `NLENE` is ~440 KB and the largest CDR is ~1.1 MB.
    ///
    /// NOTE: The motivation for a limit is to avoid parsing unseasonably large JSON objects
    /// whether supplied through incompetence or maliciousness. Large JSON objects can be constructed
    /// to have many warnings. This could bog down the function processing the JSON object.
    const MAX_STR_INPUT_LEN: usize = Self::FACTOR * Self::MEGA;

    /// Create new `ReasonableStr` object.
    fn new(s: &'buf str) -> Result<ReasonableStr<'buf>, ParseErrorKind> {
        if s.len() >= Self::MAX_STR_INPUT_LEN {
            return Err(ParseErrorKind::SizeExceedsMax);
        }

        Ok(Self(s))
    }

    /// Unpack the contained `str`.
    fn into_inner(self) -> &'buf str {
        self.0
    }
}

/// The type of OCPI objects that can be parsed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ObjectType {
    /// An OCPI Charge Detail Record.
    ///
    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
    Cdr,

    /// An OCPI tariff.
    ///
    /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>).
    Tariff,
}

impl fmt::Display for ObjectType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ObjectType::Cdr => f.write_str("CDR"),
            ObjectType::Tariff => f.write_str("tariff"),
        }
    }
}

/// Add two types together and saturate to max if the addition operation overflows.
///
/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
trait SaturatingAdd {
    /// Add two types together and saturate to max if the addition operation overflows.
    #[must_use]
    fn saturating_add(self, other: Self) -> Self;
}

/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
///
/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
trait SaturatingSub {
    /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
    #[must_use]
    fn saturating_sub(self, other: Self) -> Self;
}

/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
struct DisplayOption<T>(Option<T>)
where
    T: fmt::Display;

impl<T> fmt::Display for DisplayOption<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            Some(v) => fmt::Display::fmt(v, f),
            None => f.write_str("∅"),
        }
    }
}
