/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.webadmin.routes;

import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

import org.apache.james.core.User;
import org.apache.james.mailbox.MessageUid;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.indexer.IndexingDetailInformation;
import org.apache.james.mailbox.indexer.ReIndexer;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.task.Task;
import org.apache.james.task.TaskId;
import org.apache.james.task.TaskManager;
import org.apache.james.task.TaskNotFoundException;
import org.apache.james.webadmin.Routes;
import org.apache.james.webadmin.dto.TaskIdDto;
import org.apache.james.webadmin.service.PreviousReIndexingService;
import org.apache.james.webadmin.utils.ErrorResponder;
import org.apache.james.webadmin.utils.JsonTransformer;
import org.eclipse.jetty.http.HttpStatus;

import com.google.common.base.Strings;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import spark.Request;
import spark.Response;
import spark.Service;

@Api(tags = "ReIndexing (mailboxes)")
@Path("/mailboxes")
@Produces("application/json")
public class ReindexingRoutes implements Routes {
    @FunctionalInterface
    interface TaskGenerator {
        Task generate() throws MailboxException;
    }

    private static final String BASE_PATH = "/mailboxes";
    private static final String USER_QUERY_PARAM = "user";
    private static final String RE_INDEX_FAILED_MESSAGES_QUERY_PARAM = "reIndexFailedMessagesOf";
    private static final String MAILBOX_PARAM = ":mailbox";
    private static final String UID_PARAM = ":uid";
    private static final String MAILBOX_PATH = BASE_PATH + "/" + MAILBOX_PARAM;
    private static final String MESSAGE_PATH = MAILBOX_PATH + "/mails/" + UID_PARAM;

    private final TaskManager taskManager;
    private final PreviousReIndexingService previousReIndexingService;
    private final MailboxId.Factory mailboxIdFactory;
    private final ReIndexer reIndexer;
    private final JsonTransformer jsonTransformer;

    @Inject
    ReindexingRoutes(TaskManager taskManager, PreviousReIndexingService previousReIndexingService, MailboxId.Factory mailboxIdFactory, ReIndexer reIndexer, JsonTransformer jsonTransformer) {
        this.taskManager = taskManager;
        this.previousReIndexingService = previousReIndexingService;
        this.mailboxIdFactory = mailboxIdFactory;
        this.reIndexer = reIndexer;
        this.jsonTransformer = jsonTransformer;
    }

    @Override
    public String getBasePath() {
        return BASE_PATH;
    }

    @Override
    public void define(Service service) {
        service.post(BASE_PATH, this::reIndexAll, jsonTransformer);
        service.post(MAILBOX_PATH, this::reIndexMailbox, jsonTransformer);
        service.post(MESSAGE_PATH, this::reIndexMessage, jsonTransformer);
    }

    @POST
    @Path("/")
    @ApiOperation(value = "Re-indexes all the mails on this server")
    @ApiImplicitParams({
        @ApiImplicitParam(
            required = true,
            name = "task",
            paramType = "query parameter",
            dataType = "String",
            defaultValue = "none",
            example = "?task=reIndex",
            value = "Compulsory. Only supported value is `reIndex`"),
        @ApiImplicitParam(
            name = "user",
            paramType = "query parameter",
            dataType = "String",
            defaultValue = "none",
            example = "?user=toto%40domain.tld",
            value = "optional. If present, only mailboxes of that user will be reIndexed."),
        @ApiImplicitParam(
            name = "reIndexFailedMessagesOf",
            paramType = "query parameter",
            dataType = "String",
            defaultValue = "none",
            example = "?reIndexFailedMessagesOf=3294a976-ce63-491e-bd52-1b6f465ed7a2",
            value = "optional. References a previously run reIndexing task. if present, the messages that this previous " +
                "task failed to index will be reIndexed.")
    })
    @ApiResponses(value = {
        @ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class),
        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
        @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - details in the returned error message")
    })
    private TaskIdDto reIndexAll(Request request, Response response) {
        boolean userReIndexing = !Strings.isNullOrEmpty(request.queryParams(USER_QUERY_PARAM));
        boolean indexingCorrection = !Strings.isNullOrEmpty(request.queryParams(RE_INDEX_FAILED_MESSAGES_QUERY_PARAM));
        if (userReIndexing && indexingCorrection) {
            return rejectInvalidQueryParameterCombination();
        }
        if (userReIndexing) {
            return wrap(request, response, () -> reIndexer.reIndex(extractUser(request)));
        }
        if (indexingCorrection) {
            IndexingDetailInformation indexingDetailInformation = retrieveIndexingExecutionDetails(request);
            return wrap(request, response, () -> reIndexer.reIndex(indexingDetailInformation.failures()));
        }
        return wrap(request, response, reIndexer::reIndex);
    }

    private IndexingDetailInformation retrieveIndexingExecutionDetails(Request request) {
        TaskId taskId = getTaskId(request);
        try {
            return previousReIndexingService.retrieveIndexingExecutionDetails(taskId);
        } catch (PreviousReIndexingService.NotAnIndexingRetriableTask | PreviousReIndexingService.TaskNotYetFinishedException e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.BAD_REQUEST_400)
                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
                .message("Invalid task id")
                .cause(e)
                .haltError();
        } catch (TaskNotFoundException e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.BAD_REQUEST_400)
                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
                .message("TaskId " + taskId.asString() + " does not exist")
                .cause(e)
                .haltError();
        }
    }

    private TaskId getTaskId(Request request) {
        try {
            String id = request.queryParams(RE_INDEX_FAILED_MESSAGES_QUERY_PARAM);
            return TaskId.fromString(id);
        } catch (Exception e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.BAD_REQUEST_400)
                .cause(e)
                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
                .message("Invalid task id")
                .haltError();
        }
    }

    private TaskIdDto rejectInvalidQueryParameterCombination() {
        throw ErrorResponder.builder()
            .statusCode(HttpStatus.BAD_REQUEST_400)
            .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
            .message("Can not specify '" + USER_QUERY_PARAM + "' and '" + RE_INDEX_FAILED_MESSAGES_QUERY_PARAM + "' query parameters at the same time")
            .haltError();
    }

    @POST
    @Path("/{mailboxId}")
    @ApiOperation(value = "Re-indexes all the mails in a mailbox")
    @ApiImplicitParams({
        @ApiImplicitParam(
            required = true,
            name = "task",
            paramType = "query parameter",
            dataType = "String",
            defaultValue = "none",
            example = "?task=reIndex",
            value = "Compulsory. Only supported value is `reIndex`"),
        @ApiImplicitParam(
            required = true,
            name = "user",
            paramType = "path parameter",
            dataType = "String",
            defaultValue = "none",
            example = "benoit@apache.org",
            value = "Compulsory. Needs to be a valid username"),
        @ApiImplicitParam(
            required = true,
            name = "mailboxId",
            paramType = "path parameter",
            dataType = "String",
            defaultValue = "none",
            value = "Compulsory. Needs to be a valid mailbox ID")
    })
    @ApiResponses(value = {
        @ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class),
        @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
        @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - details in the returned error message")
    })
    private TaskIdDto reIndexMailbox(Request request, Response response) {
        return wrap(request, response,
            () -> reIndexer.reIndex(extractMailboxId(request)));
    }

    @POST
    @Path("/{mailboxId}/mails/{uid}")
    @ApiOperation(value = "Re-indexes a single email")
    @ApiImplicitParams({
        @ApiImplicitParam(
            required = true,
            name = "task",
            paramType = "query parameter",
            dataType = "String",
            defaultValue = "none",
            example = "?task=reIndex",
            value = "Compulsory. Only supported value is `reIndex`"),
        @ApiImplicitParam(
            required = true,
            name = "user",
            paramType = "path parameter",
            dataType = "String",
            defaultValue = "none",
            example = "benoit@apache.org",
            value = "Compulsory. Needs to be a valid username"),
        @ApiImplicitParam(
            required = true,
            name = "mailboxId",
            paramType = "path parameter",
            dataType = "String",
            defaultValue = "none",
            value = "Compulsory. Needs to be a valid mailbox ID"),
        @ApiImplicitParam(
            required = true,
            name = "uid",
            paramType = "path parameter",
            dataType = "Integer",
            defaultValue = "none",
            value = "Compulsory. Needs to be a valid UID")
    })
    private TaskIdDto reIndexMessage(Request request, Response response) {
        return wrap(request, response,
            () -> reIndexer.reIndex(extractMailboxId(request), extractUid(request)));
    }

    private TaskIdDto wrap(Request request, Response response, TaskGenerator taskGenerator) {
        ReIndexingRoutesUtil.enforceTaskParameter(request);
        try {
            Task task = taskGenerator.generate();
            TaskId taskId = taskManager.submit(task);
            return TaskIdDto.respond(response, taskId);
        } catch (MailboxNotFoundException e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.NOT_FOUND_404)
                .type(ErrorResponder.ErrorType.NOT_FOUND)
                .message("mailbox not found")
                .cause(e)
                .haltError();
        } catch (MailboxException e) {
            throw new RuntimeException(e);
        }
    }

    private User extractUser(Request request) {
        try {
            return User.fromUsername(request.queryParams(USER_QUERY_PARAM));
        } catch (Exception e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.BAD_REQUEST_400)
                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
                .message("Error while parsing 'user'")
                .cause(e)
                .haltError();
        }
    }

    private MailboxId extractMailboxId(Request request) {
        try {
            return mailboxIdFactory.fromString(request.params(MAILBOX_PARAM));
        } catch (Exception e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.BAD_REQUEST_400)
                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
                .message("Error while parsing 'mailbox'")
                .cause(e)
                .haltError();
        }
    }

    private MessageUid extractUid(Request request) {
        try {
            return MessageUid.of(Long.valueOf(request.params(UID_PARAM)));
        } catch (NumberFormatException e) {
            throw ErrorResponder.builder()
                .statusCode(HttpStatus.BAD_REQUEST_400)
                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
                .message("'uid' needs to be a parsable long")
                .cause(e)
                .haltError();
        }
    }
}
