/****************************************************************
 * 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.jmap.draft.methods;

import static org.apache.james.jmap.draft.methods.Method.JMAP_PREFIX;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import javax.inject.Inject;

import org.apache.james.jmap.draft.exceptions.MailboxNotOwnedException;
import org.apache.james.jmap.draft.exceptions.MailboxParentNotFoundException;
import org.apache.james.jmap.draft.model.MailboxCreationId;
import org.apache.james.jmap.draft.model.MailboxFactory;
import org.apache.james.jmap.draft.model.SetError;
import org.apache.james.jmap.draft.model.SetMailboxesRequest;
import org.apache.james.jmap.draft.model.SetMailboxesResponse;
import org.apache.james.jmap.draft.model.mailbox.Mailbox;
import org.apache.james.jmap.draft.model.mailbox.MailboxCreateRequest;
import org.apache.james.jmap.draft.utils.DependencyGraph.CycleDetectedException;
import org.apache.james.jmap.draft.utils.SortingHierarchicalCollections;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.SubscriptionManager;
import org.apache.james.mailbox.exception.InboxAlreadyCreated;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxExistsException;
import org.apache.james.mailbox.exception.MailboxNameException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.exception.TooLongMailboxNameException;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxId.Factory;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.metrics.api.TimeMetric;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.fge.lambdas.Throwing;
import com.github.fge.lambdas.functions.FunctionChainer;
import com.google.common.annotations.VisibleForTesting;

public class SetMailboxesCreationProcessor implements SetMailboxesProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(SetMailboxesCreationProcessor.class);

    private final MailboxManager mailboxManager;
    private final SortingHierarchicalCollections<Map.Entry<MailboxCreationId, MailboxCreateRequest>, MailboxCreationId> sortingHierarchicalCollections;
    private final MailboxFactory mailboxFactory;
    private final Factory mailboxIdFactory;
    private final SubscriptionManager subscriptionManager;
    private final MetricFactory metricFactory;

    @Inject
    @VisibleForTesting
    SetMailboxesCreationProcessor(MailboxManager mailboxManager, SubscriptionManager subscriptionManager, MailboxFactory mailboxFactory, Factory mailboxIdFactory, MetricFactory metricFactory) {
        this.mailboxManager = mailboxManager;
        this.subscriptionManager = subscriptionManager;
        this.metricFactory = metricFactory;
        this.sortingHierarchicalCollections =
            new SortingHierarchicalCollections<>(
                Map.Entry::getKey,
                x -> x.getValue().getParentId());
        this.mailboxFactory = mailboxFactory;
        this.mailboxIdFactory = mailboxIdFactory;
    }

    @Override
    public SetMailboxesResponse process(SetMailboxesRequest request, MailboxSession mailboxSession) {
        TimeMetric timeMetric = metricFactory.timer(JMAP_PREFIX + "SetMailboxesCreationProcessor");

        SetMailboxesResponse.Builder builder = SetMailboxesResponse.builder();
        try {
            Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId = new HashMap<>();
            sortingHierarchicalCollections.sortFromRootToLeaf(request.getCreate().entrySet())
                .forEach(entry -> 
                    createMailbox(entry.getKey(), entry.getValue(), mailboxSession, creationIdsToCreatedMailboxId, builder));
        } catch (CycleDetectedException e) {
            markRequestsAsNotCreatedDueToCycle(request, builder);
        }

        timeMetric.stopAndPublish();
        return builder.build();
    }

    private void markRequestsAsNotCreatedDueToCycle(SetMailboxesRequest request, SetMailboxesResponse.Builder builder) {
        request.getCreate().forEach((key, value) ->
            builder.notCreated(
                key,
                SetError.builder()
                    .type(SetError.Type.INVALID_ARGUMENTS)
                    .description("The created mailboxes introduce a cycle.")
                    .build()));
    }

    private void createMailbox(MailboxCreationId mailboxCreationId, MailboxCreateRequest mailboxRequest, MailboxSession mailboxSession,
            Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId, SetMailboxesResponse.Builder builder) {
        try {
            ensureValidMailboxName(mailboxRequest, mailboxSession);
            MailboxPath mailboxPath = computeMailboxPath(mailboxRequest, creationIdsToCreatedMailboxId, mailboxSession);
            Optional<MailboxId> mailboxId = mailboxManager.createMailbox(mailboxPath, mailboxSession);
            Optional<Mailbox> mailbox = mailboxId.flatMap(id -> mailboxFactory.builder()
                    .id(id)
                    .session(mailboxSession)
                    .build());
            if (mailbox.isPresent()) {
                subscriptionManager.subscribe(mailboxSession, mailboxPath.getName());
                builder.created(mailboxCreationId, mailbox.get());
                creationIdsToCreatedMailboxId.put(mailboxCreationId, mailbox.get().getId());
            } else {
                builder.notCreated(mailboxCreationId, SetError.builder()
                    .type(SetError.Type.ERROR)
                    .description("An error occurred when creating the mailbox")
                    .build());
            }
        } catch (TooLongMailboxNameException e) {
            builder.notCreated(mailboxCreationId, SetError.builder()
                .type(SetError.Type.INVALID_ARGUMENTS)
                .description("The mailbox name length is too long")
                .build());
        } catch (MailboxNotOwnedException e) {
            builder.notCreated(mailboxCreationId, SetError.builder()
                .type(SetError.Type.INVALID_ARGUMENTS)
                .description("The mailbox can not be created with a parent mailbox belonging to another user")
                .build());
        } catch (MailboxNameException | MailboxParentNotFoundException e) {
            builder.notCreated(mailboxCreationId, SetError.builder()
                    .type(SetError.Type.INVALID_ARGUMENTS)
                    .description(e.getMessage())
                    .build());
        } catch (InboxAlreadyCreated e) {
            String message = String.format("The mailbox '%s' already exists as 'INBOX'", e.getMailboxName());
            LOGGER.error(message, e);
            builder.notCreated(mailboxCreationId, SetError.builder()
                .type(SetError.Type.INVALID_ARGUMENTS)
                .description(message)
                .build());
        } catch (MailboxExistsException e) {
            String message = String.format("The mailbox '%s' already exists.", mailboxCreationId.getCreationId());
            builder.notCreated(mailboxCreationId, SetError.builder()
                    .type(SetError.Type.INVALID_ARGUMENTS)
                    .description(message)
                    .build());
        } catch (MailboxException e) {
            String message = String.format("An error occurred when creating the mailbox '%s'", mailboxCreationId.getCreationId());
            LOGGER.error(message, e);
            builder.notCreated(mailboxCreationId, SetError.builder()
                .type(SetError.Type.ERROR)
                .description(message)
                .build());
        }
    }

    private void ensureValidMailboxName(MailboxCreateRequest mailboxRequest, MailboxSession mailboxSession) throws MailboxNameException {
        String name = mailboxRequest.getName();
        char pathDelimiter = mailboxSession.getPathDelimiter();
        if (name.contains(String.valueOf(pathDelimiter))) {
            throw new MailboxNameException(String.format("The mailbox '%s' contains an illegal character: '%c'", name, pathDelimiter));
        }
    }

    private MailboxPath computeMailboxPath(MailboxCreateRequest mailboxRequest, Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId, MailboxSession mailboxSession) throws MailboxException {
        if (mailboxRequest.getParentId().isPresent()) {
            MailboxCreationId parentId = mailboxRequest.getParentId().get();
            MailboxPath parentPath = getMailboxPath(creationIdsToCreatedMailboxId, mailboxSession, parentId);

            assertBelongsToUser(parentPath, mailboxSession);

            return MailboxPath.forUser(mailboxSession.getUser(),
                parentPath.getName() + mailboxSession.getPathDelimiter() + mailboxRequest.getName());
        }
        return MailboxPath.forUser(mailboxSession.getUser(), mailboxRequest.getName());
    }

    private void assertBelongsToUser(MailboxPath mailboxPath, MailboxSession mailboxSession) throws MailboxNotOwnedException {
        if (!mailboxPath.belongsTo(mailboxSession)) {
            throw new MailboxNotOwnedException();
        }
    }

    private MailboxPath getMailboxPath(Map<MailboxCreationId, MailboxId> creationIdsToCreatedMailboxId, MailboxSession mailboxSession, MailboxCreationId parentId) throws MailboxException {
        Optional<MailboxId> mailboxId = readCreationIdAsMailboxId(parentId)
            .or(() -> Optional.ofNullable(creationIdsToCreatedMailboxId.get(parentId)));

        return getMailboxPathFromId(mailboxId, mailboxSession)
                .orElseThrow(() -> new MailboxParentNotFoundException(parentId));
    }

    private Optional<MailboxId> readCreationIdAsMailboxId(MailboxCreationId creationId) {
        try {
            return Optional.of(mailboxIdFactory.fromString(creationId.getCreationId()));
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    @VisibleForTesting
    Optional<MailboxPath> getMailboxPathFromId(Optional<MailboxId> mailboxId, MailboxSession mailboxSession) {
        FunctionChainer<MailboxId, Optional<MailboxPath>> fromMailboxIdToMailboxPath = Throwing.function(id -> {
            try {
                return Optional.of(mailboxManager.getMailbox(id, mailboxSession).getMailboxPath());
            } catch (MailboxNotFoundException e) {
                return Optional.empty();
            }
        });
        return mailboxId
            .flatMap(fromMailboxIdToMailboxPath.sneakyThrow());
    }

}
