package com.atlassian.stash.internal.mail;

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.i18n.I18nKey;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.mail.HtmlCssInliner;
import com.atlassian.bitbucket.mail.MailMessage;
import com.atlassian.bitbucket.mail.SoyMailBuilder;
import com.atlassian.bitbucket.mail.SoyMailMessageRequest;
import com.atlassian.bitbucket.timezone.UserTimeZoneSupplier;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.atlassian.stash.internal.locale.LocaleUtils;
import com.atlassian.stash.internal.user.InternalLocaleManager;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.io.StringWriter;
import java.time.ZoneId;
import java.util.*;

import static com.atlassian.bitbucket.util.Chainable.chain;
import static com.google.common.base.Preconditions.checkNotNull;

@AvailableToPlugins(SoyMailBuilder.class)
@Component
public class SoyMailBuilderImpl implements SoyMailBuilder {

    private static final Map<String, Object> SOY_INJECTED_DATA = Collections.singletonMap("productName", Product.NAME);

    private static final Logger log = LoggerFactory.getLogger(SoyMailBuilderImpl.class);

    private final SoyTemplateRenderer soyTemplateRenderer;
    private final HtmlCssInliner htmlCssInliner;
    private final I18nService i18nService;
    private final SecurityService securityService;
    private final InternalLocaleManager localeManager;
    private final UserTimeZoneSupplier userTimeZoneSupplier;

    @Autowired
    public SoyMailBuilderImpl(SoyTemplateRenderer soyTemplateRenderer,
                              HtmlCssInliner htmlCssInliner,
                              I18nService i18nService,
                              SecurityService securityService,
                              InternalLocaleManager localeManager,
                              UserTimeZoneSupplier userTimeZoneSupplier) {
        this.soyTemplateRenderer = soyTemplateRenderer;
        this.htmlCssInliner = htmlCssInliner;
        this.i18nService = i18nService;
        this.securityService = securityService;
        this.localeManager = localeManager;
        this.userTimeZoneSupplier = userTimeZoneSupplier;
    }

    @Override
    @Nonnull
    public Iterable<MailMessage> build(@Nonnull SoyMailMessageRequest message) {
        checkNotNull(message, "message");
        // null out the threadlocal locale to ensure the current user's browser setting doesn't interfere
        return LocaleUtils.withoutLocale(() -> buildWithNoLocale(message));
    }

    private Iterable<MailMessage> buildWithNoLocale(SoyMailMessageRequest message) {
        Set<MailMessage> messages = new HashSet<>();
        // Cache messages to avoid the extra cost of Soy transformation (and the extra Strings in memory, but this is
        // less important). The e-mail generator is currently a `function (recipient, message)`. The e-mail body can be
        // shared if all the arguments match. The message is fixed so we can ignore it. This leaves the
        // recipient. Only locale and timezone properties of the recipient *currently* affect the body. This allows
        // the body to be cached/shared based on these properties.
        //
        // This cache will cause e-mails to be incorrectly shared between users if more than the timezone and locale
        // of the recipient affect the output of the body. See BSERVDEV-12451 for discussion on its removal.
        Map<RecipientState, LocalizedMessage> localeToMessage = Maps.newHashMap();
        for (ApplicationUser user : chain(message.getRecipients())
                .filter(recipient -> StringUtils.isNotBlank(recipient.getEmailAddress()))) {
            RecipientState recipientState = recipientStateFor(user);
            LocalizedMessage cachedMessage = localeToMessage.get(recipientState);
            if (cachedMessage == null) {
                localeToMessage.put(recipientState, cachedMessage = getLocalizedMessage(user, message, recipientState.locale));
            }
            if (cachedMessage != null) {
                MailMessage.Builder mailMessageBuilder = new MailMessage.Builder()
                        .to(user.getEmailAddress())
                        .text(cachedMessage.text)
                        .subject(cachedMessage.subject)
                        .header("Content-Type", "text/html; charset=UTF-8");
                if (cachedMessage.locale != null) {
                    mailMessageBuilder.header("Content-Language", cachedMessage.locale.toLanguageTag());
                }
                mailMessageBuilder = message.getMessageEffector().apply(mailMessageBuilder);
                messages.add(mailMessageBuilder.build());
            }
        }

        return messages;
    }

    private LocalizedMessage getLocalizedMessage(ApplicationUser user, SoyMailMessageRequest message, Locale locale) {
        // This has nothing to do with permissions, and is about ensuring we respect the user locale and
        // timezone settings
        return securityService.impersonating(user, "Impersonate user for sending mail").call(() -> {
            try {
                String subjectText = null;
                if (message.getSubjectKey() != null) {
                    I18nKey subjectKey = message.getSubjectKey();
                    subjectText = i18nService.getMessage(subjectKey);
                }

                StringWriter writer = new StringWriter();
                soyTemplateRenderer.render(writer, message.getSoyTemplateModuleKey(),
                        message.getSoyTemplateName(), message.getContext(), SOY_INJECTED_DATA);

                String text = htmlCssInliner.inlineCss(writer.toString(), message.getCssModuleKey());
                return new LocalizedMessage(locale, subjectText, text);
            } catch (SoyException e) {
                log.error("Error rendering email notification ({}/{})", message.getSoyTemplateModuleKey(), message.getSoyTemplateName(), e);
                return null;
            }
        });
    }

    private RecipientState recipientStateFor(ApplicationUser user) {
        return new RecipientState(localeManager.getLocale(user), userTimeZoneSupplier.getTimeZone(user));
    }

    private static class LocalizedMessage {

        private final Locale locale;
        private final String subject;
        private final String text;

        private LocalizedMessage(Locale locale, String subject, String text) {
            this.locale = locale;
            this.subject = subject;
            this.text = text;
        }
    }

    /**
     * Represents the properties of a recipient that affect the body of a mail message.
     *
     * <p>Used as a key in a cache of mail bodies. This allows the mail body of {@code recipient1} to be used as the
     * email body of {@code recipient2} when {@code recipient1} and {@code recipent2} share these properties.
     *
     * @since 4.6
     */
    private static class RecipientState {

        private final Locale locale;
        private final ZoneId zoneId;

        private RecipientState(Locale locale, ZoneId zoneId) {
            this.locale = locale;
            this.zoneId = zoneId;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            RecipientState that = (RecipientState) o;
            return Objects.equals(locale, that.locale) &&
                    Objects.equals(zoneId, that.zoneId);
        }

        @Override
        public int hashCode() {
            return Objects.hash(locale, zoneId);
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                    .add("locale", locale)
                    .add("zoneId", zoneId)
                    .toString();
        }
    }
}
