/****************************************************************
 * 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.queue.rabbitmq;

import java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.james.blob.api.BlobId;
import org.apache.james.blob.mail.MimeMessagePartsId;
import org.apache.james.core.MailAddress;
import org.apache.james.core.MaybeSender;
import org.apache.james.server.core.MailImpl;
import org.apache.mailet.Attribute;
import org.apache.mailet.AttributeName;
import org.apache.mailet.AttributeValue;
import org.apache.mailet.Mail;
import org.apache.mailet.PerRecipientHeaders;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.fge.lambdas.Throwing;
import com.github.fge.lambdas.consumers.ThrowingBiConsumer;
import com.github.steveash.guavate.Guavate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

class MailReferenceDTO {

    static MailReferenceDTO fromMailReference(MailReference mailReference) {
        Mail mail = mailReference.getMail();
        MimeMessagePartsId partsId = mailReference.getPartsId();

        return new MailReferenceDTO(
            mailReference.getEnqueueId().serialize(),
            Optional.ofNullable(mail.getRecipients()).map(Collection::stream)
                .orElse(Stream.empty())
                .map(MailAddress::asString)
                .collect(Guavate.toImmutableList()),
            mail.getName(),
            mail.getMaybeSender().asOptional().map(MailAddress::asString),
            mail.getState(),
            mail.getErrorMessage(),
            Optional.ofNullable(mail.getLastUpdated()).map(Date::toInstant),
            serializedAttributes(mail),
            mail.getRemoteAddr(),
            mail.getRemoteHost(),
            fromPerRecipientHeaders(mail.getPerRecipientSpecificHeaders()),
            partsId.getHeaderBlobId().asString(),
            partsId.getBodyBlobId().asString());
    }

    private static Map<String, HeadersDto> fromPerRecipientHeaders(PerRecipientHeaders perRecipientHeaders) {
        return perRecipientHeaders.getHeadersByRecipient()
            .asMap()
            .entrySet()
            .stream()
            .collect(ImmutableMap.toImmutableMap(
                entry -> entry.getKey().asString(),
                entry -> HeadersDto.from(entry.getValue())));
    }

    private static ImmutableMap<String, String> serializedAttributes(Mail mail) {
        Function<Attribute, String> name = attribute -> attribute.getName().asString();
        Function<Attribute, String> value = attribute -> attribute.getValue().toJson().toString();
        return mail
                .attributes()
                .collect(Guavate.toImmutableMap(name, value));
    }

    private final String enqueueId;
    private final ImmutableList<String> recipients;
    private final String name;
    private final Optional<String> sender;
    private final String state;
    private final String errorMessage;
    private final Optional<Instant> lastUpdated;
    private final ImmutableMap<String, String> attributes;
    private final String remoteAddr;
    private final String remoteHost;
    private final Map<String, HeadersDto> perRecipientHeaders;
    private final String headerBlobId;
    private final String bodyBlobId;

    @JsonCreator
    private MailReferenceDTO(@JsonProperty("enqueueId") String enqueueId,
                             @JsonProperty("recipients") ImmutableList<String> recipients,
                             @JsonProperty("name") String name,
                             @JsonProperty("sender") Optional<String> sender,
                             @JsonProperty("state") String state,
                             @JsonProperty("errorMessage") String errorMessage,
                             @JsonProperty("lastUpdated") Optional<Instant> lastUpdated,
                             @JsonProperty("attributes") ImmutableMap<String, String> attributes,
                             @JsonProperty("remoteAddr") String remoteAddr,
                             @JsonProperty("remoteHost") String remoteHost,
                             @JsonProperty("perRecipientHeaders") Map<String, HeadersDto> perRecipientHeaders,
                             @JsonProperty("headerBlobId") String headerBlobId,
                             @JsonProperty("bodyBlobId") String bodyBlobId) {
        this.enqueueId = enqueueId;
        this.recipients = recipients;
        this.name = name;
        this.sender = sender;
        this.state = state;
        this.errorMessage = errorMessage;
        this.lastUpdated = lastUpdated;
        this.attributes = attributes;
        this.remoteAddr = remoteAddr;
        this.remoteHost = remoteHost;
        this.perRecipientHeaders = perRecipientHeaders;
        this.headerBlobId = headerBlobId;
        this.bodyBlobId = bodyBlobId;
    }

    @JsonProperty("enqueueId")
    public String getEnqueueId() {
        return enqueueId;
    }

    @JsonProperty("recipients")
    Collection<String> getRecipients() {
        return recipients;
    }

    @JsonProperty("name")
    String getName() {
        return name;
    }

    @JsonProperty("sender")
    Optional<String> getSender() {
        return sender;
    }

    @JsonProperty("state")
    String getState() {
        return state;
    }

    @JsonProperty("errorMessage")
    String getErrorMessage() {
        return errorMessage;
    }

    @JsonProperty("lastUpdated")
    Optional<Instant> getLastUpdated() {
        return lastUpdated;
    }

    @JsonProperty("attributes")
    Map<String, String> getAttributes() {
        return attributes;
    }

    @JsonProperty("remoteAddr")
    String getRemoteAddr() {
        return remoteAddr;
    }

    @JsonProperty("remoteHost")
    String getRemoteHost() {
        return remoteHost;
    }

    @JsonProperty("perRecipientHeaders")
    Map<String, HeadersDto>  getPerRecipientHeaders() {
        return perRecipientHeaders;
    }

    @JsonProperty("headerBlobId")
    String getHeaderBlobId() {
        return headerBlobId;
    }

    @JsonProperty("bodyBlobId")
    String getBodyBlobId() {
        return bodyBlobId;
    }

    MailReference toMailReference(BlobId.Factory blobIdFactory) {
        MimeMessagePartsId messagePartsId = MimeMessagePartsId.builder()
            .headerBlobId(blobIdFactory.from(headerBlobId))
            .bodyBlobId(blobIdFactory.from(bodyBlobId))
            .build();

        return new MailReference(EnqueueId.ofSerialized(enqueueId), mailMetadata(), messagePartsId);
    }

    private MailImpl mailMetadata() {
        MailImpl.Builder builder = MailImpl.builder()
            .name(name)
            .sender(sender.map(MaybeSender::getMailSender).orElse(MaybeSender.nullSender()))
            .addRecipients(recipients.stream()
                .map(Throwing.<String, MailAddress>function(MailAddress::new).sneakyThrow())
                .collect(Guavate.toImmutableList()))
            .errorMessage(errorMessage)
            .remoteAddr(remoteAddr)
            .remoteHost(remoteHost)
            .state(state);

        lastUpdated
            .map(Instant::toEpochMilli)
            .map(Date::new)
            .ifPresent(builder::lastUpdated);

        ThrowingBiConsumer<String, String> attributeSetter = (name, value) ->
            builder.addAttribute(new Attribute(AttributeName.of(name), AttributeValue.fromJsonString(value)));

        attributes
            .forEach(Throwing.biConsumer(attributeSetter).sneakyThrow());

        builder.addAllHeadersForRecipients(retrievePerRecipientHeaders());

        return builder.build();
    }

    private PerRecipientHeaders retrievePerRecipientHeaders() {
        PerRecipientHeaders perRecipientHeaders = new PerRecipientHeaders();
        this.perRecipientHeaders.entrySet()
            .stream()
            .flatMap(entry -> entry.getValue().toHeaders().stream()
                .map(Throwing.function(header -> Pair.of(new MailAddress(entry.getKey()), header))))
            .forEach(pair -> perRecipientHeaders.addHeaderForRecipient(pair.getValue(), pair.getKey()));
        return perRecipientHeaders;
    }

    @Override
    public final boolean equals(Object o) {
        if (o instanceof MailReferenceDTO) {
            MailReferenceDTO mailDTO = (MailReferenceDTO) o;

            return Objects.equals(this.enqueueId, mailDTO.enqueueId)
                && Objects.equals(this.recipients, mailDTO.recipients)
                && Objects.equals(this.name, mailDTO.name)
                && Objects.equals(this.sender, mailDTO.sender)
                && Objects.equals(this.state, mailDTO.state)
                && Objects.equals(this.errorMessage, mailDTO.errorMessage)
                && Objects.equals(this.lastUpdated, mailDTO.lastUpdated)
                && Objects.equals(this.attributes, mailDTO.attributes)
                && Objects.equals(this.remoteAddr, mailDTO.remoteAddr)
                && Objects.equals(this.remoteHost, mailDTO.remoteHost)
                && Objects.equals(this.perRecipientHeaders, mailDTO.perRecipientHeaders)
                && Objects.equals(this.headerBlobId, mailDTO.headerBlobId)
                && Objects.equals(this.bodyBlobId, mailDTO.bodyBlobId);
        }
        return false;
    }

    @Override
    public final int hashCode() {
        return Objects.hash(enqueueId, recipients, name, sender, state, errorMessage, lastUpdated, attributes, remoteAddr, remoteHost, perRecipientHeaders, headerBlobId, bodyBlobId);
    }
}
