#[cfg(test)]
mod tests;

#[cfg(test)]
mod tests_ir;

#[cfg(test)]
mod tests_cdr_ir;

use crate::{json, schema::Warning, Caveat};

use super::{build, BuilderKind, Enum, Field, Integrity, Number, Object, Scalar, Schema, Str};

// Maximum character lengths for OCPI string fields, as declared in the spec
// via the `string(n)` / `CiString(n)` notation.

// -- Identifiers ---------------------------------------------------------
const MAX_LEN_IDENTIFIER: usize = 36;
const MAX_LEN_LONG_IDENTIFIER: usize = 39;
const MAX_LEN_EVSE_ID: usize = 48;

// -- Codes ---------------------------------------------------------------
const MAX_LEN_CURRENCY: usize = 3;
const MAX_LEN_COUNTRY: usize = 3;
const MAX_LEN_LANGUAGE: usize = 2;

// -- Geographic and address ----------------------------------------------
const MAX_LEN_LATITUDE: usize = 10;
const MAX_LEN_LONGITUDE: usize = 11;
const MAX_LEN_POSTAL_CODE: usize = 10;
const MAX_LEN_ADDRESS: usize = 45;
const MAX_LEN_CITY: usize = 45;

// -- Free text and names -------------------------------------------------
/// Human-readable name of a location.
const MAX_LEN_NAME: usize = 255;
const MAX_LEN_URL: usize = 255;
const MAX_LEN_METER_ID: usize = 255;
const MAX_LEN_REMARK: usize = 255;
const MAX_LEN_TIME_ZONE: usize = 255;
const MAX_LEN_ENERGY_MIX_NAME: usize = 64;
const MAX_LEN_DISPLAY_TEXT: usize = 512;
const MAX_LEN_FLOOR_LEVEL: usize = 4;
const MAX_LEN_PHYSICAL_REFERENCE: usize = 16;

// Item schema for arrays of objects this layer does not model (e.g. `Image`,
// `StatusSchedule`, `AdditionalGeoLocation`).
static ANY: Schema = Schema::Scalar(Scalar::Any);

// -- Enum value sets ---------------------------------------------------------
// Permitted spellings for each OCPI 2.1.1 enum that appears in the CDR/Tariff
// graph, taken verbatim from the spec. They are matched case-insensitively.

// `ConnectorFormat` (mod_locations).
const CONNECTOR_FORMAT_VALUES: &[&str] = &["SOCKET", "CABLE"];

// `PowerType` (mod_locations).
const POWER_TYPE_VALUES: &[&str] = &["AC_1_PHASE", "AC_3_PHASE", "DC"];

// `ConnectorType` (mod_locations).
const CONNECTOR_TYPE_VALUES: &[&str] = &[
    "CHADEMO",
    "DOMESTIC_A",
    "DOMESTIC_B",
    "DOMESTIC_C",
    "DOMESTIC_D",
    "DOMESTIC_E",
    "DOMESTIC_F",
    "DOMESTIC_G",
    "DOMESTIC_H",
    "DOMESTIC_I",
    "DOMESTIC_J",
    "DOMESTIC_K",
    "DOMESTIC_L",
    "IEC_60309_2_single_16",
    "IEC_60309_2_three_16",
    "IEC_60309_2_three_32",
    "IEC_60309_2_three_64",
    "IEC_62196_T1",
    "IEC_62196_T1_COMBO",
    "IEC_62196_T2",
    "IEC_62196_T2_COMBO",
    "IEC_62196_T3A",
    "IEC_62196_T3C",
    "TESLA_R",
    "TESLA_S",
];

// Capability (mod_locations).
const CAPABILITY_VALUES: &[&str] = &[
    "CHARGING_PROFILE_CAPABLE",
    "CREDIT_CARD_PAYABLE",
    "REMOTE_START_STOP_CAPABLE",
    "RESERVABLE",
    "RFID_READER",
    "UNLOCK_CAPABLE",
];

// `ParkingRestriction` (mod_locations).
const PARKING_RESTRICTION_VALUES: &[&str] =
    &["EV_ONLY", "PLUGGED", "DISABLED", "CUSTOMERS", "MOTORCYCLES"];

// Status (mod_locations).
const STATUS_VALUES: &[&str] = &[
    "AVAILABLE",
    "BLOCKED",
    "CHARGING",
    "INOPERATIVE",
    "OUTOFORDER",
    "PLANNED",
    "REMOVED",
    "RESERVED",
    "UNKNOWN",
];

// Facility (mod_locations).
const FACILITY_VALUES: &[&str] = &[
    "HOTEL",
    "RESTAURANT",
    "CAFE",
    "MALL",
    "SUPERMARKET",
    "SPORT",
    "RECREATION_AREA",
    "NATURE",
    "MUSEUM",
    "BUS_STOP",
    "TAXI_STAND",
    "TRAIN_STATION",
    "AIRPORT",
    "CARPOOL_PARKING",
    "FUEL_STATION",
    "WIFI",
];

// `LocationType` (mod_locations).
const LOCATION_TYPE_VALUES: &[&str] = &[
    "ON_STREET",
    "PARKING_GARAGE",
    "UNDERGROUND_GARAGE",
    "PARKING_LOT",
    "OTHER",
    "UNKNOWN",
];

// `AuthMethod` (`mod_cdrs`).
const AUTH_METHOD_VALUES: &[&str] = &["AUTH_REQUEST", "WHITELIST"];

// `CdrDimensionType` (`mod_cdrs`).
const CDR_DIMENSION_TYPE_VALUES: &[&str] = &[
    "ENERGY",
    "FLAT",
    "MAX_CURRENT",
    "MIN_CURRENT",
    "PARKING_TIME",
    "TIME",
];

// `TariffDimensionType` (mod_tariffs).
const TARIFF_DIMENSION_TYPE_VALUES: &[&str] = &["ENERGY", "FLAT", "PARKING_TIME", "TIME"];

// `DayOfWeek` (mod_tariffs).
const DAY_OF_WEEK_VALUES: &[&str] = &[
    "MONDAY",
    "TUESDAY",
    "WEDNESDAY",
    "THURSDAY",
    "FRIDAY",
    "SATURDAY",
    "SUNDAY",
];

// `EnergySourceCategory` (mod_locations).
const ENERGY_SOURCE_CATEGORY_VALUES: &[&str] = &[
    "NUCLEAR",
    "GENERAL_FOSSIL",
    "COAL",
    "GAS",
    "GENERAL_GREEN",
    "SOLAR",
    "WIND",
    "WATER",
];

// `EnvironmentalImpactCategory` (mod_locations).
const ENVIRONMENTAL_IMPACT_CATEGORY_VALUES: &[&str] = &["NUCLEAR_WASTE", "CARBON_DIOXIDE"];

// Item schemas for arrays of enum values, which serialize as JSON strings.
static CAPABILITY: Schema = Schema::Scalar(Scalar::Enum(CAPABILITY_VALUES));
static PARKING_RESTRICTION: Schema = Schema::Scalar(Scalar::Enum(PARKING_RESTRICTION_VALUES));
static FACILITY: Schema = Schema::Scalar(Scalar::Enum(FACILITY_VALUES));
static DAY_OF_WEEK: Schema = Schema::Scalar(Scalar::Enum(DAY_OF_WEEK_VALUES));

static GEO_LOCATION_OBJ: Object = Object {
    // Latitude/longitude are decimal-degree strings in OCPI, not numbers.
    fields: &[
        Field::required("latitude", Scalar::StringMax(MAX_LEN_LATITUDE)),
        Field::required("longitude", Scalar::StringMax(MAX_LEN_LONGITUDE)),
    ],
    kind: BuilderKind::Ignore,
};

static DISPLAY_TEXT_OBJ: Object = Object {
    fields: &[
        Field::required("language", Scalar::StringMax(MAX_LEN_LANGUAGE)),
        Field::required("text", Scalar::StringMax(MAX_LEN_DISPLAY_TEXT)),
    ],
    kind: BuilderKind::Ignore,
};
static DISPLAY_TEXT: Schema = Schema::Object(&DISPLAY_TEXT_OBJ);

static ENERGY_SOURCE_OBJ: Object = Object {
    fields: &[
        Field::required("percentage", Scalar::Number),
        Field::required("source", Scalar::Enum(ENERGY_SOURCE_CATEGORY_VALUES)),
    ],
    kind: BuilderKind::Ignore,
};
static ENERGY_SOURCE: Schema = Schema::Object(&ENERGY_SOURCE_OBJ);

// In 2.1.1 the impact category field is named "source", not "category" (2.2.1).

static ENVIRONMENTAL_IMPACT_OBJ: Object = Object {
    fields: &[
        Field::required("amount", Scalar::Number),
        Field::required("source", Scalar::Enum(ENVIRONMENTAL_IMPACT_CATEGORY_VALUES)),
    ],
    kind: BuilderKind::Ignore,
};
static ENVIRONMENTAL_IMPACT: Schema = Schema::Object(&ENVIRONMENTAL_IMPACT_OBJ);

static ENERGY_MIX_OBJ: Object = Object {
    fields: &[
        Field::optional(
            "energy_product_name",
            Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME),
        ),
        Field::optional_array("energy_sources", &ENERGY_SOURCE),
        Field::optional_array("environ_impact", &ENVIRONMENTAL_IMPACT),
        Field::required("is_green_energy", Scalar::Boolean),
        Field::optional("supplier_name", Scalar::StringMax(MAX_LEN_ENERGY_MIX_NAME)),
    ],
    kind: BuilderKind::Ignore,
};

// -- Connector ---------------------------------------------------------------

static CONNECTOR_OBJ: Object = Object {
    fields: &[
        Field::required("amperage", Scalar::Number),
        Field::required("format", Scalar::Enum(CONNECTOR_FORMAT_VALUES)),
        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
        Field::required("last_updated", Scalar::String),
        Field::required("power_type", Scalar::Enum(POWER_TYPE_VALUES)),
        Field::required("standard", Scalar::Enum(CONNECTOR_TYPE_VALUES)),
        Field::optional("tariff_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
        Field::optional("terms_and_conditions", Scalar::StringMax(MAX_LEN_URL)),
        Field::required("voltage", Scalar::Number),
    ],
    kind: BuilderKind::Ignore,
};
static CONNECTOR: Schema = Schema::Object(&CONNECTOR_OBJ);

// -- EVSE --------------------------------------------------------------------

static EVSE_OBJ: Object = Object {
    fields: &[
        Field::optional_array("capabilities", &CAPABILITY),
        Field::required_array("connectors", &CONNECTOR),
        Field::optional_object("coordinates", &GEO_LOCATION_OBJ),
        Field::optional_array("directions", &DISPLAY_TEXT),
        Field::optional("evse_id", Scalar::StringMax(MAX_LEN_EVSE_ID)),
        Field::optional("floor_level", Scalar::StringMax(MAX_LEN_FLOOR_LEVEL)),
        Field::optional_array("images", &ANY),
        Field::required("last_updated", Scalar::String),
        Field::optional_array("parking_restrictions", &PARKING_RESTRICTION),
        Field::optional(
            "physical_reference",
            Scalar::StringMax(MAX_LEN_PHYSICAL_REFERENCE),
        ),
        Field::required("status", Scalar::Enum(STATUS_VALUES)),
        Field::optional_array("status_schedule", &ANY),
        Field::required("uid", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
    ],
    kind: BuilderKind::Ignore,
};
static EVSE: Schema = Schema::Object(&EVSE_OBJ);

// In 2.1.1 the CDR embeds a full `Location` snapshot (not a `CdrLocation` as in 2.2.1).
// `BusinessDetails` (`operator`/`suboperator`/`owner`) and Hours
// (`opening_times`) are opaque to this schema layer and validated as Any.

static LOCATION_OBJ: Object = Object {
    fields: &[
        Field::required("address", Scalar::StringMax(MAX_LEN_ADDRESS)),
        Field::optional("charging_when_closed", Scalar::Boolean),
        Field::required("city", Scalar::StringMax(MAX_LEN_CITY)),
        Field::required_object("coordinates", &GEO_LOCATION_OBJ),
        Field::required("country", Scalar::StringMax(MAX_LEN_COUNTRY)),
        Field::optional_array("directions", &DISPLAY_TEXT),
        Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
        Field::optional_array("evses", &EVSE),
        Field::optional_array("facilities", &FACILITY),
        Field::required("id", Scalar::StringMax(MAX_LEN_LONG_IDENTIFIER)),
        Field::optional_array("images", &ANY),
        Field::required("last_updated", Scalar::String),
        Field::optional("name", Scalar::StringMax(MAX_LEN_NAME)),
        Field::optional("opening_times", Scalar::Any),
        Field::optional("operator", Scalar::Any),
        Field::optional("owner", Scalar::Any),
        Field::required("postal_code", Scalar::StringMax(MAX_LEN_POSTAL_CODE)),
        Field::optional_array("related_locations", &ANY),
        Field::optional("suboperator", Scalar::Any),
        Field::optional("time_zone", Scalar::StringMax(MAX_LEN_TIME_ZONE)),
        Field::required("type", Scalar::Enum(LOCATION_TYPE_VALUES)),
    ],
    kind: BuilderKind::Ignore,
};

static CDR_DIMENSION_OBJ: Object = Object {
    fields: &[
        Field::required("type", Scalar::Enum(CDR_DIMENSION_TYPE_VALUES)),
        Field::required("volume", Scalar::Number),
    ],
    kind: BuilderKind::V211CdrDimension,
};
static CDR_DIMENSION: Schema = Schema::Object(&CDR_DIMENSION_OBJ);

// In 2.1.1 `ChargingPeriod` has no "tariff_id" field (added in 2.2.1).

static CHARGING_PERIOD_OBJ: Object = Object {
    fields: &[
        Field::required_array("dimensions", &CDR_DIMENSION),
        Field::required("start_date_time", Scalar::String),
    ],
    kind: BuilderKind::V211ChargingPeriod,
};
static CHARGING_PERIOD: Schema = Schema::Object(&CHARGING_PERIOD_OBJ);

// In 2.1.1 `PriceComponent` has no "vat" field (added in 2.2.1).

static PRICE_COMPONENT_OBJ: Object = Object {
    fields: &[
        Field::required("price", Scalar::Number),
        Field::required("step_size", Scalar::Number),
        Field::required("type", Scalar::Enum(TARIFF_DIMENSION_TYPE_VALUES)),
    ],
    kind: BuilderKind::V211PriceComponent,
};
static PRICE_COMPONENT: Schema = Schema::Object(&PRICE_COMPONENT_OBJ);

// In 2.1.1 `TariffRestrictions` has no `max_current`, `min_current`, or
// `reservation` fields (added in 2.2.1).

static TARIFF_RESTRICTIONS_OBJ: Object = Object {
    fields: &[
        Field::optional_array("day_of_week", &DAY_OF_WEEK),
        Field::optional("end_date", Scalar::String),
        Field::optional("end_time", Scalar::String),
        Field::optional("max_duration", Scalar::Number),
        Field::optional("max_kwh", Scalar::Number),
        Field::optional("max_power", Scalar::Number),
        Field::optional("min_duration", Scalar::Number),
        Field::optional("min_kwh", Scalar::Number),
        Field::optional("min_power", Scalar::Number),
        Field::optional("start_date", Scalar::String),
        Field::optional("start_time", Scalar::String),
    ],
    kind: BuilderKind::V211Restrictions,
};

static TARIFF_ELEMENT_OBJ: Object = Object {
    fields: &[
        Field::required_array("price_components", &PRICE_COMPONENT),
        Field::optional_object("restrictions", &TARIFF_RESTRICTIONS_OBJ),
    ],
    kind: BuilderKind::V211Element,
};
static TARIFF_ELEMENT: Schema = Schema::Object(&TARIFF_ELEMENT_OBJ);

// In 2.1.1 Tariff has no `country_code`, `end_date_time`, `max_price`,
// `min_price`, `party_id`, `start_date_time`, or `type` fields (added in 2.2.1).

static TARIFF_OBJ: Object = Object {
    fields: &[
        Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
        Field::required_array("elements", &TARIFF_ELEMENT),
        Field::optional_object("energy_mix", &ENERGY_MIX_OBJ),
        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
        Field::required("last_updated", Scalar::String),
        Field::optional_array("tariff_alt_text", &DISPLAY_TEXT),
        Field::optional("tariff_alt_url", Scalar::StringMax(MAX_LEN_URL)),
    ],
    kind: BuilderKind::V211Tariff,
};

/// OCPI 2.1.1 Tariff schema.
///
/// Validates top-level `Tariff` objects: required fields are `currency`,
/// `elements`, `id`, and `last_updated`.
static TARIFF: Schema = Schema::Object(&TARIFF_OBJ);

/// -- CDR ---------------------------------------------------------------------
/// Key differences from 2.2.1:
///   - "`auth_id`" (string) instead of a "`cdr_token`" object
///   - "location" (full embedded Location) instead of "`cdr_location`"
///   - "`stop_date_time`" instead of "`end_date_time`"
///   - "`total_cost`" is a plain number, not a Price object
///   - no "`country_code`", "`party_id`", "credit", "`credit_reference_id`",
///     "`home_charging_compensation`", "`invoice_reference_id`", "`session_id`",
///     "`signed_data`", "`authorization_reference`", or itemized cost fields
static CDR_OBJ: Object = Object {
    fields: &[
        Field::required("auth_id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
        Field::required("auth_method", Scalar::Enum(AUTH_METHOD_VALUES)),
        Field::required_array("charging_periods", &CHARGING_PERIOD),
        Field::required("currency", Scalar::StringMax(MAX_LEN_CURRENCY)),
        Field::required("id", Scalar::StringMax(MAX_LEN_IDENTIFIER)),
        Field::required("last_updated", Scalar::String),
        Field::required_object("location", &LOCATION_OBJ),
        Field::optional("meter_id", Scalar::StringMax(MAX_LEN_METER_ID)),
        Field::optional("remark", Scalar::StringMax(MAX_LEN_REMARK)),
        Field::required("start_date_time", Scalar::String),
        Field::required("stop_date_time", Scalar::String),
        Field::optional_array("tariffs", &TARIFF),
        Field::required("total_cost", Scalar::Number),
        Field::required("total_energy", Scalar::Number),
        Field::optional("total_parking_time", Scalar::Number),
        Field::required("total_time", Scalar::Number),
    ],
    kind: BuilderKind::V211Cdr,
};

/// OCPI 2.1.1 CDR (Charge Detail Record) schema.
///
/// Validates top-level `CDR` objects including all nested types: `Location`,
/// `EVSE`, `Connector`, `GeoLocation`, `ChargingPeriod`, `CdrDimension`,
/// `EnergyMix`, and embedded `Tariff` objects.
static CDR: Schema = Schema::Object(&CDR_OBJ);

// -- CDR intermediate representation (IR) ------------------------------------
//
// Mirrors the modeled fields of `CDR_OBJ` and its children. Compared with 2.2.1:
// `stop_date_time` (not `end_date_time`), `total_cost` is a plain number (not a
// `Price` object), and there are no itemized cost totals. As elsewhere, every field
// is an `Integrity` value, so building is infallible.

/// The intermediate representation of a `v2.1.1` CDR.
#[derive(Clone, Default, Debug)]
pub(crate) struct Cdr<'buf> {
    pub currency: Integrity<Str<'buf>>,
    pub start_date_time: Integrity<Str<'buf>>,
    pub stop_date_time: Integrity<Str<'buf>>,
    pub charging_periods: Integrity<Vec<Integrity<ChargingPeriod<'buf>>>>,
    pub total_cost: Integrity<Number<'buf>>,
    pub total_energy: Integrity<Number<'buf>>,
    pub total_time: Integrity<Number<'buf>>,
    pub total_parking_time: Integrity<Option<Number<'buf>>>,
    pub tariffs: Integrity<Option<Vec<Integrity<Tariff<'buf>>>>>,
}

/// The intermediate representation of a `v2.1.1` CDR charging period.
#[derive(Clone, Default, Debug)]
pub(crate) struct ChargingPeriod<'buf> {
    pub start_date_time: Integrity<Str<'buf>>,
    pub dimensions: Integrity<Vec<Integrity<Dimension<'buf>>>>,
}

/// The intermediate representation of a `v2.1.1` CDR dimension.
#[derive(Clone, Default, Debug)]
pub(crate) struct Dimension<'buf> {
    pub dimension_type: Integrity<Enum<'buf>>,
    pub volume: Integrity<Number<'buf>>,
}

// -- Tariff intermediate representation (IR) ---------------------------------
//
// The `v2.1.1` tariff IR mirrors the modeled fields of `TARIFF_OBJ` and its children.
// Compared with 2.2.1 it has no `country_code`, `party_id`, `min_price`, `max_price`,
// or `start`/`end_date_time`; a price component has no `vat`; and restrictions have no
// `min_current`/`max_current` or `reservation`. Every field is an `Integrity` value,
// so building is infallible.

/// The intermediate representation of a `v2.1.1` tariff.
#[derive(Clone, Default, Debug)]
pub(crate) struct Tariff<'buf> {
    pub currency: Integrity<Str<'buf>>,
    pub id: Integrity<Str<'buf>>,
    pub elements: Integrity<Vec<Integrity<Element<'buf>>>>,
}

/// The intermediate representation of a `v2.1.1` tariff element.
#[derive(Clone, Default, Debug)]
pub(crate) struct Element<'buf> {
    pub price_components: Integrity<Vec<Integrity<PriceComponent<'buf>>>>,
    pub restrictions: Integrity<Option<Restrictions<'buf>>>,
}

/// The intermediate representation of a `v2.1.1` price component (no `vat` field).
#[derive(Clone, Default, Debug)]
pub(crate) struct PriceComponent<'buf> {
    pub dimension_type: Integrity<Enum<'buf>>,
    pub price: Integrity<Number<'buf>>,
    pub step_size: Integrity<Number<'buf>>,
}

/// The intermediate representation of a `v2.1.1` tariff element's restrictions (no
/// `min_current`/`max_current` or `reservation`).
#[derive(Clone, Default, Debug)]
pub(crate) struct Restrictions<'buf> {
    pub start_time: Integrity<Option<Str<'buf>>>,
    pub end_time: Integrity<Option<Str<'buf>>>,
    pub start_date: Integrity<Option<Str<'buf>>>,
    pub end_date: Integrity<Option<Str<'buf>>>,
    pub min_kwh: Integrity<Option<Number<'buf>>>,
    pub max_kwh: Integrity<Option<Number<'buf>>>,
    pub min_power: Integrity<Option<Number<'buf>>>,
    pub max_power: Integrity<Option<Number<'buf>>>,
    pub min_duration: Integrity<Option<Number<'buf>>>,
    pub max_duration: Integrity<Option<Number<'buf>>>,
    pub day_of_week: Integrity<Option<Vec<Integrity<Enum<'buf>>>>>,
}

/// Validate a `v2.1.1` tariff `json::Document` and build its intermediate representation.
///
/// Building is infallible: the returned [`Tariff`] always exists, with each field in
/// an [`Integrity`] state, alongside the [`Warning`]s found during the walk.
pub(crate) fn build_tariff<'buf>(doc: &json::Document<'buf>) -> Caveat<Tariff<'buf>, Warning> {
    let (node, warnings) = super::walk(doc, &TARIFF).into_parts();

    // An object root yields `Node::V211Tariff` (the `TARIFF` schema's `BuilderKind`); a
    // non-object root yields no tariff builder, so it falls back to an empty tariff.
    let tariff = if let build::Node::V211Tariff(tariff) = node {
        tariff
    } else {
        Tariff::default()
    };

    Caveat::new(tariff, warnings)
}

/// Build and validate a `v2.1.1` CDR from a `json::Document`.
///
/// Building is infallible: the returned [`Cdr`] always exists, with each field in
/// an [`Integrity`] state, alongside the [`Warning`]s found during the walk.
pub(crate) fn build_cdr<'buf>(doc: &json::Document<'buf>) -> Caveat<Cdr<'buf>, Warning> {
    let (node, warnings) = super::walk(doc, &CDR).into_parts();

    // An object root yields `Node::V211Cdr` (the `CDR` schema's `BuilderKind`); a
    // non-object root yields no CDR builder, so it falls back to an empty CDR.
    let cdr = if let build::Node::V211Cdr(cdr) = node {
        cdr
    } else {
        Cdr::default()
    };

    Caveat::new(cdr, warnings)
}
