//! 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, explain, from_warning_all, guess, json,
    lint, money, number, schema, string,
    warning::{self, Caveat, IntoCaveat as _},
};

#[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),

    /// A tariff element has a `reservation` restriction (`RESERVATION` or `RESERVATION_EXPIRES`).
    ///
    /// Such elements apply only to reservation sessions, not to regular charging sessions. Because
    /// reservation pricing is not supported, the element is treated as permanently inactive.
    ///
    /// * See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#mod_tariffs_reservationrestrictiontype_enum>
    ReservationElementSkipped,

    /// 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::ReservationElementSkipped => f.write_str(
                "A tariff element has a `reservation` restriction and will not apply to regular \
                 charging sessions. Reservation pricing is not supported.",
            ),
            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::ReservationElementSkipped => {
                warning::Id::from_static("reservation_element_skipped")
            }
            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>,
}

/// Infer which OCPI [`Version`] a tariff [`json::Document`] is, without validating it.
///
/// Use this when the version of the tariff is not known up front. The [`json::Document`] is
/// obtained by calling [`json::parse_object`]. The returned [`guess::TariffVersion`] is either
/// [`Certain`](guess::Version::Certain) or [`Uncertain`](guess::Version::Uncertain) about the version.
///
/// To check the tariff against the OCPI schema for a known [`Version`], use [`build`].
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{json, tariff, Version};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let doc = json::parse_object(TARIFF_JSON)?;
/// let tariff = tariff::infer_version(doc).certain_or(Version::V221);
///
/// # Ok::<(), json::ParseError>(())
/// ```
pub fn infer_version(json: json::Document<'_>) -> guess::TariffVersion<'_> {
    guess::tariff_version(json)
}

/// Build and validate a [`json::Document`] against the OCPI tariff schema for the given [`Version`][^spec-v211][^spec-v221].
///
/// The [`json::Document`] is obtained by calling [`json::parse_object`]. Any unexpected, missing,
/// or wrongly typed fields are reported as a [`warning::Set`] of [`schema::Warning`]s carried by
/// the returned [`Caveat`].
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{json, tariff, Version};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let doc = json::parse_object(TARIFF_JSON)?;
/// let (tariff, warnings) = tariff::build(doc, Version::V211).into_parts();
///
/// if !warnings.is_empty() {
///     eprintln!("The tariff has `{}` schema warnings.", warnings.len_warnings());
/// }
///
/// # Ok::<(), json::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 build(
    json: json::Document<'_>,
    version: crate::Version,
) -> Caveat<Versioned<'_>, schema::Warning> {
    let (version, warnings) = match version {
        crate::Version::V221 => {
            let (tariff, warnings) = schema::v221::build_tariff(&json).into_parts();
            (Version::V221(tariff), warnings)
        }
        crate::Version::V211 => {
            let (tariff, warnings) = schema::v211::build_tariff(&json).into_parts();
            (Version::V211(tariff), warnings)
        }
    };
    let versioned = Versioned { doc: json, version };
    versioned.into_caveat(warnings)
}

/// Validate a [`VersionedJson`] against the OCPI tariff schema for its known [`Version`].
///
/// Use this when the [`Version`] has already been resolved - for example a
/// [`VersionedJson`] obtained from [`infer_version`] via [`certain_or`](guess::Version::certain_or).
pub fn build_versioned(json: VersionedJson<'_>) -> Caveat<Versioned<'_>, schema::Warning> {
    let VersionedJson { doc, version } = json;
    build(doc, version)
}

/// A `json::Document` that has been processed by [`infer_version`] and has been identified
/// as being a concrete [`Version`].
#[derive(Clone)]
pub struct VersionedJson<'buf> {
    /// The parsed JSON.
    doc: json::Document<'buf>,

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

/// A `json::Document` that has been processed by [`build`] or [`build_versioned`].
#[derive(Clone)]
pub struct Versioned<'buf> {
    /// The parsed JSON.
    doc: json::Document<'buf>,

    /// The `Version` of the tariff, determined during parsing.
    version: Version<'buf>,
}

impl fmt::Debug for Versioned<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            match &self.version {
                Version::V211(tariff) => fmt::Debug::fmt(&tariff, f),
                Version::V221(tariff) => fmt::Debug::fmt(&tariff, 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) -> crate::Version {
        match self.version {
            Version::V211(_) => crate::Version::V211,
            Version::V221(_) => crate::Version::V221,
        }
    }
}

impl<'buf> Versioned<'buf> {
    /// Return the inner [`json::Document`] and discard the version info.
    pub fn into_doc(self) -> json::Document<'buf> {
        self.doc
    }

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

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

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

#[expect(
    clippy::large_enum_variant,
    reason = "the v2.1.1 and v2.2.1 tariff IRs differ in size; this short-lived versioned \
              value is not worth boxing"
)]
#[derive(Clone)]
enum Version<'buf> {
    V211(schema::v211::Tariff<'buf>),
    V221(schema::v221::Tariff<'buf>),
}

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

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

impl<'buf> VersionedJson<'buf> {
    /// Create a new `Versioned` object.
    pub(crate) fn new(doc: json::Document<'buf>, version: crate::Version) -> Self {
        Self { doc, version }
    }

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

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

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

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

/// A [`json::Document`] that has been processed by [`infer_version`]
/// and was determined to not be one of the supported [`Version`]s.
#[derive(Debug)]
pub struct Unversioned<'buf> {
    doc: json::Document<'buf>,
}

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

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

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

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

    fn force_into_versioned(self, version: crate::Version) -> VersionedJson<'buf> {
        let Self { doc } = self;
        VersionedJson { doc, version }
    }
}

/// Lint the given tariff and return a [`lint::tariff::Report`] of any `Warning`s found.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{guess, json, tariff, warning};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let doc = json::parse_object(TARIFF_JSON)?;
/// let guess::Version::Certain(tariff) = tariff::infer_version(doc) else {
///     return Err("Unable to guess the version of given tariff JSON.".into());
/// };
/// let tariff = tariff::build_versioned(tariff).ignore_warnings();
///
/// 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)
}

/// Explain the given tariff in human language, returning the explanation as Markdown.
///
/// The tariff is parsed into the normalized `v2.2.1` form first, so a `v2.1.1` tariff is explained as
/// its `v2.2.1` equivalent. Warnings raised while parsing are returned alongside the explanation; a
/// hard parse failure returns an [`ErrorSet`](warning::ErrorSet) instead.
///
/// # Example
///
/// ```rust
/// # use ocpi_tariffs::{guess, json, tariff};
/// #
/// # const TARIFF_JSON: &str = include_str!("tariff.json");
///
/// let json = json::parse_object(TARIFF_JSON).unwrap();
/// let version = tariff::infer_version(json);
/// let tariff = tariff::build_versioned(version.certain_or_none().unwrap()).ignore_warnings();
///
/// let Ok(explanation) = tariff::explain(&tariff) else {
///     return Err("The tariff could not be parsed well enough to explain.".into());
/// };
///
/// println!("{}", explanation.ignore_warnings());
///
/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
/// ```
pub fn explain(tariff: &Versioned<'_>) -> crate::Verdict<String, Warning> {
    explain::tariff(tariff)
}
