#![doc = include_str!("../README.md")]

mod error;
mod serialize;
mod types;
use error::Error;
use types::*;

use async_trait::async_trait;
use rcore::{
    Journey, Location, Mode, ProductsSelection, Provider, Requester, RequesterBuilder, TariffClass,
};
use serde_json::json;
use url::Url;
use uuid::Uuid;

use std::collections::{HashMap, HashSet};

pub const API_URL: &str = "https://app.vendo.noncd.db.de/mob";
const TZ: chrono_tz::Tz = chrono_tz::Europe::Berlin;

const LOCATION_MIME: &str = "application/x.db.vendo.mob.location.v3+json";
const VERBINDUNGSSUCHE_MIME: &str = "application/x.db.vendo.mob.verbindungssuche.v9+json";

#[derive(Clone)]
pub struct DbMovasClient<R: Requester> {
    requester: R,
    url: Url,
}

impl<R: Requester> DbMovasClient<R> {
    pub fn new<RB: RequesterBuilder<Requester = R>>(requester: RB) -> Self {
        Self {
            requester: requester.build(),
            url: Url::parse(API_URL).expect("Failed to parse API_URL"),
        }
    }
}

fn products_to_api_type(selection: ProductsSelection) -> Vec<&'static str> {
    HashSet::<Mode>::from(selection)
        .into_iter()
        .flat_map(mode_to_api_type)
        .collect()
}

fn mode_to_api_type(mode: Mode) -> Vec<&'static str> {
    match mode {
        Mode::HighSpeedTrain => vec![
            "HOCHGESCHWINDIGKEITSZUEGE",
            "INTERCITYUNDEUROCITYZUEGE",
            "INTERREGIOUNDSCHNELLZUEGE",
        ],
        Mode::RegionalTrain => vec!["NAHVERKEHRSONSTIGEZUEGE"],
        Mode::SuburbanTrain => vec!["SBAHNEN"],
        Mode::Subway => vec!["UBAHN"],
        Mode::Tram => vec!["STRASSENBAHN"],
        Mode::Bus => vec!["BUSSE"],
        Mode::Ferry => vec!["SCHIFFE"],
        Mode::Cablecar => vec![],
        Mode::OnDemand => vec!["ANRUFPFLICHTIGEVERKEHRE"],
        Mode::Unknown => vec![],
    }
}

fn headers_for_content_type<'a>(
    mime: &'static str,
    correlation: &'a str,
) -> HashMap<&'static str, &'a str> {
    [
        ("X-Correlation-ID", correlation),
        ("Accept", mime),
        ("Content-Type", mime),
    ]
    .into()
}

fn correlation_id() -> String {
    Uuid::new_v4().to_string() + "_" + &Uuid::new_v4().to_string()
}

#[cfg_attr(feature = "rt-multi-thread", async_trait)]
#[cfg_attr(not(feature = "rt-multi-thread"), async_trait(?Send))]
impl<R: Requester> Provider<R> for DbMovasClient<R> {
    type Error = Error;

    async fn journeys(
        &self,
        from: rcore::Place,
        to: rcore::Place,
        opts: rcore::JourneysOptions,
    ) -> Result<rcore::JourneysResponse, rcore::Error<<R as Requester>::Error, Self::Error>> {
        // TODO:
        // - via
        // - results
        // - stopovers
        // - polylines
        // - tickets
        // - start_with_walking
        // - accessibility
        // - transfers
        // - transfer_time
        // - language
        // - loyalty_card
        // - passenger_age
        let place_to_id = |p| match p {
            rcore::Place::Station(s) => s.id,
            rcore::Place::Location(Location::Address { address, .. }) => address,
            // TODO: Error when not set
            rcore::Place::Location(Location::Point { id, name, .. }) => {
                id.or(name).unwrap_or_default()
            }
        };

        let mut url = self.url.clone();
        url.path_segments_mut()
            .expect("API URL cannot-be-a-base")
            .push("angebote")
            .push("fahrplan");

        let time = opts
            .departure
            .or(opts.arrival)
            .map(|t| t.with_timezone(&TZ))
            .unwrap_or(chrono::Local::now().with_timezone(&TZ));
        let time_type = if opts.arrival.is_some() {
            "ANKUNFT"
        } else {
            "ABFAHRT"
        };
        let context = opts.earlier_than.or(opts.later_than);
        let class = match opts.tariff_class {
            TariffClass::First => "KLASSE_1",
            TariffClass::Second => "KLASSE_2",
        };

        let mut query = json!({
            "autonomeReservierung": false,
            "einstiegsTypList": ["STANDARD"],
            "klasse": class,
            "reiseHin": {
                "wunsch": {
                    "abgangsLocationId": place_to_id(from),
                    "verkehrsmittel": products_to_api_type(opts.products),
                    "zeitWunsch": {
                        "reiseDatum": time.format("%Y-%m-%dT%H:%M:%S%:z").to_string(),
                        "zeitPunktArt": time_type,
                    },
                    "zielLocationId": place_to_id(to)
                },
            },
            "reisendenProfil": {
                "reisende": [
                    { "ermaessigungen": ["KEINE_ERMAESSIGUNG KLASSENLOS"], "reisendenTyp": "ERWACHSENER" }
                ]
            },
            "reservierungsKontingenteVorhanden": false
        });

        if let Some(context) = context {
            query["reiseHin"]["wunsch"]["context"] = context.into();
        }
        if opts.bike_friendly {
            query["reiseHin"]["wunsch"]["fahrradmitnahme"] = true.into();
        }

        let response = self
            .requester
            .post(
                &url,
                &serde_json::to_vec(&query).expect("Failed to serialize body"),
                headers_for_content_type(VERBINDUNGSSUCHE_MIME, &correlation_id()),
            )
            .await
            .map_err(rcore::Error::Request)?;

        let response: DBTripsResponse = serde_json::from_slice(&response)
            .map_err(|e| rcore::Error::Provider(Error::Json(e)))?;

        Ok(response.into())
    }

    async fn locations(
        &self,
        opts: rcore::LocationsOptions,
    ) -> Result<rcore::LocationsResponse, rcore::Error<<R as Requester>::Error, Self::Error>> {
        // TODO
        // - results
        // - language
        let mut url = self.url.clone();
        url.path_segments_mut()
            .expect("API URL cannot-be-a-base")
            .push("location")
            .push("search");

        let query = json!({
            "searchTerm": opts.query,
            "locationTypes": [ "ALL" ],
            "maxResults": opts.results,
        });

        let response = self
            .requester
            .post(
                &url,
                &serde_json::to_vec(&query).expect("Failed to serialize body"),
                headers_for_content_type(LOCATION_MIME, &correlation_id()),
            )
            .await
            .map_err(rcore::Error::Request)?;

        let response: Vec<DBLocationsResponse> = serde_json::from_slice(&response)
            .map_err(|e| rcore::Error::Provider(Error::Json(e)))?;

        Ok(response.into_iter().map(Into::into).collect())
    }

    async fn station_board(
        &self,
        _place: rcore::Place,
        _kind: rcore::StationBoardKind,
        _opts: rcore::StationBoardOptions,
    ) -> Result<rcore::StationBoardResponse, rcore::Error<<R as Requester>::Error, Self::Error>>
    {
        Err(rcore::Error::NotImplemented)
    }

    async fn refresh_journey(
        &self,
        journey: &Journey,
        _opts: rcore::RefreshJourneyOptions,
    ) -> Result<rcore::RefreshJourneyResponse, rcore::Error<<R as Requester>::Error, Self::Error>>
    {
        let mut url = self.url.clone();
        url.path_segments_mut()
            .expect("API URL cannot-be-a-base")
            .push("trip")
            .push("recon");

        let query = json!({
            "reconCtx": journey.id,
        });

        let response = self
            .requester
            .post(
                &url,
                &serde_json::to_vec(&query).expect("Failed to serialize body"),
                headers_for_content_type(VERBINDUNGSSUCHE_MIME, &correlation_id()),
            )
            .await
            .map_err(rcore::Error::Request)?;

        let response: DBConnection = serde_json::from_slice(&response)
            .map_err(|e| rcore::Error::Provider(Error::Json(e)))?;

        Ok(Journey {
            id: journey.id.clone(),
            legs: response
                .verbindungs_abschnitte
                .into_iter()
                .map(Into::into)
                .collect(),
            price: journey.price.clone(),
        })
    }
}

#[cfg(test)]
mod test {
    use rcore::{
        JourneysOptions, Location, LocationsOptions, Place, ReqwestRequesterBuilder, Station,
    };

    use super::*;

    pub async fn check_search<S: AsRef<str>>(
        search: S,
        expected: S,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let client = DbMovasClient::new(ReqwestRequesterBuilder::default());
        let locations = client
            .locations(LocationsOptions {
                query: search.as_ref().to_string(),
                ..Default::default()
            })
            .await?;
        let results = locations
            .into_iter()
            .flat_map(|p| match p {
                Place::Station(s) => s.name,
                Place::Location(Location::Address { address, .. }) => Some(address),
                Place::Location(Location::Point { name, .. }) => name,
            })
            .collect::<Vec<_>>();
        assert!(
            results.iter().find(|s| s == &expected.as_ref()).is_some(),
            "expected {} to be contained in {:#?}",
            expected.as_ref(),
            results
        );
        Ok(())
    }

    pub async fn check_journey<S: AsRef<str>>(
        from: S,
        to: S,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let client = DbMovasClient::new(ReqwestRequesterBuilder::default());
        let journeys = client
            .journeys(
                Place::Station(Station {
                    id: from.as_ref().to_string(),
                    ..Default::default()
                }),
                Place::Station(Station {
                    id: to.as_ref().to_string(),
                    ..Default::default()
                }),
                JourneysOptions::default(),
            )
            .await?;
        assert!(
            !journeys.journeys.is_empty(),
            "expected journey from {} to {} to exist",
            from.as_ref(),
            to.as_ref()
        );
        Ok(())
    }

    #[tokio::test]
    async fn search_berlin() -> Result<(), Box<dyn std::error::Error>> {
        check_search("Berl", "Berlin Hbf").await
    }

    #[tokio::test]
    async fn journey_tuebingen_augsburg() -> Result<(), Box<dyn std::error::Error>> {
        check_journey("A=1@O=Tübingen Hbf@X=9055410@Y=48515807@U=80@L=8000141@B=1@p=1711395084@i=U×008029318@", "A=1@O=Augsburg Hbf@X=10885568@Y=48365444@U=80@L=8000013@B=1@p=1711395084@i=U×008002140@").await
    }
}
