package ru.yandex.webmaster3.core.zora;

import java.io.IOException;
import java.net.IDN;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
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.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.jetbrains.annotations.NotNull;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
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.JavaMethodWitness;
import ru.yandex.webmaster3.core.util.functional.ThrowingFunction;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.core.zora.go_data.request.GoZoraRequest;
import ru.yandex.webmaster3.core.zora.go_data.response.GoZoraError;
import ru.yandex.webmaster3.core.zora.go_data.response.GoZoraResponseFetchUrl;
import ru.yandex.webmaster3.core.zora.go_data.response.SimpleGoZoraResponse;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

/**
 * @author kravhcenko99
 * Прокси хождение через go Zora
 */
@Slf4j
public class GoZoraService extends AbstractExternalAPIService {

    public static final String UNABLE_TO_DOWNLOAD_URL_WITH_ZORA = "Unable to download url with GoZora";
    private static final String HEADER_CLIENT_ID = "X-Ya-Client-Id";


    private CloseableHttpClient httpClient;

    @Setter
    private URI zoraGoUrl;
    @Setter
    private TVMTokenService tvmTokenService;


    public void init() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(1)
                .setConnectTimeout(100)
                .setSocketTimeout(60 * 1_000)
                .setRedirectsEnabled(false)
                .build();

        httpClient = HttpClients.custom()
                .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
                .setMaxConnPerRoute(150)
                .setMaxConnTotal(150)
                .setConnectionTimeToLive(10, TimeUnit.SECONDS)
                .setDefaultRequestConfig(requestConfig)
                .disableCookieManagement()
                .build();
    }

    @NotNull
    @ExternalDependencyMethod("get-entity")
    public SimpleGoZoraResponse executeRequest(GoZoraRequest request) {
        return trackQuery(new JavaMethodWitness() {
        }, ALL_ERRORS_INTERNAL, () -> {
            HttpGet httpReq = getHttpGetRequest(request);
            try (CloseableHttpResponse response = httpClient.execute(httpReq)) {
                checkGoZoraError(request.getUrl(), response);

                String responseBody = null;
                HttpEntity responseEntity = response.getEntity();
                if (responseEntity == null) {
                    log.error("Go Zora responded with no body");
                } else if (request.isNeedBody()) {
                    responseBody = IOUtils.toString(responseEntity.getContent(), StandardCharsets.UTF_8);
                }

                return new SimpleGoZoraResponse(responseBody, response.getStatusLine().getStatusCode(),
                        response.getAllHeaders());

            } catch (IOException e) {
                throw new WebmasterException("Go Zora request failed for url - " + request.getUrl(),
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(GoZoraService.class, "Zora udf " +
                                "request " +
                                "failed"), e);
            }
        });
    }

    @NotNull
    @ExternalDependencyMethod("get-entity-fetch-response")
    public GoZoraResponseFetchUrl executeRequestFetchResponse(GoZoraRequest request) {
        return trackQuery(new JavaMethodWitness() {
        }, ALL_ERRORS_INTERNAL, () -> {
            HttpGet httpReq = getHttpGetRequest(request);

            long startTime = System.currentTimeMillis();
            try (CloseableHttpResponse response = httpClient.execute(httpReq)) {
                checkGoZoraError(request.getUrl(), response);

                long responseTime = System.currentTimeMillis() - startTime;
                return new GoZoraResponseFetchUrl(response, responseTime, request.isNeedBody());
            } catch (SocketTimeoutException e) {
                throw new WebmasterException("GoZora request timed out for url - " + request.getUrl(),
                        new WebmasterErrorResponse.GoZoraTimedOutResponse(GoZoraService.class)
                );
            } catch (IOException e) {
                throw new WebmasterException("GoZora request failed for url - " + request.getUrl(),
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(GoZoraService.class, "Zora udf " +
                                "request " +
                                "failed"), e);
            }
        });
    }

    private void checkGoZoraError(String url, CloseableHttpResponse response) {
        GoZoraError goZoraError = GoZoraError.getGoZoraErrorCode(response);
        if (goZoraError != null) {
            log.error("Go Zora return error - {}, code - {} for url - {}", goZoraError, goZoraError.value(), url);
            if (GoZoraError.INTERNAL_ERRORS.contains(goZoraError)) {
                throw new IllegalStateException("Go Zora return error - " + goZoraError + ", code - " + goZoraError.value() + " for url - " + url);
            }
        }
    }

    @NotNull
    public HttpGet getHttpGetRequest(GoZoraRequest request) {
        HttpGet httpReq = new HttpGet(zoraGoUrl);

        URL url;
        String auth;
        try {
            url = SupportedProtocols.getURL(request.getUrl());
            auth = url.getUserInfo() != null ? url.getUserInfo() : request.getUserInfo();
            url = encodeUrl(url);
        } catch (Exception e) {
            log.error("while convert url[{}] to ascii error was occurred - {}", request.getUrl(), e.getMessage(), e);
            throw new RuntimeException(e);
        }

        if (auth != null) {
            String encoding = Base64.encodeBase64String(auth.getBytes());
            httpReq.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoding);
        }

        httpReq.addHeader("x-ya-dest-url", url.toString());
        if (request.isIgnoreCerts()) {
            httpReq.addHeader("X-Ya-Ignore-Certs", Boolean.TRUE.toString());
        }
        if (request.isFollowRedirects()) {
            httpReq.setHeader("X-Ya-Follow-Redirects", Boolean.TRUE.toString());
        }
        log.info("Zora request - headers: {}, url: {}", JsonMapping.writeValueAsString(httpReq.getAllHeaders()),
                zoraGoUrl); // without tvm
        httpReq.addHeader(TVMTokenService.TVM2_TICKET_HEADER, tvmTokenService.getToken());
        httpReq.addHeader(HEADER_CLIENT_ID, "webmaster_robot");
        return httpReq;
    }

    @NotNull
    private static URL encodeUrl(URL url) throws MalformedURLException {
        String asciiHost = IDN.toASCII(url.getHost());
        String path = encodePath(url);
        Optional<String> query = encodeQuery(url).map(q -> "?" + q);

        return new URL(url.getProtocol(), asciiHost, url.getPort(), path + query.orElse(""));
    }

    private static Optional<String> encodeQuery(URL url) {
        if (url.getQuery() == null) {
            return Optional.empty();
        }
        String encodedQuery = Arrays.stream(url.getQuery().split("&"))
                .map(x -> Arrays.stream(x.split("=", 2)))
                .map(x -> x.map(y -> URLEncoder.encode(y, StandardCharsets.UTF_8)))
                .map(x -> x.collect(Collectors.joining("=")))
                .collect(Collectors.joining("&"));
        return Optional.of(encodedQuery);
    }

    @NotNull
    private static String encodePath(URL url) {
        String path = url.getPath();
        return Arrays.stream(path.split("/")).map(x -> URLEncoder.encode(x, StandardCharsets.UTF_8))
                .collect(Collectors.joining("/", "", path.endsWith("/") ? "/" : ""));
    }

    /**
     * Поточно обрабатывает некий документ через прокси Go Zora. Полезно для случая прокачки больших документов, которые
     * целиком нам не нужны, флаг request.needBody здесь бесполезен.
     *
     * @param request         -
     * @param entityProcessor - обработчик HttpResponse
     * @param <T>
     * @return
     */
    @ExternalDependencyMethod("process-entity")
    public <T> T processResponse(GoZoraRequest request,
                                 ThrowingFunction<HttpResponse, T, IOException> entityProcessor) {
        return trackQuery(new JavaMethodWitness() {
        }, ALL_ERRORS_INTERNAL, () -> {
            HttpGet httpRequest = getHttpGetRequest(request);
            try (CloseableHttpResponse httpResponse = httpClient.execute(httpRequest)) {
                checkGoZoraError(request.getUrl(), httpResponse);
                int code = httpResponse.getStatusLine().getStatusCode();
                if (code != HttpStatus.SC_OK) {
                    log.error("Zora error for {}. Code = {}, Status line = {}",
                            request.getUrl(), code, httpResponse.getStatusLine());
                    throw new WebmasterException("GoZora error: ",
                            new WebmasterErrorResponse.SitaErrorResponse(getClass(), "GoZora return error code " + code
                                    , code, null));
                }
                return entityProcessor.apply(httpResponse);
            } catch (IOException e) {
                throw new WebmasterException(UNABLE_TO_DOWNLOAD_URL_WITH_ZORA,
                        new WebmasterErrorResponse.SitaErrorResponse(getClass(), UNABLE_TO_DOWNLOAD_URL_WITH_ZORA, e)
                        , e);
            }
        });
    }
}
