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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.mirroring.mirror.*;
import com.atlassian.bitbucket.internal.mirroring.mirror.jwt.JwtSignedRequestFactory;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringNavBuilder;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringUrl;
import com.atlassian.bitbucket.json.JsonRenderer;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServer;
import com.atlassian.bitbucket.mirroring.mirror.client.UpstreamClient;
import com.atlassian.bitbucket.util.ValidationUtils;
import com.atlassian.httpclient.api.*;
import com.atlassian.util.concurrent.Promise;
import com.google.common.base.Function;
import com.google.common.net.MediaType;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;

import javax.annotation.Nonnull;
import javax.validation.Validator;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
import java.util.function.Supplier;

import static com.atlassian.bitbucket.internal.mirroring.mirror.MirrorDescriptionUtils.describe;
import static com.atlassian.httpclient.api.HttpStatus.TOO_MANY_REQUESTS;
import static com.google.common.base.Functions.compose;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.slf4j.LoggerFactory.getLogger;

public abstract class AbstractUpstreamClient implements UpstreamClient {
    private static final Logger log = getLogger(AbstractUpstreamClient.class);

    protected final HttpClient httpClient;
    protected final I18nService i18nService;
    protected final JsonFactory jsonFactory;
    protected final JsonRenderer jsonRenderer;
    protected final MirroringNavBuilder mirroringNavBuilder;
    protected final JwtSignedRequestFactory jwtRequestFactory;
    protected final UpstreamServer upstream;
    protected final Validator validator;

    public AbstractUpstreamClient(HttpClient httpClient, I18nService i18nService, JsonRenderer jsonRenderer,
                                  MirroringNavBuilder mirroringNavBuilder, JwtSignedRequestFactory jwtRequestFactory,
                                  UpstreamServer upstream, Validator validator) {
        this.httpClient = httpClient;
        this.i18nService = i18nService;
        this.jsonRenderer = jsonRenderer;
        this.mirroringNavBuilder = mirroringNavBuilder;
        this.jwtRequestFactory = jwtRequestFactory;
        this.upstream = upstream;
        this.validator = validator;

        jsonFactory = new ObjectMapper().getJsonFactory();
    }

    protected abstract void ensureErrorResponseIsFromUpstream(Response response);

    protected <T> Function<Response, T> ensureErrorResponseIsFromUpstreamAndThen(Function<Response, T> andThen) {
        return compose(andThen, (Function<Response, Response>) response -> {
            ensureErrorResponseIsFromUpstream(response);
            return response;
        });
    }

    @Nonnull
    @Override
    public Request.Builder newRequest(@Nonnull String relativePath) {
        return newAuthenticatedRequest(mirroringNavBuilder.upstream(upstream).relativeToBase(relativePath));
    }

    protected Promise<ExternalRepository> getRepositoryAs(@Nonnull String repoId, Class<? extends ExternalRepository> repositoryRestType) {
        MirroringUrl repoUrl = mirroringNavBuilder.upstream(upstream).repositoryById(repoId);

        log.debug("Retrieving upstream repository with id {}", repoId);
        Promise<ExternalRepository> promise = newAuthenticatedJsonRequest(repoUrl)
                .get()
                .transform(this.<ExternalRepository>repositoryEndpointResponseBuilder(repoUrl)
                        .notFound(ensureErrorResponseIsFromUpstreamAndThen(returnNull()))
                        .ok(response -> parseEntity(response, repositoryRestType).orElseThrow(requestFailedSupplier(response)))
                        .build());
        promise.done(repository -> log.debug("Finished retrieving upstream repository with id {}", repoId));
        promise.fail(error -> log.debug("Failed retrieving upstream repository with id {}", repoId));
        return promise;
    }

    protected boolean isContentType(Response response, MediaType mediaType) {
        return !StringUtils.isEmpty(response.getContentType())
                && MediaType.parse(response.getContentType()).is(mediaType);
    }

    protected Request.Builder newAuthenticatedJsonRequest(MirroringUrl url) {
        return sendingAndAcceptingJson(newAuthenticatedRequest(url));
    }

    protected Request.Builder newAuthenticatedRequest(MirroringUrl url) {
        if (log.isDebugEnabled()) {
            log.debug("{}: making JWT-authenticated request to {}", describe(upstream), url.getAbsolute());
        }
        return jwtRequestFactory.newRequest(upstream, url);
    }

    protected Request.Builder newUnauthenticatedJsonRequest(String url) {
        return sendingAndAcceptingJson(httpClient.newRequest(url));
    }

    protected Request.Builder newUnauthenticatedRequest(MirroringUrl url) {
        if (log.isDebugEnabled()) {
            log.debug("{}: making unauthenticated request to {}", describe(upstream), url.getAbsolute());
        }
        return httpClient.newRequest(url.getAbsolute());
    }

    protected <T> Optional<T> parseEntity(Response response, Class<T> type) {
        String entity = response.getEntity();

        if (StringUtils.isBlank(entity)) {
            log.debug("The upstream server responded with code {} but the response entity was empty", response.getStatusCode());
            return empty();
        }

        log.trace("The upstream server responded with code {} and entity {}", response.getStatusCode(), entity);

        try {
            try (JsonParser jsonParser = jsonFactory.createJsonParser(entity)) {
                return of(ValidationUtils.validate(validator, jsonParser.readValueAs(type)));
            }
        } catch (IOException e) {
            log.warn("Failed to deserialize an instance of {} from the response", type.getName(), e);
        }

        return empty();
    }

    protected <T> Function<Response, T> returnNull() {
        return notFoundResponse -> null;
    }

    protected Request.Builder sendingAndAcceptingJson(Request.Builder builder) {
        return builder.setContentType(JSON_UTF_8.toString()).setAccept("application/json");
    }

    protected <T> ResponseTransformation.Builder<T> repositoryEndpointResponseBuilder(MirroringUrl url) {
        return DefaultResponseTransformation.<T>builder()
                .unauthorized(this::throwUnauthorized)
                .on(TOO_MANY_REQUESTS, response -> throwRepositoryRequestRateExceeded(url))
                .others(this::throwRequestFailed)
                .otherwise(this::throwOnError);
    }

    protected <T> ResponseTransformation.Builder<T> standardResponseBuilder(MirroringUrl url) {
        return DefaultResponseTransformation.<T>builder()
                .unauthorized(this::throwUnauthorized)
                .on(TOO_MANY_REQUESTS, response -> throwRequestRateExceeded(url))
                .others(this::throwRequestFailed)
                .otherwise(this::throwOnError);
    }

    protected Supplier<UpstreamServerRequestFailedException> requestFailedSupplier(Response response) {
        return requestFailedSupplier(response, null);
    }

    protected Supplier<UpstreamServerRequestFailedException> requestFailedSupplier(Response response, Throwable cause) {
        return () -> new UpstreamServerRequestFailedException(
                i18nService.createKeyedMessage("bitbucket.mirroring.upstream.request.failed",
                        response.getStatusCode(), response.getStatusText()), cause);
    }

    protected <T> T throwOnError(Throwable cause) {
        if (cause instanceof UpstreamServerRequestFailedException) {
            throw (UpstreamServerRequestFailedException) cause;
        } else {
            String message = null;
            if (cause instanceof UnexpectedResponseException) {
                // UnexpectedResponseException unhelpfully doesn't set its message to its more useful toString() output
                message = cause.toString();
            }

            if (message == null) {
                message = cause.getMessage();
            }

            throw new UpstreamServerRequestFailedException(
                    i18nService.createKeyedMessage("bitbucket.mirroring.upstream.request.error", message), cause);
        }
    }

    protected <T> T throwRepositoryRequestRateExceeded(MirroringUrl url) {
        throw new RepositoryRequestRateExceededException(
                i18nService.createKeyedMessage("bitbucket.mirroring.upstream.repo.request.rate.exceeded",
                        describe(upstream), URI.create(url.getAbsolute()).getPath()));
    }

    protected <T> T throwRequestRateExceeded(MirroringUrl url) {
        throw new UpstreamServerRequestRateExceededException(
                i18nService.createKeyedMessage("bitbucket.mirroring.upstream.request.rate.exceeded",
                        describe(upstream), URI.create(url.getAbsolute()).getPath()));
    }

    protected <T> T throwRequestFailed(Response response) {
        throw requestFailedSupplier(response).get();
    }

    protected <T> T throwUnauthorized(Response response) {
        throw new UnauthorizedException(
                i18nService.createKeyedMessage("bitbucket.mirroring.upstream.request.unauthorized",
                        response.getStatusCode(), response.getStatusText()));
    }

    protected UpstreamServerRequestFailedException throwUnexpectedNotFoundResponse(Response response) {
        throw new UpstreamServerRequestFailedException(
                i18nService.createKeyedMessage("bitbucket.mirroring.upstream.request.unexpected.notfound.response",
                        response.getStatusCode(), response.getStatusText(), response.getContentType(), response.getHeaders()));
    }
}
