/****************************************************************
 * 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.imap.processor;

import static org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode.IGNORE;
import static org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode.RETRIEVE;

import java.util.EnumSet;
import java.util.List;
import java.util.Optional;

import javax.inject.Inject;
import javax.mail.Flags;

import org.apache.james.imap.api.ImapConfiguration;
import org.apache.james.imap.api.display.HumanReadableText;
import org.apache.james.imap.api.message.Capability;
import org.apache.james.imap.api.message.StatusDataItems;
import org.apache.james.imap.api.message.response.StatusResponseFactory;
import org.apache.james.imap.api.process.ImapSession;
import org.apache.james.imap.main.PathConverter;
import org.apache.james.imap.message.request.StatusRequest;
import org.apache.james.imap.message.response.MailboxStatusResponse;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode;
import org.apache.james.mailbox.MessageUid;
import org.apache.james.mailbox.ModSeq;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.ComposedMessageIdWithMetaData;
import org.apache.james.mailbox.model.FetchGroup;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageRange;
import org.apache.james.mailbox.model.MessageResult;
import org.apache.james.mailbox.model.UidValidity;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.util.MDCBuilder;
import org.apache.james.util.ReactorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * STATUS command initial definition: https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10
 *
 * STATUS=SIZE extension: https://www.rfc-editor.org/rfc/rfc8438.html
 * DELETED extension: https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.11
 *     and https://datatracker.ietf.org/doc/html/rfc9208#section-4.1.4
 * DELETED-STORAGE extension: https://datatracker.ietf.org/doc/html/rfc9208#section-4.1.4
 */
public class StatusProcessor extends AbstractMailboxProcessor<StatusRequest> implements CapabilityImplementingProcessor {
    private static final Logger LOGGER = LoggerFactory.getLogger(StatusProcessor.class);

    private ImapConfiguration imapConfiguration;

    @Inject
    public StatusProcessor(MailboxManager mailboxManager, StatusResponseFactory factory,
                           MetricFactory metricFactory) {
        super(StatusRequest.class, mailboxManager, factory, metricFactory);
    }

    @Override
    public void configure(ImapConfiguration imapConfiguration) {
        super.configure(imapConfiguration);
        this.imapConfiguration = imapConfiguration;
    }

    @Override
    public List<Capability> getImplementedCapabilities(ImapSession session) {
        return ImmutableList.of(Capability.of("STATUS=SIZE"));
    }

    @Override
    protected Mono<Void> processRequestReactive(StatusRequest request, ImapSession session, Responder responder) {
        MailboxPath mailboxPath = PathConverter.forSession(session).buildFullPath(request.getMailboxName());
        MailboxSession mailboxSession = session.getMailboxSession();
        StatusDataItems statusDataItems = request.getStatusDataItems();

        return logInitialRequest(mailboxPath)
            .then(sendStatus(mailboxPath, statusDataItems, responder, session, mailboxSession))
            .then(unsolicitedResponses(session, responder, false))
            .then(Mono.fromRunnable(() -> okComplete(request, responder)))
            .onErrorResume(MailboxException.class, e -> {
                no(request, responder, HumanReadableText.STATUS_FAILED);
                return ReactorUtils.logAsMono(() -> LOGGER.error("Status failed for mailbox {}", mailboxPath, e));
            })
            .then();
    }

    private Mono<MailboxStatusResponse> sendStatus(MailboxPath mailboxPath, StatusDataItems statusDataItems, Responder responder, ImapSession session, MailboxSession mailboxSession) {
        return Mono.from(getMailboxManager().getMailboxReactive(mailboxPath, mailboxSession))
            .flatMap(mailbox -> sendStatus(mailbox, statusDataItems, responder, session, mailboxSession));
    }

    Mono<MailboxStatusResponse> sendStatus(MessageManager mailbox, StatusDataItems statusDataItems, Responder responder, ImapSession session, MailboxSession mailboxSession) {
        return retrieveMetadata(mailbox, statusDataItems, mailboxSession)
            .flatMap(metaData -> computeStatusResponse(mailbox, statusDataItems, metaData, mailboxSession)
                .doOnNext(response -> {
                    // Enable CONDSTORE as this is a CONDSTORE enabling command
                    if (response.getHighestModSeq() != null) {
                        condstoreEnablingCommand(session, responder, metaData, false);
                    }
                    responder.respond(response);
                }));
    }

    private Mono<Void> logInitialRequest(MailboxPath mailboxPath) {
        if (LOGGER.isDebugEnabled()) {
            return ReactorUtils.logAsMono(() -> LOGGER.debug("Status called on mailbox named {}", mailboxPath));
        } else {
            return Mono.empty();
        }
    }

    private Mono<MessageManager.MailboxMetaData> retrieveMetadata(MessageManager mailbox, StatusDataItems statusDataItems, MailboxSession mailboxSession) {
        EnumSet<MessageManager.MailboxMetaData.Item> fetchGroup = computeFetchGroup(statusDataItems);
        RecentMode recentMode = computeRecentMode(statusDataItems);

        try {
            return mailbox.getMetaDataReactive(recentMode, mailboxSession, fetchGroup);
        } catch (MailboxException e) {
            throw new RuntimeException(e);
        }
    }

    private RecentMode computeRecentMode(StatusDataItems statusDataItems) {
        if (statusDataItems.isRecent()) {
            return RETRIEVE;
        }
        return IGNORE;
    }

    private Mono<MailboxStatusResponse> computeStatusResponse(MessageManager mailbox,
                                                              StatusDataItems statusDataItems,
                                                              MessageManager.MailboxMetaData metaData,
                                                              MailboxSession session) {
        return iterateMailbox(statusDataItems, mailbox, session)
            .map(maybeIterationResult -> {
                Optional<Long> appendLimit = appendLimit(statusDataItems);
                Long messages = messages(statusDataItems, metaData);
                Long recent = recent(statusDataItems, metaData);
                MessageUid uidNext = uidNext(statusDataItems, metaData);
                UidValidity uidValidity = uidValidity(statusDataItems, metaData);
                Long unseen = unseen(statusDataItems, metaData);
                ModSeq highestModSeq = highestModSeq(statusDataItems, metaData);
                MailboxId mailboxId = mailboxId(statusDataItems, mailbox);
                return new MailboxStatusResponse(
                    appendLimit,
                    maybeIterationResult.flatMap(result -> result.getSize(statusDataItems)).orElse(null),
                    maybeIterationResult.flatMap(result -> result.getDeleted(statusDataItems)).orElse(null),
                    maybeIterationResult.flatMap(result -> result.getDeletedStorage(statusDataItems)).orElse(null),
                    messages, recent, uidNext, highestModSeq, uidValidity, unseen, mailbox.getMailboxPath().getName(), mailboxId);
            });
    }

    private EnumSet<MessageManager.MailboxMetaData.Item> computeFetchGroup(StatusDataItems statusDataItems) {
        EnumSet<MessageManager.MailboxMetaData.Item> result = EnumSet.noneOf(MessageManager.MailboxMetaData.Item.class);
        if (statusDataItems.isUnseen() || statusDataItems.isMessages()) {
            result.add(MessageManager.MailboxMetaData.Item.MailboxCounters);
        }
        if (statusDataItems.isHighestModSeq()) {
            result.add(MessageManager.MailboxMetaData.Item.HighestModSeq);
        }
        if (statusDataItems.isUidNext()) {
            result.add(MessageManager.MailboxMetaData.Item.NextUid);
        }
        return result;
    }

    private Long unseen(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
        if (statusDataItems.isUnseen()) {
            return metaData.getUnseenCount();
        } else {
            return null;
        }
    }
    
    private MailboxId mailboxId(StatusDataItems statusDataItems, MessageManager mailbox) {
        if (statusDataItems.isMailboxId()) {
            return mailbox.getId();
        } else {
            return null;
        }
    }

    private Optional<Long> appendLimit(StatusDataItems statusDataItems) {
        if (statusDataItems.isAppendLimit()) {
            return imapConfiguration.getAppendLimit();
        } else {
            return null;
        }
    }

    private UidValidity uidValidity(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
        if (statusDataItems.isUidValidity()) {
            return metaData.getUidValidity();
        } else {
            return null;
        }
    }

    private ModSeq highestModSeq(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
        if (statusDataItems.isHighestModSeq()) {
            return metaData.getHighestModSeq();
        } else {
            return null;
        }
    }
    
    private MessageUid uidNext(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
        if (statusDataItems.isUidNext()) {
            return metaData.getUidNext();
        } else {
            return null;
        }
    }

    private Long recent(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
        if (statusDataItems.isRecent()) {
            return metaData.countRecent();
        } else {
            return null;
        }
    }

    private Long messages(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
        if (statusDataItems.isMessages()) {
           return metaData.getMessageCount();
        } else {
            return null;
        }
    }

    private Mono<Optional<MailboxIterationResult>> iterateMailbox(StatusDataItems statusDataItems, MessageManager messageManager, MailboxSession session) {
        if (statusDataItems.isSize() || statusDataItems.isDeletedStorage()) {
            return Flux.from(messageManager.getMessagesReactive(MessageRange.all(), FetchGroup.MINIMAL, session))
                .reduce(new MailboxIterationResult(), MailboxIterationResult::accumulate)
                .map(Optional::of);
        }
        if (statusDataItems.isDeleted()) {
            return Flux.from(messageManager.listMessagesMetadata(MessageRange.all(), session))
                .reduce(new MailboxIterationResult(), MailboxIterationResult::accumulateDeleted)
                .map(Optional::of);
        }
        return Mono.just(Optional.empty());
    }

    public static class MailboxIterationResult {
        private long size = 0;
        private long deleted = 0;
        private long deletedStorage = 0;

        public MailboxIterationResult accumulate(MessageResult messageResult) {
            if (messageResult.getFlags().contains(Flags.Flag.DELETED)) {
                deleted++;
                deletedStorage += messageResult.getSize();
            }
            size += messageResult.getSize();
            return this;
        }

        public MailboxIterationResult accumulateDeleted(ComposedMessageIdWithMetaData metaData) {
            if (metaData.getFlags().contains(Flags.Flag.DELETED)) {
                deleted++;
            }
            return this;
        }

        public Optional<Long> getSize(StatusDataItems items) {
            if (items.isSize()) {
                return Optional.of(size);
            }
            return Optional.empty();
        }

        public Optional<Long> getDeleted(StatusDataItems items) {
            if (items.isDeleted()) {
                return Optional.of(deleted);
            }
            return Optional.empty();
        }

        public Optional<Long> getDeletedStorage(StatusDataItems items) {
            if (items.isDeletedStorage()) {
                return Optional.of(deletedStorage);
            }
            return Optional.empty();
        }
    }


    @Override
    protected MDCBuilder mdc(StatusRequest request) {
        return MDCBuilder.create()
            .addToContext(MDCBuilder.ACTION, "STATUS")
            .addToContext("mailbox", request.getMailboxName())
            .addToContext("parameters", request.getStatusDataItems().toString());
    }
}
