use rcore::{
    IntermediateLocation, Journey, JourneysResponse, Leg, Line, Location, Mode, Place, Price,
    Product, Remark, RemarkAssociation, RemarkType, Station,
};
use serde::{Deserialize, Serialize};

use crate::serialize;

use std::borrow::Cow;

const TZ: chrono_tz::Tz = chrono_tz::Europe::Berlin;

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBLocationsResponse {
    name: String,
    location_id: String,
    coordinates: DBCoordinate,
    products: Vec<DBProduct>,
    location_type: DBLocationType,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBCoordinate {
    latitude: f32,
    longitude: f32,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum DBProduct {
    Hochgeschwindigkeitszuege,
    Intercityundeurocityzuege,
    Interregioundschnellzuege,
    Nahverkehrsonstigezuege,
    Sbahnen,
    Busse,
    Schiffe,
    Ubahn,
    Strassenbahn,
    Anrufpflichtigeverkehre,
}

impl From<DBProduct> for Product {
    fn from(product: DBProduct) -> Self {
        match product {
            DBProduct::Hochgeschwindigkeitszuege => Product {
                mode: Mode::HighSpeedTrain,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Intercityundeurocityzuege => Product {
                mode: Mode::HighSpeedTrain,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Interregioundschnellzuege => Product {
                mode: Mode::HighSpeedTrain,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Nahverkehrsonstigezuege => Product {
                mode: Mode::RegionalTrain,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Sbahnen => Product {
                mode: Mode::Subway,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Busse => Product {
                mode: Mode::Bus,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Schiffe => Product {
                mode: Mode::Ferry,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Ubahn => Product {
                mode: Mode::Subway,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Strassenbahn => Product {
                mode: Mode::Tram,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
            DBProduct::Anrufpflichtigeverkehre => Product {
                mode: Mode::OnDemand,
                name: Cow::Borrowed(""),
                short: Cow::Borrowed(""),
            },
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum DBLocationType {
    #[serde(rename = "ST")]
    Station,
    #[serde(rename = "POI")]
    Poi,
    #[serde(rename = "ADR")]
    Address,
}

impl From<DBLocationsResponse> for Place {
    fn from(location: DBLocationsResponse) -> Self {
        match location.location_type {
            DBLocationType::Station => Place::Station(Station {
                id: location.location_id.clone(),
                name: Some(location.name.clone()),
                location: Some(Location::Point {
                    id: Some(location.location_id),
                    name: Some(location.name),
                    poi: Some(false),
                    latitude: location.coordinates.latitude,
                    longitude: location.coordinates.longitude,
                }),
                products: location.products.into_iter().map(Into::into).collect(),
            }),
            DBLocationType::Poi => Place::Location(Location::Point {
                id: Some(location.location_id),
                name: Some(location.name),
                poi: Some(true),
                latitude: location.coordinates.latitude,
                longitude: location.coordinates.longitude,
            }),
            DBLocationType::Address => Place::Location(Location::Address {
                address: location.name,
                latitude: location.coordinates.latitude,
                longitude: location.coordinates.longitude,
            }),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBTripsResponse {
    verbindungen: Vec<DBTrip>,
    frueher_context: Option<String>,
    spaeter_context: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBTrip {
    verbindung: DBConnection,
    angebote: DBOffer,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBConnection {
    kontext: String,
    pub(crate) verbindungs_abschnitte: Vec<DBLeg>,
    echtzeit_notizen: Vec<DBNote>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBLoad {
    klasse: String,
    stufe: u8,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBLeg {
    typ: String,
    distanz: Option<u64>,
    administration_id: Option<String>,
    kurztext: Option<String>,
    mitteltext: Option<String>,
    langtext: Option<String>,
    zuglauf_id: Option<String>,
    nummer: Option<u64>,
    halte: Vec<DBStop>,
    verkehrsmittel_nummer: Option<String>,
    richtung: Option<String>,
    produkt_gattung: Option<String>,
    abgangs_ort: DBLocation,
    ankunfts_ort: DBLocation,
    #[serde(with = "serialize::time")]
    abgangs_datum: chrono::DateTime<chrono::FixedOffset>,
    #[serde(with = "serialize::time")]
    ankunfts_datum: chrono::DateTime<chrono::FixedOffset>,
    #[serde(with = "serialize::optional_time")]
    #[serde(default)]
    ez_abgangs_datum: Option<chrono::DateTime<chrono::FixedOffset>>,
    #[serde(with = "serialize::optional_time")]
    #[serde(default)]
    ez_ankunfts_datum: Option<chrono::DateTime<chrono::FixedOffset>>,
    echtzeit_notizen: Vec<DBNote>,
    attribut_notizen: Vec<DBNote>,
    him_notizen: Vec<DBNote>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBStop {
    #[serde(with = "serialize::optional_time")]
    #[serde(default)]
    abgangs_datum: Option<chrono::DateTime<chrono::FixedOffset>>,
    #[serde(with = "serialize::optional_time")]
    #[serde(default)]
    ankunfts_datum: Option<chrono::DateTime<chrono::FixedOffset>>,
    #[serde(with = "serialize::optional_time")]
    #[serde(default)]
    ez_abgangs_datum: Option<chrono::DateTime<chrono::FixedOffset>>,
    #[serde(with = "serialize::optional_time")]
    #[serde(default)]
    ez_ankunfts_datum: Option<chrono::DateTime<chrono::FixedOffset>>,
    ort: DBLocation,
    gleis: Option<String>,
    ez_gleis: Option<String>,
    auslastungs_infos: Vec<DBLoad>,
    echtzeit_notizen: Vec<DBNote>,
    attribut_notizen: Vec<DBNote>,
    him_notizen: Vec<DBNote>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBLocation {
    name: String,
    location_id: String,
    position: DBCoordinate,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBOffer {
    preise: Option<DBPrice>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBPrice {
    gesamt: DBTotalPrice,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBTotalPrice {
    ab: DBTotalPriceStart,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBTotalPriceStart {
    waehrung: String,
    betrag: f64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DBNote {
    text: String,
    key: Option<String>,
}

impl From<DBTripsResponse> for JourneysResponse {
    fn from(value: DBTripsResponse) -> Self {
        Self {
            earlier_ref: value.frueher_context,
            later_ref: value.spaeter_context,
            journeys: value.verbindungen.into_iter().map(Into::into).collect(),
        }
    }
}

impl From<DBTrip> for Journey {
    fn from(value: DBTrip) -> Self {
        Self {
            // The journey context after a certain point (in my testing "¶KCC¶") can change when searching the journey multiple times.
            // Cut off only the first, non-changing part.
            // Note that this in particular does not break reloading, which does not seem to use the second part.
            id: value
                .verbindung
                .kontext
                .split_once("¶KCC¶")
                .map(|s| s.0.to_owned())
                .unwrap_or_else(|| value.verbindung.kontext),
            legs: value
                .verbindung
                .verbindungs_abschnitte
                .into_iter()
                .flat_map(Option::<Leg>::from)
                .collect(),
            price: value.angebote.preise.map(|p| Price {
                amount: p.gesamt.ab.betrag,
                currency: p.gesamt.ab.waehrung,
            }),
        }
    }
}

impl From<DBLeg> for Option<Leg> {
    fn from(value: DBLeg) -> Self {
        // DB adds transitions at the same station between platforms as "walking".
        // In Railway, this would lead to ridiculous "walking" times and no time waiting.
        // As far as I can see, their API actually does not differentiate between walking and waiting anymore, we can therefore not nicely fix it.
        // Instead, interpret all legs that have the same start and end station as "waiting".
        if value.abgangs_ort.location_id == value.ankunfts_ort.location_id {
            return None;
        }

        Some(Leg {
            origin: value.abgangs_ort.into(),
            destination: value.ankunfts_ort.into(),
            departure: Some(
                value
                    .ez_abgangs_datum
                    .unwrap_or(value.abgangs_datum)
                    .with_timezone(&TZ),
            ),
            planned_departure: Some(value.abgangs_datum.with_timezone(&TZ)),
            arrival: Some(
                value
                    .ez_ankunfts_datum
                    .unwrap_or(value.ankunfts_datum)
                    .with_timezone(&TZ),
            ),
            planned_arrival: Some(value.ankunfts_datum.with_timezone(&TZ)),
            reachable: true, // TODO
            trip_id: value.zuglauf_id,
            line: if value.typ == "FAHRZEUG" {
                Some(Line {
                    name: value.mitteltext,
                    fahrt_nr: value.nummer.map(|n| n.to_string()),
                    mode: Mode::Unknown,
                    product: Product::unknown(),
                    operator: None,
                    product_name: value.kurztext,
                })
            } else {
                None
            },
            direction: value.richtung,
            arrival_platform: value
                .halte
                .last()
                .and_then(|h| h.ez_gleis.as_ref().or(h.gleis.as_ref()).cloned()),
            planned_arrival_platform: value.halte.last().and_then(|h| h.gleis.clone()),
            departure_platform: value
                .halte
                .first()
                .and_then(|h| h.ez_gleis.as_ref().or(h.gleis.as_ref()).cloned()),
            planned_departure_platform: value.halte.first().and_then(|h| h.gleis.clone()),
            frequency: None, // TODO
            cancelled: value
                .halte
                .first()
                .iter()
                .chain(value.halte.last().iter())
                .flat_map(|h| &h.echtzeit_notizen)
                .any(|n| &n.text == "Halt entfällt" || n.text == "Stop cancelled"),
            load_factor: match value
                .halte
                .iter()
                .flat_map(|h| &h.auslastungs_infos)
                .map(|a| a.stufe)
                .max()
            {
                // TODO: Check if correct.
                Some(1) => Some(rcore::LoadFactor::LowToMedium),
                Some(2) => Some(rcore::LoadFactor::LowToMedium),
                Some(3) => Some(rcore::LoadFactor::High),
                _ => None,
            },
            intermediate_locations: value.halte.into_iter().map(Into::into).collect(),
            remarks: value
                .echtzeit_notizen
                .into_iter()
                .chain(value.attribut_notizen)
                .chain(value.him_notizen)
                .map(Into::into)
                .collect(),
            walking: value.typ == "FUSSWEG",
            transfer: false, // TODO
            distance: value.distanz,
            #[cfg(feature = "polylines")]
            polyline: None,
        })
    }
}

impl From<DBLocation> for Place {
    fn from(value: DBLocation) -> Self {
        // TODO: Parse type based on locationId
        Place::Station(Station {
            id: value.location_id.clone(),
            name: Some(value.name.clone()),
            location: Some(Location::Point {
                id: Some(value.location_id),
                name: Some(value.name),
                poi: Some(false),
                latitude: value.position.latitude,
                longitude: value.position.longitude,
            }),
            products: vec![],
        })
    }
}

impl From<DBStop> for IntermediateLocation {
    fn from(value: DBStop) -> Self {
        IntermediateLocation::Stop(rcore::Stop {
            place: value.ort.into(),
            departure: value
                .ez_abgangs_datum
                .or(value.abgangs_datum)
                .map(|d| d.with_timezone(&TZ)),
            planned_departure: value.abgangs_datum.map(|d| d.with_timezone(&TZ)),
            arrival: value
                .ez_ankunfts_datum
                .or(value.ankunfts_datum)
                .map(|d| d.with_timezone(&TZ)),
            planned_arrival: value.ankunfts_datum.map(|d| d.with_timezone(&TZ)),
            arrival_platform: value.ez_gleis.as_ref().or(value.gleis.as_ref()).cloned(),
            planned_arrival_platform: value.gleis.clone(),
            departure_platform: value.ez_gleis.as_ref().or(value.gleis.as_ref()).cloned(),
            planned_departure_platform: value.gleis.clone(),
            cancelled: value
                .echtzeit_notizen
                .iter()
                .any(|n| &n.text == "Halt entfällt" || &n.text == "Stop cancelled"),
            remarks: value
                .echtzeit_notizen
                .into_iter()
                .chain(value.attribut_notizen)
                .chain(value.him_notizen)
                .map(Into::into)
                .collect(),
        })
    }
}

impl From<DBNote> for Remark {
    fn from(value: DBNote) -> Self {
        Remark {
            text: value.text,
            r#type: if value.key.is_none() {
                RemarkType::Status
            } else {
                RemarkType::Hint
            },
            association: value
                .key
                .as_ref()
                .map(remark_association_from_key)
                .unwrap_or(RemarkAssociation::None),
            summary: None,
            trip_id: None,
            code: value.key.unwrap_or_default(),
        }
    }
}

fn remark_association_from_key<S: AsRef<str>>(key: S) -> RemarkAssociation {
    match key.as_ref() {
        "FB" => RemarkAssociation::Bike, // Fahrradmitnahme begrenzt möglich
        "EA" => RemarkAssociation::Accessibility, // Behindertengerechte Ausstattung
        "ER" => RemarkAssociation::Accessibility, // Rampe im Zug
        "EH" => RemarkAssociation::Accessibility, // Fahrzeuggebundene Einstiegshilfe vorhanden
        "LS" => RemarkAssociation::Power, // Laptop-Steckdosen
        "KL" => RemarkAssociation::AirConditioning, // Klimaanlage
        "WV" => RemarkAssociation::WiFi, // WLAN verfügbar
        _ => RemarkAssociation::Unknown,
    }
}
