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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.mirroring.mirror.ConnectAddonSupport;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServer;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.UpstreamInstallationParameters;
import com.atlassian.bitbucket.internal.mirroring.mirror.rest.webhook.BaseRestWebHookEvent;
import com.atlassian.bitbucket.internal.mirroring.mirror.rest.webhook.RestWebHookEventConverter;
import com.atlassian.bitbucket.internal.mirroring.mirror.webhook.MirroringWebHookEventHandler;
import com.atlassian.bitbucket.mirroring.mirror.*;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.bitbucket.rest.util.RestPage;
import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.bitbucket.util.ValidationUtils;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.plugins.rest.common.security.CorsAllowed;
import com.google.common.collect.ImmutableList;
import com.sun.jersey.spi.resource.Singleton;
import org.apache.commons.lang3.StringUtils;

import javax.validation.Validator;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Optional;

@AnonymousAllowed
@Path("upstreamServers")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class UpstreamServerResource {

    private final ConnectAddonSupport addonSupport;
    private final I18nService i18nService;
    private final InternalUpstreamService upstreamService;
    private final MirroredRepositoryService mirroredRepositoryService;
    private final RestWebHookEventConverter webHookEventConverter;
    private final MirroringWebHookEventHandler webHookEventHandler;
    private final Validator validator;

    public UpstreamServerResource(ConnectAddonSupport addonSupport, I18nService i18nService,
                                  InternalUpstreamService upstreamService,
                                  MirroredRepositoryService mirroredRepositoryService,
                                  RestWebHookEventConverter webHookEventConverter,
                                  MirroringWebHookEventHandler webHookEventHandler,
                                  Validator validator) {
        this.addonSupport = addonSupport;
        this.i18nService = i18nService;
        this.upstreamService = upstreamService;
        this.mirroredRepositoryService = mirroredRepositoryService;
        this.webHookEventConverter = webHookEventConverter;
        this.webHookEventHandler = webHookEventHandler;
        this.validator = validator;
    }

    /**
     * Retrieves all available clone urls for the specified repository.
     *
     * @param upstreamId the upstream server ID
     * @param upstreamRepoId the repository ID
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname mirrorRepoInfo
     * @response.representation.200.example {@link RestMirroredRepository#EXAMPLE}
     * @response.representation.200.doc the mirrored repository's information
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server or the repository could not be found.
     */
    @GET
    @CorsAllowed
    @Path("{upstreamId}/repos/{upstreamRepoId}")
    public Response getMirroredRepository(@PathParam("upstreamId") String upstreamId,
                                          @PathParam("upstreamRepoId") String upstreamRepoId) {
        return ResponseFactory.ok()
                .entity(new RestMirroredRepository(mirroredRepositoryService.getRepository(upstreamId, upstreamRepoId)))
                .build();
    }

    /**
     * Retrieves a page of upstream servers
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname page
     * @response.representation.200.example {@link RestUpstreamServer#EXAMPLE_PAGE}
     * @response.representation.200.doc a page of upstream servers
     */
    @GET
    public Response listUpstreamServers(@Context PageRequest pageRequest) {
        UpstreamServer upstream = upstreamService.get();
        Page<UpstreamServer> result = upstream == null
                ? PageUtils.<UpstreamServer>createEmptyPage(pageRequest)
                : PageUtils.createPage(ImmutableList.of(upstream), true, pageRequest);
        return ResponseFactory.ok().entity(new RestPage<>(result, RestUpstreamServer::new)).build();
    }

    /**
     * Retrieves upstream server details by ID
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname upstreamServer
     * @response.representation.200.example {@link RestUpstreamServer#EXAMPLE_UPSTREAM_SERVER}
     * @response.representation.200.doc the upstream server
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server could not be found.
     */
    @GET
    @Path("{upstreamId}")
    public Response getUpstreamServer(@PathParam("upstreamId") String upstreamId) {
        UpstreamServer upstream = getUpstreamOrThrow(upstreamId);
        return ResponseFactory.ok(new RestUpstreamServer(upstream)).build();
    }

    /**
     * Retrieves synchronization progress state for the specified upstream server.
     *
     * If there's no progress to report, this resource will return
     * <pre>
     * {@code {"discovering":false,"syncedRepos":0,"totalRepos":0}}
     * </pre>
     *
     * If there are repositories in the process of synchronizing, but the precise number hasn't been discovered yet,
     * this resource will return:
     * <pre>
     * {@code {"discovering":true,"syncedRepos":3,"totalRepos":100}}
     * </pre>
     *
     * If there is progress to report and the total number of repositories is known, this resource will return:
     * <pre>
     * {@code {"discovering":false,"syncedRepos":242,"totalRepos":1071}}
     * </pre>
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname progress
     * @response.representation.200.example {@link RestSyncProgress#EXAMPLE}
     * @response.representation.200.doc the synchronization progress state
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server could not be found.
     */
    @GET
    @Path("{upstreamId}/progress")
    public Response getSynchronizationProgress(@PathParam("upstreamId") String upstreamId) {
        UpstreamServer upstream = upstreamService.get();
        if (upstream == null || !upstream.getId().equals(upstreamId)) {
            throw noSuchUpstream(upstreamId);
        }

        SyncProgress progress = upstreamService.getSynchronizationProgress(upstream);

        return ResponseFactory.ok(new RestSyncProgress(progress)).build();
    }

    /**
     * Starts a synchronization of the specified repository from the upstream server.  Requests to synchronize
     * repositories that do not exist or should not be mirrored because they are not supported by the mirror or
     * included in the mirror's configuration are accepted, but do nothing.
     *
     * @param upstreamId the upstream server ID
     * @param upstreamRepoId the upstream repository ID
     * @param syncLevel the level of synchronization to perform
     * @since 4.6
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.202.mediaType application/json
     * @response.representation.202.doc The synchronization request was accepted.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server could not be found.
     */
    @POST
    @Path("{upstreamId}/repos/{upstreamRepoId}/synchronization")
    public Response synchronizeRepositoryWithUpstream(@PathParam("upstreamId") String upstreamId,
                                                      @PathParam("upstreamRepoId") String upstreamRepoId,
                                                      @QueryParam("level") @DefaultValue("DEFAULT") String syncLevel) {
        SyncLevel level = getSyncLevelOrThrow(syncLevel);
        upstreamService.startRepositorySynchronization(getUpstreamOrThrow(upstreamId), upstreamRepoId, level);

        return Response.status(Response.Status.ACCEPTED).build();
    }

    /**
     * Starts a synchronization with the specified upstream server
     *
     * @param upstreamId the upstream server ID
     * @param syncLevel the level of synchronization to perform
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.202.mediaType application/json
     * @response.representation.202.doc The synchronization request was accepted.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server could not be found.
     */
    @POST
    @Path("{upstreamId}/synchronization")
    public Response synchronizeWithUpstream(@PathParam("upstreamId") String upstreamId,
                                            @QueryParam("level") @DefaultValue("DEFAULT") String syncLevel) {
        SyncLevel level = getSyncLevelOrThrow(syncLevel);
        getUpstreamOrThrow(upstreamId);
        upstreamService.startSynchronization(level);

        return Response.status(Response.Status.ACCEPTED).build();
    }

    /**
     * Returns the mirroring atlassian-connect add-on descriptor for the upstream server identified by {@code upstreamId}.
     * <p>
     * This endpoint should only be called by upstream servers
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname json descriptor
     * @response.representation.200.doc atlassian-connect add-on descriptor
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server could not be found.
     */
    @GET
    @Path("{upstreamId}/addon/descriptor")
    public Response getAddonDescriptor(@PathParam("upstreamId") String upstreamId) {
        return upstreamService.getById(upstreamId)
                .map(addonSupport::getDescriptor)
                .map(ResponseFactory::ok)
                .orElseGet(ResponseFactory::notFound)
                .build();
    }

    /**
     * This REST endpoint is retained for backwards compatibility only. It is a no-op.
     * <p>
     * Starting from 4.6.0, mirrors no longer specify a disabled lifecycle callback in their addon descriptor. Prior
     * to 4.6.0, this was the callback method that was called when the mirroring atlassian-connect add-on has been
     * disabled in the upstream server identified by {@code upstreamId}.
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc an empty response indicating that callback has been processed
     * @deprecated since 4.6
     */
    @Path("{upstreamId}/addon/disabled")
    @POST
    public Response onAddonDisabled(@PathParam("upstreamId") String upstreamId) {
        return ResponseFactory.noContent().build();
    }

    /**
     * This REST endpoint is retained for backwards compatibility only. It is a no-op.
     * <p>
     * Starting from 4.6.0, mirrors no longer specify an enabled lifecycle callback in their addon descriptor. Prior
     * to 4.6.0, this was the callback method that was called when the mirroring atlassian-connect add-on has been
     * enabled in the upstream server identified by {@code upstreamId}.
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc an empty response indicating that callback has been processed
     * @deprecated since 4.6
     */
    @Path("{upstreamId}/addon/enabled")
    @POST
    public Response onAddonEnabled(@PathParam("upstreamId") String upstreamId) {
        return ResponseFactory.noContent().build();
    }

    /**
     * Callback method that should be called when the mirroring atlassian-connect add-on has been installed in the
     * upstream server identified by {@code upstreamId}
     * <p>
     * This endpoint should only be called by upstream servers
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestConnectInstalledEvent#EXAMPLE_SERVER}
     * @request.representation.example {@link RestConnectInstalledEvent#EXAMPLE_CLOUD}
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc an empty response indicating that callback has been processed
     */
    @Path("{upstreamId}/addon/installed")
    @POST
    public Response onAddonInstalled(@PathParam("upstreamId") String upstreamId, RestConnectInstalledEvent event) {

        ValidationUtils.validate(validator, event);

        UpstreamInstallationParameters.Builder builder = UpstreamInstallationParameters.builder()
                .upstreamId(upstreamId)
                .issuerId(event.getClientKey())
                .secret(event.getSharedSecret())
                .apiBaseUrl(event.getApiBaseUrl())
                .team(event.getTeamId(), event.getTeamName());

        upstreamService.onInstalled(builder.build());
        return ResponseFactory.noContent().build();
    }

    /**
     * Callback method that should be called when the mirroring atlassian-connect add-on has been uninstalled from the
     * upstream server identified by {@code upstreamId}
     * <p>
     * This endpoint should only be called by upstream servers
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc an empty response indicating that callback has been processed
     */
    @Path("{upstreamId}/addon/uninstalled")
    @POST
    public Response onAddonUninstalled(@PathParam("upstreamId") String upstreamId) {
        upstreamService.onUninstalled(upstreamId);
        return ResponseFactory.noContent().build();
    }

    /**
     * Callback method for web hook events this mirror is interested in.
     * <p>
     * This endpoint should only be called by upstream servers
     *
     * @param upstreamId the upstream server ID
     *
     * @request.representation.mediaType application/json
     *
     * @response.representation.204.mediaType application/json
     * @response.representation.204.doc an empty response indicating that callback has been processed
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#EXAMPLE}
     * @response.representation.400.doc The payload was not in the expected format for any of the supported web hooks
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname not-found
     * @response.representation.404.doc The upstream server could not be found.
     */
    @Path("{upstreamId}/addon/events")
    @POST
    public Response onWebhookEvent(@PathParam("upstreamId") String upstreamId, RestUnknownEntity unknown) {
        Optional<InternalUpstreamServer> upstreamServer = upstreamService.getById(upstreamId);
        if (!upstreamServer.isPresent()) {
            return ResponseFactory.badRequest(i18nService.getMessage("bitbucket.mirroring.no.such.upstream.server", upstreamId)).build();
        }

        InternalUpstreamServer upstream = upstreamServer.get();
        Optional<? extends BaseRestWebHookEvent> webHookEvent = webHookEventConverter.convert(upstream.getType(), unknown);
        if (webHookEvent.isPresent()) {
            webHookEventHandler.handleEvent(upstream.getId(), webHookEvent.get());

            return ResponseFactory.noContent().build();
        } else {
            return ResponseFactory.badRequest(i18nService.getMessage("bitbucket.mirroring.webhook.not.understood")).build();
        }
    }

    private SyncLevel getSyncLevelOrThrow(String syncLevel) {
        try {
            return SyncLevel.valueOf(StringUtils.upperCase(syncLevel));
        } catch (IllegalArgumentException e) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.mirroring.sync.level.not.understood",
                    syncLevel, StringUtils.join(SyncLevel.values(), ", ")));
        }
    }

    private UpstreamServer getUpstreamOrThrow(String upstreamId) {
        UpstreamServer upstream = upstreamService.get();
        if (upstream == null || !upstream.getId().equals(upstreamId)) {
            throw noSuchUpstream(upstreamId);
        }
        return upstream;
    }

    private NoSuchUpstreamException noSuchUpstream(String upstreamId) {
        throw new NoSuchUpstreamException(i18nService.createKeyedMessage("bitbucket.mirroring.no.such.upstream.server", upstreamId));
    }
}
