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

fn init_media<A: kittybox::indieauth::backend::AuthBackend>(auth_backend: A, blobstore_uri: &str) -> axum::Router {
    match blobstore_uri.split_once(':').unwrap().0 {
        "file" => {
            let folder = std::path::PathBuf::from(
                blobstore_uri.strip_prefix("file://").unwrap()
            );
            let blobstore = kittybox::media::storage::file::FileStore::new(folder);

            kittybox::media::router::<_, _>(blobstore, auth_backend)
        },
        other => unimplemented!("Unsupported backend: {other}")
    }
}

async fn compose_kittybox_with_auth<A>(
    http: reqwest::Client,
    auth_backend: A,
    backend_uri: &str,
    blobstore_uri: &str
) -> axum::Router
where A: kittybox::indieauth::backend::AuthBackend
{
    match backend_uri.split_once(':').unwrap().0 {
        "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);
                    }
                }
            };

            // Technically, if we don't construct the micropub router,
            // we could use some wrapper that makes the database
            // read-only.
            //
            // This would allow to exclude all code to write to the
            // database and separate reader and writer processes of
            // Kittybox to improve security.
            let homepage: axum::routing::MethodRouter<_> = axum::routing::get(
                kittybox::frontend::homepage::<FileStorage>
            )
                .layer(axum::Extension(database.clone()));
            let fallback = axum::routing::get(
                kittybox::frontend::catchall::<FileStorage>
            )
                .layer(axum::Extension(database.clone()));

            let micropub = kittybox::micropub::router(
                database.clone(),
                http.clone(),
                auth_backend.clone()
            );
            let onboarding = kittybox::frontend::onboarding::router(
                database.clone(), http.clone()
            );

            axum::Router::new()
                .route("/", homepage)
                .fallback(fallback)
                .route("/.kittybox/micropub", micropub)
                .route("/.kittybox/onboarding", onboarding)
                .nest("/.kittybox/media", init_media(auth_backend.clone(), blobstore_uri))
                .merge(kittybox::indieauth::router(auth_backend.clone(), database.clone(), http.clone()))
                .route(
                    "/.kittybox/health",
                    axum::routing::get(health_check::<kittybox::database::FileStorage>)
                        .layer(axum::Extension(database))
                )
        },
        "redis" => unimplemented!("Redis backend is not supported."),

        other => unimplemented!("Unsupported backend: {other}")
    }
}

async fn compose_kittybox(backend_uri: &str, blobstore_uri: &str, authstore_uri: &str) -> axum::Router {
    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()
    };

    let router = match authstore_uri.split_once(':').unwrap().0 {
        "file" => {
            let auth_backend = {
                let folder = authstore_uri
                    .strip_prefix("file://")
                    .unwrap();
                kittybox::indieauth::backend::fs::FileBackend::new(folder)
            };

            compose_kittybox_with_auth(http, auth_backend, backend_uri, blobstore_uri).await
        }
        other => unimplemented!("Unsupported backend: {other}")
    };

    router
        .route(
            "/.kittybox/static/:path",
            axum::routing::get(kittybox::frontend::statics)
        )
        .route("/.kittybox/coffee", teapot_route())
        .nest("/.kittybox/micropub/client", kittybox::companion::router())
        .layer(tower::ServiceBuilder::new()
            .layer(tower_http::trace::TraceLayer::new_for_http())
            .into_inner())
        .layer(tower_http::catch_panic::CatchPanicLayer::new())
}

fn teapot_route() -> axum::routing::MethodRouter {
    axum::routing::get(|| async {
        use axum::http::{header, StatusCode};
        (StatusCode::IM_A_TEAPOT, [(header::CONTENT_TYPE, "text/plain")], "Sorry, can't brew coffee yet!")
    })
}

async fn health_check</*A, B, */D>(
    //axum::Extension(auth): axum::Extension<A>,
    //axum::Extension(blob): axum::Extension<B>,
    axum::Extension(data): axum::Extension<D>,
) -> impl axum::response::IntoResponse
where
    //A: kittybox::indieauth::backend::AuthBackend,
    //B: kittybox::media::storage::MediaStore,
    D: kittybox::database::Storage
{
    (axum::http::StatusCode::OK, std::borrow::Cow::Borrowed("OK"))
}

#[tokio::main]
async fn main() {
    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 = env::var("BACKEND_URI")
        .unwrap_or_else(|_| {
            error!("BACKEND_URI is not set, cannot find a database");
            std::process::exit(1);
        });
    let blobstore_uri: String = env::var("BLOBSTORE_URI")
        .unwrap_or_else(|_| {
            error!("BLOBSTORE_URI is not set, can't find media store");
            std::process::exit(1);
        });

    let authstore_uri: String = env::var("AUTH_STORE_URI")
        .unwrap_or_else(|_| {
            error!("AUTH_STORE_URI is not set, can't find authentication store");
            std::process::exit(1);
        });

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

    let router = compose_kittybox(
        backend_uri.as_str(),
        blobstore_uri.as_str(),
        authstore_uri.as_str()
    ).await;

    // 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_addr).unwrap()
        };
        // Set the socket to non-blocking so tokio can poll 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(router.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);
    }
}