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

use crate::{
    define_enum_from_json, expect_array_or_bail, expect_object_or_bail,
    json::{self, FieldsAsExt as _, FromJson as _},
    number::FromDecimal as _,
    parse_nullable_or_bail,
    price::{v221, PeriodRange, Warning},
    required_field_or_bail,
    warning::{self, GatherWarnings as _, IntoCaveat as _},
    Enum, IntoEnum, Kwh, Money, Price, ToDuration, Verdict,
};

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

    /// Stop timestamp of the charging session.
    stop_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.
    charging_periods: Vec<ChargingPeriod>,

    /// Total cost of this transaction.
    total_cost: Decimal,

    /// Total energy charged, in kWh.
    total_energy: Decimal,

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

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

/// The CDR object describes the Charging Session and its costs. How these costs are build up etc.
///
/// The tariffs field is split off as it's used apart from the rest of the fields in the `Cdr` object.
///
/// See: [OCPI spec 2.1.1 spec: CDR]<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>.
pub struct WithTariffs<'buf> {
    /// The CDR object describes the Charging Session and its costs. How these costs are build up etc.
    ///
    /// See: [OCPI spec 2.1.1 spec: CDR]<https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>.
    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).
    tariffs: Vec<json::Element<'buf>>,
}

/// The volume that has been consumed for a specific dimension during a charging period.
#[derive(Debug, Clone)]
struct Dimension {
    /// The dimension type.
    dimension_type: DimensionType,

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

/// The volume that has been consumed for a specific dimension during a charging period.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum DimensionType {
    /// Consumed energy in `kWh`.
    Energy,

    /// Flat fee, no unit.
    Flat,

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

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

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

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

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("flat") {
            Self::Flat
        } 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("parking_time") {
            Self::ParkingTime
        } else if s.eq_ignore_ascii_case("time") {
            Self::Time
        } else {
            return Enum::Unknown(s.to_owned());
        };

        Enum::Known(dt)
    }
}

define_enum_from_json!(DimensionType, display_name: "dimension type", warning_id: "dimension_type");

/// A single charging period, containing a nonempty list of charge dimensions.
#[derive(Clone, Debug)]
struct ChargingPeriod {
    /// Start timestamp of the charging period. This period ends when a next period starts, the
    /// last period ends when the session ends.
    start_date_time: DateTime<Utc>,

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

impl From<Cdr> for v221::Cdr {
    fn from(cdr: Cdr) -> Self {
        let Cdr {
            start_date_time,
            stop_date_time,
            charging_periods,
            total_cost,
            total_energy,
            duration_charging,
            duration_idle,
        } = cdr;

        Self {
            end_date_time: stop_date_time,
            start_date_time,
            charging_periods: charging_periods
                .into_iter()
                .map(ChargingPeriod::into)
                .collect(),
            totals: v221::cdr::Totals {
                cost: Price {
                    excl_vat: Money::from_decimal(total_cost),
                    // The v211 tariffs can't determine VAT.
                    incl_vat: None,
                },
                energy: Kwh::from_decimal(total_energy),
                energy_cost: None,
                duration_charging,
                duration_charging_cost: None,
                fixed_cost: None,
                duration_idle,
                duration_idle_cost: None,
            },
        }
    }
}

impl<'a> From<WithTariffs<'a>> for v221::cdr::WithTariffs<'a> {
    fn from(value: WithTariffs<'a>) -> Self {
        let WithTariffs { cdr, tariffs } = value;

        Self {
            cdr: cdr.into(),
            tariffs,
        }
    }
}

impl From<ChargingPeriod> for v221::cdr::ChargingPeriod {
    fn from(period: ChargingPeriod) -> Self {
        let ChargingPeriod {
            start_date_time,
            dimensions,
        } = period;
        let dimensions = dimensions
            .into_iter()
            .filter_map(|d| {
                let Dimension {
                    dimension_type,
                    volume,
                } = d;

                if let DimensionType::Flat = dimension_type {
                    // We can safely ignore the flat dimension since this can be determined from the tariff and
                    // period time-stamps.
                    return None;
                }

                let dimension_type = match dimension_type {
                    DimensionType::Energy => v221::cdr::DimensionType::Energy,
                    DimensionType::MaxCurrent => v221::cdr::DimensionType::MaxCurrent,
                    DimensionType::MinCurrent => v221::cdr::DimensionType::MinCurrent,
                    DimensionType::ParkingTime => v221::cdr::DimensionType::ParkingTime,
                    DimensionType::Time => v221::cdr::DimensionType::Time,
                    DimensionType::Flat => return None,
                };

                Some(v221::cdr::Dimension {
                    dimension_type,
                    volume,
                })
            })
            .collect();
        Self {
            start_date_time,
            dimensions,
        }
    }
}

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 warnings = warning::Set::<Warning>::new();

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

        let (cdr, warnings) = cdr_from_fields(elem, &fields)?.into_parts();

        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::default()
        };

        let cdr = WithTariffs { cdr, tariffs };

        Ok(cdr.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();

    // Get refs to the required and optional fields.
    let start_date_time_elem = required_field_or_bail!(elem, fields, "start_date_time", warnings);
    let stop_date_time_elem = required_field_or_bail!(elem, fields, "stop_date_time", warnings);
    let charging_periods_elem = required_field_or_bail!(elem, fields, "charging_periods", warnings);
    let total_cost_elem = required_field_or_bail!(elem, fields, "total_cost", warnings);
    let total_energy_elem = required_field_or_bail!(elem, fields, "total_energy", warnings);
    let total_time_elem = required_field_or_bail!(elem, fields, "total_time", warnings);

    let start_date_time =
        DateTime::from_json(start_date_time_elem)?.gather_warnings_into(&mut warnings);

    let stop_date_time =
        DateTime::from_json(stop_date_time_elem)?.gather_warnings_into(&mut 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_cost = Decimal::from_json(total_cost_elem)?.gather_warnings_into(&mut warnings);
    let total_energy = Decimal::from_json(total_energy_elem)?.gather_warnings_into(&mut warnings);
    let total_time = Decimal::from_json(total_time_elem)?.gather_warnings_into(&mut warnings);
    let total_parking_time =
        parse_nullable_or_bail!(fields, "total_parking_time", Decimal, warnings);

    let cdr_range = start_date_time..stop_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,
        stop_date_time,
        charging_periods,
        total_cost,
        total_energy,
        duration_charging: total_time.to_duration(),
        duration_idle: total_parking_time.as_ref().map(ToDuration::to_duration),
    };

    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 volume_elem = required_field_or_bail!(elem, fields, "volume", warnings);
        let volume = Decimal::from_json(volume_elem)?.gather_warnings_into(&mut warnings);

        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`, or `TIME`"
                    ),
                );
                return Ok(None.into_caveat(warnings));
            }
        };

        let dimension = Dimension {
            dimension_type,
            volume,
        };

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