package cz.senslog.analytics.server;

import cz.senslog.analytics.domain.ListHolder;
import cz.senslog.analytics.domain.RawObservation;
import cz.senslog.analytics.module.ObservationReceiverModule;
import cz.senslog.analytics.module.api.Module;
import cz.senslog.analytics.server.ws.AuthorizationType;
import cz.senslog.analytics.server.ws.DisableAuthorizationHandler;
import cz.senslog.analytics.server.ws.ExceptionHandler;
import cz.senslog.analytics.server.ws.OpenAPIHandler;
import cz.senslog.analytics.utils.ResourcesUtils;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.KeyStoreOptions;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.*;
import io.vertx.ext.web.openapi.OpenAPIHolder;
import io.vertx.ext.web.openapi.RouterBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.nio.file.Path;
import java.util.function.Function;

import static cz.senslog.analytics.server.ws.AuthorizationType.BEARER;
import static java.util.Objects.requireNonNull;

public final class HttpVertxServer extends AbstractVerticle {
    private static final Logger logger = LogManager.getLogger(HttpVertxServer.class);


    public HttpVertxServer() {

    }

    @Override
    public void start(Promise<Void> startPromise) {
        final AuthorizationType openAPIAuthType = BEARER;

        Path openApiUrl = ResourcesUtils.getPath("openAPISpec.yaml");
        logger.info("Loading the OpenAPI spec from '{}'", openApiUrl);

        RouterBuilder.create(vertx, requireNonNull(openApiUrl, "Open API Specification was not found as a resource.").toString())
                .onSuccess(openAPIRouterBuilder -> {
                    logger.info("The OpenAPI specification was loaded successfully.");

                    openAPIRouterBuilder.rootHandler(LoggerHandler.create(false, LoggerFormat.SHORT));

                    openAPIRouterBuilder.rootHandler(CorsHandler.create()
                            .allowedMethod(HttpMethod.GET)
                            .allowedMethod(HttpMethod.POST)
                            .allowedMethod(HttpMethod.PUT)
                            .allowedMethod(HttpMethod.DELETE)

                            .allowedHeader("x-requested-with")
                            .allowedHeader("Access-Control-Allow-Origin")
                            .allowedHeader("Origin")
                            .allowedHeader("Content-Type")
                            .allowedHeader("Accept")
                    );

                    JsonObject authConfig = config().getJsonObject("auth");
                    if (authConfig.getBoolean("disabled")) {
                        logger.info("Security schema for all endpoints is disabled.");
                        openAPIRouterBuilder.securityHandler(openAPIAuthType.getSecuritySchemaKey(), DisableAuthorizationHandler.create());
                    } else {
                        logger.info("Setting security schema for the type '{}' with the schema key '{}'.", openAPIAuthType.name(), openAPIAuthType.getSecuritySchemaKey());
                        openAPIRouterBuilder.securityHandler(openAPIAuthType.getSecuritySchemaKey(), JWTAuthHandler.create(JWTAuth.create(vertx, new JWTAuthOptions()
                                .setKeyStore(new KeyStoreOptions()
                                        .setPath(authConfig.getString("keystore.path"))
                                        .setType(authConfig.getString("keystore.type"))
                                        .setPassword(authConfig.getString("keystore.password"))))));
                    }


                    // The order matters, so adding the body handler should happen after any PLATFORM or SECURITY_POLICY handler(s).
                    openAPIRouterBuilder.rootHandler(BodyHandler.create());


                    OpenAPIHandler apiHandler = getOpenAPIHandler(openAPIRouterBuilder.getOpenAPI());

                    openAPIRouterBuilder.operation("infoGET").handler(apiHandler::info);
                    openAPIRouterBuilder.operation("observationsPOST").handler(apiHandler::observationsPOST);
                    openAPIRouterBuilder.operation("observationsGET").handler(apiHandler::observationsGET);

                    Router mainRouter = openAPIRouterBuilder.createRouter();

                    mainRouter.route().failureHandler(ExceptionHandler.createAsJSON());

                    mainRouter.errorHandler(404, h -> h.fail(404,
                            new Throwable(String.format("Resource '%s' not found.", h.request().uri()))
                    ));

                    JsonObject serverConfig = config().getJsonObject("server");
                    vertx.createHttpServer()
                            .requestHandler(mainRouter)
                            .listen(serverConfig.getInteger("http.port"))
                            .onSuccess(server -> {
                                logger.info("HTTP server running on port {}.", server.actualPort());
                                startPromise.complete();
                            })
                            .onFailure(fail -> {
                                logger.error(fail);
                                startPromise.fail(fail);
                            });
                })
                .onFailure(startPromise::fail);
    }

    private OpenAPIHandler getOpenAPIHandler(OpenAPIHolder openAPI) {
        final String observationReceiverBusId = Module.of(ObservationReceiverModule.class).id();

        Function<ListHolder<RawObservation>, Future<String>> observationReceiver = listHolder -> {
            Promise<String> replyPromise = Promise.promise();

            vertx.eventBus().request(observationReceiverBusId, listHolder, reply -> {
                if (reply.succeeded()) {
                    replyPromise.complete(reply.result().body().toString());
                } else {
                    replyPromise.fail(reply.cause());
                }
            });

            return replyPromise.future();
        };

        return OpenAPIHandler.create(openAPI, observationReceiver);
    }
}
