use anyhow::anyhow;
use async_trait::async_trait;
use hafas_rs::api::journeys::JourneysOptions;
use hafas_rs::client::HafasClient;
use hafas_rs::profile::db::DbProfile;
use hafas_rs::requester::hyper::HyperRustlsRequester;
use hafas_rs::Location;
use hafas_rs::Place;
use hafas_rs::Stop;
use hyper::service::{make_service_fn, service_fn};
use hyper::{header, Body, Request, Response, Server, StatusCode};
use log::warn;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;

#[async_trait]
trait HafasClientExt {
    async fn handle_journeys_request(
        self: Arc<Self>,
        query: &str,
    ) -> anyhow::Result<Response<Body>>;
    async fn handle_locations_request(
        self: Arc<Self>,
        query: &str,
    ) -> anyhow::Result<Response<Body>>;
    async fn handle_refresh_journey_request(
        self: Arc<Self>,
        refresh_token: &str,
        query: &str,
    ) -> anyhow::Result<Response<Body>>;
    async fn handle_request(self: Arc<Self>, req: Request<Body>) -> anyhow::Result<Response<Body>>;
    async fn try_handle_request(self: Arc<Self>, req: Request<Body>) -> Response<Body>;
}

fn parse_query_place(query_map: &HashMap<&str, &str>, prefix: &str) -> anyhow::Result<Place> {
    let res = if let Some(id) = query_map.get(prefix) {
        Place::Stop(Stop {
            name: None,
            products: None,
            location: None,
            id: id
                .parse()
                .map_err(|e| anyhow!("couldn't parse '{}' parameter: {}", prefix, e))?,
        })
    } else {
        let latitude = query_map
            .get(format!("{}.latitude", prefix).as_str())
            .map(|x| {
                x.parse()
                    .map_err(|e| anyhow!("couldn't parse '{}.latitude' parameter: {}", prefix, e))
            })
            .transpose()?;
        let longitude = query_map
            .get(format!("{}.longitude", prefix).as_str())
            .map(|x| {
                x.parse()
                    .map_err(|e| anyhow!("couldn't parse '{}.longitude' parameter: {}", prefix, e))
            })
            .transpose()?;
        match (latitude, longitude) {
            (Some(latitude), Some(longitude)) => {
                if let Some(address) = query_map.get(format!("{}.address", prefix).as_str()) {
                    Place::Location(Location::Address {
                        address: address.to_string(),
                        latitude,
                        longitude,
                    })
                } else {
                    Place::Location(Location::Point {
                        latitude,
                        longitude,
                        poi: None,
                        name: query_map
                            .get(format!("{}.name", prefix).as_str())
                            .map(|x| x.to_string()),
                        id: query_map
                            .get(format!("{}.id", prefix).as_str())
                            .map(|x| x.to_string()),
                    })
                }
            }
            (None, Some(_)) => return Err(anyhow!("missing '{}.latitude'", prefix)),
            (Some(_), None) => return Err(anyhow!("missing '{}.longitude'", prefix)),
            (None, None) => return Err(anyhow!("missing '{}'", prefix)),
        }
    };
    Ok(res)
}

#[derive(Deserialize)]
struct RefreshToken {
    refresh_token: String,
}

#[async_trait]
impl HafasClientExt for HafasClient {
    async fn handle_journeys_request(
        self: Arc<Self>,
        query: &str,
    ) -> anyhow::Result<Response<Body>> {
        let query_map = query
            .split("&")
            .map(|x| {
                let mut iter = x.splitn(2, "=");
                let first = iter.next().ok_or(anyhow!("invalid query string"))?;
                let second = iter.next().ok_or(anyhow!("invalid query string"))?;
                Ok((first, second))
            })
            .collect::<anyhow::Result<HashMap<&str, &str>>>()?;
        let from = parse_query_place(&query_map, "from")?;
        let to = parse_query_place(&query_map, "to")?;
        let products = serde_urlencoded::from_str(&query)?;
        let mut opts: JourneysOptions = serde_urlencoded::from_str(&query)?;
        opts.products = products;
        let resp = self.journeys(from, to, opts).await?;
        Ok(Response::builder()
            .body(serde_json::to_string(&resp)?.into())
            .unwrap())
    }

    async fn handle_locations_request(
        self: Arc<Self>,
        query: &str,
    ) -> anyhow::Result<Response<Body>> {
        let opts = serde_urlencoded::from_str(&query)?;
        let locations = self.locations(opts).await?;
        Ok(Response::builder()
            .body(serde_json::to_string(&locations)?.into())
            .unwrap())
    }

    async fn handle_refresh_journey_request(
        self: Arc<Self>,
        refresh_token: &str,
        query: &str,
    ) -> anyhow::Result<Response<Body>> {
        let opts = serde_urlencoded::from_str(&query)?;
        let fake_query = format!("refresh_token={}", refresh_token);
        let RefreshToken { refresh_token } = serde_urlencoded::from_str(&fake_query)?;
        let journey = self.refresh_journey(&refresh_token, opts).await?;
        Ok(Response::builder()
            .body(serde_json::to_string(&journey)?.into())
            .unwrap())
    }

    async fn handle_request(self: Arc<Self>, req: Request<Body>) -> anyhow::Result<Response<Body>> {
        let path = req.uri().path();
        let query = req.uri().query().unwrap_or("");

        Ok(
            if let Some(refresh_token) = path.strip_prefix("/journeys/") {
                self.handle_refresh_journey_request(refresh_token, query)
                    .await?
            } else if path == "/locations" {
                self.handle_locations_request(query).await?
            } else if path == "/journeys" {
                self.handle_journeys_request(query).await?
            } else {
                Response::builder()
                    .status(StatusCode::NOT_FOUND)
                    .body("404 Route not found".into())
                    .unwrap()
            },
        )
    }

    async fn try_handle_request(self: Arc<Self>, req: Request<Body>) -> Response<Body> {
        let mut resp = self.handle_request(req).await.unwrap_or_else(|e| {
            warn!("Error while handling request: {}", e);
            Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(format!("Server Error: {}", e).into())
                .unwrap()
        });
        resp.headers_mut().insert(
            header::ACCESS_CONTROL_ALLOW_ORIGIN,
            header::HeaderValue::from_static("*"),
        );
        resp
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    env_logger::init();

    let c = Arc::new(HafasClient::new(DbProfile, HyperRustlsRequester::new()));

    let port = std::env::var("PORT")
        .or(Ok::<String, ()>("8123".to_string()))
        .unwrap()
        .parse::<u16>()
        .unwrap();
    let addr = ([127, 0, 0, 1], port).into();

    let make_svc = make_service_fn(|_| {
        let c = c.clone();
        async move {
            Ok::<_, std::convert::Infallible>(service_fn(move |req: Request<Body>| {
                let c = c.clone();
                async move { Ok::<_, std::convert::Infallible>(c.try_handle_request(req).await) }
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    println!("listening on port {}", port);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }

    Ok(())
}
