package com.atlassian.bitbucket.internal.mirroring.upstream;

import com.atlassian.bitbucket.internal.mirroring.jwt.HttpUriRequestHelper;
import com.atlassian.bitbucket.internal.mirroring.jwt.JwtTokenGenerationException;
import com.atlassian.bitbucket.internal.mirroring.jwt.JwtTokenHelper;
import com.atlassian.httpclient.api.Request;
import com.atlassian.jwt.httpclient.CanonicalHttpUriRequest;
import com.atlassian.oauth.consumer.ConsumerService;
import com.atlassian.plugin.connect.bitbucket.BitbucketAddonAccessor;
import com.atlassian.uri.Uri;
import com.atlassian.uri.UriBuilder;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

@Component
public class DefaultMirrorLinkService implements MirrorLinkService {

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

    private final BitbucketAddonAccessor connectAddonAccessor;
    private final ConsumerService consumerService;
    private final JwtTokenHelper jwtTokenHelper;

    @Autowired
    DefaultMirrorLinkService(BitbucketAddonAccessor connectAddonAccessor, ConsumerService consumerService,
                             JwtTokenHelper jwtTokenHelper) {
        this.connectAddonAccessor = connectAddonAccessor;
        this.consumerService = consumerService;
        this.jwtTokenHelper = jwtTokenHelper;
    }

    @Nonnull
    @Override
    public Optional<String> generateAuthorizedLink(@Nonnull InternalMirrorServer mirrorServer,
                                                   @Nonnull String restResource) {
        requireNonNull(mirrorServer, "mirrorServer");
        requireNonNull(restResource, "restResource");

        String addonKey = mirrorServer.getAddonKey();
        return connectAddonAccessor.getSharedSecret(addonKey).map(sharedSecret -> {
            String connectAddonDescriptor = connectAddonAccessor.getDescriptor(addonKey);

            if (isNotBlank(sharedSecret) && isNotEmpty(sharedSecret) && isNotEmpty(connectAddonDescriptor)) {
                String restLink = requireNonNull(getPath(connectAddonDescriptor, Arrays.asList("links", "rest")), "restLink");
                String relativeUrl = restLink + restResource;

                String baseUrl = StringUtils.stripEnd(mirrorServer.getBaseUrl(), "/");
                URI uri = URI.create(baseUrl + relativeUrl);
                UriBuilder uriBuilder = new UriBuilder(Uri.fromJavaUri(uri));
                Map<String, String[]> parameters = HttpUriRequestHelper
                        .convertParameters(uriBuilder.toUri().getQueryParameters());

                try {
                    CanonicalHttpUriRequest request = new CanonicalHttpUriRequest(Request.Method.GET.name(),
                            relativeUrl, "", parameters);
                    return mirrorServer.getBaseUrl() + relativeUrl + "?jwt=" +
                            jwtTokenHelper.generateToken(request, TimeUnit.HOURS.toSeconds(24L),
                                    consumerService.getConsumer().getKey(), null, sharedSecret);
                } catch (JwtTokenGenerationException e) {
                    log.warn("Could not generate JWT token.", e);
                }
            } else {
                log.warn("Could not generate JWT token. Failed to find shared secret or descriptor for add-on '{0}",
                        addonKey);
            }

            return null;
        });
    }

    /**
     * Parses a Connect add-on descriptor and returns the path specified.
     *
     * @param descriptor the add-on descriptor to parse
     * @param path       an array of path components
     * @return the specified path or {@code null} if the path could not be found
     */
    private static String getPath(String descriptor, List<String> path) {
        ObjectMapper mapper = new ObjectMapper();
        JsonFactory factory = mapper.getJsonFactory();
        try {
            JsonNode json = mapper.readTree(factory.createJsonParser(descriptor));
            for (String s : path) {
                json = json.path(s);
            }
            return json.isMissingNode() ? null : json.asText();
        } catch (IOException e) {
            log.warn("Could not parse descriptor. Reason: {0}", e);
            return null;
        }
    }
}
