use authdb::{AuthDB, AuthError, AuthToken, SessionToken, Username};
use std::{
    convert::Infallible,
    net::{IpAddr, Ipv4Addr, SocketAddr},
    path::PathBuf,
    sync::Arc,
};
use warp::{
    header,
    http::StatusCode,
    reply::{Json, Reply},
    Filter,
};

mod handlers;
use handlers::handle_auth;

#[derive(Debug)]
struct Unauthorized;
impl warp::reject::Reject for Unauthorized {}

#[derive(Debug)]
struct AuthDBError(AuthError);
impl warp::reject::Reject for AuthDBError {}

fn with_session(
    auth_ctx: Arc<AuthDB>,
) -> impl Filter<Extract = (Username,), Error = warp::Rejection> + Clone {
    header("authentication").and_then({
        move |value: String| {
            let auth_ctx = auth_ctx.clone();
            async move {
                match auth_ctx.validate_session(SessionToken::from(value)).await {
                    Ok(Some(username)) => Ok(username),
                    Ok(None) => Err(warp::reject::custom(Unauthorized)),
                    Err(err) => Err(warp::reject::custom(AuthDBError(err))),
                }
            }
        }
    })
}

fn route_echo_unauthenticated() -> impl Filter<Extract = (Json,), Error = warp::Rejection> + Clone {
    warp::path!("api" / "v1" / "echo" / String).map(|param: String| {
        println!("param: {}", param);
        warp::reply::json(&vec!["unauthenticated", param.as_str()])
    })
}

fn route_authenticate(
    auth_ctx: Arc<AuthDB>,
) -> impl Filter<Extract = (Json,), Error = warp::Rejection> + Clone {
    let auth_ctx = auth_ctx.clone();
    warp::path!("api" / "v1" / "auth")
        .and(warp::post())
        .and(warp::body::json())
        .map(move |param: AuthToken| {
            let res = handle_auth(&auth_ctx, param.clone());
            warp::reply::json(&param)
        })
}

fn route_echo_authenticated(
    auth_ctx: Arc<AuthDB>,
) -> impl Filter<Extract = (Json,), Error = warp::Rejection> + Clone {
    warp::path!("api" / "v1" / "echo" / String)
        .and(with_session(auth_ctx.clone()))
        .map(move |param: String, username: Username| {
            println!("param: {:?}", username);
            println!("param: {}", param);
            warp::reply::json(&vec!["authenticated", username.as_str(), param.as_str()])
        })
}

async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
    if let Some(Unauthorized) = err.find() {
        Ok(warp::reply::with_status(
            "".to_owned(),
            StatusCode::UNAUTHORIZED,
        ))
    } else {
        Ok(warp::reply::with_status(
            "".to_owned(),
            StatusCode::INTERNAL_SERVER_ERROR,
        ))
    }
}

#[tokio::main]
pub async fn main() {
    let auth_db = AuthDB::new(PathBuf::from("./auth_db.sqlite"))
        .await
        .expect("AuthDB should initialize");
    let auth_ctx: Arc<AuthDB> = Arc::new(auth_db);

    let filter = route_echo_authenticated(auth_ctx.clone())
        .or(route_authenticate(auth_ctx.clone()))
        .or(route_echo_unauthenticated())
        .recover(handle_rejection);

    let server = warp::serve(filter);
    server
        .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8001))
        .await;
}
