package com.atlassian.bitbucket.internal.importer.github;


import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.importer.*;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
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.httpclient.api.Response;
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang.StringUtils;
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.beans.factory.annotation.Qualifier;

import javax.annotation.Nonnull;
import javax.validation.Validator;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.atlassian.bitbucket.internal.importer.ImporterConstants.PROPERTY_GITHUB_API_URL;
import static com.atlassian.bitbucket.internal.importer.ImporterConstants.PROPERTY_GITHUB_API_URL_DEFAULT;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.empty;
import static org.apache.commons.collections.CollectionUtils.subtract;

public class GitHubRepositoryService implements ExternalRepositoryService {

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

    private final GitHubUrlBuilder gitHubUrlBuilder;
    private final I18nService i18nService;
    private final ExternalRequestExecutor requestExecutor;
    private final Validator validator;

    @Autowired
    public GitHubRepositoryService(@Qualifier("i18nService") I18nService i18nService,
                                   @Qualifier("propertiesService") ApplicationPropertiesService propertiesService,
                                   @Qualifier("requestExecutor") ExternalRequestExecutor requestExecutor,
                                   @Qualifier("validator") Validator validator) {
        this.requestExecutor = requestExecutor;
        this.i18nService = i18nService;
        this.validator = validator;

        gitHubUrlBuilder = new GitHubUrlBuilder(propertiesService.getPluginProperty(PROPERTY_GITHUB_API_URL, PROPERTY_GITHUB_API_URL_DEFAULT));
    }

    @Nonnull
    @Override
    public Iterable<OAuthScope> getMissingOAuthScopes(@Nonnull ExternalRequest request) {
        UserUrlBuilder userUrlBuilder = gitHubUrlBuilder.user(request.getCredential().getUsername());
        request.getSource().getUrl().ifPresent(userUrlBuilder::url);
        List<OAuthScope> oAuthScopes = requestExecutor.executeHeadRequest(request.getCredential(), userUrlBuilder.build(),
                responseParser(response -> {
                    String scopes = response.getHeader("X-OAuth-Scopes");
                    if (StringUtils.isBlank(scopes)) {
                        return Optional.of(emptyList());
                    }

                    return Optional.of(stream(scopes.split(","))
                            .map(String::trim)
                            .map(GitHubOAuthScope::fromString)
                            .filter(Optional::isPresent)
                            .map(Optional::get)
                            .map(GitHubOAuthScope::getScope)
                            .collect(Collectors.toList()));
                }));
        return subtract(asList(OAuthScope.values()), oAuthScopes);
    }

    @Nonnull
    @Override
    public ExternalUser getUser(ExternalRequest request) {
        UserUrlBuilder userUrlBuilder = gitHubUrlBuilder.user(request.getCredential().getUsername());
        request.getSource().getUrl().ifPresent(userUrlBuilder::url);
        return requestExecutor.executeGetRequest(request.getCredential(), userUrlBuilder.build(),
                responseParser(response -> parseEntity(GitHubUser.class, response)));
    }

    @Nonnull
    @Override
    public Page<ExternalOwner> searchOwners(@Nonnull ExternalOwnerSearchRequest request,
                                            @Nonnull PageRequest pageRequest) {
        OrganizationsUrlBuilder urlBuilder = gitHubUrlBuilder.userOrganizations(pageRequest);
        requireNonNull(request, "request").getSource().getUrl().ifPresent(urlBuilder::url);

        return requestExecutor.executeGetRequest(request.getCredential(),
                urlBuilder.build(),
                responseParser(response -> parsePage(GitHubOrganization.class, pageRequest, Function.identity(), response)));
    }

    @Nonnull
    @Override
    public Page<ExternalRepository> searchRepositories(@Nonnull ExternalRepositorySearchRequest request,
                                                       @Nonnull PageRequest pageRequest) {
        RepositorySearchUrlBuilder urlBuilder = gitHubUrlBuilder.repositorySearch(pageRequest);
        requireNonNull(request, "request").getSource().getUrl().ifPresent(urlBuilder::url);

        Credential credential = request.getCredential();
        request.getFilter().ifPresent(filter -> urlBuilder.filter(filter + "+in", "name,description"));
        urlBuilder.filter("user", request.getOwnerName().orElseGet(credential::getUsername));
        urlBuilder.filter("fork", Boolean.toString(true));

        return requestExecutor.executeGetRequest(credential, urlBuilder.build(),
                responseParser(response -> parsePage(GitHubRepository.class, pageRequest, node -> node.get("items"), response)));
    }

    private <T, R extends T> Optional<Page<T>> parsePage(Class<R> type, PageRequest pageRequest,
                                                         Function<JsonNode, JsonNode> function,
                                                         Response response) {
        String entity = response.getEntity();
        if (StringUtils.isBlank(entity)) {
            log.warn("The external repository source responded with status code {} but the response entity was empty",
                    response.getStatusCode());
            return empty();
        }

        String link = response.getHeader("Link");
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode page = mapper.readTree(entity);
            if (page.isNull()) {
                log.warn("The response from the external repository source is not a JSON object");
                return empty();
            }

            JsonNode valuesNode = function.apply(page);
            if (valuesNode == null || valuesNode.isNull()) {
                log.warn("The response from the external repository source is not in a proper JSON format");
                return empty();
            }

            ImmutableList.Builder<T> values = ImmutableList.builder();
            for (JsonNode node : valuesNode) {
                values.add(ValidationUtils.validate(validator, mapper.readValue(node, type)));
            }
            return Optional.of(PageUtils.createPage(values.build(), !StringUtils.contains(link, "rel=\"next\""),
                    pageRequest));
        } catch (IOException e) {
            log.warn("Failed to deserialize response from the external repository source", e);
        }
        return empty();
    }

    private <T, R extends T> Optional<T> parseEntity(Class<R> type,
                                                     Response response) {
        String entity = response.getEntity();
        if (StringUtils.isBlank(entity)) {
            log.warn("The external repository source responded with status code {} but the response entity was empty",
                    response.getStatusCode());
            return empty();
        }

        try {
            ObjectMapper mapper = new ObjectMapper();
            return Optional.of(ValidationUtils.validate(validator, mapper.readValue(entity, type)));
        } catch (IOException e) {
            log.warn("Failed to deserialize response from the external repository source", e);
        }
        return empty();
    }

    private <T> Function<Response, T> responseParser(Function<Response, Optional<T>> parser) {
        return response ->
                parser.apply(response)
                        .orElseThrow(() ->
                                new ExternalRequestFailedException(
                                        i18nService.createKeyedMessage("bitbucket.importer.external.request.failed.errorcode",
                                                response.getStatusCode(), response.getStatusText()))
                        );
    }
}
