use crate::client::HafasClient;
use crate::format::ToHafas;
use crate::{Accessibility, Client, Error, Place, ProductsSelection, Result, TariffClass};
use crate::{Journey, LoyaltyCard};
use chrono::offset::Utc;
use chrono::NaiveDateTime;
use chrono::TimeZone;
#[cfg(feature = "wasm-bindings")]
use js_sys::Promise;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
#[cfg(feature = "wasm-bindings")]
use wasm_bindgen::prelude::wasm_bindgen;
#[cfg(feature = "wasm-bindings")]
use wasm_bindgen::JsValue;
#[cfg(feature = "wasm-bindings")]
use wasm_bindgen_futures::future_to_promise;

#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct JourneysOptions {
    pub via: Option<Vec<Place>>,
    pub earlier_than: Option<String>,
    pub later_than: Option<String>,
    pub results: Option<u64>,
    pub stopovers: Option<bool>,
    #[cfg(feature = "polylines")]
    pub polylines: Option<bool>,
    // pub remarks: Option<bool>,
    pub bike_friendly: Option<bool>,
    pub tickets: Option<bool>,
    pub start_with_walking: Option<bool>,
    //pub scheduled_days: Option<bool>,
    pub accessibility: Option<Accessibility>,
    pub transfers: Option<i64>,
    pub transfer_time: Option<u64>,
    pub arrival: Option<NaiveDateTime>, // XXX: Naive does not really make sense here.
    pub departure: Option<NaiveDateTime>, // XXX: Naive does not really make sense here.
    #[serde(default)]
    pub products: ProductsSelection,
    pub tariff_class: Option<TariffClass>,
    pub language: Option<String>,
    pub loyalty_card: Option<LoyaltyCard>,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct JourneysResponse {
    pub earlier_ref: Option<String>,
    pub later_ref: Option<String>,
    pub journeys: Vec<Journey>,
}

impl HafasClient {
    pub async fn journeys(
        &self,
        from: Place,
        to: Place,
        opts: JourneysOptions,
    ) -> Result<JourneysResponse> {
        let timezone = self.profile.timezone();
        let (when, is_departure) = match (opts.departure, opts.arrival) {
            (Some(_), Some(_)) => Err(Error::InvalidInput(
                "departure and arrival are mutually exclusive".to_string(),
            ))?,
            (Some(departure), None) => (departure, true),
            (None, Some(arrival)) => (arrival, false),
            (None, None) => (Utc::now().naive_utc(), true),
        };
        let when = timezone.from_utc_datetime(&when);

        let tariff_class = opts.tariff_class.unwrap_or(TariffClass::Second);

        let mut req = json!({
            "svcReqL": [
                {
                    "cfg": {
                        "polyEnc": "GPA"
                    },
                    "meth": "TripSearch",
                    "req": {
                        "ctxScr": null,
                        "getPasslist": opts.stopovers.unwrap_or(false),
                        "maxChg": opts.transfers.unwrap_or(-1),
                        "minChgTime": opts.transfer_time.unwrap_or(0),
                        "numF": opts.results.unwrap_or(5),
                        "depLocL": [ from.to_hafas() ],
                        "viaLocL": json!(opts.via.unwrap_or_default().into_iter().map(|x| json!({ "loc": x.to_hafas() })).collect::<Vec<_>>()),
                        "arrLocL": [ to.to_hafas() ],
                        "jnyFltrL": [
                            {
                                "type": "PROD",
                                "mode": "INC",
                                "value": opts.products.to_hafas(),
                            },
                            {
                                "type": "META",
                                "mode": "INC",
                                "meta": opts.accessibility.unwrap_or(Accessibility::r#None).to_hafas(),
                            }
                        ],
                        "gisFltrL": [],
                        "getTariff": opts.tickets.unwrap_or(true),
                        "ushrp": opts.start_with_walking.unwrap_or(true),
                        "getPT": true,
                        "getIV": false,
                        "outFrwd": is_departure,
                        "outDate": when.format("%Y%m%d").to_string(),
                        "outTime": when.format("%H%M%S").to_string(),
                        "trfReq": {
                            "jnyCl": tariff_class.to_hafas(),
                            "tvlrProf": [
                                {
                                    "type": "E",
                                    "redtnCard": opts.loyalty_card.map(LoyaltyCard::to_id),
                                }
                            ],
                            "cType": "PK"
                        }
                    }
                }
            ],
            "lang": opts.language.as_deref().unwrap_or("en"),
        });
        #[cfg(feature = "polylines")]
        {
            req["svcReqL"][0]["req"]["getPolyline"] = json!(opts.polylines.unwrap_or(false));
        }
        #[cfg(not(feature = "polylines"))]
        {
            req["svcReqL"][0]["req"]["getPolyline"] = json!(false);
        }
        if let Some(r) = opts.later_than.or(opts.earlier_than) {
            req["svcReqL"][0]["req"]["ctxScr"] = json!(r);
        }
        if let Some(true) = opts.bike_friendly {
            req["svcReqL"][0]["req"]["jnyFltrL"]
                .as_array_mut()
                .unwrap()
                .push(json!({"type": "BC", "mode": "INC"}))
        }
        let data = self.request(req).await?;

        Ok(self.profile.parse_journeys_response(data, tariff_class)?)
    }
}

#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
impl HafasClient {
    #[wasm_bindgen(js_name = "journeys")]
    pub fn wasm_journeys(&self, from: JsValue, to: JsValue, opts: JsValue) -> Promise {
        let client = self.clone();
        future_to_promise(async move {
            let from = from.into_serde().map_err(|e| e.to_string())?;
            let to = to.into_serde().map_err(|e| e.to_string())?;
            let opts = opts.into_serde().map_err(|e| e.to_string())?;
            let res = client
                .journeys(from, to, opts)
                .await
                .map_err(|e| e.to_string())?;
            Ok(JsValue::from_serde(&res).map_err(|e| e.to_string())?)
        })
    }
}
