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

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

use async_trait::async_trait;
use rcore::{
    Journey, JourneysOptions, Location, Mode, ProductsSelection, Provider, Requester,
    RequesterBuilder,
};
use url::Url;

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

pub const API_URL: &str = "https://search.ch/timetable/api";

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

impl<R: Requester> SearchChClient<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) -> String {
    HashSet::<Mode>::from(selection)
        .into_iter()
        .flat_map(mode_to_api_type)
        .fold(String::new(), |acc, m| format!("{},{}", acc, m))
}

fn mode_to_api_type(mode: Mode) -> Option<&'static str> {
    match mode {
        Mode::HighSpeedTrain => Some("train"),
        Mode::RegionalTrain => Some("train"),
        Mode::SuburbanTrain => Some("train"),
        Mode::Subway => Some("tram"),
        Mode::Tram => Some("tram"),
        Mode::Bus => Some("bus"),
        Mode::Ferry => Some("ship"),
        Mode::Cablecar => Some("cableway"),
        Mode::OnDemand => None,
        Mode::Unknown => None,
    }
}

#[cfg_attr(feature = "rt-multi-thread", async_trait)]
#[cfg_attr(not(feature = "rt-multi-thread"), async_trait(?Send))]
impl<R: Requester> Provider<R> for SearchChClient<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>> {
        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 time = opts
            .earlier_than
            .as_ref()
            .and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok())
            .or_else(|| {
                opts.later_than
                    .as_ref()
                    .and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok())
            })
            .or_else(|| opts.departure.as_ref().map(|t| t.fixed_offset()))
            .or_else(|| opts.arrival.as_ref().map(|t| t.fixed_offset()))
            .map(|t| t.with_timezone(&chrono_tz::Europe::Zurich));
        let time_type = if opts.arrival.is_some() {
            "arrival"
        } else {
            "depart"
        };
        let num = if opts.earlier_than.is_some() {
            0
        } else {
            opts.results
        };
        let pre = if opts.earlier_than.is_some() {
            opts.results
        } else {
            0
        };
        let transport_types = products_to_api_type(opts.products);

        let mut url = self.url.clone();
        url.path_segments_mut()
            .expect("API URL cannot-be-a-base")
            .push("route.json");
        url.query_pairs_mut()
            .append_pair("from", &place_to_id(from))
            .append_pair("to", &place_to_id(to))
            .append_pair("show_delays", "1")
            .append_pair("show_trackchanges", "1")
            .append_pair(
                "date",
                &time
                    .as_ref()
                    .map(|t| t.format("%d.%m.%Y").to_string())
                    .unwrap_or_else(|| "today".to_owned()),
            )
            .append_pair(
                "time",
                &time
                    .as_ref()
                    .map(|t| t.format("%H:%M").to_string())
                    .unwrap_or_else(|| "now".to_owned()),
            )
            .append_pair("time_type", time_type)
            .append_pair("num", &num.to_string())
            .append_pair("pre", &pre.to_string())
            .append_pair("transportation_types", &transport_types);

        let response = self
            .requester
            .get(&url, &[], HashMap::new())
            .await
            .map_err(rcore::Error::Request)?;

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

        Ok(response.into())
    }

    /// Note: search.ch does not support setting the search language or the number of results
    async fn locations(
        &self,
        opts: rcore::LocationsOptions,
    ) -> Result<rcore::LocationsResponse, 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("completion.json");
        url.query_pairs_mut()
            .append_pair("term", &opts.query)
            .append_pair("show_ids", "1")
            .append_pair("show_coordinates", "1");
        let response = self
            .requester
            .get(&url, &[], HashMap::new())
            .await
            .map_err(rcore::Error::Request)?;

        let response: SearchChLocationsResponse = 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)
    }

    // Note: search.ch does not support any of those options.
    // This method is not guaranteed to find the same journey. But I think this is the best we can do as search.ch does not provide a refresh-API.
    async fn refresh_journey(
        &self,
        journey: &Journey,
        _opts: rcore::RefreshJourneyOptions,
    ) -> Result<rcore::RefreshJourneyResponse, rcore::Error<<R as Requester>::Error, Self::Error>>
    {
        // Note: Each journey should have at least one leg.
        let from = &journey.legs[0].origin;
        let to = &journey.legs[journey.legs.len() - 1].destination;
        let jopts = JourneysOptions {
            // In most cases, the journey to refresh is likely the first one. Relax this requirement a bit.
            results: 3,
            departure: journey.legs[0].planned_departure,
            ..Default::default()
        };
        self.journeys(from.clone(), to.clone(), jopts)
            .await?
            .journeys
            .into_iter()
            .find(|j| j.id == journey.id)
            .clone()
            .ok_or(rcore::Error::Provider(Error::RefreshJourneyNotFound))
    }
}

#[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 = SearchChClient::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 = SearchChClient::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_luzern() -> Result<(), Box<dyn std::error::Error>> {
        check_search("Lu", "Luzern").await
    }

    #[tokio::test]
    async fn journey_winterthur_lausanne() -> Result<(), Box<dyn std::error::Error>> {
        check_journey("8506000", "8501120").await
    }
}
