package ru.yandex.wmtools.common.service;

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.wmtools.common.data.yt.YTAccept;
import ru.yandex.wmtools.common.data.yt.YTAcceptEnum;
import ru.yandex.wmtools.common.data.yt.YTCommand;
import ru.yandex.wmtools.common.data.yt.YTCommandEnum;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;

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

    private static final int SOCKET_TIMEOUT = 60000;
    private static final int CONNECTION_TIMEOUT = 60000;
    private static final Charset UTF8 = Charset.forName("UTF-8");

    protected String ytApiVersion = "v2";
    private String oAuthToken;
    private String externalServantUrl;
    private volatile String cachedHeavyProxy = null;

    protected String getHeavyProxy() throws InternalException {
        HttpGet hostsRequest = new HttpGet(externalServantUrl + "/hosts");
        hostsRequest.setHeader("Accept", "text/plain");
        try (CloseableHttpResponse response = HttpClients.createDefault().execute(hostsRequest)) {
            if (response.getStatusLine().getStatusCode() != 200) {
                String message = "Failed to get yt heavy proxies: status code was " + response.getStatusLine().getStatusCode();
                if (cachedHeavyProxy != null) {
                    log.error(message + ". Returning cached value");
                    return cachedHeavyProxy;
                } else {
                    throw new InternalException(InternalProblem.INTERNAL_PROBLEM, message);
                }
            }
            String result = new BufferedReader(new InputStreamReader(response.getEntity().getContent())).readLine();
            log.info("Picked heavy proxy " + result);
            return result;
        } catch (IOException e) {
            if (cachedHeavyProxy != null) {
                log.error("Failed to get yt heavy proxies. Returning cached value", e);
                return cachedHeavyProxy;
            } else {
                throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Failed to get yt heavy proxies", e);
            }
        }
    }

    protected CloseableHttpResponse request(YTCommand ytCommand, Map<String, Object> params, YTAccept accept) throws InternalException {
        return request(ytCommand, params, accept, null);
    }

    protected CloseableHttpResponse request(YTCommand ytCommand, Map<String, Object> params, YTAccept accept, HttpEntity entity) throws InternalException {
        String host = ytCommand.isHeavy() ? "http://" + getHeavyProxy() : externalServantUrl;
        StringBuilder sb = new StringBuilder(host)
                .append("/api/")
                .append(ytApiVersion)
                .append("/")
                .append(ytCommand.getCode());
        boolean first = true;
        if (params != null) {
            sb.append("?");
            for (Map.Entry<String, Object> param : params.entrySet()) {
                try {
                    sb.append(first ? "" : "&")
                            .append(URLEncoder.encode(param.getKey(), "UTF-8"))
                            .append("=")
                            .append(URLEncoder.encode(param.getValue().toString(), "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Unsupported encoding");
                }
                first = false;
            }
        }

        try {
            log.info(String.format("YT call: { 'operation' : '%1$s', 'params' : [%2$s] dd }",
                    ytCommand.getCode(), String.valueOf(params)));

            ConnectionConfig connectionConfig = ConnectionConfig.custom().setCharset(UTF8).build();
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectionRequestTimeout(CONNECTION_TIMEOUT)
                    .setSocketTimeout(SOCKET_TIMEOUT)
                    .build();

            CloseableHttpClient client = HttpClients.custom()
                    .setConnectionTimeToLive(600, TimeUnit.SECONDS)
                    .setDefaultConnectionConfig(connectionConfig)
                    .setDefaultRequestConfig(requestConfig)
                    .setMaxConnPerRoute(10)
                    .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
                    .build();

            HttpUriRequest httpRequest;
            try {
                switch (ytCommand.getHttpMethod()) {
                    case GET:
                        httpRequest = new HttpGet(new URI(sb.toString()));
                        break;
                    case POST:
                        HttpPost httpPost = new HttpPost(new URI(sb.toString()));
                        if (entity != null) {
                            httpPost.setEntity(entity);
                        }
                        httpRequest = httpPost;
                        break;
                    case PUT:
                        HttpPut httpPut = new HttpPut(new URI(sb.toString()));
                        if (entity != null) {
                            httpPut.setEntity(entity);
                        }
                        httpRequest = httpPut;
                        break;
                    default:
                        throw new UnsupportedOperationException("http method " + ytCommand.getHttpMethod() + " is not supported");
                }
            } catch (URISyntaxException e) {
                throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "URI Syntax error");
            }

            httpRequest.setHeader("Accept", accept.getAcceptHeader());
            httpRequest.setHeader("Authorization", "OAuth " + oAuthToken);

            return client.execute(httpRequest);
        } catch (MalformedURLException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Malformed URL", e);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.SERVANT_FAILED, "YT method \"" + ytCommand.getCode() + "\" failed", e);
        }
    }

    protected CloseableHttpResponse request(YTCommand ytCommand, String absoluteUrl, YTAccept accept, HttpEntity entity) throws InternalException {
        log.info("YT call: 'operation' : '{}', 'url': {}", ytCommand.getCode(), absoluteUrl);
        ConnectionConfig connectionConfig = ConnectionConfig.custom().setCharset(UTF8).build();
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(CONNECTION_TIMEOUT)
                .setSocketTimeout(SOCKET_TIMEOUT)
                .build();

        CloseableHttpClient client = HttpClients.custom()
                .setConnectionTimeToLive(10, TimeUnit.SECONDS)
                .setDefaultConnectionConfig(connectionConfig)
                .setDefaultRequestConfig(requestConfig)
                .setMaxConnPerRoute(10)
                .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
                .build();

        HttpUriRequest httpRequest;
        try {
            switch (ytCommand.getHttpMethod()) {
                case GET:
                    httpRequest = new HttpGet(new URI(absoluteUrl));
                    break;
                case POST:
                    HttpPost httpPost = new HttpPost(new URI(absoluteUrl));
                    if (entity != null) {
                        httpPost.setEntity(entity);
                    }
                    httpRequest = httpPost;
                    break;
                case PUT:
                    HttpPut httpPut = new HttpPut(new URI(absoluteUrl));
                    if (entity != null) {
                        httpPut.setEntity(entity);
                    }
                    httpRequest = httpPut;
                    break;
                default:
                    throw new UnsupportedOperationException("http method " + ytCommand.getHttpMethod() + " is not supported");
            }
        } catch (URISyntaxException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "URI Syntax error");
        }

        httpRequest.setHeader("Accept", accept.getAcceptHeader());
        httpRequest.setHeader("Authorization", "OAuth " + oAuthToken);

        try {
            return client.execute(httpRequest);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "IOException");
        }
    }

    public void read(String path, YTRecordWithSubkeyHandler handler) throws InternalException {
        try (CloseableHttpResponse response = request(
                YTCommandEnum.READ,
                Collections.<String, Object>singletonMap("path", path),
                YTAcceptEnum.TSV_WITH_SUBKEY
        )) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), UTF8));
            String line;
            while ((line = reader.readLine()) != null) {
                String[] lineArr = line.split("\t", 3);
                handler.handle(lineArr[0], lineArr[1], lineArr[2]);
            }
        } catch (IOException e) {
            throw new InternalException(InternalProblem.SERVANT_FAILED, "YT error", e);
        }
    }

    @Required
    public void setOAuthToken(String oAuthToken) {
        this.oAuthToken = oAuthToken;
    }

    @Required
    public void setExternalServantUrl(String externalServantUrl) {
        this.externalServantUrl = externalServantUrl;
    }

    public interface YTRecordWithSubkeyHandler {
        public void handle(String key, String subkey, String value);
    }
}
