use kittybox::database::FileStorage;
use std::{env, time::Duration};
use tracing::{debug, error, info};
use url::Url;

#[tokio::main]
async fn main() {
    // TODO use tracing instead of log
    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
    Registry::default()
        .with(EnvFilter::from_default_env())
        .with(tracing_subscriber::fmt::layer().json())
        .init();

    info!("Starting the kittybox server...");

    let backend_uri: String = match env::var("BACKEND_URI") {
        Ok(val) => {
            debug!("Backend URI: {}", val);
            val
        }
        Err(_) => {
            error!("BACKEND_URI is not set, cannot find a database");
            std::process::exit(1);
        }
    };

    let token_endpoint: Url = match env::var("TOKEN_ENDPOINT") {
        Ok(val) => {
            debug!("Token endpoint: {}", val);
            match Url::parse(&val) {
                Ok(val) => val,
                _ => {
                    error!("Token endpoint URL cannot be parsed, aborting.");
                    std::process::exit(1)
                }
            }
        }
        Err(_) => {
            error!("TOKEN_ENDPOINT is not set, will not be able to authorize users!");
            std::process::exit(1)
        }
    };

    let authorization_endpoint: Url = match env::var("AUTHORIZATION_ENDPOINT") {
        Ok(val) => {
            debug!("Auth endpoint: {}", val);
            match Url::parse(&val) {
                Ok(val) => val,
                _ => {
                    error!("Authorization endpoint URL cannot be parsed, aborting.");
                    std::process::exit(1)
                }
            }
        }
        Err(_) => {
            error!("AUTHORIZATION_ENDPOINT is not set, will not be able to confirm token and ID requests using IndieAuth!");
            std::process::exit(1)
        }
    };

    let listen_at = match env::var("SERVE_AT")
        .ok()
        .unwrap_or_else(|| "[::]:8080".to_string())
        .parse::<std::net::SocketAddr>()
    {
        Ok(addr) => addr,
        Err(e) => {
            error!("Cannot parse SERVE_AT: {}", e);
            std::process::exit(1);
        }
    };

    let http: reqwest::Client = {
        #[allow(unused_mut)]
        let mut builder = reqwest::Client::builder().user_agent(concat!(
            env!("CARGO_PKG_NAME"),
            "/",
            env!("CARGO_PKG_VERSION")
        ));
        // TODO: add a root certificate if there's an environment variable pointing at it
        //builder = builder.add_root_certificate(reqwest::Certificate::from_pem(todo!()));

        builder.build().unwrap()
    };

    if backend_uri.starts_with("redis") {
        println!("The Redis backend is deprecated.");
        std::process::exit(1);
    } else if backend_uri.starts_with("file") {
        let database = {
            let folder = backend_uri.strip_prefix("file://").unwrap();
            let path = std::path::PathBuf::from(folder);
            match kittybox::database::FileStorage::new(path).await {
                Ok(db) => db,
                Err(err) => {
                    error!("Error creating database: {:?}", err);
                    std::process::exit(1);
                }
            }
        };

        let endpoints = kittybox::frontend::IndiewebEndpoints {
            authorization_endpoint: authorization_endpoint.to_string(),
            token_endpoint: token_endpoint.to_string(),
            webmention: None,
            microsub: None,
        };

        /*let micropub = warp::path("micropub")
            .and(warp::path::end()
                 .and(kittybox::micropub::micropub(
                     database.clone(),
                     token_endpoint.to_string(),
                     http.clone()
                 ))
                 .or(warp::get()
                     .and(warp::path("client"))
                     .and(warp::path::end())
                     .map(|| warp::reply::html(kittybox::MICROPUB_CLIENT))));

        let static_files = warp::path("static")
            .and(kittybox::frontend::static_files());

        let media = warp::path("media")
            .and(warp::path::end()
                 .and(kittybox::media::media())
                 .or(kittybox::util::require_host()
                     .and(warp::path::param())
                     .map(|_host: Authority, path: String| format!("media file {}", path))));

        let technical = warp::path(".kittybox")
            .and(warp::path("onboarding")
                 .and(warp::path::end())
                 .and(kittybox::frontend::onboarding(
                     database.clone(),
                     endpoints.clone(),
                     http.clone()
                 ))
                 .or(warp::path("health")
                     .and(warp::path::end())
                     .and(warp::get())
                     // TODO make healthcheck report on database status too
                     .map(|| "OK"))
                 .or(warp::path("metrics")
                     .and(warp::path::end())
                     .and(warp::get())
                     .map(kittybox::metrics::gather))
                 .or(micropub)
                 .or(media)
                 .or(static_files)
                 .or(warp::path("login")
                     .and(warp::path("callback")
                          .map(|| "callback!")
                          // TODO form on GET and handler on POST
                          .or(warp::path::end()
                              .map(|| "login page!"))
                     )
                 )
            );

        // TODO prettier error response
        let coffee = warp::path("coffee")
            .map(|| warp::reply::with_status("I'm a teapot!", warp::http::StatusCode::IM_A_TEAPOT));

        et catchall = ;

        let app = homepage
            .or(technical)
            .or(coffee)
            .or(catchall)
            .with(warp::log("kittybox"))
            .with(kittybox::metrics::metrics(vec![
                ".kittybox".to_string()
            ]))
            ;

        let svc = warp::service(app);
         */

        let svc = axum::Router::new()
            .route(
                "/",
                axum::routing::get(kittybox::frontend::homepage::<FileStorage>),
            )
            .route(
                "/.kittybox/coffee",
                axum::routing::get(|| async {
                    use axum::http::{header, StatusCode};
                    (
                        StatusCode::IM_A_TEAPOT,
                        [(header::CONTENT_TYPE, "text/plain")],
                        "Sorry, can't brew coffee yet!",
                    )
                }),
            )
            .route(
                "/.kittybox/onboarding",
                axum::routing::get(kittybox::frontend::onboarding::get)
                    .post(kittybox::frontend::onboarding::post::<FileStorage>),
            )
            .route(
                "/.kittybox/micropub",
                axum::routing::get(kittybox::micropub::query::<FileStorage>)
                    .post(kittybox::micropub::post::<FileStorage>)
                    .layer(
                        tower_http::cors::CorsLayer::new()
                            .allow_methods([axum::http::Method::GET, axum::http::Method::POST])
                            .allow_origin(tower_http::cors::Any),
                    ),
            )
            .route(
                "/.kittybox/micropub/client",
                axum::routing::get(|| {
                    std::future::ready(axum::response::Html(kittybox::MICROPUB_CLIENT))
                }),
            )
            .route(
                "/.kittybox/health",
                axum::routing::get(|| async {
                    // TODO health-check the database
                    "OK"
                }),
            )
            .route(
                "/.kittybox/metrics",
                axum::routing::get(|| async { todo!() }),
            )
            .nest(
                "/.kittybox/media",
                axum::Router::new()
                    .route(
                        "/",
                        axum::routing::get(|| async { todo!() }).post(|| async { todo!() }),
                    )
                    .route("/:filename", axum::routing::get(|| async { todo!() })),
            )
            .route(
                "/.kittybox/static/:path",
                axum::routing::get(kittybox::frontend::statics),
            )
            .fallback(axum::routing::get(
                kittybox::frontend::catchall::<FileStorage>,
            ))
            .layer(axum::Extension(database))
            .layer(axum::Extension(http))
            .layer(axum::Extension(kittybox::indieauth::TokenEndpoint(
                token_endpoint,
            )))
            .layer(
                tower::ServiceBuilder::new()
                    .layer(tower_http::trace::TraceLayer::new_for_http())
                    .into_inner(),
            );

        // A little dance to turn a potential file descriptor into a guaranteed async network socket
        let tcp_listener: std::net::TcpListener = {
            let mut listenfd = listenfd::ListenFd::from_env();

            let tcp_listener = if let Ok(Some(listener)) = listenfd.take_tcp_listener(0) {
                listener
            } else {
                std::net::TcpListener::bind(listen_at).unwrap()
            };
            // Set the socket to non-blocking so tokio can work with it properly
            // This is the async magic
            tcp_listener.set_nonblocking(true).unwrap();

            tcp_listener
        };
        info!("Listening on {}", tcp_listener.local_addr().unwrap());

        let server = hyper::server::Server::from_tcp(tcp_listener)
            .unwrap()
            // Otherwise Chrome keeps connections open for too long
            .tcp_keepalive(Some(Duration::from_secs(30 * 60)))
            .serve(svc.into_make_service())
            .with_graceful_shutdown(async move {
                // Defer to C-c handler whenever we're not on Unix
                // TODO consider using a diverging future here
                #[cfg(not(unix))]
                return tokio::signal::ctrl_c().await.unwrap();
                #[cfg(unix)]
                {
                    use tokio::signal::unix::{signal, SignalKind};

                    signal(SignalKind::terminate())
                        .unwrap()
                        .recv()
                        .await
                        .unwrap()
                }
            });

        if let Err(err) = server.await {
            error!("Error serving requests: {}", err);
            std::process::exit(1);
        }
    } else {
        println!("Unknown backend, not starting.");
        std::process::exit(1);
    }
}