mod auth;
mod graphql;

use std::{io, net::SocketAddr, sync::Arc};

use async_graphql::{
    extensions::{Logger, Tracing},
    http::{playground_source, GraphQLPlaygroundConfig},
    Request, Response, Schema,
};
use async_graphql_axum::GraphQLSubscription;
use async_session::{async_trait, MemoryStore, SessionStore};
use auth::{authorized, gitea};
use axum::{
    extract::{rejection::TypedHeaderRejectionReason, FromRequest, RequestParts},
    headers,
    http::{header, Method},
    response::{Html, IntoResponse, Redirect},
    routing::{self, get_service},
    Extension, Json, Router, TypedHeader,
};
use graphql::{
    mutation::MutationRoot, query::QueryRoot, schema::ScelSchema, subscription::SubscriptionRoot,
};
use reqwest::StatusCode;
use scel_core::App;
use serde::{Deserialize, Serialize};
use tower_http::{
    cors::CorsLayer,
    services::ServeDir,
    trace::{DefaultMakeSpan, TraceLayer},
};

async fn graphql_playground() -> impl IntoResponse {
    Html(playground_source(
        GraphQLPlaygroundConfig::new("/graphql").subscription_endpoint("/ws"),
    ))
}
async fn graphql_handler(
    schema: Extension<ScelSchema>,
    req: Json<Request>,
    _: User,
) -> Json<Response> {
    schema.execute(req.0).await.into()
}

pub struct Server {
    app: Router,
    addr: SocketAddr,
}

impl Server {
    pub fn new(app: Arc<App>) -> Server {
        let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
            .extension(Tracing)
            .extension(Logger)
            .data(app)
            .finish();

        let cors = vec![
            "http://localhost:3000"
                .parse()
                .expect("Could not parse url"),
            "https://scel.front.kjuulh.io"
                .parse()
                .expect("Could not parse url"),
        ];

        let api_router = Router::new()
            .route(
                "/graphql",
                routing::get(graphql_playground).post(graphql_handler),
            )
            .route("/ws", GraphQLSubscription::new(schema.clone()))
            .route("/auth/gitea", routing::get(gitea))
            .route("/auth/authorized", routing::get(authorized))
            //        .merge(axum_extra::routing::SpaRouter::new(
            //            "/assets",
            //            "src/web/dist/assets",
            //        ))
            .fallback(get_service(ServeDir::new("./src/web/dist/")).handle_error(handle_error))
            .layer(Extension(schema))
            .layer(Extension(MemoryStore::new()))
            .layer(Extension(auth::oauth_client()))
            .layer(
                CorsLayer::new()
                    .allow_origin(cors)
                    .allow_headers([axum::http::header::CONTENT_TYPE])
                    .allow_methods([Method::GET, Method::POST, Method::OPTIONS]),
            )
            .layer(TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::default()));

        let app = Router::new().nest("/api", api_router);

        let addr = SocketAddr::from(([0, 0, 0, 0], 3000));

        Server { app, addr }
    }

    pub async fn start(self) -> anyhow::Result<()> {
        tracing::info!("listening on {}", self.addr);

        match axum::Server::bind(&self.addr)
            .serve(self.app.into_make_service())
            .await
        {
            Ok(_) => Ok(()),
            Err(e) => Err(e.into()),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct User {
    #[serde(alias = "sub")]
    id: String,
    #[serde(alias = "picture")]
    avatar: Option<String>,
    #[serde(alias = "email")]
    email: String,
    #[serde(alias = "preferred_username")]
    username: String,
}

struct AuthRedirect;

impl IntoResponse for AuthRedirect {
    fn into_response(self) -> axum::response::Response {
        Redirect::temporary("/auth/gitea").into_response()
    }
}

const COOKIE_NAME: &str = "auth";

#[async_trait]
impl<B> FromRequest<B> for User
where
    B: Send,
{
    type Rejection = AuthRedirect;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let Extension(store) = Extension::<MemoryStore>::from_request(req)
            .await
            .expect("MemoryStore extension is missing");

        let cookies = TypedHeader::<headers::Cookie>::from_request(req)
            .await
            .map_err(|e| match *e.name() {
                header::COOKIE => match e.reason() {
                    TypedHeaderRejectionReason::Missing => AuthRedirect,
                    _ => panic!("unexpected error getting Cookie header(s): {}", e),
                },
                _ => panic!("unexpected error getting cookies: {}", e),
            })?;

        let session_cookie = cookies.get(COOKIE_NAME).ok_or(AuthRedirect)?;

        let session = store
            .load_session(session_cookie.to_string())
            .await
            .expect("could not load session")
            .ok_or(AuthRedirect)?;

        let user = session.get::<User>("user").ok_or(AuthRedirect)?;

        Ok(user)
    }
}

async fn handle_error(_err: io::Error) -> impl IntoResponse {
    (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
}
