package com.atlassian.bitbucket.internal.ssh.fragments;

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.internal.ssh.server.SshServer;
import com.atlassian.bitbucket.internal.ssh.server.SshServerState;
import com.atlassian.bitbucket.internal.ssh.server.SshServerStatus;
import com.atlassian.bitbucket.internal.ssh.service.SshBaseUrlResolver;
import com.atlassian.bitbucket.ssh.SshConfiguration;
import com.atlassian.bitbucket.ssh.SshConfigurationService;
import com.atlassian.bitbucket.ui.ContextualFormFragment;
import com.atlassian.bitbucket.ui.ValidationErrors;
import com.atlassian.bitbucket.util.RequestUtils;
import com.atlassian.bitbucket.view.TemplateRenderingException;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import org.apache.commons.lang3.StringUtils;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.io.IOException;
import java.net.BindException;
import java.util.Collection;
import java.util.Map;

/**
 * Injects SSH server configuration fields into the Server Settings page.
 */
public class SshConfigurationFormFragment implements ContextualFormFragment {

    private static final String SERVER_SOY_MODULE_KEY = "com.atlassian.stash.ssh-plugin:ssh-server-config-soy";
    private static final String CONFIG_TEMPLATE = "bitbucket.internal.fragment.ssh.server.config";

    // flag indicating that SSH server start error message should be displayed
    private static final String SSH_SERVER_FAILED_TO_START = "sshServerFailedToStart";

    // request params
    public static final String SSH_PORT = "sshPort";
    public static final String SSH_ACCESS_KEYS_ENABLED = "sshAccessKeysEnabled";
    public static final String SSH_ENABLED = "sshEnabled";
    public static final String SSH_BASE_URL = "sshBaseUrl";
    public static final String SSH_DEFAULT_BASE_URL = "sshDefaultBaseUrl";

    private final SshConfigurationService sshConfigurationService;
    private final SoyTemplateRenderer soyTemplateRenderer;
    private final Validator validator;
    private final SshBaseUrlResolver baseUrlResolver;
    private final SshServer sshServer;

    public SshConfigurationFormFragment(SshConfigurationService sshConfigurationService,
                                        SoyTemplateRenderer soyTemplateRenderer, Validator validator,
                                        SshBaseUrlResolver sshTransport, SshServer sshServer) {
        this.sshConfigurationService = sshConfigurationService;
        this.soyTemplateRenderer = soyTemplateRenderer;
        this.validator = validator;
        this.baseUrlResolver = sshTransport;
        this.sshServer = sshServer;
    }

    @Override
    public void doView(Appendable appendable, Map<String, Object> context) throws IOException {
        SshConfiguration storedConfig = sshConfigurationService.getConfiguration();
        createBaseContext(context, storedConfig);
        context.put("currentPort", storedConfig.getPort());
        context.put("product", Product.NAME);
        context.put(SSH_ACCESS_KEYS_ENABLED, storedConfig.isAccessKeysEnabled());
        context.put(SSH_ENABLED, storedConfig.isEnabled());
        context.put(SSH_PORT, String.valueOf(storedConfig.getPort())); // putting a raw int adds comma separated '000s
        context.put(SSH_BASE_URL, storedConfig.getBaseUrl());
        render(appendable, context);
    }

    @Override
    public void validate(Map<String, String[]> requestParams, ValidationErrors errors, Map<String, Object> context) {
        for (ConstraintViolation constraintViolation : validator.validate(unmarshallForm(requestParams))) {
            errors.addFieldError(
                toFormFragmentFieldName(constraintViolation.getPropertyPath().toString()),
                constraintViolation.getMessage()
            );
        }
    }

    @Override
    public void doError(Appendable appendable, Map<String, String[]> requestParams,
                        Map<String, Collection<String>> fieldErrors, Map<String, Object> context) throws IOException {
        SshConfigurationForm form = unmarshallForm(requestParams);
        createBaseContext(context, sshConfigurationService.getConfiguration());
        context.put("errors", fieldErrors);
        context.put("product", Product.NAME);
        context.put(SSH_ACCESS_KEYS_ENABLED, form.isAccessKeysEnabled());
        context.put(SSH_ENABLED, form.isEnabled());
        context.put(SSH_PORT, form.getPort());
        context.put(SSH_BASE_URL, form.getBaseUrl());
        render(appendable, context);
    }

    @Override
    public void execute(Map<String, String[]> requestParams, Map<String, Object> context) {
        SshConfigurationForm sshConfiguration = unmarshallForm(requestParams);
        sshConfigurationService.setConfiguration(sshConfiguration.toSshConfiguration());
    }

    private Map<String, Object> createBaseContext(Map<String, Object> context, SshConfiguration currentConfig) {
        SshServerStatus state = sshServer.getStatus();
        context.put(SSH_SERVER_FAILED_TO_START, state.getState() == SshServerState.FAILED_TO_START);
        context.put("hasBindException", state.getFailureCause() instanceof BindException);
        context.put("currentPort", currentConfig.getPort());
        context.put(SSH_DEFAULT_BASE_URL, baseUrlResolver.getDefaultBaseUrl());
        return context;
    }

    private String toFormFragmentFieldName(String beanFieldName) {
        return "ssh" + StringUtils.capitalize(beanFieldName);
    }

    private SshConfigurationForm unmarshallForm(Map<String, String[]> requestParams) {
        boolean accessKeysEnabled = getCheckboxValue(SSH_ACCESS_KEYS_ENABLED, requestParams);
        boolean enabled = getCheckboxValue(SSH_ENABLED, requestParams);
        String port = getParamValue(SSH_PORT, requestParams);
        String sshBaseUrl = getParamValue(SSH_BASE_URL, requestParams);
        return new SshConfigurationForm(enabled, accessKeysEnabled, port, sshBaseUrl);
    }

    private boolean getCheckboxValue(String paramName, Map<String, String[]> requestParams) {
        return "on".equals(getParamValue(paramName, requestParams));
    }

    private String getParamValue(String paramName, Map<String, String[]> requestParams) {
        return RequestUtils.getParamValue(paramName, requestParams);
    }

    private void render(Appendable appendable, Map<String, Object> context) {
        try {
            soyTemplateRenderer.render(appendable, SERVER_SOY_MODULE_KEY, CONFIG_TEMPLATE, context);
        } catch (SoyException e) {
            throw new TemplateRenderingException("Failed to render " + CONFIG_TEMPLATE, e);
        }
    }

}
