package ru.yandex.webmaster3.storage.originaltext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
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.config.ConnectionConfig;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
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.util.joda.jackson.WebmasterDurationModule;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author avhaliullin
 */
public class ProxyOriginalTextService implements OriginalTextService {
    private static final Logger log = LoggerFactory.getLogger(ProxyOriginalTextService.class);

    private static final ObjectMapper OM = new ObjectMapper()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .registerModule(new JodaModule())
            .registerModule(new WebmasterJsonModule(true))
            .registerModule(new Jdk8Module())
            .registerModule(new WebmasterDurationModule(false))
            .registerModule(new ParameterNamesModule());

    private static final String ACTION_PREFIX = "/tmp/originalsproxy/";

    private int connectionTTLSeconds = 10;
    private int socketTimeoutMillis = 5_000;
    private int connectionTimeoutMillis = HttpConstants.DEFAULT_CONNECT_TIMEOUT;

    private String proxyHostList;

    private List<String> proxyHosts;
    private CloseableHttpClient httpClient;

    public void init() {
        proxyHosts = Arrays.asList(proxyHostList.split(","));

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectionTimeoutMillis)
                .setSocketTimeout(socketTimeoutMillis)
                .setRedirectsEnabled(false)
                .build();

        ConnectionConfig connectionConfig = ConnectionConfig.custom()
                .setCharset(StandardCharsets.UTF_8)
                .build();

        httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionTimeToLive(connectionTTLSeconds, TimeUnit.SECONDS)
                .setDefaultConnectionConfig(connectionConfig)
                .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
                .setMaxConnTotal(16)
                .setMaxConnPerRoute(16)
                .build();
    }

    @Override
    public OriginalsResponse listTexts(WebmasterHostId hostId, int offset, int limit) {
        ObjectNode req = OM.createObjectNode();
        req.set("hostId", new TextNode(hostId.toString()));
        req.set("offset", new IntNode(offset));
        req.set("limit", new IntNode(limit));
        return parseResponse(doRequest("list", req), OriginalsResponse.class);
    }

    @Override
    public OriginalsResponse addText(WebmasterHostId hostId, String text) {
        ObjectNode req = OM.createObjectNode();
        req.set("hostId", new TextNode(hostId.toString()));
        req.set("text", new TextNode(text));
        return parseResponse(doRequest("add", req), OriginalsResponse.class);
    }

    @Override
    public OriginalTextLimits getLimits(WebmasterHostId hostId) {
        ObjectNode req = OM.createObjectNode();
        req.set("hostId", new TextNode(hostId.toString()));
        return parseResponse(doRequest("limits", req), OriginalTextLimits.class);
    }

    @Override
    public void deleteText(WebmasterHostId hostId, String textId) {
        ObjectNode req = OM.createObjectNode();
        req.set("hostId", new TextNode(hostId.toString()));
        req.set("textId", new TextNode(textId));
        parseResponse(doRequest("delete", req), Void.class);
    }

    private static <T> T parseResponse(JsonNode response, Class<T> responseClass) {
        try {
            if (response.has("errors") && response.get("errors").size() > 0) {
                throw new WebmasterException("Proxy originals request failed with response " + OM.writeValueAsString(response), new WebmasterErrorResponse.InternalUnknownErrorResponse(ProxyOriginalTextService.class, null), null);
            }
            if (responseClass == Void.class) {
                return null;
            }
            return OM.readValue(new TreeTraversingParser(response.get("data").get("data")), responseClass);
        } catch (IOException e) {
            throw new RuntimeException("Should never happen", e);
        }
    }

    private JsonNode doRequest(String action, ObjectNode request) {
        String reqId = MDC.get("actionRouter_requestId");
        String path = ACTION_PREFIX + action + ".json?";
        if (reqId != null) {
            path += "balancerRequestId=" + "prx-" + reqId;
        }
        String requestJsonString;
        try {
            requestJsonString = OM.writeValueAsString(request);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Should never happen", e);
        }
        List<String> candidates = new ArrayList<>(proxyHosts);
        Collections.shuffle(candidates);
        IOException exception = null;
        for (String host : candidates) {
            HttpPost postReq = new HttpPost(host + path);
            postReq.setEntity(new StringEntity(requestJsonString, ContentType.APPLICATION_JSON));
            try (CloseableHttpResponse res = httpClient.execute(postReq)) {
                return OM.readTree(res.getEntity().getContent());
            } catch (IOException e) {
                log.warn("Originals request failed at host " + host, e);
                exception = e;
            }
        }
        throw new WebmasterException("All requested proxying candidates failed to respond", new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null), exception);

    }

    @Required
    public void setProxyHostList(String proxyHostList) {
        this.proxyHostList = proxyHostList;
    }
}
