use crate::serialize;

use rcore::{
    IntermediateLocation, Journey, JourneysResponse, Leg, Line, Location, Mode, Operator, Place,
    Product, Remark, Station, Stop,
};

use chrono::{DateTime, Duration, NaiveDateTime};
use serde::{Deserialize, Serialize};

use std::collections::HashMap;

fn convert_datetime(t: NaiveDateTime) -> Option<DateTime<chrono_tz::Tz>> {
    // Note: Should in theory never return `None` as the returned time is in Europe/Zurich time.
    // This time could be ambiguous though (when switching between summer/winter time), not sure what we can do there as the API does not specify which is the correct one.
    t.and_local_timezone(chrono_tz::Europe::Zurich).earliest()
}

fn convert_datetime_with_delay<S: AsRef<str>>(
    t: NaiveDateTime,
    d: Option<S>,
) -> Option<DateTime<chrono_tz::Tz>> {
    let time = convert_datetime(t)?;
    if let Some(d) = d {
        let d = d.as_ref();
        let delay = d
            .parse::<i64>()
            .map(Duration::minutes)
            .unwrap_or_else(|_| Duration::zero());
        Some(time + delay)
    } else {
        Some(time)
    }
}

fn type_string_to_mode<S: AsRef<str>>(s: S) -> Mode {
    match s.as_ref() {
        "strain" => Mode::SuburbanTrain,
        "walk" => Mode::Unknown,
        "tram" => Mode::Tram,
        "express_train" => Mode::HighSpeedTrain,
        "bus" => Mode::Bus,
        // TODO
        _ => Mode::Unknown,
    }
}

pub type SearchChLocationsResponse = Vec<SearchChLocationsResponseItem>;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChLocationsResponseItem {
    label: String,
    id: Option<String>,
    lon: Option<f32>,
    lat: Option<f32>,
}

impl From<SearchChLocationsResponseItem> for Place {
    fn from(item: SearchChLocationsResponseItem) -> Place {
        if let Some(id) = item.id {
            Place::Station(Station {
                id: id.clone(),
                name: Some(item.label.clone()),
                location: Some(Location::Point {
                    id: Some(id),
                    name: Some(item.label),
                    poi: None,
                    latitude: item.lat.unwrap_or_default(),
                    longitude: item.lon.unwrap_or_default(),
                }),
                products: vec![],
            })
        } else {
            // Lat/Lon not given.
            Place::Location(Location::Address {
                address: item.label,
                latitude: item.lat.unwrap_or_default(),
                longitude: item.lon.unwrap_or_default(),
            })
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChJourneysResponse {
    connections: Vec<SearchChConnection>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChConnection {
    from: String,
    #[serde(with = "serialize::time")]
    departure: chrono::NaiveDateTime,
    dep_delay: Option<String>,
    to: String,
    #[serde(with = "serialize::time")]
    arrival: chrono::NaiveDateTime,
    arr_delay: Option<String>,
    // Note: Sometimes int, sometimes float
    duration: f64, // is_main, disruptions
    legs: Vec<SearchChLeg>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChLeg {
    #[serde(default)]
    #[serde(with = "serialize::optional_time")]
    departure: Option<chrono::NaiveDateTime>,
    tripid: Option<String>,
    stopid: String,
    // x, y
    name: String,
    // sbb_name, terminal, fgcolor, bgcolor
    r#type: Option<String>,
    line: Option<String>,
    #[serde(rename = "*G")]
    star_g: Option<String>,
    #[serde(rename = "*L")]
    star_l: Option<String>,
    operator: Option<String>,
    stops: Option<Vec<SearchChStop>>,
    // contop_stop, runningtime
    exit: Option<SearchChExit>,
    // occupancy
    dep_delay: Option<String>,
    track: Option<String>,
    type_name: Option<String>,
    lon: f32,
    lat: f32,
    cancelled: Option<bool>,
    // Note: This either is a map or an empty vec.
    disruptions: Option<SearchChDisruptions>,
    // TODO: Attributes
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum SearchChDisruptions {
    Map(HashMap<String, SearchChDisruption>),
    Vec(Vec<SearchChDisruption>),
}

impl From<SearchChDisruptions> for Vec<Remark> {
    fn from(disruptions: SearchChDisruptions) -> Self {
        match disruptions {
            SearchChDisruptions::Map(m) => m.into_values().map(Into::into).collect(),
            SearchChDisruptions::Vec(v) => v.into_iter().map(Into::into).collect(),
        }
    }
}

impl Default for SearchChDisruptions {
    fn default() -> Self {
        Self::Vec(Vec::new())
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChDisruption {
    id: String,
    texts: SearchChDisruptionTexts, // A lot more fields which are not too interesting.
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChDisruptionTexts {
    // Also has M and L which seem to be mostly the same to S. Not sure what is the difference.
    // public-transport-enabler seems to only use S: <https://gitlab.com/opentransitmap/public-transport-enabler/-/blob/683c6036243c74249d5005696b2542735074e1c2/src/de/schildbach/pte/CHSearchProvider.java>
    #[serde(rename = "S")]
    s: SearchChDisruptionText,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChDisruptionText {
    summary: Option<String>,
    reason: Option<String>,
    duration: Option<String>,
    consequence: Option<String>,
    recommendation: Option<String>,
}

impl From<SearchChDisruption> for Remark {
    fn from(disruption: SearchChDisruption) -> Remark {
        let texts = disruption.texts.s;
        Remark {
            code: disruption.id,
            text: vec![
                texts.summary.clone(),
                texts.reason,
                texts.duration,
                texts.consequence,
                texts.recommendation,
            ]
            .into_iter()
            .flatten()
            .map(|t| t + ".")
            .collect::<Vec<_>>()
            .join(" "),
            r#type: rcore::RemarkType::Status,
            association: rcore::RemarkAssociation::None,
            summary: texts.summary,
            trip_id: None,
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChStop {
    #[serde(default)]
    #[serde(with = "serialize::optional_time")]
    arrival: Option<chrono::NaiveDateTime>,
    arr_delay: Option<String>,
    #[serde(default)]
    #[serde(with = "serialize::optional_time")]
    departure: Option<chrono::NaiveDateTime>,
    dep_delay: Option<String>,
    name: String,
    stopid: String,
    // x, y,
    lon: f32,
    lat: f32,
}

impl From<SearchChStop> for IntermediateLocation {
    fn from(stop: SearchChStop) -> IntermediateLocation {
        if stop.departure.is_some() {
            Self::Stop(Stop {
                place: Place::Station(Station {
                    id: stop.stopid.clone(),
                    name: Some(stop.name.clone()),
                    location: Some(Location::Point {
                        id: Some(stop.stopid),
                        name: Some(stop.name),
                        poi: None,
                        latitude: stop.lat,
                        longitude: stop.lon,
                    }),
                    products: vec![],
                }),
                departure: stop
                    .departure
                    .and_then(|d| convert_datetime_with_delay(d, stop.dep_delay.as_ref())),
                planned_departure: stop.departure.and_then(convert_datetime),
                arrival: stop
                    .arrival
                    .and_then(|d| convert_datetime_with_delay(d, stop.arr_delay.as_ref())),
                planned_arrival: stop.arrival.and_then(convert_datetime),
                // Note: The API does not provide track information for stopovers.
                arrival_platform: None,
                planned_arrival_platform: None,
                departure_platform: None,
                planned_departure_platform: None,
                // Note: The API does not provide cancellation or remark information for stopovers.
                cancelled: false,
                remarks: vec![],
            })
        } else {
            Self::Railway(Place::Location(Location::Point {
                id: Some(stop.stopid),
                name: Some(stop.name),
                poi: None,
                latitude: stop.lat,
                longitude: stop.lon,
            }))
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SearchChExit {
    #[serde(with = "serialize::time")]
    arrival: chrono::NaiveDateTime,
    stopid: String,
    // x, y
    name: String,
    // sbb_name, waittime,
    track: Option<String>,
    arr_delay: Option<String>,
    lon: f32,
    lat: f32,
}

impl From<SearchChJourneysResponse> for JourneysResponse {
    fn from(response: SearchChJourneysResponse) -> JourneysResponse {
        Self {
            earlier_ref: response
                .connections
                .first()
                .and_then(|c| convert_datetime(c.departure))
                .map(|t| t.to_rfc3339()),
            later_ref: response
                .connections
                .last()
                .and_then(|c| convert_datetime(c.departure))
                .map(|t| t.to_rfc3339()),
            journeys: response.connections.into_iter().map(Into::into).collect(),
        }
    }
}

impl From<SearchChConnection> for Journey {
    fn from(connection: SearchChConnection) -> Self {
        let mut legs: Vec<Leg> = connection.legs.into_iter().map(Into::into).collect();
        // Note: Last "leg" is always destination.
        legs.remove(legs.len() - 1);
        Self {
            id: legs
                .iter()
                .flat_map(|l| l.trip_id.as_ref())
                .map(|s| &s[..])
                .collect(),

            legs,
            price: None,
        }
    }
}

impl From<SearchChLeg> for Leg {
    fn from(leg: SearchChLeg) -> Leg {
        let origin = Place::Station(Station {
            id: leg.stopid.clone(),
            name: Some(leg.name.clone()),
            location: Some(Location::Point {
                id: Some(leg.stopid),
                name: Some(leg.name),
                poi: None,
                latitude: leg.lat,
                longitude: leg.lon,
            }),
            products: vec![],
        });
        let destination = Place::Station(Station {
            id: leg
                .exit
                .as_ref()
                .map(|e| e.stopid.clone())
                .unwrap_or_default(),
            name: leg.exit.as_ref().map(|e| e.name.clone()),
            location: Some(Location::Point {
                id: leg.exit.as_ref().map(|e| e.stopid.clone()),
                name: leg.exit.as_ref().map(|e| e.name.clone()),
                poi: None,
                latitude: leg.exit.as_ref().map(|e| e.lat).unwrap_or_default(),
                longitude: leg.exit.as_ref().map(|e| e.lon).unwrap_or_default(),
            }),
            products: vec![],
        });
        let planned_departure = leg.departure.and_then(convert_datetime);
        let planned_arrival = leg.exit.as_ref().and_then(|e| convert_datetime(e.arrival));
        let departure = leg
            .departure
            .and_then(|d| convert_datetime_with_delay(d, leg.dep_delay.as_ref()));
        let arrival = leg
            .exit
            .as_ref()
            .and_then(|e| convert_datetime_with_delay(e.arrival, e.arr_delay.as_ref()));
        let arrival_platform = leg.exit.as_ref().and_then(|e| e.track.clone());
        let departure_platform = leg.track.clone();
        let r#type = leg.r#type.unwrap_or_default();
        let mode = type_string_to_mode(&r#type);

        let mut intermediate_locations = vec![IntermediateLocation::Stop(Stop {
            place: origin.clone(),
            departure,
            planned_departure,
            arrival: None,
            planned_arrival: None,
            arrival_platform: None,
            planned_arrival_platform: None,
            // TODO: Track changes
            // When tracks change, the API does not return which was the old track but only the new one with an exclamation-mark.
            departure_platform: departure_platform.clone(),
            planned_departure_platform: departure_platform.clone(),
            cancelled: false,
            remarks: vec![],
        })];
        // Note: Should we consider filtering out "Bahn-2000-Strecke" and others. This is no real stopover.
        // Or should we change semantics of a "stopover" to also include that?
        // See also <https://en.wikipedia.org/wiki/Rail_2000> and <https://de.wikipedia.org/wiki/Liste_der_Schweizer_Tunnel#Bahntunnel>.
        intermediate_locations.extend(leg.stops.unwrap_or_default().into_iter().map(Into::into));
        intermediate_locations.push(IntermediateLocation::Stop(Stop {
            place: destination.clone(),
            departure: None,
            planned_departure: None,
            arrival,
            planned_arrival,
            // TODO: Track changes
            // When tracks change, the API does not return which was the old track but only the new one with an exclamation-mark.
            arrival_platform: arrival_platform.clone(),
            planned_arrival_platform: arrival_platform.clone(),
            departure_platform: None,
            planned_departure_platform: None,
            cancelled: false,
            remarks: vec![],
        }));

        Leg {
            origin,
            destination,
            departure,
            planned_departure,
            arrival,
            planned_arrival,
            reachable: true,
            trip_id: leg.tripid,
            line: if r#type != "walk" {
                Some(Line {
                    name: leg.line.clone(),
                    fahrt_nr: leg.star_l,
                    mode: mode.clone(),
                    product: Product {
                        mode,
                        name: leg.star_g.clone().unwrap_or_default().into(),
                        short: leg.star_g.clone().unwrap_or_default().into(),
                    },
                    operator: leg.operator.map(|o| Operator {
                        id: o.clone(),
                        name: o.clone(),
                    }),
                    product_name: leg.star_g,
                })
            } else {
                None
            },
            direction: None,
            // TODO: Track changes
            // When tracks change, the API does not return which was the old track but only the new one with an exclamation-mark.
            arrival_platform: arrival_platform.clone(),
            planned_arrival_platform: arrival_platform,
            departure_platform: departure_platform.clone(),
            planned_departure_platform: departure_platform,
            intermediate_locations,
            #[cfg(feature = "polylines")]
            polyline: None,
            walking: r#type == "walk",
            cancelled: leg.cancelled.unwrap_or_default(),
            remarks: leg.disruptions.unwrap_or_default().into(),
            // Information not provided.
            load_factor: None,
            transfer: false,
            distance: None,
            frequency: None,
        }
    }
}
