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

import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServer;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringNavBuilder;
import com.atlassian.bitbucket.mirroring.mirror.IntegrationState;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServer;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.server.ApplicationModeSupplier;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;

import static com.atlassian.bitbucket.internal.mirroring.mirror.web.MirrorWebConstants.PLUGIN_KEY;
import static com.atlassian.bitbucket.internal.mirroring.mirror.web.MirrorWebConstants.TEMPLATE_MODULE_KEY;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.*;
import static org.apache.commons.lang.StringUtils.stripToNull;

public class MirrorServlet extends HttpServlet {

    private static final String PARAM_MESSAGE = "error_description";
    private static final String PARAM_REASON = "error";
    private static final String PLACEHOLDER_MODULE_KEY = PLUGIN_KEY + ":mirror-placeholder-page-resources";
    private static final String REASON_ACCESS_DENIED = "access_denied";
    private static final String SOY_ERROR = "error";
    private static final String SOY_MIRROR_STATE = "mirrorState";
    private static final String SOY_MODULE = "moduleKey";
    private static final String SOY_REGISTRATION_URL = "registrationUrl";
    private static final String SOY_RECONNECT_URL = "reconnectUrl";
    private static final String SOY_SERVER_ID = "serverId";
    private static final String SOY_UPSTREAM = "upstream";
    private static final String SOY_UPSTREAM_URL = "upstreamUrl";
    private static final String SOY_UPSTREAM_TYPE = "type";
    private static final String TEMPLATE_ACTIVE = "bitbucket.internal.mirror.page.active";
    private static final String TEMPLATE_CONNECTING = "bitbucket.internal.mirror.page.connecting";
    private static final String TEMPLATE_PENDING = "bitbucket.internal.mirror.page.pending";
    private static final String TEMPLATE_REMOVED = "bitbucket.internal.mirror.page.removed";
    private static final String TEMPLATE_UNKNOWN = "bitbucket.internal.mirror.page.unknown";

    private final SoyTemplateRenderer soyTemplateRenderer;
    private final MirroringNavBuilder mirroringNavBuilder;
    private final ApplicationModeSupplier mode;
    private final ApplicationPropertiesService propertiesService;
    private final InternalUpstreamService upstreamService;

    public MirrorServlet(InternalUpstreamService upstreamService,
                         SoyTemplateRenderer soyTemplateRenderer,
                         MirroringNavBuilder mirroringNavBuilder,
                         ApplicationModeSupplier mode,
                         ApplicationPropertiesService propertiesService) {
        this.soyTemplateRenderer = soyTemplateRenderer;
        this.mirroringNavBuilder = mirroringNavBuilder;
        this.mode = mode;
        this.propertiesService = propertiesService;
        this.upstreamService = upstreamService;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        if (mode.getMode() != ApplicationMode.MIRROR) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        resp.setContentType(MediaType.HTML_UTF_8.toString());

        if (req.getParameterMap().containsKey("reconnect")) {
            // the mirrorService will only reregister when allowed (from INITIALIZING or REMOVED)
            upstreamService.register();
            redirectToSelf(resp);
            return;
        }

        InternalUpstreamServer upstream = upstreamService.get();
        if (upstream == null) {
            handleInitialising(resp.getWriter(), Optional.empty());
            return;
        }

        if (handleSetupParameter(req, resp, upstream)) {
            return;
        }

        IntegrationState state = upstream.getState();
        try {
            if (state == IntegrationState.INITIALIZING) {
                handleInitialising(resp.getWriter(), of(upstream));
            } else if (state == IntegrationState.PENDING) {
                handlePending(req, resp, upstream);
            } else if (state == IntegrationState.INSTALLED) {
                handleInstalled(req, resp, upstream);
            } else if (state == IntegrationState.REMOVED) {
                renderRemoved(resp.getWriter(), upstream);
            } else if (state == IntegrationState.UNKNOWN) {
                renderPage(resp.getWriter(), upstream, TEMPLATE_UNKNOWN);
            } else {
                throw new RuntimeException(String.format("Unexpected upstream state '%s'.", state));
            }
        } catch (SoyException e) {
            Throwable cause = e.getCause();
            if (cause instanceof IOException) {
                throw (IOException) cause;
            }
            throw new ServletException(e);
        }
    }

    private void handleInstalled(HttpServletRequest req, HttpServletResponse resp, InternalUpstreamServer upstream)
            throws IOException {
        if (upstream.getType() == UpstreamServerType.BITBUCKET_CLOUD) {
            String clientKey = StringUtils.stripToNull(req.getParameter("clientKey"));
            if (clientKey != null && of(clientKey).equals(upstream.getIssuerId())) {
                String redirect = mirroringNavBuilder.upstream(upstream).admin().getAbsolute();
                resp.sendRedirect(redirect);
                return;
            }
        }
        renderPage(resp.getWriter(), upstream, TEMPLATE_ACTIVE);
    }

    private void handleInitialising(PrintWriter writer, Optional<? extends UpstreamServer> server) {
        ImmutableMap<String, Object> templateContext = new ImmutableMap.Builder<String, Object>()
                .put(SOY_MIRROR_STATE, IntegrationState.INITIALIZING.getId())
                .put(SOY_UPSTREAM_URL, server.map(UpstreamServer::getBaseUrl).orElse(""))
                .put(SOY_UPSTREAM, server.map(MirrorServlet::getUpstreamHost).orElse(""))
                .build();
        soyTemplateRenderer.render(writer, TEMPLATE_MODULE_KEY, TEMPLATE_CONNECTING, templateContext);
    }

    private void handlePending(HttpServletRequest req, HttpServletResponse resp, UpstreamServer upstream)
            throws IOException {
        renderPage(resp.getWriter(), upstream, TEMPLATE_PENDING, getErrorFromRequest(req));
    }

    private boolean handleSetupParameter(HttpServletRequest req, HttpServletResponse resp, UpstreamServer upstream)
            throws IOException {
        if (Boolean.parseBoolean(req.getParameter("setup"))) {
            if (upstream.getType() == UpstreamServerType.BITBUCKET_CLOUD &&
                    upstream.getState() == IntegrationState.PENDING) {
                String redirect = mirroringNavBuilder.cloudUpstream(upstream).authorizeAddon().getAbsolute();
                resp.sendRedirect(redirect);
                return true;
            } else {
                //Redirect to self without the setup parameter.
                redirectToSelf(resp);
                return true;
            }
        }
        return false;
    }

    private void redirectToSelf(HttpServletResponse resp) throws IOException {
        resp.sendRedirect(mirroringNavBuilder.placeholder().getAbsolute());
    }

    private void renderPage(PrintWriter writer, UpstreamServer server, String templateName) {
        renderPage(writer, server, templateName, empty());
    }

    private void renderPage(PrintWriter writer, UpstreamServer server, String templateName, Optional<String> error) {
        String state = server.getState().getId();
        String registrationUrl = mirroringNavBuilder.upstream(server).authorizeAddon().getAbsolute();
        String type = server.getType().getId();
        String upstream = getUpstreamHost(server);

        ImmutableMap<String, Object> templateContext = new ImmutableMap.Builder<String, Object>()
                .put(SOY_MIRROR_STATE, state)
                .put(SOY_REGISTRATION_URL, registrationUrl)
                .put(SOY_SERVER_ID, requireNonNull(propertiesService.getServerId()))
                .put(SOY_UPSTREAM_URL, server.getBaseUrl())
                .put(SOY_UPSTREAM, upstream)
                .put(SOY_UPSTREAM_TYPE, type)
                .put(SOY_MODULE, PLACEHOLDER_MODULE_KEY)
                .put(SOY_ERROR, error.orElse(""))
                .build();
        soyTemplateRenderer.render(writer, TEMPLATE_MODULE_KEY, templateName, templateContext);
    }

    private void renderRemoved(PrintWriter writer, UpstreamServer server) {
        ImmutableMap<String, Object> templateContext = ImmutableMap.of(
                SOY_MIRROR_STATE, IntegrationState.REMOVED.getId(),
                SOY_UPSTREAM_URL, server.getBaseUrl(),
                SOY_RECONNECT_URL, mirroringNavBuilder.placeholder().getAbsolute() + "?reconnect",
                SOY_MODULE, PLACEHOLDER_MODULE_KEY,
                SOY_UPSTREAM, getUpstreamHost(server));
        soyTemplateRenderer.render(writer, TEMPLATE_MODULE_KEY, TEMPLATE_REMOVED, templateContext);
    }

    private static Optional<String> getErrorFromRequest(ServletRequest request) {
        String reason = stripToNull(request.getParameter(PARAM_REASON));
        //When the user clicks cancel an access_denied is reported. We don't want to show this as the user has just
        //clicked cancel and knows what the deal is.
        if (REASON_ACCESS_DENIED.equals(reason)) {
            return empty();
        } else {
            return ofNullable(stripToNull(request.getParameter(PARAM_MESSAGE)));
        }
    }

    private static String getUpstreamHost(UpstreamServer server) {
        try {
            String host = stripToNull(new URI(server.getBaseUrl()).getHost());
            if (host == null) {
                return server.getBaseUrl();
            } else {
                return host;
            }
        } catch (URISyntaxException e) {
            return server.getBaseUrl();
        }
    }
}
