//! Generate a CDR from a tariff.

#[cfg(test)]
mod test;

#[cfg(test)]
mod test_clamp_date_time_span;

#[cfg(test)]
mod test_gen_time_events;

#[cfg(test)]
mod test_generate;

#[cfg(test)]
mod test_generate_from_single_elem_tariff;

#[cfg(test)]
mod test_local_to_utc;

#[cfg(test)]
mod test_periods;

#[cfg(test)]
mod test_power_to_time;

#[cfg(test)]
mod test_popular_tariffs;

mod v2x;

use std::{
    cmp::{max, min},
    fmt,
    ops::Range,
};

use chrono::{DateTime, Datelike as _, NaiveDateTime, NaiveTime, TimeDelta, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use tracing::{debug, instrument, warn};

use crate::{
    country, currency,
    duration::{AsHms as _, ToHoursDecimal},
    energy::{Ampere, Kw, Kwh},
    from_warning_all,
    json::FromJson as _,
    number::{FromDecimal as _, RoundDecimal as _},
    price, tariff,
    warning::{self, GatherWarnings as _, IntoCaveat as _, WithElement as _},
    Price, SaturatingAdd as _, ToDuration as _, Version, Versioned as _,
};

/// The minimum duration of a CDR. Anything below this will result in an Error.
const MIN_CS_DURATION_SECS: i64 = 120;

type DateTimeSpan = Range<DateTime<Utc>>;
pub type Verdict<T> = crate::Verdict<T, Warning>;
pub type Caveat<T> = warning::Caveat<T, Warning>;

/// Return the value if `Some`. Otherwise, bail(return) with an `Error::Internal` containing the giving message.
macro_rules! some_dec_or_bail {
    ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
        match $opt {
            Some(v) => v,
            None => {
                return $warnings.bail($elem.as_element(), Warning::Decimal($msg));
            }
        }
    };
}

/// Return the value if `Some`. Otherwise, bail(return) with an `Error::Internal` containing the giving message.
macro_rules! some_time_delta_or_bail {
    ($elem:expr, $opt:expr, $warnings:expr, $msg:literal) => {
        match $opt {
            Some(v) => v,
            None => {
                return $warnings.bail($elem.as_element(), Warning::TimeDelta($msg));
            }
        }
    };
}

/// The outcome of calling [`crate::cdr::generate_from_tariff`].
#[derive(Debug)]
pub struct Report {
    /// The ID of the parsed tariff.
    pub tariff_id: String,

    // The currency code of the parsed tariff.
    pub tariff_currency_code: currency::Code,

    /// A partial CDR that can be fleshed out by the caller.
    ///
    /// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
    /// does not know anything about the EVSE location or the token used to authenticate the chargesession.
    ///
    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
    pub partial_cdr: PartialCdr,
}

/// A partial CDR generated by the `cdr_from_tariff` function.
///
/// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
/// does not know anything about the EVSE location or the token used to authenticate the chargesession.
///
/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
/// * See: [OCPI spec 2.1.1: Tariff](https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md).
#[derive(Debug)]
pub struct PartialCdr {
    /// ISO-3166 alpha-2 country code of the CPO that 'owns' this CDR.
    pub currency_code: currency::Code,

    /// The five character ID of the CPO that 'owns' this CDR.
    ///
    /// 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.
    ///
    /// None if a v211 tariff was used to generate the CDR.
    /// The v211 tariff does not define a country code or `party_id` field.
    pub party_id: Option<CpoId>,

    /// Start timestamp of the charging session.
    pub start_date_time: DateTime<Utc>,

    /// End timestamp of the charging session.
    pub end_date_time: DateTime<Utc>,

    /// Total energy charged, in kWh.
    pub total_energy: Option<Kwh>,

    /// Total duration charging.
    ///
    /// Some if the charging happened during the session.
    pub total_charging_duration: Option<TimeDelta>,

    /// Total duration not charging.
    ///
    /// Some if there was idle time during the session.
    pub total_idle_duration: Option<TimeDelta>,

    /// Total cost of this transaction.
    pub total_cost: Option<Price>,

    /// Total cost related to the energy dimension.
    pub total_energy_cost: Option<Price>,

    /// Total cost of the flat dimension.
    pub total_fixed_cost: Option<Price>,

    /// Total cost related to the idle time dimension.
    pub total_idle_duration_cost: Option<Price>,

    /// Total cost related to the charging time dimension.
    pub total_charging_duration_cost: Option<Price>,

    /// List of charging periods that make up this charging session. A session should consist of 1 or
    /// more periods, where each period has a different relevant Tariff.
    pub charging_periods: Vec<ChargingPeriod>,
}

/// 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 struct CpoId {
    /// The ISO-3166 alpha-2 country code.
    pub country_code: country::Code,

    /// The ISO-15118 ID.
    pub id: String,
}

impl<'buf> From<tariff::CpoId<'buf>> for CpoId {
    fn from(value: tariff::CpoId<'buf>) -> Self {
        let tariff::CpoId { country_code, id } = value;
        CpoId {
            country_code,
            id: id.to_string(),
        }
    }
}

/// Display the CPO ID formatted like `NLENE`.
impl fmt::Display for CpoId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.country_code.into_alpha_2_str(), self.id)
    }
}

/// A single charging period, containing a nonempty list of charge dimensions.
///
/// * See: [OCPI spec 2.2.1: CDR ChargingPeriod](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#146-chargingperiod-class>).
#[derive(Debug)]
pub struct ChargingPeriod {
    /// Start timestamp of the charging period. This period ends when a next period starts, the
    /// last period ends when the session ends.
    pub start_date_time: DateTime<Utc>,

    /// List of relevant values for this charging period.
    pub dimensions: Vec<Dimension>,

    /// Unique identifier of the Tariff that is relevant for this Charging Period.
    /// In the OCPI spec the `tariff_id` field is optional but, we always know the tariff ID
    /// when generating a CDR.
    pub tariff_id: Option<String>,
}

/// The volume that has been consumed for a specific dimension during a charging period.
///
/// * See: [OCPI spec 2.2.1: CDR Dimension](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#mod_cdrs_cdrdimension_class>).
#[derive(Debug)]
pub struct Dimension {
    pub dimension_type: DimensionType,

    /// Volume of the dimension consumed, measured according to the dimension type.
    pub volume: Decimal,
}

/// The volume that has been consumed for a specific dimension during a charging period.
///
/// * See: [OCPI spec 2.2.1 CDR DimensionType](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc#mod_cdrs_cdrdimension_class>).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DimensionType {
    /// Consumed energy in `kWh`.
    Energy,

    /// The peak current, in 'A', during this period.
    MaxCurrent,

    /// The lowest current, in `A`, during this period.
    MinCurrent,

    /// The maximum power, in 'kW', reached during this period.
    MaxPower,

    /// The minimum power, in 'kW', reached during this period.
    MinPower,

    /// The parking time, in hours, consumed in this period.
    ParkingTime,

    /// The reservation time, in hours, consumed in this period.
    ReservationTime,

    /// The charging time, in hours, consumed in this period.
    Time,
}

/// The config for generating a CDR from a tariff.
#[derive(Clone)]
pub struct Config {
    /// The timezone of the EVSE: The timezone where the chargesession took place.
    pub timezone: chrono_tz::Tz,

    /// The end date of the generated CDR.
    pub end_date_time: DateTime<Utc>,

    /// The maximum DC current that can be delivered to the battery.
    pub max_current_supply_amp: Decimal,

    /// The amount of energy(kWh) the requested to be delivered.
    ///
    /// We don't model charging curves for the battery, so we don't care about the existing change of
    /// the battery.
    pub requested_kwh: Decimal,

    /// The maximum DC power(kw) that can be delivered to the battery.
    ///
    /// This is modeled as a DC system as we don't care if the delivery medium is DC or one of the
    /// various AC forms. We only care what the effective DC power is. The caller of `cdr_from_tariff`
    /// should convert the delivery medium into a DC kw power by using a power factor.
    ///
    /// In practice the maximum power bottleneck is either the EVSE, the cable or the battery itself.
    /// But whatever the bottleneck is, the caller should work that out and set the maximum expected.
    pub max_power_supply_kw: Decimal,

    /// The start date of the generated CDR.
    pub start_date_time: DateTime<Utc>,
}

/// Generate a CDR from a given tariff.
pub fn cdr_from_tariff(tariff_elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<Report> {
    let mut warnings = warning::Set::new();
    // To generate a CDR from a tariff first, the tariff is parsed into structured data.
    // Then some broad metrics are calculated that define limits on the chargesession.
    //
    // A Timeline of Events is then constructed by generating Events for each Element and each restriction.
    // Some restrictions are periodic and can result in many `Event`s.
    //
    // The `Timeline` of `Event`s are then sorted by time and converted into a list of `ChargePeriods`.
    let (metrics, timezone) = metrics(tariff_elem, config)?.gather_warnings_into(&mut warnings);

    let tariff = match tariff_elem.version() {
        Version::V211 => {
            let tariff = tariff::v211::Tariff::from_json(tariff_elem.as_element())?
                .gather_warnings_into(&mut warnings);

            tariff::v221::Tariff::from(tariff)
        }
        Version::V221 => tariff::v221::Tariff::from_json(tariff_elem.as_element())?
            .gather_warnings_into(&mut warnings),
    };

    if !is_tariff_active(&metrics.start_date_time, &tariff) {
        warnings.insert(tariff_elem.as_element(), tariff::Warning::NotActive.into());
    }

    let timeline = timeline(timezone, &metrics, &tariff);
    let charging_periods = charge_periods(&metrics, timeline);

    let report = price::periods(metrics.end_date_time, timezone, &tariff, charging_periods)
        .with_element(tariff_elem.as_element())?
        .gather_warnings_into(&mut warnings);

    let price::PeriodsReport {
        billable: _,
        periods,
        totals,
        total_costs,
    } = report;

    let charging_periods = periods
        .into_iter()
        .map(|period| {
            let price::PeriodReport {
                start_date_time,
                end_date_time: _,
                dimensions,
            } = period;
            let duration_charging = dimensions.duration_charging.as_ref().map(|dim| Dimension {
                dimension_type: DimensionType::Time,
                volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
            });
            let duration_idle = dimensions.duration_idle.as_ref().map(|dim| Dimension {
                dimension_type: DimensionType::ParkingTime,
                volume: ToHoursDecimal::to_hours_dec_in_ocpi_precision(&dim.volume),
            });
            let energy = dimensions.energy.as_ref().map(|dim| Dimension {
                dimension_type: DimensionType::Energy,
                volume: dim.volume.into(),
            });
            let dimensions = vec![energy, duration_idle, duration_charging]
                .into_iter()
                .flatten()
                .collect();

            ChargingPeriod {
                start_date_time,
                dimensions,
                tariff_id: Some(tariff.id.to_string()),
            }
        })
        .collect();

    let mut total_cost = total_costs.total();

    if let Some(total_cost) = total_cost.as_mut() {
        if let Some(min_price) = tariff.min_price {
            if *total_cost < min_price {
                *total_cost = min_price;
                warnings.insert(
                    tariff_elem.as_element(),
                    tariff::Warning::TotalCostClampedToMin.into(),
                );
            }
        }

        if let Some(max_price) = tariff.max_price {
            if *total_cost > max_price {
                *total_cost = max_price;
                warnings.insert(
                    tariff_elem.as_element(),
                    tariff::Warning::TotalCostClampedToMax.into(),
                );
            }
        }
    }

    let report = Report {
        tariff_id: tariff.id.to_string(),
        tariff_currency_code: tariff.currency,
        partial_cdr: PartialCdr {
            party_id: tariff.party_id.map(CpoId::from),
            start_date_time: metrics.start_date_time,
            end_date_time: metrics.end_date_time,
            currency_code: tariff.currency,
            total_energy: totals.energy.round_to_ocpi_scale(),
            total_charging_duration: totals.duration_charging,
            total_idle_duration: totals.duration_idle,
            total_cost: total_cost.round_to_ocpi_scale(),
            total_energy_cost: total_costs.energy.round_to_ocpi_scale(),
            total_fixed_cost: total_costs.fixed.round_to_ocpi_scale(),
            total_idle_duration_cost: total_costs.duration_idle.round_to_ocpi_scale(),
            total_charging_duration_cost: total_costs.duration_charging.round_to_ocpi_scale(),
            charging_periods,
        },
    };

    Ok(report.into_caveat(warnings))
}

/// An `Event` collector that filters any `Event`s that are after the `session_end_time`.
struct EventCollector {
    /// The duration of the session, there is no point in adding events that occur after this.
    session_duration: TimeDelta,

    /// The list of `Event`s generated from the tariff.
    events: Vec<Event>,
}

impl EventCollector {
    /// Create a new `Event` collector from the duration of a session.
    fn with_session_duration(session_duration: TimeDelta) -> Self {
        Self {
            session_duration,
            events: vec![],
        }
    }

    /// Add an `Event` to the list if the start duration is within the session duration.
    fn push(&mut self, duration_from_start: TimeDelta, event_kind: EventKind) {
        if duration_from_start <= self.session_duration {
            self.events.push(Event {
                duration_from_start,
                kind: event_kind,
            });
        }
    }

    /// Consume the collector and return the list of `Event`s.
    fn into_inner(self) -> Vec<Event> {
        self.events
    }
}

/// Make a `Timeline` of `Event`s using the `Metric`s and `Tariff`.
fn timeline(
    timezone: chrono_tz::Tz,
    metrics: &Metrics,
    tariff: &tariff::v221::Tariff<'_>,
) -> Timeline {
    let Metrics {
        start_date_time: cdr_start,
        end_date_time: cdr_end,
        duration_charging,
        duration_parking,
        max_power_supply,
        max_current_supply,

        energy_supplied: _,
    } = metrics;

    let mut events = {
        let session_duration = duration_parking.map(|d| duration_charging.saturating_add(d));
        let mut events =
            EventCollector::with_session_duration(session_duration.unwrap_or(*duration_charging));

        events.push(TimeDelta::seconds(0), EventKind::SessionStart);
        events.push(*duration_charging, EventKind::ChargingEnd);

        if let Some(dt) = session_duration {
            events.push(
                dt,
                EventKind::ParkingEnd {
                    start: *duration_charging,
                },
            );
        }

        events
    };

    // True if `min_current` or `max_current` restrictions are defined.
    // Then we set current to be consumed for each period.
    let mut emit_current = false;

    // True if `min_power` or `max_power` restrictions are defined.
    // Then we set power to be consumed for each period.
    let mut emit_power = false;

    for elem in &tariff.elements {
        // Elements with a `reservation` restriction set never apply.
        // Applying them could result in inaccurate pricing.
        if elem
            .restrictions
            .as_ref()
            .is_some_and(|r| r.reservation.is_some())
        {
            continue;
        }

        if let Some((time_restrictions, energy_restrictions)) = elem
            .restrictions
            .as_ref()
            .map(tariff::v221::Restrictions::restrictions_by_category)
        {
            generate_time_events(
                &mut events,
                timezone,
                *cdr_start..*cdr_end,
                time_restrictions,
            );

            let v2x::EnergyRestrictions {
                min_kwh,
                max_kwh,
                min_current,
                max_current,
                min_power,
                max_power,
            } = energy_restrictions;

            if !emit_current {
                // If the generator current is contained within the restriction, then we set
                // an amount of current to be consumed for each period.
                //
                // Note: The generator supplies maximum current.
                emit_current = (min_current..=max_current).contains(&Some(*max_current_supply));
            }

            if !emit_power {
                // If the generator power is contained within the restriction, then we set
                // an amount of power to be consumed for each period.
                //
                // Note: The generator supplies maximum power.
                emit_power = (min_power..=max_power).contains(&Some(*max_power_supply));
            }

            generate_energy_events(
                &mut events,
                metrics.duration_charging,
                metrics.energy_supplied,
                min_kwh,
                max_kwh,
            );
        }
    }

    let events = events.into_inner();

    Timeline {
        events,
        emit_current,
        emit_power,
    }
}

/// Generate a list of `Event`s based on the `TimeRestrictions` an `Element` has.
fn generate_time_events(
    events: &mut EventCollector,
    timezone: chrono_tz::Tz,
    cdr_span: DateTimeSpan,
    restrictions: v2x::TimeRestrictions,
) {
    const MIDNIGHT: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0)
        .expect("The hour, minute and second values are correct and hardcoded");
    const ONE_DAY: TimeDelta = TimeDelta::days(1);

    let v2x::TimeRestrictions {
        start_time,
        end_time,
        start_date,
        end_date,
        min_duration,
        max_duration,
        weekdays,
    } = restrictions;

    let cdr_duration = cdr_span.end.signed_duration_since(cdr_span.start);

    if let Some(dt) = min_duration {
        if cdr_duration > dt {
            events.push(dt, EventKind::MinDuration);
        }
    }

    if let Some(dt) = max_duration {
        if cdr_duration > dt {
            events.push(dt, EventKind::MaxDuration);
        }
    }

    // Here we create the `NaiveDateTime` range by combining the `start_date` (`NaiveDate`) and
    // `start_time` (`NaiveTime`) and the associated `end_date` and `end_time`.
    //
    // If `start_time` or `end_time` are `None` then their respective `NaiveDate` is combined
    // with the `NaiveTime` of `00:00:00` to form a `NaiveDateTime`.
    //
    // If the `end_time < start_time` then the period wraps around to the following day.
    //
    // See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#146-tariffrestrictions-class>
    let (start_date_time, end_date_time) =
        if let (Some(start_time), Some(end_time)) = (start_time, end_time) {
            if end_time < start_time {
                (
                    start_date.map(|d| d.and_time(start_time)),
                    end_date.map(|d| {
                        let (end_time, _) = end_time.overflowing_add_signed(ONE_DAY);
                        d.and_time(end_time)
                    }),
                )
            } else {
                (
                    start_date.map(|d| d.and_time(start_time)),
                    end_date.map(|d| d.and_time(end_time)),
                )
            }
        } else {
            (
                start_date.map(|d| d.and_time(start_time.unwrap_or(MIDNIGHT))),
                end_date.map(|d| d.and_time(end_time.unwrap_or(MIDNIGHT))),
            )
        };

    // If `start_date` or `end_date` is set we clamp the cdr_span to those dates.
    // As we are not going to produce any events before `start_date` or after `end_date`.
    let event_span = clamp_date_time_span(
        start_date_time.and_then(|d| local_to_utc(timezone, d)),
        end_date_time.and_then(|d| local_to_utc(timezone, d)),
        cdr_span,
    );

    if let Some(start_time) = start_time {
        gen_naive_time_events(
            events,
            &event_span,
            timezone,
            start_time,
            &weekdays,
            EventKind::StartTime,
        );
    }

    if let Some(end_time) = end_time {
        gen_naive_time_events(
            events,
            &event_span,
            timezone,
            end_time,
            &weekdays,
            EventKind::EndTime,
        );
    }
}

/// Convert a `NaiveDateTime` to a `DateTime<Utc>` using the local timezone.
///
/// Return Some `DateTime<Utc>` if the conversion from `NaiveDateTime` results in either a single
/// or ambiguous `DateTime`. If the conversion is _ambiguous_ due to a _fold_ in the local time,
/// then we return the earliest `DateTime`.
fn local_to_utc(timezone: chrono_tz::Tz, date_time: NaiveDateTime) -> Option<DateTime<Utc>> {
    use chrono::offset::LocalResult;

    let result = date_time.and_local_timezone(timezone);

    let local_date_time = match result {
        LocalResult::Single(d) => d,
        LocalResult::Ambiguous(earliest, _latest) => earliest,
        LocalResult::None => return None,
    };

    Some(local_date_time.to_utc())
}

/// Generate `Event`s for the `start_time` or `end_time` restriction.
fn gen_naive_time_events(
    events: &mut EventCollector,
    event_span: &Range<DateTime<Utc>>,
    timezone: chrono_tz::Tz,
    time: NaiveTime,
    weekdays: &v2x::WeekdaySet,
    kind: EventKind,
) {
    let local_start_time = event_span.start.with_timezone(&timezone).time();
    let time_delta = time.signed_duration_since(local_start_time);
    let cdr_duration = event_span.end.signed_duration_since(event_span.start);

    // If the time is before the local session start, advance to the next day's occurrence.
    let time_delta = if time_delta.num_seconds().is_negative() {
        time_delta.saturating_add(TimeDelta::days(1))
    } else {
        time_delta
    };

    // If the start delta is still negative after moving it forward 24 hours
    if time_delta.num_seconds().is_negative() {
        return;
    }

    // The time is after the CDR start.
    let Some(remainder) = cdr_duration.checked_sub(&time_delta) else {
        warn!("TimeDelta overflow");
        return;
    };

    if remainder.num_seconds().is_positive() {
        let duration_from_start = time_delta;
        let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
            warn!("Date out of range");
            return;
        };

        if weekdays.contains(date.weekday()) {
            // The time is before the CDR end.
            events.push(time_delta, kind);
        }

        for day in 1..=remainder.num_days() {
            let Some(duration_from_start) = time_delta.checked_add(&TimeDelta::days(day)) else {
                warn!("Date out of range");
                break;
            };
            let Some(date) = event_span.start.checked_add_signed(duration_from_start) else {
                warn!("Date out of range");
                break;
            };

            if weekdays.contains(date.weekday()) {
                events.push(duration_from_start, kind);
            }
        }
    }
}

/// Generate a list of `Event`s based on the `TimeRestrictions` an `Element` has.
fn generate_energy_events(
    events: &mut EventCollector,
    duration_charging: TimeDelta,
    energy_supplied: Kwh,
    min_kwh: Option<Kwh>,
    max_kwh: Option<Kwh>,
) {
    if let Some(dt) = min_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
    {
        events.push(dt, EventKind::MinKwh);
    }

    if let Some(dt) = max_kwh.and_then(|kwh| power_to_time(kwh, energy_supplied, duration_charging))
    {
        events.push(dt, EventKind::MaxKwh);
    }
}

/// Map power usage to time presuming a linear power consumption.
#[instrument]
fn power_to_time(power: Kwh, power_total: Kwh, duration_total: TimeDelta) -> Option<TimeDelta> {
    // Handle power == power_total as a special case to avoid loss of precision
    // due to TimeDelta -> Decimal -> TimeDelta conversion.
    if power == power_total {
        return Some(duration_total);
    }

    // Find the time that the `min_kwh` amount of power was reached.
    // It has to be within the charging time.
    let power = Decimal::from(power);
    // The total power supplied during the chargesession
    let power_total = Decimal::from(power_total);

    // The factor minimum of the total power supplied.
    let Some(factor) = power.checked_div(power_total) else {
        return Some(TimeDelta::zero());
    };

    if factor.is_sign_negative() || factor > dec!(1.0) {
        return None;
    }

    let hours_dec = duration_total.to_hours_dec();
    let duration_from_start = factor.checked_mul(hours_dec)?;
    Some(duration_from_start.to_duration())
}

/// Generate a list of charging periods for the given tariffs timeline.
fn charge_periods(metrics: &Metrics, timeline: Timeline) -> Vec<price::Period> {
    /// Keep track of whether we are charging or parking.
    enum ChargingPhase {
        Charging,
        Parking,
    }

    let Metrics {
        start_date_time: cdr_start,
        max_power_supply,
        max_current_supply,

        end_date_time: _,
        duration_charging: _,
        duration_parking: _,
        energy_supplied: _,
    } = metrics;

    let Timeline {
        mut events,
        emit_current,
        emit_power,
    } = timeline;

    events.sort_unstable_by_key(|e| e.duration_from_start);

    let mut periods = vec![];
    let emit_current = emit_current.then_some(*max_current_supply);
    let emit_power = emit_power.then_some(*max_power_supply);
    // Charging starts instantly in this model.
    let mut charging_phase = ChargingPhase::Charging;

    for items in events.windows(2) {
        let [event, event_next] = items else {
            unreachable!("The window size is 2");
        };

        let Event {
            duration_from_start,
            kind,
        } = event;

        if let EventKind::ChargingEnd = kind {
            charging_phase = ChargingPhase::Parking;
        }

        let Some(duration) = event_next
            .duration_from_start
            .checked_sub(duration_from_start)
        else {
            warn!("TimeDelta overflow");
            break;
        };

        let Some(start_date_time) = cdr_start.checked_add_signed(*duration_from_start) else {
            warn!("TimeDelta overflow");
            break;
        };

        let consumed = if let ChargingPhase::Charging = charging_phase {
            let Some(energy) =
                Decimal::from(*max_power_supply).checked_mul(duration.to_hours_dec())
            else {
                warn!("Decimal overflow");
                break;
            };
            price::Consumed {
                duration_charging: Some(duration),
                duration_idle: None,
                energy: Some(Kwh::from_decimal(energy)),
                current_max: emit_current,
                current_min: emit_current,
                power_max: emit_power,
                power_min: emit_power,
            }
        } else {
            price::Consumed {
                duration_charging: None,
                duration_idle: Some(duration),
                energy: None,
                current_max: None,
                current_min: None,
                power_max: None,
                power_min: None,
            }
        };

        let period = price::Period {
            start_date_time,
            consumed,
        };

        periods.push(period);
    }

    periods
}

/// A `DateTimeSpan` bounded by a minimum and a maximum.
///
/// If the input `DateTimeSpan` is less than `min_date` then this returns `min_date`.
/// If input is greater than `max_date` then this returns `max_date`.
/// Otherwise, this returns input `DateTimeSpan`.
fn clamp_date_time_span(
    min_date: Option<DateTime<Utc>>,
    max_date: Option<DateTime<Utc>>,
    span: DateTimeSpan,
) -> DateTimeSpan {
    // Make sure the `min_date` is the earlier of the `min`, max pair.
    let (min_date, max_date) = (min(min_date, max_date), max(min_date, max_date));

    let start = min_date.filter(|d| &span.start < d).unwrap_or(span.start);
    let end = max_date.filter(|d| &span.end > d).unwrap_or(span.end);

    DateTimeSpan { start, end }
}

/// A timeline of events that are used to generate the `ChargePeriods` of the CDR.
struct Timeline {
    /// The list of `Event`s generated from the tariff.
    events: Vec<Event>,

    /// The current is within the \[`min_current`..`max_current`\] range.
    emit_current: bool,

    /// The power is within the \[`min_power`..`max_power`\] range.
    emit_power: bool,
}

/// An event at a time along the timeline.
struct Event {
    /// The duration of the Event from the start of the timeline/chargesession.
    duration_from_start: TimeDelta,

    /// The kind of Event.
    kind: EventKind,
}

impl fmt::Debug for Event {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Event")
            .field("duration_from_start", &self.duration_from_start.as_hms())
            .field("kind", &self.kind)
            .finish()
    }
}

/// The kind of `Event`.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum EventKind {
    /// The moment a session starts.
    ///
    /// This is added to the list of `Event`s so that the algorithm to generate the `ChargingPeriods`
    /// can iterate over the `Event`s using a window of size 2. The first iteration will always have
    /// `SessionStart` as the first window element and the `Event` of interest as the second.
    SessionStart,

    /// The moment charging ends.
    ///
    /// Charging starts at time 0. When `ChargingEnd`s, parking starts.
    /// This could also be the last `Event` of the chargesession.
    ChargingEnd,

    /// The moment Parking ends.
    ///
    /// This could also be the last `Event` of the chargesession.
    /// If a `ParkingEnd` `Event` is present in the `Timeline` then a `ChargingEnd` `Event` will precede it.
    ParkingEnd {
        /// The parking started when `ChargingEnd`ed.
        start: TimeDelta,
    },

    StartTime,

    EndTime,

    /// Minimum duration in seconds the Charging Session MUST last (inclusive).
    ///
    /// When the duration of a Charging Session is longer than the defined value, this `TariffElement` is or becomes active.
    /// Before that moment, this `TariffElement` is not yet active.
    MinDuration,

    /// Maximum duration in seconds the Charging Session MUST last (exclusive).
    ///
    /// When the duration of a Charging Session is shorter than the defined value, this `TariffElement` is or becomes active.
    /// After that moment, this `TariffElement` is no longer active.
    MaxDuration,

    /// Minimum consumed energy in kWh, for example 20, valid from this amount of energy (inclusive) being used.
    MinKwh,

    /// Maximum consumed energy in kWh, for example 50, valid until this amount of energy (exclusive) being used.
    MaxKwh,
}

impl fmt::Debug for EventKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::SessionStart => write!(f, "SessionStart"),
            Self::ChargingEnd => write!(f, "ChargingEnd"),
            Self::ParkingEnd { start } => f
                .debug_struct("ParkingEnd")
                .field("start", &start.as_hms())
                .finish(),
            Self::StartTime => write!(f, "StartTime"),
            Self::EndTime => write!(f, "EndTime"),
            Self::MinDuration => write!(f, "MinDuration"),
            Self::MaxDuration => write!(f, "MaxDuration"),
            Self::MinKwh => write!(f, "MinKwh"),
            Self::MaxKwh => write!(f, "MaxKwh"),
        }
    }
}

/// Broad metrics calculated about the chargesession which is given as input for generating a `Timeline` of `Event`s.
#[derive(Debug)]
struct Metrics {
    /// The end date the generated CDR.
    end_date_time: DateTime<Utc>,

    /// The start date the generated CDR.
    start_date_time: DateTime<Utc>,

    /// The time spent charging the battery.
    ///
    /// Charging begins instantly and continues without interruption until the battery is full or the
    /// session time has elapsed.
    duration_charging: TimeDelta,

    /// The time spent parking after charging the battery.
    ///
    /// This duration may be `None` if the battery did not finish charging within the session time.
    duration_parking: Option<TimeDelta>,

    /// The energy that's supplied during the charging period.
    energy_supplied: Kwh,

    /// The maximum DC current that can be delivered to the battery.
    max_current_supply: Ampere,

    /// The maximum DC power(kw) that can be delivered to the battery.
    max_power_supply: Kw,
}

/// Validate the `Config` and compute various `Metrics` based on the `Config`s fields.
#[instrument(skip_all)]
fn metrics(elem: &tariff::Versioned<'_>, config: &Config) -> Verdict<(Metrics, chrono_tz::Tz)> {
    let warnings = warning::Set::new();

    let Config {
        start_date_time,
        end_date_time,
        max_power_supply_kw,
        requested_kwh: max_energy_battery_kwh,
        max_current_supply_amp,
        timezone,
    } = config;
    let duration_session = end_date_time.signed_duration_since(start_date_time);

    debug!("duration_session: {}", duration_session.as_hms());

    // Duration must be positive, if the end time is before the start the conversion will fail.
    if duration_session.abs() != duration_session {
        return warnings.bail(elem.as_element(), Warning::StartDateTimeIsAfterEndDateTime);
    }

    if duration_session.num_seconds() < MIN_CS_DURATION_SECS {
        return warnings.bail(elem.as_element(), Warning::DurationBelowMinimum);
    }

    if max_energy_battery_kwh.is_zero() {
        return warnings.bail(elem.as_element(), Warning::RequestedKwhIsZero);
    }

    // The time needed to charge the battery = battery_capacity(kWh) / power(kw)
    let duration_full_charge = some_dec_or_bail!(
        elem,
        max_energy_battery_kwh.checked_div(*max_power_supply_kw),
        warnings,
        "Unable to calculate charging time"
    )
    .to_duration();
    debug!("duration_full_charge: {}", duration_full_charge.as_hms());

    // The charge duration taking into account that the end of the session can occur before the battery is fully charged.
    let duration_charging = TimeDelta::min(duration_full_charge, duration_session);

    let energy_supplied_kwh = some_dec_or_bail!(
        elem,
        max_power_supply_kw.checked_mul(duration_charging.to_hours_dec()),
        warnings,
        "Unable to calculate the energy supplied during the charging time"
    );

    let duration_parking = some_time_delta_or_bail!(
        elem,
        duration_session.checked_sub(&duration_charging),
        warnings,
        "Unable to calculate `idle_duration`"
    );

    debug!(
        "duration_charging: {}, duration_parking: {}",
        duration_charging.as_hms(),
        duration_parking.as_hms()
    );

    let metrics = Metrics {
        end_date_time: *end_date_time,
        start_date_time: *start_date_time,
        duration_charging,
        duration_parking: Some(duration_parking).filter(|dt| dt.num_seconds().is_positive()),
        energy_supplied: Kwh::from_decimal(energy_supplied_kwh),
        max_current_supply: Ampere::from_decimal(*max_current_supply_amp),
        max_power_supply: Kw::from_decimal(*max_power_supply_kw),
    };

    Ok((metrics, *timezone).into_caveat(warnings))
}

fn is_tariff_active(cdr_start: &DateTime<Utc>, tariff: &tariff::v221::Tariff<'_>) -> bool {
    match (tariff.start_date_time, tariff.end_date_time) {
        (None, None) => true,
        (None, Some(end)) => (..end).contains(cdr_start),
        (Some(start), None) => (start..).contains(cdr_start),
        (Some(start), Some(end)) => (start..end).contains(cdr_start),
    }
}

#[derive(Debug)]
pub enum Warning {
    /// A `Decimal` operation failed.
    Decimal(&'static str),

    /// The duration of the chargesession is below the minimum allowed.
    DurationBelowMinimum,

    Price(price::Warning),

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

    /// The `requested_kwh` in the `Config` is zero.
    RequestedKwhIsZero,

    Tariff(tariff::Warning),

    /// A `TimeDelta` operation failed.
    TimeDelta(&'static str),
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::Decimal(_) => warning::Id::from_static("decimal_error"),
            Self::DurationBelowMinimum => warning::Id::from_static("duration_below_minimum"),
            Self::Price(kind) => kind.id(),
            Self::StartDateTimeIsAfterEndDateTime => {
                warning::Id::from_static("start_time_after_end_time")
            }
            Self::RequestedKwhIsZero => warning::Id::from_static("requested_kwh_is_zero"),
            Self::TimeDelta(_) => warning::Id::from_static("timedelta_error"),
            Self::Tariff(kind) => kind.id(),
        }
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Decimal(msg) | Self::TimeDelta(msg) => f.write_str(msg),
            Self::DurationBelowMinimum => write!(
                f,
                "The duration of the chargesession is below the minimum: {MIN_CS_DURATION_SECS}"
            ),
            Self::Price(warnings) => {
                write!(f, "Price warnings: {warnings:?}")
            }
            Self::StartDateTimeIsAfterEndDateTime => {
                write!(f, "The `start_date_time` is after the `end_date_time`")
            }
            Self::RequestedKwhIsZero => write!(f, "The `requested_kwh` in the `Config` is zero"),
            Self::Tariff(warnings) => {
                write!(f, "Tariff warnings: {warnings:?}")
            }
        }
    }
}

from_warning_all!(
    tariff::Warning => Warning::Tariff,
    price::Warning => Warning::Price
);
