package com.atlassian.bitbucket.internal.mirroring.mirror.fragments;

import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.client.UpstreamValidator;
import com.atlassian.bitbucket.internal.mirroring.mirror.feature.CloudMirroringFeature;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.server.FeatureManager;
import com.atlassian.bitbucket.ui.ContextualFormFragment;
import com.atlassian.bitbucket.ui.ValidationErrors;
import com.atlassian.bitbucket.util.RequestUtils;
import com.atlassian.bitbucket.validation.annotation.HttpURL;
import com.atlassian.bitbucket.validation.annotation.RequiredField;
import com.atlassian.bitbucket.validation.annotation.RequiredString;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants.PROPERTY_UPSTREAM_TYPE;
import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorConstants.PROPERTY_UPSTREAM_URL;
import static com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType.BITBUCKET_CLOUD;
import static com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType.BITBUCKET_SERVER;
import static org.apache.commons.lang3.StringUtils.stripToNull;

/**
 * Fragment to render the mirror specific components on the setup form.
 */
public class MirrorSetupFragment implements ContextualFormFragment {

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

    private static final String FRAGMENT_KEY = "com.atlassian.bitbucket.server.bitbucket-mirroring-mirror:mirror-setup-fragments";
    private static final String CONFIG_TEMPLATE = "bitbucket.internal.mirror.fragment.setup.upstreamConfig";

    private static final String MODE_CLOUD = "cloud";
    private static final String MODE_SERVER = "server";

    private static final String TEMPLATE_KEY_CLOUD_SUPPORTED = "cloudSupported";
    private static final String TEMPLATE_KEY_FORM = "form";
    private static final String TEMPLATE_KEY_FIELD_ERRORS = "fieldErrors";

    private static final String FIELD_UPSTREAM_URL = "upstreamUrl";

    private final FeatureManager featureManager;
    private final ApplicationPropertiesService propertiesService;
    private final SoyTemplateRenderer soyTemplateRenderer;
    private final InternalUpstreamService upstreamService;
    private final UpstreamValidator upstreamValidator;
    private final Validator validator;

    public MirrorSetupFragment(FeatureManager featureManager, ApplicationPropertiesService propertiesService,
                               InternalUpstreamService upstreamService,
                               SoyTemplateRenderer soyTemplateRenderer,
                               UpstreamValidator upstreamValidator,
                               Validator validator) {

        this.featureManager = featureManager;
        this.propertiesService = propertiesService;
        this.soyTemplateRenderer = soyTemplateRenderer;
        this.upstreamService = upstreamService;
        this.upstreamValidator = upstreamValidator;
        this.validator = validator;
    }

    @Override
    public void doView(Appendable appendable, Map<String, Object> context) throws IOException {

        String pluginProperty = propertiesService.getPluginProperty(PROPERTY_UPSTREAM_URL, null);
        String upstreamType = propertiesService.getPluginProperty(PROPERTY_UPSTREAM_TYPE, null);

        if (!isCloudSupported() && MODE_CLOUD.equals(upstreamType)) {
            upstreamType = MODE_SERVER;
        }
        MirrorConfigForm configForm = MirrorConfigForm.parse(pluginProperty, upstreamType);

        appendable.append(soyTemplateRenderer.render(FRAGMENT_KEY, CONFIG_TEMPLATE, renderContext(configForm)));
    }

    @Override
    public void validate(Map<String, String[]> requestParams, ValidationErrors errors, Map<String, Object> context) {
        MirrorConfigForm configForm = MirrorConfigForm.parse(requestParams);
        Set<ConstraintViolation<MirrorConfigForm>> violations = validator.validate(configForm);
        if (!violations.isEmpty()) {
            for (ConstraintViolation violation : violations) {
                errors.addFieldError(violation.getPropertyPath().toString(), violation.getMessage());
            }
        } else {
            upstreamValidator.validate(configForm.getType(), configForm.getUpstreamUrl())
                    .ifPresent(keyedMessage -> errors.addFieldError(FIELD_UPSTREAM_URL, keyedMessage.getLocalisedMessage()));
        }
    }

    @Override
    public void doError(Appendable appendable, Map<String, String[]> requestParams,
                        Map<String, Collection<String>> fieldErrors,
                        Map<String, Object> context) throws IOException {
        MirrorConfigForm configForm = MirrorConfigForm.parse(requestParams);
        appendable.append(soyTemplateRenderer.render(FRAGMENT_KEY, CONFIG_TEMPLATE,
                renderContext(configForm, fieldErrors)));
    }

    @Override
    public void execute(Map<String, String[]> requestParams, Map<String, Object> context) {
        MirrorConfigForm configForm = MirrorConfigForm.parse(requestParams);
        UpstreamServerType type = configForm.getType();
        if (type == BITBUCKET_CLOUD) {
            upstreamService.createForCloud();
        } else if (type == BITBUCKET_SERVER) {
            upstreamService.createForServer(configForm.getUpstreamUrl());
            try {
                //try to register during the setup wizard. There is a background task that will take over if it
                //fails.
                upstreamService.register().get();
            } catch (InterruptedException | ExecutionException | RuntimeException e) {
                log.debug("Failed to register upstream in setup wizard.", e);
            }
        } else {
            throw new IllegalStateException(String.format("Unexpected type '%s'.", type));
        }
    }

    private boolean isCloudSupported() {
        return featureManager.isEnabled(CloudMirroringFeature.CLOUD_SUPPORT);
    }

    private ImmutableMap<String, Object> renderContext(MirrorConfigForm configForm) {
        return renderContext(configForm, ImmutableMap.of());
    }

    private ImmutableMap<String, Object> renderContext(MirrorConfigForm configForm,
                                                       Map<String, Collection<String>> fieldErrors) {
        return ImmutableMap.of(
                TEMPLATE_KEY_CLOUD_SUPPORTED, isCloudSupported(),
                TEMPLATE_KEY_FORM, configForm,
                TEMPLATE_KEY_FIELD_ERRORS, fieldErrors);
    }

    /**
     * Form component used to represent any mirror configuration. Mainly used for validation.
     */
    public static class MirrorConfigForm {

        private static final String PARAM_UPSTREAM_URL = "upstreamUrl";
        private static final String PARAM_UPSTREAM_TYPE = "upstreamType";

        private final UpstreamServerType upstreamType;
        private final String upstreamUrl;

        private MirrorConfigForm(String upstreamUrl, UpstreamServerType upstreamType) {
            this.upstreamUrl = stripToNull(upstreamUrl);
            this.upstreamType = upstreamType;
        }

        public String getUpstreamUrl() {
            return upstreamUrl;
        }

        private static UpstreamServerType getUpstreamType(String type) {
            return UpstreamServerType.fromId(type).orElse(null);
        }

        private static MirrorConfigForm parse(String url, String type) {
            UpstreamServerType upstreamType = getUpstreamType(type);
            if (upstreamType == BITBUCKET_SERVER) {
                return new ServerMirrorConfigForm(url, upstreamType);
            } else {
                return new MirrorConfigForm(url, upstreamType);
            }
        }

        private static MirrorConfigForm parse(Map<String, String[]> params) {
            String upstreamUrl = getParamValue(PARAM_UPSTREAM_URL, params);
            String upstreamType = getParamValue(PARAM_UPSTREAM_TYPE, params);
            return parse(upstreamUrl, upstreamType);
        }

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

        @RequiredField(message = "{bitbucket.mirroring.setup.upstream.location.required}")
        public String getUpstreamType() {
            if (upstreamType == null) {
                return null;
            } else {
                return upstreamType.getId();
            }
        }

        public UpstreamServerType getType() {
            return upstreamType;
        }

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

        @Override
        public int hashCode() {
            return Objects.hash(upstreamType, upstreamUrl);
        }

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

    public static class ServerMirrorConfigForm extends MirrorConfigForm {
        private ServerMirrorConfigForm(String upstreamUrl, UpstreamServerType type) {
            super(upstreamUrl, type);
        }

        @Override
        @RequiredString
        @HttpURL
        public String getUpstreamUrl() {
            return super.getUpstreamUrl();
        }
    }
}
