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

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.mirroring.MirroringRequest;
import com.atlassian.bitbucket.mirroring.upstream.MirrorIntegrationFailedException;
import com.atlassian.bitbucket.user.ServiceUser;
import com.atlassian.httpclient.api.HttpClient;
import com.atlassian.httpclient.api.Response;
import com.atlassian.plugin.connect.api.ConnectAddonController;
import com.atlassian.plugin.connect.api.lifecycle.ConnectAddonInstallException;
import com.google.common.base.Throwables;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonToken;
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 javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Optional.ofNullable;
import static org.apache.commons.lang3.StringUtils.prependIfMissing;
import static org.apache.commons.lang3.StringUtils.stripEnd;

@Component
public class DefaultMirrorAddonManager implements MirrorAddonManager {

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

    private final ConnectAddonController addonController;
    private final AddonUserHelper addonUserHelper;
    private final HttpClient httpClient;
    private final I18nService i18nService;
    private final JsonFactory jsonFactory;

    @Autowired
    public DefaultMirrorAddonManager(ConnectAddonController addonController, AddonUserHelper addonUserHelper,
                                     HttpClient httpClient, I18nService i18nService) {
        this.addonController = addonController;
        this.httpClient = httpClient;
        this.i18nService = i18nService;
        this.addonUserHelper = addonUserHelper;

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

    @Nonnull
    @Override
    public MirrorAddon install(@Nonnull MirroringRequest request) {
        try {
            URI addonDescriptorUri = request.getAddonDescriptorUri();
            if (!addonDescriptorUri.isAbsolute()) {
                addonDescriptorUri = concatUrl(request.getBaseUrl(), addonDescriptorUri.toASCIIString());
            }
            Response response = httpClient.newRequest(addonDescriptorUri).get().claim();
            String entity = response.getEntity();

            if (response.isOk() && isJson(getContentType(response), entity)) {
                addonController.installAddon(entity);

                String addonKey = parseAddonKey(entity);
                if (addonKey == null) {
                    // should never happen because the installAddon call above will fail
                    throw new MirrorIntegrationFailedException(
                            i18nService.createKeyedMessage("bitbucket.mirroring.install.failed",
                                    request.getMirrorName(), addonDescriptorUri, "Add-on key not found"));
                }
                ServiceUser user = addonUserHelper.getUserForAddon(addonKey);
                if (user == null) {
                    throw new MirrorIntegrationFailedException(
                            i18nService.createKeyedMessage("bitbucket.mirroring.install.failed.no.user",
                                    request.getMirrorName(),
                                    addonKey));
                }
                return new SimpleMirrorAddon(addonKey, user);
            }

            log.debug("Failed to download descriptor from {}. Response code was: {}. Mime type: {}",
                    addonDescriptorUri, response.getStatusCode(), getContentType(response).orElse("<none>"));
            if (log.isTraceEnabled()) {
                log.trace("Response entity: {}", entity);
            }
            throw new MirrorIntegrationFailedException(
                    i18nService.createKeyedMessage("bitbucket.mirroring.requests.descriptor.download.failed",
                            request.getMirrorName(),
                            addonDescriptorUri));
        } catch (RuntimeException | ConnectAddonInstallException e) {
            Throwables.propagateIfInstanceOf(e, MirrorIntegrationFailedException.class);
            throw new MirrorIntegrationFailedException(
                    i18nService.createKeyedMessage("bitbucket.mirroring.install.failed",
                            request.getMirrorName(),
                            request.getAddonDescriptorUri(),
                            StringUtils.defaultIfEmpty(e.getMessage(), "not specified")), e);
        }
    }

    @Override
    public boolean isInstalled(@Nonnull String addonKey) {
        return addonController.isInstalled(addonKey);
    }

    @Override
    public void uninstall(@Nonnull InternalMirrorServer mirror) {
        String addonKey = checkNotNull(mirror, "mirror").getAddonKey();
        addonController.uninstallAddon(addonKey);
    }

    private Optional<String> getContentType(Response response) {
        return ofNullable(response.getHeader(HttpHeaders.CONTENT_TYPE));
    }

    private boolean isJson(Optional<String> contentType, String entity) {
        return contentType.filter(ct -> ct.startsWith(MediaType.APPLICATION_JSON)).isPresent() && isJsonObject(entity);
    }

    private boolean isJsonObject(String entity) {
        try {
            try (JsonParser parser = jsonFactory.createJsonParser(entity)) {
                return parser.nextValue() == JsonToken.START_OBJECT;
            }
        } catch (IOException e) {
            return false;
        }
    }

    private String parseAddonKey(String descriptorJson) {
        try (JsonParser parser = jsonFactory.createJsonParser(descriptorJson)) {
            JsonNode key = parser.readValueAsTree().get("key");
            if (key != null) {
                return key.asText();
            }
        } catch (IOException e) {
            return null;
        }
        return null;
    }

    private static URI concatUrl(String baseUrl, String relativePath) {
        return URI.create(stripEnd(baseUrl, "/") + prependIfMissing(relativePath, "/"));
    }
}
