package com.atlassian.stash.internal.web.admin;

import com.atlassian.bitbucket.mail.MailException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.mail.InternalMailService;
import com.atlassian.stash.internal.web.soy.StashSoyResponseBuilder;
import com.atlassian.bitbucket.mail.MailHostConfiguration;
import com.atlassian.bitbucket.mail.MailMessage;
import com.atlassian.bitbucket.mail.MailProtocol;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.validation.annotation.ConditionalNotEmpty;
import com.atlassian.bitbucket.validation.annotation.OptionalString;
import com.atlassian.bitbucket.validation.annotation.RequiredString;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;
import java.util.Arrays;

@Controller
@RequestMapping("/admin/mail-server")
public final class MailServerConfigurationController {

    private static final Logger LOG = LoggerFactory.getLogger(MailServerConfigurationController.class);
    private static final String VIEW = "bitbucket.internal.admin.mailserverconfig";
    private static final String REDIRECT_URL = "redirect:/admin/mail-server";

    private final I18nService i18nService;
    private final InternalMailService mailService;
    private final ApplicationPropertiesService propertiesService;

    @Autowired
    public MailServerConfigurationController(InternalMailService mailService, I18nService i18nService,
                                             ApplicationPropertiesService propertiesService) {
        this.mailService = mailService;
        this.i18nService = i18nService;
        this.propertiesService = propertiesService;
    }

    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView showSettings() {
        Form config = loadConfig();
        return new StashSoyResponseBuilder(VIEW)
                .put("isUnconfigured", !mailService.isHostConfigured())
                .put("config", config)
                .put("availableProtocols", Arrays.asList(MailProtocol.values()))
                .build();
    }

    @RequestMapping(method = RequestMethod.POST)
    public ModelAndView updateSettings(@Valid Form config, Errors errors, RedirectAttributes redirectAttributes) {
        if (errors.hasErrors()) {
            return new StashSoyResponseBuilder(VIEW)
                    .put("config", config)
                    .put("availableProtocols", Arrays.asList(MailProtocol.values()))
                    .putValidationErrors(errors).build();
        } else {
            updateConfig(config);
            redirectAttributes.addFlashAttribute("saved", true);
            return new ModelAndView(REDIRECT_URL);
        }
    }

    @RequestMapping(method = RequestMethod.POST, params = "test")
    public ModelAndView testSettings(@Valid TestForm config, Errors errors) {
        final StashSoyResponseBuilder rb = new StashSoyResponseBuilder(VIEW)
                .put("config", config)
                .put("availableProtocols", Arrays.asList(MailProtocol.values()));


        // This lets us set config.passwordChanged so the password field can be filled correctly
        MailHostConfiguration currentConfig = propertiesService.getMailHostConfiguration();
        String configuredPassword;
        if (currentConfig != null) {
            configuredPassword = currentConfig.getPassword();
        } else {
            configuredPassword = config.getPassword();
        }

        if (errors.hasErrors()) {
            config.setPasswordChanged(StringUtils.isNotEmpty(configuredPassword));
            return rb.putValidationErrors(errors).build();
        } else {
            try {
                MailHostConfiguration.Builder mailHostConfigBuilder = new MailHostConfiguration.Builder()
                        .hostname(config.getHostname())
                        .port(parsePort(config.getPort()))
                        .username(config.getUsername())
                        .protocol(MailProtocol.fromString(config.getProtocol()))
                        .useStartTls(config.isUseTls())
                        .requireStartTls(config.isRequireTls());

                // When testing after the mail server settings had been saved, and the password wasn't changed, then we need to get
                // the real password from the saved properties as the password we will receive via the form will just be a placeholder.
                //
                // If the password IS being changed, then we can use the supplied password to test.
                if (config.isPasswordChanged()) {
                    mailHostConfigBuilder.password(config.getPassword());
                } else {
                    mailHostConfigBuilder.password(configuredPassword);
                    config.setPasswordChanged(StringUtils.isNotEmpty(configuredPassword));
                }

                mailService.sendTest(
                        mailHostConfigBuilder.build(),
                        new MailMessage.Builder()
                                .subject(i18nService.getMessage("bitbucket.web.admin.mail.test.subject",
                                        propertiesService.getDisplayName()))
                                .text(i18nService.getMessage("bitbucket.web.admin.mail.test.body",
                                        propertiesService.getDisplayName()))
                                .to(config.getTestAddress())
                                .from(config.getServerEmail())
                                .build());
                return rb.put("tested", true).build();
            } catch (MailException e) {
                LOG.warn(String.format("Failed to send a test email message to %s", config.getTestAddress()), e);
                return rb.put("tested", true).put("testFailureMessage", e.getLocalizedMessage()).build();
            }
        }
    }

    /* --- load, persist and remove the mail host configuration --- */

    private Form loadConfig() {
        final MailHostConfiguration configuration = propertiesService.getMailHostConfiguration();
        final Form form = new Form();
        if (configuration != null) {
            form.hostname = configuration.getHostname();
            form.port = configuration.getPort() != null ? configuration.getPort().toString() : null;
            form.username = configuration.getUsername();
            form.passwordChanged = StringUtils.isNotEmpty(configuration.getPassword());
            form.serverEmail = propertiesService.getServerEmailAddress();
            form.protocol = configuration.getProtocol().name();
            form.useTls = configuration.isUseStartTls();
            form.requireTls = configuration.isRequireStartTls();
        }
        return form;
    }

    /**
     * Update the mail host configuration.
     *
     * @param form the mail server configuration to persist
     */
    private void updateConfig(Form form) {
        MailHostConfiguration.Builder newConfigBuilder = new MailHostConfiguration.Builder()
                .hostname(form.getHostname())
                .port(parsePort(form.getPort()))
                .username(form.getUsername())
                .protocol(MailProtocol.fromString(form.getProtocol()))
                .useStartTls(form.isUseTls())
                .requireStartTls(form.isRequireTls());

        if (StringUtils.isEmpty(form.username)) {
            newConfigBuilder.password(null);
        } else if (form.passwordChanged) {
            newConfigBuilder.password(form.password);
        } else {
            MailHostConfiguration oldConfig = propertiesService.getMailHostConfiguration();
            if (oldConfig != null) {
                newConfigBuilder.password(oldConfig.getPassword());
            }
        }

        propertiesService.setMailHostConfiguration(newConfigBuilder.build());
        propertiesService.setServerEmailAddress(form.getServerEmail());
    }

    /* --- utils --- */

    private Integer parsePort(String port) {
        return port != null ? Integer.parseInt(port) : null;
    }

    /* --- form & validation --- */

    @ConditionalNotEmpty.List({
            // display a validation error if any field is set and the hostname is unset
            @ConditionalNotEmpty(
                    targetField = "hostname",
                    fields = {"port", "username", "password", "serverEmail"},
                    message = "{com.atlassian.bitbucket.validation.mail.server.hostname.message}"),
            // display a validation error if any field is set and the serverEmail is unset
            @ConditionalNotEmpty(
                    targetField = "serverEmail",
                    fields = {"hostname", "port", "username", "password"},
                    message = "{com.atlassian.bitbucket.validation.server.email.address.message}"
            )
    })
    public static class Form {

        private String hostname;
        private String serverEmail;
        private String port; // 'String' so that Spring does not throw an exception (before the JSR-303 validation) if the user enters a very large number
        private String username;
        private String password;
        private boolean passwordChanged;
        private String protocol = MailProtocol.SMTP.name();
        private boolean useTls;
        private boolean requireTls;

        @RequiredString
        public String getHostname() {
            return hostname;
        }

        @Email
        public String getServerEmail() {
            return serverEmail;
        }

        @Range(min = 0, max = 65535)
        public String getPort() {
            return port;
        }

        @OptionalString
        public String getUsername() {
            return username;
        }

        @OptionalString
        public String getPassword() {
            return password;
        }

        @RequiredString
        public String getProtocol() {
            return protocol;
        }

        public boolean isUseTls() {
            return useTls;
        }

        public boolean isRequireTls() {
            return requireTls;
        }

        public boolean isPasswordChanged() {
            return passwordChanged;
        }

        public void setHostname(String hostname) {
            this.hostname = StringUtils.trimToNull(hostname);
        }

        public void setServerEmail(final String serverEmail) {
            this.serverEmail = serverEmail;
        }

        public void setPort(String port) {
            this.port = StringUtils.trimToNull(port);
        }

        public void setUsername(String username) {
            this.username = StringUtils.trimToNull(username);
        }

        public void setPassword(String password) {
            this.password = StringUtils.isEmpty(password) ? null : password;
        }

        public void setPasswordChanged(boolean passwordChanged) {
            this.passwordChanged = passwordChanged;
        }

        public void setProtocol(String protocol) {
            this.protocol = protocol;
        }

        public void setUseTls(boolean useTls) {
            this.useTls = useTls;
        }

        public void setRequireTls(boolean requireTls) {
            this.requireTls = requireTls;
        }
    }

    public static class TestForm extends Form {

        private String testAddress;

        @Email
        @NotEmpty
        public String getTestAddress() {
            return testAddress;
        }

        public void setTestAddress(String testAddress) {
            this.testAddress = testAddress;
        }
    }
}
