package ru.yandex.webmaster3.core.metrika.counters;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Service;
import ru.yandex.webmaster3.core.util.enums.EnumResolver;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.HttpConstants;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.http.WebmasterJsonModule;
import ru.yandex.webmaster3.core.metrics.externals.AbstractExternalAPIService;
import ru.yandex.webmaster3.core.metrics.externals.ExternalDependencyMethod;
import ru.yandex.webmaster3.core.security.tvm.TVMTokenService;
import ru.yandex.webmaster3.core.util.Either;
import ru.yandex.webmaster3.core.util.JavaMethodWitness;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * Created by ifilippov5 on 24.10.17.
 */
public class MetrikaCountersInternalService extends AbstractExternalAPIService {
    private static final Logger log = LoggerFactory.getLogger(MetrikaCountersInternalService.class);

    private static final int SOCKET_TIMEOUT = 10_000;
    private static final ObjectMapper OM = new ObjectMapper()
            .registerModule(new WebmasterJsonModule(false))
            .registerModule(new JodaModule())
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    private CloseableHttpClient httpClient;

    private String internalMetrikaCountersUrl;
    private TVMTokenService tvmTokenService;

    public enum ErrorCodeEnum {
        WRONG_DOMAIN,
        ALREADY_DELETED,
        ALREADY_EXISTS,
        UNSUPPORTED_ERROR
    }

    public enum ResponseStatusEnum {
        DELETED,
        OK, // Метрика автоматически подтвердила счетчик, https://st.yandex-team.ru/METR-30730
        NEED_METRIKA_CONFIRM,
        NEED_WEBMASTER_CONFIRM,
        UNKNOWN
        ;

        public static EnumResolver<ResponseStatusEnum> R = EnumResolver.er(ResponseStatusEnum.class);
    }

    public void init() {

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(HttpConstants.DEFAULT_CONNECTION_REQUEST_TIMEOUT)
                .setConnectTimeout(HttpConstants.DEFAULT_CONNECT_TIMEOUT)
                .setSocketTimeout(SOCKET_TIMEOUT)
                .build();

        httpClient = HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionTimeToLive(10, TimeUnit.MINUTES)
                .build();
    }

    @ExternalDependencyMethod("request")
    public Either<ResponseStatusEnum, ErrorCodeEnum> sendRequest(String domain, long userId, long counterId, CounterRequestTypeEnum requestType) {
        return trackQuery(new JavaMethodWitness() {}, ALL_ERRORS_INTERNAL, () -> {
            HttpPost post = new HttpPost(internalMetrikaCountersUrl + requestType.getPath()
                    + "?counter_id=" + counterId
                    + "&domain=" + domain
                    + "&uid=" + userId);
            post.addHeader(TVMTokenService.TVM2_TICKET_HEADER, tvmTokenService.getToken());
            log.info("Requesting metrika {}", post);
            try (CloseableHttpResponse response = httpClient.execute(post)) {
                String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8.name());
                log.info("Metrika response {}", content);

                JsonNode root = OM.readTree(content);
                if (response.getStatusLine().getStatusCode() / 100 != 2) {
                    ErrorCodeEnum code = extractErrorCode(root);
                    if (code != null) {
                        log.info("Metrika user error: {}", code);
                        return Either.right(code);
                    } else {
                        String message = root.get("message").asText();
                        log.error("Unknown error message from Metrika: {}", message);
                        throw new WebmasterException("Synchronization failed: " + root,
                                new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), message));
                    }
                } else {
                    ResponseStatusEnum status = parseMetrikaResponse(root);
                    if (status == null) {
                        log.error("Unable to parse status from Metrika response: {}", root);
                        throw new WebmasterException("Unable to parse status from Metrika response: " + root,
                                new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null));
                    }

                    return Either.left(status);
                }

            } catch (IOException e) {
                throw new WebmasterException("Unable to query " + post,
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null), e);
            }
        });
    }

    @Nullable
    private ResponseStatusEnum parseMetrikaResponse(JsonNode root) {
        JsonNode linkNode = root.get("webmaster_link");
        if (linkNode == null) {
            log.error("Missing required 'webmaster_link' field in Metrika response");
            return null;
        }

        String statusString = Optional.ofNullable(linkNode.get("status")).map(JsonNode::asText).orElse(null);
        if (statusString == null) {
            log.error("Missing required 'status' field in Metrika response");
            return null;
        }

        ResponseStatusEnum status = ResponseStatusEnum.R.valueOf(statusString);
        if (status == ResponseStatusEnum.UNKNOWN) {
            // Нам не нужно знать про все статусы, но для истории запишем в лог
            log.error("Unknown status: {}", statusString);
        }

        return ResponseStatusEnum.R.valueOf(statusString);
    }

    private static ErrorCodeEnum extractErrorCode(JsonNode response) {
        String message = Optional.ofNullable(response.get("message")).map(JsonNode::asText).orElse(null);
        if (message == null) {
            return null;
        }

        if (message.contains("doesn't belong to counter")) {
            return ErrorCodeEnum.WRONG_DOMAIN;
        } else if (message.contains("delete nonexistent")) {
            return ErrorCodeEnum.ALREADY_DELETED;
        } else if (message.contains("webmaster link already exists")) {
            return ErrorCodeEnum.ALREADY_EXISTS;
        } else {
            return ErrorCodeEnum.UNSUPPORTED_ERROR;
        }
    }

    @Required
    public void setInternalMetrikaCountersUrl(String internalMetrikaCountersUrl) {
        this.internalMetrikaCountersUrl = internalMetrikaCountersUrl;
    }

    @Required
    public void setTvmTokenService(TVMTokenService tvmTokenService) {
        this.tvmTokenService = tvmTokenService;
    }
}
