package org.elasticsearch.plugin.zentity;

import io.zentity.model.Model;
import io.zentity.model.ValidationException;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequestBuilder;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
import org.elasticsearch.action.delete.DeleteRequestBuilder;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequestBuilder;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;

import static org.elasticsearch.rest.RestRequest.Method;
import static org.elasticsearch.rest.RestRequest.Method.DELETE;
import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;

public class ModelsAction extends BaseRestHandler {

    public static final String INDEX_NAME = ".zentity-models";

    @Inject
    public ModelsAction(RestController controller) {
        controller.registerHandler(GET, "_zentity/models", this);
        controller.registerHandler(GET, "_zentity/models/{entity_type}", this);
        controller.registerHandler(POST, "_zentity/models/{entity_type}", this);
        controller.registerHandler(PUT, "_zentity/models/{entity_type}", this);
        controller.registerHandler(DELETE, "_zentity/models/{entity_type}", this);
    }

    /**
     * Check if the .zentity-models index exists, and if it doesn't, then create it.
     *
     * @param client The client that will communicate with Elasticsearch.
     * @throws ForbiddenException
     */
    public static void ensureIndex(NodeClient client) throws ForbiddenException {
        try {
            IndicesExistsRequestBuilder request = client.admin().indices().prepareExists(INDEX_NAME);
            IndicesExistsResponse response = request.get();
            if (!response.isExists())
                SetupAction.createIndex(client);
        } catch (ElasticsearchSecurityException se) {
            throw new ForbiddenException("The .zentity-models index does not exist and you do not have the 'create_index' privilege. An authorized user must create the index by submitting: POST _zentity/_setup");
        }
    }

    /**
     * Retrieve all entity models.
     *
     * @param client The client that will communicate with Elasticsearch.
     * @return The response from Elasticsearch.
     * @throws ForbiddenException
     */
    public static SearchResponse getEntityModels(NodeClient client) throws ForbiddenException {
        SearchRequestBuilder request = client.prepareSearch(INDEX_NAME);
        request.setSize(10000);
        try {
            return request.get();
        } catch (IndexNotFoundException e) {
            try {
                SetupAction.createIndex(client);
            } catch (ElasticsearchSecurityException se) {
                throw new ForbiddenException("The .zentity-models index does not exist and you do not have the 'create_index' privilege. An authorized user must create the index by submitting: POST _zentity/_setup");
            }
            return request.get();
        }
    }

    /**
     * Retrieve one entity model by its type.
     *
     * @param entityType The entity type.
     * @param client     The client that will communicate with Elasticsearch.
     * @return The response from Elasticsearch.
     * @throws ForbiddenException
     */
    public static GetResponse getEntityModel(String entityType, NodeClient client) throws ForbiddenException {
        GetRequestBuilder request = client.prepareGet(INDEX_NAME, "doc", entityType);
        try {
            return request.get();
        } catch (IndexNotFoundException e) {
            try {
                SetupAction.createIndex(client);
            } catch (ElasticsearchSecurityException se) {
                throw new ForbiddenException("The .zentity-models index does not exist and you do not have the 'create_index' privilege. An authorized user must create the index by submitting: POST _zentity/_setup");
            }
            return request.get();
        }
    }

    /**
     * Index one entity model by its type. Return error if an entity model already exists for that entity type.
     *
     * @param entityType  The entity type.
     * @param requestBody The request body.
     * @param client      The client that will communicate with Elasticsearch.
     * @return The response from Elasticsearch.
     * @throws ForbiddenException
     */
    public static IndexResponse indexEntityModel(String entityType, String requestBody, NodeClient client) throws ForbiddenException {
        ensureIndex(client);
        IndexRequestBuilder request = client.prepareIndex(INDEX_NAME, "doc", entityType);
        request.setSource(requestBody, XContentType.JSON).setCreate(true).setRefreshPolicy("wait_for");
        return request.get();
    }

    /**
     * Update one entity model by its type. Does not support partial updates.
     *
     * @param entityType  The entity type.
     * @param requestBody The request body.
     * @param client      The client that will communicate with Elasticsearch.
     * @return The response from Elasticsearch.
     * @throws ForbiddenException
     */
    public static IndexResponse updateEntityModel(String entityType, String requestBody, NodeClient client) throws ForbiddenException {
        ensureIndex(client);
        IndexRequestBuilder request = client.prepareIndex(INDEX_NAME, "doc", entityType);
        request.setSource(requestBody, XContentType.JSON).setCreate(false).setRefreshPolicy("wait_for");
        return request.get();
    }

    /**
     * Delete one entity model by its type.
     *
     * @param entityType The entity type.
     * @param client     The client that will communicate with Elasticsearch.
     * @return The response from Elasticsearch.
     * @throws ForbiddenException
     */
    private static DeleteResponse deleteEntityModel(String entityType, NodeClient client) throws ForbiddenException {
        DeleteRequestBuilder request = client.prepareDelete(INDEX_NAME, "doc", entityType);
        request.setRefreshPolicy("wait_for");
        try {
            return request.get();
        } catch (IndexNotFoundException e) {
            try {
                SetupAction.createIndex(client);
            } catch (ElasticsearchSecurityException se) {
                throw new ForbiddenException("The .zentity-models index does not exist and you do not have the 'create_index' privilege. An authorized user must create the index by submitting: POST _zentity/_setup");
            }
            return request.get();
        }
    }

    @Override
    public String getName() {
        return "zentity_models_action";
    }

    @Override
    protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {

        // Parse request
        String entityType = restRequest.param("entity_type");
        Boolean pretty = restRequest.paramAsBoolean("pretty", false);
        Method method = restRequest.method();
        String requestBody = restRequest.content().utf8ToString();

        return channel -> {
            try {

                // Validate input
                if (method == POST || method == PUT) {

                    // Parse the request body.
                    if (requestBody == null || requestBody.equals(""))
                        throw new ValidationException("Request body is missing.");

                    // Parse and validate the entity model.
                    new Model(requestBody);
                }

                // Handle request
                if (method == GET && (entityType == null || entityType.equals(""))) {
                    // GET _zentity/models
                    SearchResponse response = getEntityModels(client);
                    XContentBuilder content = XContentFactory.jsonBuilder();
                    if (pretty)
                        content.prettyPrint();
                    content = response.toXContent(content, ToXContent.EMPTY_PARAMS);
                    channel.sendResponse(new BytesRestResponse(RestStatus.OK, content));

                } else if (method == GET && !entityType.equals("")) {
                    // GET _zentity/models/{entity_type}
                    GetResponse response = getEntityModel(entityType, client);
                    XContentBuilder content = XContentFactory.jsonBuilder();
                    if (pretty)
                        content.prettyPrint();
                    content = response.toXContent(content, ToXContent.EMPTY_PARAMS);
                    channel.sendResponse(new BytesRestResponse(RestStatus.OK, content));

                } else if (method == POST && !entityType.equals("")) {
                    // POST _zentity/models/{entity_type}
                    if (requestBody.equals(""))
                        throw new ValidationException("Request body cannot be empty when indexing an entity model.");
                    IndexResponse response = indexEntityModel(entityType, requestBody, client);
                    XContentBuilder content = XContentFactory.jsonBuilder();
                    if (pretty)
                        content.prettyPrint();
                    content = response.toXContent(content, ToXContent.EMPTY_PARAMS);
                    channel.sendResponse(new BytesRestResponse(RestStatus.OK, content));

                } else if (method == PUT && !entityType.equals("")) {
                    // PUT _zentity/models/{entity_type}
                    if (requestBody.equals(""))
                        throw new ValidationException("Request body cannot be empty when updating an entity model.");
                    IndexResponse response = updateEntityModel(entityType, requestBody, client);
                    XContentBuilder content = XContentFactory.jsonBuilder();
                    if (pretty)
                        content.prettyPrint();
                    content = response.toXContent(content, ToXContent.EMPTY_PARAMS);
                    channel.sendResponse(new BytesRestResponse(RestStatus.OK, content));

                } else if (method == DELETE && !entityType.equals("")) {
                    // DELETE _zentity/models/{entity_type}
                    DeleteResponse response = deleteEntityModel(entityType, client);
                    XContentBuilder content = XContentFactory.jsonBuilder();
                    if (pretty)
                        content.prettyPrint();
                    content = response.toXContent(content, ToXContent.EMPTY_PARAMS);
                    channel.sendResponse(new BytesRestResponse(RestStatus.OK, content));

                } else {
                    throw new NotImplementedException("Method and endpoint not implemented.");
                }

            } catch (ValidationException e) {
                channel.sendResponse(new BytesRestResponse(channel, RestStatus.BAD_REQUEST, e));
            } catch (ForbiddenException e) {
                channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, e));
            } catch (NotImplementedException e) {
                channel.sendResponse(new BytesRestResponse(channel, RestStatus.NOT_IMPLEMENTED, e));
            }
        };
    }
}