use chrono::{DateTime, TimeDelta, Utc};
use rust_decimal::Decimal;

use super::super::Consumed;
use crate::{
    country, currency, expect_array_or_bail, expect_object_or_bail,
    json::{self, FieldsAsExt as _, FromJson as _},
    money::PriceOrNumber,
    number::FromDecimal as _,
    parse_nullable_or_bail, parse_required_or_bail,
    price::{Period, PeriodRange, Warning},
    required_field, required_field_or_bail, string,
    warning::{self, GatherWarnings as _, IntoCaveat as _, WithElement as _},
    Ampere, Enum, IntoEnum, Kw, Kwh, Price, ToDuration as _, Verdict,
};

/// The CDR object describes the Charging Session and its costs. How these costs are build up etc.
///
/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
#[derive(Debug)]
pub struct Cdr {
    /// Start timestamp of the charging session.
    pub start_date_time: DateTime<Utc>,

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

    /// 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>,

    pub totals: Totals,
}

#[derive(Debug)]
pub struct Totals {
    /// Total cost of this transaction.
    pub cost: Price,

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

    /// Total energy charged, in kWh.
    pub energy: Kwh,

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

    /// Total time charging, in hours.
    pub duration_charging: TimeDelta,

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

    /// Total time not charging, in hours.
    pub duration_idle: Option<TimeDelta>,

    /// Total cost related to the `parking`/idle time dimension.
    pub duration_idle_cost: Option<Price>,
}

/// The volume that has been consumed for a specific dimension during a charging period.
#[derive(Debug, Clone)]
pub(crate) 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.
#[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.
    ///
    /// NOTE: We use the term `idle` in variables and fields instead of `parking` to avoid confusion
    /// in the definition of parking. The OCPI spec defines `parking` as the time spent not charging.
    ParkingTime,

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

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

/// 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(Clone, 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>,
}

/// The [`Cdr`] object describes the charging session and how costs are built up.
///
/// The `tariffs` array is split off from the [`Cdr`] as it's used apart from the rest of the fields.
///
/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
pub struct WithTariffs<'buf> {
    /// The [`Cdr`] object describes the charging session and how costs are built up.
    ///
    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
    pub cdr: Cdr,

    /// List of relevant tariff elements.
    ///
    /// The tariffs have already been checked for unknown fields when the CDR JSON was parsed into
    /// a [`cdr::Versioned`](crate::cdr::Versioned).
    pub tariffs: Vec<json::Element<'buf>>,
}

impl WithTariffs<'_> {
    /// Discard the CDR's internal tariffs.
    pub fn discard_tariffs(self) -> Cdr {
        self.cdr
    }
}

impl<'buf> json::FromJson<'buf> for Cdr {
    type Warning = Warning;

    /// Parse the JSON element into a structured CDR.
    ///
    /// Validate the CDR and sanitize the data so that the next phase can make presumptions about
    /// the data. Presumptions such as; the charging periods are sorted.
    ///
    /// Return warnings if the CDR isn't internally consistent.
    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
        let warnings = warning::Set::<Warning>::new();

        let fields = expect_object_or_bail!(elem, warnings);
        let fields = fields.as_raw_map();

        cdr_from_fields(elem, &fields)
    }
}

impl<'buf> json::FromJson<'buf> for WithTariffs<'buf> {
    type Warning = Warning;

    /// Parse the JSON element into a structured CDR.
    ///
    /// Validate the CDR and sanitize the data so that the next phase can make presumptions about
    /// the data. Presumptions such as; the charging periods are sorted.
    ///
    /// Return warnings if the CDR isn't internally consistent.
    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::<Warning>::new();

        let fields = expect_object_or_bail!(elem, warnings);
        let fields = fields.as_raw_map();

        let cdr = cdr_from_fields(elem, &fields)?.gather_warnings_into(&mut warnings);
        let tariffs_elem = fields.get("tariffs");

        let tariffs = if let Some(elem) = tariffs_elem {
            let tariffs = expect_array_or_bail!(elem, warnings);
            tariffs.to_vec()
        } else {
            vec![]
        };

        Ok(WithTariffs { cdr, tariffs }.into_caveat(warnings))
    }
}

fn cdr_from_fields<'buf>(
    elem: &json::Element<'buf>,
    fields: &json::RawRefMap<'_, 'buf>,
) -> Verdict<Cdr, Warning> {
    let mut warnings = warning::Set::<Warning>::new();

    {
        // We don't use the `country_code`, `currency` or `party_id` while pricing
        // but there's no harm in warning the user about issues with them.
        if let Some(elem) = required_field!(elem, fields, "country_code", warnings) {
            let code_set = country::CodeSet::from_json(elem).gather_warnings_into(&mut warnings)?;

            if let country::CodeSet::Alpha3(_) = code_set {
                warnings.insert(elem, Warning::CountryShouldBeAlpha2);
            }
        }
        if let Some(elem) = required_field!(elem, fields, "currency", warnings) {
            let _ignore_value = currency::Code::from_json(elem).gather_warnings_into(&mut warnings);
        }

        if let Some(elem) = required_field!(elem, fields, "party_id", warnings) {
            let _ignore_value =
                string::CiExactLen::<'_, 3>::from_json(elem).gather_warnings_into(&mut warnings);
        }
    }

    let start_date_time =
        parse_required_or_bail!(elem, fields, "start_date_time", DateTime<Utc>, warnings);
    let end_date_time =
        parse_required_or_bail!(elem, fields, "end_date_time", DateTime<Utc>, warnings);
    let total_cost =
        parse_required_or_bail!(elem, fields, "total_cost", PriceOrNumber, warnings).into_inner();
    let total_energy = parse_required_or_bail!(elem, fields, "total_energy", Kwh, warnings);
    let total_time = parse_required_or_bail!(elem, fields, "total_time", Decimal, warnings);

    let charging_periods_elem = required_field_or_bail!(elem, fields, "charging_periods", warnings);
    let charging_periods = expect_array_or_bail!(charging_periods_elem, warnings)
        .iter()
        .map(ChargingPeriod::from_json)
        .collect::<Result<Vec<_>, _>>()?;

    let mut charging_periods = charging_periods.gather_warnings_into(&mut warnings);

    if charging_periods.is_empty() {
        return warnings.bail(charging_periods_elem, Warning::NoPeriods);
    }

    let total_parking_time =
        parse_nullable_or_bail!(fields, "total_parking_time", Decimal, warnings);
    let total_fixed_cost =
        parse_nullable_or_bail!(fields, "total_fixed_cost", PriceOrNumber, warnings)
            .map(PriceOrNumber::into_inner);
    let total_energy_cost =
        parse_nullable_or_bail!(fields, "total_energy_cost", PriceOrNumber, warnings)
            .map(PriceOrNumber::into_inner);
    let total_time_cost =
        parse_nullable_or_bail!(fields, "total_time_cost", PriceOrNumber, warnings)
            .map(PriceOrNumber::into_inner);
    let total_parking_cost =
        parse_nullable_or_bail!(fields, "total_parking_cost", PriceOrNumber, warnings)
            .map(PriceOrNumber::into_inner);

    let cdr_range = start_date_time..end_date_time;
    charging_periods.sort_unstable_by_key(|p| p.start_date_time);

    match charging_periods.as_slice() {
        [] => (),
        [period] => {
            if !cdr_range.contains(&period.start_date_time) {
                warnings.insert(
                    elem,
                    Warning::PeriodsOutsideStartEndDateTime {
                        cdr_range,
                        period_range: PeriodRange::Single(period.start_date_time),
                    },
                );
            }
        }
        [period_earliest, .., period_latest] => {
            let period_range = period_earliest.start_date_time..period_latest.start_date_time;

            if !(cdr_range.contains(&period_range.start) && cdr_range.contains(&period_range.end)) {
                warnings.insert(
                    elem,
                    Warning::PeriodsOutsideStartEndDateTime {
                        cdr_range,
                        period_range: PeriodRange::Many(period_range),
                    },
                );
            }
        }
    }

    let cdr = Cdr {
        start_date_time,
        end_date_time,
        charging_periods,
        totals: Totals {
            cost: total_cost,
            fixed_cost: total_fixed_cost,
            energy: total_energy,
            energy_cost: total_energy_cost,
            duration_charging: total_time.to_duration(),
            duration_charging_cost: total_time_cost,
            duration_idle: total_parking_time.map(|d| d.to_duration()),
            duration_idle_cost: total_parking_cost,
        },
    };

    Ok(cdr.into_caveat(warnings))
}

impl json::FromJson<'_> for ChargingPeriod {
    type Warning = Warning;

    fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::<Warning>::new();
        let fields = expect_object_or_bail!(elem, warnings);
        let fields = fields.as_raw_map();

        let start_date_time_elem =
            required_field_or_bail!(elem, fields, "start_date_time", warnings);
        let start_date_time =
            DateTime::from_json(start_date_time_elem)?.gather_warnings_into(&mut warnings);

        let dimensions_elem = required_field_or_bail!(elem, fields, "dimensions", warnings);

        // If the `DimensionType` of a `Dimension` is unknown the entire `Dimension` will be returned as `None`.
        // There can still be Warnings generated from the `Ok` and `Err` paths from the `from_json` call.
        // Warnings in the `Err` path will cause an early return.
        let dimensions = expect_array_or_bail!(dimensions_elem, warnings)
            .iter()
            .map(Option::<Dimension>::from_json)
            .collect::<Result<Vec<_>, _>>()?;

        // This collection has all Results resolved but still might contain Dimensions with
        // unknown `DimensionType`s. We gather up these Warnings and flatten the `None` Dimensions.
        // This leaves us with a collection of `Dimensions` where the `DimensionType` is known.
        let dimensions = dimensions
            .gather_warnings_into(&mut warnings)
            .into_iter()
            .flatten()
            .collect();

        let elem = Self {
            start_date_time,
            dimensions,
        };

        Ok(elem.into_caveat(warnings))
    }
}

impl json::FromJson<'_> for Option<Dimension> {
    type Warning = Warning;

    fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::<Warning>::new();
        let fields = expect_object_or_bail!(elem, warnings);
        let fields = fields.as_raw_map();

        let type_elem = required_field_or_bail!(elem, fields, "type", warnings);
        let dimension_type =
            Enum::<DimensionType>::from_json(type_elem)?.gather_warnings_into(&mut warnings);

        let dimension_type = match dimension_type {
            Enum::Known(v) => v,
            Enum::Unknown(s) => {
                warnings.insert(
                    type_elem,
                    Warning::field_invalid_value(s,
                        "A CDR DimensionType should be one of `ENERGY`, `MAX_CURRENT`, `MIN_CURRENT`, `MAX_POWER`, `MIN_POWER`, `PARKING_TIME`, `RESERVATION_TIME` or `TIME`"
                    ),
                );
                return Ok(None.into_caveat(warnings));
            }
        };

        let volume_elem = required_field_or_bail!(elem, fields, "volume", warnings);
        let volume = Decimal::from_json(volume_elem)?.gather_warnings_into(&mut warnings);

        let dimension = Dimension {
            dimension_type,
            volume,
        };

        Ok(Some(dimension).into_caveat(warnings))
    }
}

impl IntoEnum for DimensionType {
    fn enum_from_str(s: &str) -> Enum<DimensionType> {
        let dt = if s.eq_ignore_ascii_case("energy") {
            Self::Energy
        } else if s.eq_ignore_ascii_case("max_current") {
            Self::MaxCurrent
        } else if s.eq_ignore_ascii_case("min_current") {
            Self::MinCurrent
        } else if s.eq_ignore_ascii_case("max_power") {
            Self::MaxPower
        } else if s.eq_ignore_ascii_case("min_power") {
            Self::MinPower
        } else if s.eq_ignore_ascii_case("parking_time") {
            Self::ParkingTime
        } else if s.eq_ignore_ascii_case("reservation_time") {
            Self::ReservationTime
        } else if s.eq_ignore_ascii_case("time") {
            Self::Time
        } else {
            return Enum::Unknown(s.to_owned());
        };

        Enum::Known(dt)
    }
}

impl json::FromJson<'_> for Enum<DimensionType> {
    type Warning = Warning;

    fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::new();

        let Some(s) = elem.to_raw_str() else {
            return warnings.bail(
                elem,
                Warning::FieldInvalidType {
                    expected_type: json::ValueKind::String,
                },
            );
        };

        let s = s
            .decode_escapes()
            .with_element(elem)
            .gather_warnings_into(&mut warnings);
        let dt = DimensionType::enum_from_str(&s);
        Ok(dt.into_caveat(warnings))
    }
}

impl From<ChargingPeriod> for Period {
    fn from(period: ChargingPeriod) -> Self {
        let ChargingPeriod {
            start_date_time,
            dimensions,
        } = period;
        let mut consumed = Consumed {
            current_max: None,
            current_min: None,
            duration_charging: None,
            duration_idle: None,
            energy: None,
            power_max: None,
            power_min: None,
        };

        for dimension in dimensions {
            let Dimension {
                dimension_type,
                volume,
            } = dimension;

            match dimension_type {
                DimensionType::MinCurrent => {
                    consumed.current_min = Some(Ampere::from_decimal(volume));
                }
                DimensionType::MaxCurrent => {
                    consumed.current_max = Some(Ampere::from_decimal(volume));
                }
                DimensionType::MaxPower => {
                    consumed.power_max = Some(Kw::from_decimal(volume));
                }
                DimensionType::MinPower => {
                    consumed.power_min = Some(Kw::from_decimal(volume));
                }
                DimensionType::Energy => {
                    consumed.energy = Some(Kwh::from_decimal(volume));
                }
                DimensionType::Time => {
                    consumed.duration_charging = Some(volume.to_duration());
                }
                DimensionType::ParkingTime => {
                    consumed.duration_idle = Some(volume.to_duration());
                }
                DimensionType::ReservationTime => {
                    // The pricer does not use reservation time.
                }
            }
        }

        Period {
            start_date_time,
            consumed,
        }
    }
}
