package com.datafibers.processor;

import com.datafibers.util.DFAPIMessage;
import io.vertx.core.Vertx;
import io.vertx.core.WorkerExecutor;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import java.net.ConnectException;
import io.vertx.ext.web.client.WebClient;
import org.bson.types.ObjectId;
import org.json.JSONException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.apache.log4j.Logger;
import com.datafibers.util.ConstantApp;
import com.datafibers.util.HelpFunc;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;

public class ProcessorTopicSchemaRegistry {
    private static final Logger LOG = Logger.getLogger(ProcessorTopicSchemaRegistry.class);

    /**
     * Retrieve all subjects first; and then retrieve corresponding subject's schema information.
     * Here, we'll filter topic-value and topic-key subject since these are used by the kafka and CR.
     * These subject are not available until SourceRecord is available.
     * Use block rest client, but unblock using vertx worker.
     *
     * @param routingContext
     * @param schema_registry_host_and_port
     */
    public static void forwardGetAllSchemas(Vertx vertx, RoutingContext routingContext,
                                            String schema_registry_host_and_port) {
        StringBuffer returnString = new StringBuffer();
        WorkerExecutor executor = vertx.createSharedWorkerExecutor("forwardGetAllSchemas_pool_" + new ObjectId(),
                ConstantApp.WORKER_POOL_SIZE, ConstantApp.MAX_RUNTIME);
        executor.executeBlocking(future -> {
            String restURI = "http://" + schema_registry_host_and_port + "/subjects";
            int status_code = ConstantApp.STATUS_CODE_OK;
            try {
                HttpResponse<String> res = Unirest
                        .get(restURI)
                        .header("accept", ConstantApp.AVRO_REGISTRY_CONTENT_TYPE)
                        .asString();

                if (res == null) {
                    status_code = ConstantApp.STATUS_CODE_BAD_REQUEST;
                } else if (res.getStatus() != ConstantApp.STATUS_CODE_OK) {
                    status_code = res.getStatus();
                } else {
                    String subjects = res.getBody();
                    // ["Kafka-value","Kafka-key"]
                    LOG.debug("All subjects received are " + subjects);
                    StringBuffer strBuff = new StringBuffer();
                    int count = 0;
                    if (subjects.compareToIgnoreCase("[]") != 0) { // Has active subjects
                        for (String subject : subjects.substring(2, subjects.length() - 2).split("\",\"")) {
                            // If the subject is internal one, such as topic-key or topic-value
                            if(subject.contains("df_meta") || subject.contains("-value") || subject.contains("-key"))
                                continue;

                            HttpResponse<JsonNode> resSubject = Unirest
                                    .get(restURI + "/" + subject + "/versions/latest")
                                    .header("accept", ConstantApp.HTTP_HEADER_APPLICATION_JSON_CHARSET)
                                    .asJson();
                            if (resSubject == null) {
                                status_code = ConstantApp.STATUS_CODE_BAD_REQUEST;
                            } else if (resSubject.getStatus() != ConstantApp.STATUS_CODE_OK) {
                                status_code = resSubject.getStatus();
                            } else {
                                JSONObject jsonSchema = resSubject.getBody().getObject();
                                String compatibility =
                                        getCompatibilityOfSubject(schema_registry_host_and_port, subject);
                                if (compatibility == null || compatibility.isEmpty())
                                    compatibility = "NONE";
                                jsonSchema.put(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY, compatibility);
                                // Repack subject to id, id to schema id
                                jsonSchema.put("schemaId", jsonSchema.get("id"));
                                jsonSchema.put("id", subject);
                                String schema = jsonSchema.toString();
                                if (count == 0)
                                    strBuff.append("[");
                                count ++;
                                strBuff.append(schema).append(",");
                            }
                        }
                        if (count > 0)
                            returnString.append(strBuff.toString().substring(0, strBuff.toString().length() - 1) + "]");
                        //LOG.debug("returnString: " + returnString.toString());
                    }
                }
            } catch (JSONException | UnirestException e) {
                LOG.error(DFAPIMessage.logResponseMessage(9027, " exception -" + e.getCause()));
                status_code = ConstantApp.STATUS_CODE_BAD_REQUEST;
            }
            future.complete(status_code);
        }, res -> {
            Object result = HelpFunc.coalesce(res.result(), ConstantApp.STATUS_CODE_BAD_REQUEST);
            try {
                if (returnString == null || returnString.toString().isEmpty()) {
                    HelpFunc.responseCorsHandleAddOn(routingContext.response())
                            .setStatusCode(Integer.parseInt(result.toString()))
                            .putHeader("X-Total-Count", "0")
                            .end("[]");

                } else {
                    HelpFunc.responseCorsHandleAddOn(routingContext.response())
                            .setStatusCode(Integer.parseInt(result.toString()))
                            .putHeader("X-Total-Count", new JSONArray(returnString.toString()).length() + "")
                            .end(HelpFunc.stringToJsonFormat(
                                    HelpFunc.sortJsonArray(routingContext,
                                            new JSONArray(returnString.toString())
                                    ).toString())
                            );
                }
            } catch (JSONException je) {
                LOG.error(DFAPIMessage.logResponseMessage(9027, " exception - " + je.getCause()));
            }
            executor.close();
        });
    }

    /**
     * Retrieve the specified subject's schema information. Use unblock rest client.
     *
     * @param routingContext
     * @param webClient
     * @param schemaRegistryRestHost
     * @param schemaRegistryRestPort
     */
    public static void forwardGetOneSchema(RoutingContext routingContext, WebClient webClient,
                                           String schemaRegistryRestHost, int schemaRegistryRestPort) {
        final String subject = routingContext.request().getParam("id");
        webClient.get(schemaRegistryRestPort, schemaRegistryRestHost,
                ConstantApp.SR_REST_URL_SUBJECTS + "/" + subject + ConstantApp.SR_REST_URL_VERSIONS + "/latest")
                .putHeader(ConstantApp.HTTP_HEADER_CONTENT_TYPE, ConstantApp.AVRO_REGISTRY_CONTENT_TYPE)
                .send(ar -> {
                    if (ar.succeeded() && (ar.result().statusCode() == ConstantApp.STATUS_CODE_OK)) {
                        // get compatibility of schema
                        JsonObject schema = ar.result().bodyAsJsonObject();
                        webClient.get(schemaRegistryRestPort, schemaRegistryRestHost,
                                ConstantApp.SR_REST_URL_CONFIG + "/" + subject)
                                .putHeader(ConstantApp.HTTP_HEADER_CONTENT_TYPE, ConstantApp.AVRO_REGISTRY_CONTENT_TYPE)
                                .send(arc -> {
                                        JsonObject res = arc.result().bodyAsJsonObject();
                                        // When failed to get compatibility, return NONE
                                        String compatibility =
                                                res.containsKey(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY_LEVEL)?
                                                res.getString(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY_LEVEL):
                                                "NONE";

                                        schema.put(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY, compatibility);

                                        HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                                .setStatusCode(ConstantApp.STATUS_CODE_OK)
                                                .end(Json.encodePrettily(schema));
                                        LOG.info(DFAPIMessage.logResponseMessage(1030, subject));
                                });
                    } else {
                        HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                .setStatusCode(ConstantApp.STATUS_CODE_NOT_FOUND)
                                .end(DFAPIMessage.getResponseMessage(9041, subject));
                        LOG.error(DFAPIMessage.logResponseMessage(9041, subject));
                    }
                });
    }

    /**
     * Add one schema to schema registry with non-blocking rest client. Add schema and update schema have the same API
     * specification only difference is to packing subject and compatibility from different field.
     *
     * @param routingContext This is the connect from REST API
     * @param webClient This is non-blocking rest client used for forwarding
     * @param schemaRegistryRestHost Schema Registry Rest Host
     * @param schemaRegistryRestPort Schema Registry Rest Port
     */
    public static void forwardAddOneSchema(RoutingContext routingContext, WebClient webClient,
                                           String schemaRegistryRestHost, int schemaRegistryRestPort) {

        addOneSchemaCommon(routingContext, webClient,
                schemaRegistryRestHost, schemaRegistryRestPort,
                "Schema is created.", 1025,
                "Schema creation is failed", 9039
        );

    }

    /**
     * Update one schema including compatibility to schema registry with non-blocking rest client
     *
     * @param routingContext This is the connect from REST API
     * @param webClient This is vertx non-blocking rest client used for forwarding
     * @param schemaRegistryRestHost Schema Registry Rest Host
     * @param schemaRegistryRestPort Schema Registry Rest Port
     */
    public static void forwardUpdateOneSchema(RoutingContext routingContext, WebClient webClient,
                                              String schemaRegistryRestHost, int schemaRegistryRestPort) {

        addOneSchemaCommon(routingContext, webClient,
                schemaRegistryRestHost, schemaRegistryRestPort,
                "Schema is updated.", 1017,
                "Schema update is failed", 9023
        );
    }

    /**
     * This is commonly used utility
     *
     * @param routingContext This is the connect from REST API
     * @param webClient This is vertx non-blocking rest client used for forwarding
     * @param schemaRegistryRestHost Schema Registry Rest Host
     * @param schemaRegistryRestPort Schema Registry Rest Port
     * @param successMsg Message to response when succeeded
     * @param successCode Status code to response when succeeded
     * @param errorMsg Message to response when failed
     * @param errorCode Status code to response when failed
     */
    public static void addOneSchemaCommon(RoutingContext routingContext, WebClient webClient,
                                          String schemaRegistryRestHost, int schemaRegistryRestPort,
                                          String successMsg, int successCode, String errorMsg, int errorCode) {

        JsonObject jsonObj = routingContext.getBodyAsJson();
        JsonObject schemaObj = jsonObj.getJsonObject(ConstantApp.SCHEMA_REGISTRY_KEY_SCHEMA);

        if(!jsonObj.containsKey("id") && !jsonObj.containsKey(ConstantApp.SCHEMA_REGISTRY_KEY_SUBJECT))
            LOG.error(DFAPIMessage.logResponseMessage(9040, "Subject of Schema is missing."));

        // get subject from id (web ui assigned) and assign it to subject
        String subject = jsonObj.containsKey("id")? jsonObj.getString("id"):
                jsonObj.getString(ConstantApp.SCHEMA_REGISTRY_KEY_SUBJECT);

        // Set schema name from subject if it does not has name or empty
        if(!schemaObj.containsKey("name") || schemaObj.getString("name").isEmpty()) {
            schemaObj.put("name", subject);
            jsonObj.put(ConstantApp.SCHEMA_REGISTRY_KEY_SCHEMA, schemaObj);
        }

        String compatibility = jsonObj.containsKey(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY) ?
                jsonObj.getString(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY) : "NONE";

        webClient.post(schemaRegistryRestPort, schemaRegistryRestHost,
                ConstantApp.SR_REST_URL_SUBJECTS + "/" + subject + ConstantApp.SR_REST_URL_VERSIONS)
                .putHeader(ConstantApp.HTTP_HEADER_CONTENT_TYPE, ConstantApp.AVRO_REGISTRY_CONTENT_TYPE)
                .sendJsonObject( new JsonObject()
                                .put(ConstantApp.SCHEMA_REGISTRY_KEY_SCHEMA, schemaObj.toString()),
                        // Must toString above according SR API spec.
                        ar -> {
                            if (ar.succeeded()) {
                                LOG.info(DFAPIMessage.logResponseMessage(successCode, subject + "-SCHEMA"));
                                // Once successful, we will update schema compatibility
                                webClient.put(schemaRegistryRestPort, schemaRegistryRestHost,
                                        ConstantApp.SR_REST_URL_CONFIG + "/" + subject)
                                        .putHeader(ConstantApp.HTTP_HEADER_CONTENT_TYPE,
                                                ConstantApp.AVRO_REGISTRY_CONTENT_TYPE)
                                        .sendJsonObject(new JsonObject()
                                                .put(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY, compatibility),
                                                arc -> {
                                                    if (arc.succeeded()) {
                                                        HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                                                .setStatusCode(ConstantApp.STATUS_CODE_OK)
                                                                .end(Json.encodePrettily(jsonObj));
                                                        LOG.info(DFAPIMessage.logResponseMessage(1017,
                                                                successMsg + "-COMPATIBILITY"));
                                                    } else {
                                                        // If response is failed, repose df ui and still keep the task
                                                        HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                                                .setStatusCode(ConstantApp.STATUS_CODE_BAD_REQUEST)
                                                                .end(DFAPIMessage.getResponseMessage(errorCode,
                                                                        subject, errorMsg + "-COMPATIBILITY"));
                                                        LOG.info(DFAPIMessage.logResponseMessage(errorCode,
                                                                subject + "-COMPATIBILITY"));
                                                    }
                                                }
                                        );
                            } else {
                                // If response is failed, repose df ui and still keep the task
                                HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                        .setStatusCode(ConstantApp.STATUS_CODE_BAD_REQUEST)
                                        .end(DFAPIMessage.getResponseMessage(errorCode, subject,
                                                errorMsg + "-SCHEMA"));
                                LOG.info(DFAPIMessage.logResponseMessage(errorCode, subject  + "-SCHEMA"));
                            }
                        }
                );
    }

    /**
     * This method first decode the REST DELETE request to get schema subject. Then, it forward the DELETE to
     * Schema Registry.
     *
     * @param routingContext This is the connect from REST API
     * @param webClient This is vertx non-blocking rest client used for forwarding
     * @param schemaRegistryRestHost Schema Registry Rest Host
     * @param schemaRegistryRestPort Schema Registry Rest Port
     */
    public static void forwardDELETEAsDeleteOne (RoutingContext routingContext, WebClient webClient,
                                                 String schemaRegistryRestHost, int schemaRegistryRestPort) {

        String subject = routingContext.request().getParam("id");
        // Create REST Client for Kafka Connect REST Forward
        webClient.delete(schemaRegistryRestPort, schemaRegistryRestHost,
                ConstantApp.SR_REST_URL_SUBJECTS + "/" + subject)
                .putHeader(ConstantApp.HTTP_HEADER_CONTENT_TYPE, ConstantApp.AVRO_REGISTRY_CONTENT_TYPE)
                .sendJsonObject(DFAPIMessage.getResponseJsonObj(1026),
                        ar -> {
                            if (ar.succeeded() &&
                                    ar.result().statusCode() == ConstantApp.STATUS_CODE_OK) {
                                    HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                            .setStatusCode(ConstantApp.STATUS_CODE_OK)
                                            .end(DFAPIMessage.getResponseMessage(1026, subject));
                                    LOG.info(DFAPIMessage.logResponseMessage(1026,
                                            "subject = " + subject));
                            } else {
                                // If response is failed, repose df ui and still keep the task
                                HelpFunc.responseCorsHandleAddOn(routingContext.response())
                                        .setStatusCode(ConstantApp.STATUS_CODE_BAD_REQUEST)
                                        .end(DFAPIMessage.getResponseMessage(9029, subject,
                                                "Schema Subject DELETE Failed"));
                                LOG.info(DFAPIMessage.logResponseMessage(9029, subject + "-"
                                        + ar.cause().getMessage()
                                ));
                            }
                        }
                );
    }

    /**
     * Get schema compatibility from schema registry.
     *
     * @param schemaUri
     * @param schemaSubject
     * @return
     * @throws ConnectException
     */
    public static String getCompatibilityOfSubject(String schemaUri, String schemaSubject) {
        String compatibility = null;

        String fullUrl = String.format("http://%s/config/%s", schemaUri, schemaSubject);
        HttpResponse<String> res = null;

        try {
            res = Unirest.get(fullUrl).header("accept", "application/vnd.schemaregistry.v1+json").asString();
            LOG.debug("Subject:" + schemaSubject + " " + res.getBody());
            if (res.getBody() != null) {
                if (res.getBody().indexOf("40401") > 0) {
                } else {
                    JSONObject jason = new JSONObject(res.getBody().toString());
                    // This attribute is different from confluent doc. TODo check update later
                    compatibility = jason.getString(ConstantApp.SCHEMA_REGISTRY_KEY_COMPATIBILITY_LEVEL);
                }
            }
        } catch (UnirestException e) {
            LOG.error(DFAPIMessage.logResponseMessage(9006, "exception - " + e.getCause()));
        }

        return compatibility;
    }

}