package ru.yandex.webmaster3.core.zora;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
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.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.common.util.http.charset.CharsetDetector;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.addurl.RecrawlServiceException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.data.WebmasterHostId.Schema;
import ru.yandex.webmaster3.core.http.HttpConstants;
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.semantic.semantic_document_parser.microformats.FrontEnd;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.JavaMethodWitness;
import ru.yandex.webmaster3.core.util.functional.ThrowingFunction;
import ru.yandex.webmaster3.core.zora.go_data.request.GoZoraRequest;

/**
 * Created by leonidrom on 03/07/2017.
 * Прокси хождение через offline Zora
 */

/**
 * Сбрасывает кэш robots.txt для хоста.
 * Подробнее: WMC-4053
 */
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ZoraForValidatorsService extends AbstractExternalAPIService {
    public static final String HOST_ALREADY_RENEWED_MESSAGE = "host has been renewed recently";
    // Заголовок в запросе, идентифицирующий источник запроса
    private static final String SOURCE_NAME_HEADER_NAME = "X-Yandex-Sourcename";
    private static final String SOURCE_NAME_HEADER_VALUE = "WebmasterRenewer";
    // Заголовок в ответе, поясняющий причину ошибки или информацию об успешном обновлении
    private static final String STATUS_HEADER_NAME = "X-Yandex-Status";
    // Необходимо выставлять для https хостов
    public static final String USE_HTTPS_HEADER_NAME = "X-Yandex-Use-Https";
    // Тип запроса к Зоре
    private static final String REQUEST_TYPE_HEADER_NAME = "X-Yandex-Requesttype";
    private static final String REQUEST_TYPE_ONLINE_VALUE = "online";
    public static final String UNABLE_TO_DOWNLOAD_URL_WITH_ZORA = "Unable to download url with Zora";

    private final TVMTokenService tvmTokenService;
    private final GoZoraService goZoraService;

    @Setter
    private URI zoraServiceUrl;
    private CloseableHttpClient fastHttpClient;

    private int fastSocketTimeoutMs = 1000;
    private int connectTimeoutMs = HttpConstants.DEFAULT_CONNECT_TIMEOUT;

    public void init() {
        RequestConfig fastRequestConfig = RequestConfig.custom()
                .setSocketTimeout(fastSocketTimeoutMs)
                .setConnectTimeout(connectTimeoutMs)
                .setProxy(new HttpHost(zoraServiceUrl.getHost(), zoraServiceUrl.getPort()))
                .build();

        fastHttpClient = HttpClients.custom()
                .disableCookieManagement()
                .setDefaultRequestConfig(fastRequestConfig)
                .setConnectionTimeToLive(30, TimeUnit.SECONDS)
                .build();
    }

    @ExternalDependencyMethod("renew-host")
    public void renewHost(WebmasterHostId hostId) throws RecrawlServiceException {
        boolean isHttps = hostId.getSchema() == Schema.HTTPS;
        if (isHttps) {
            hostId = IdUtils.withSchema(hostId, Schema.HTTP);
        }
        String urlStr = IdUtils.hostIdToUrl(hostId);
        trackExecution(new JavaMethodWitness() {
        }, ALL_ERRORS_INTERNAL, () -> {

            HttpGet httpRequest = new HttpGet(urlStr);

            httpRequest.setHeader(TVMTokenService.TVM2_TICKET_HEADER, tvmTokenService.getToken());
            httpRequest.setHeader(SOURCE_NAME_HEADER_NAME, SOURCE_NAME_HEADER_VALUE);
            httpRequest.setHeader(REQUEST_TYPE_HEADER_NAME, REQUEST_TYPE_ONLINE_VALUE);
            if (isHttps) {
                httpRequest.setHeader(USE_HTTPS_HEADER_NAME, "1");
            }

            try (CloseableHttpResponse httpResponse = fastHttpClient.execute(httpRequest)) {
                int code = httpResponse.getStatusLine().getStatusCode();

                String statusHeader = Arrays.stream(httpResponse.getHeaders(STATUS_HEADER_NAME))
                        .map(Header::getValue)
                        .filter(Objects::nonNull)
                        .collect(Collectors.joining());

                if (code != HttpStatus.SC_ACCEPTED && !statusHeader.contains(HOST_ALREADY_RENEWED_MESSAGE)) {
                    log.error("Zora error for {}. Code = {}, Request headers = {}, Status header = {}, Status line = {}",
                            urlStr, code, Arrays.toString(httpRequest.getAllHeaders()), statusHeader, httpResponse.getStatusLine());
                    throw new RecrawlServiceException("Unable to renew url with Zora: " + urlStr + " status=" + code);
                }
            } catch (IOException e) {
                throw new RecrawlServiceException("Unable to renew url with Zora: " + urlStr, e);
            }
        });
    }

    /**
     * Прокачивает документ через GO ZORA
     *
     * @param url
     * @return
     */
    @ExternalDependencyMethod("download-document")
    public String downloadDocumentAsStringGoZora(String url) {
        return trackQuery(new JavaMethodWitness() {
        }, ALL_ERRORS_INTERNAL, () -> processEntityGoZora(url, httpEntity -> {
            try {
                final ContentType contentType = ContentType.get(httpEntity);
                final Charset enc;
                if (contentType != null && contentType.getCharset() != null) {
                    enc = contentType.getCharset();
                } else {
                    enc = StandardCharsets.UTF_8;
                }
                log.info("Proposed charset - {}", enc.displayName());
                final CharsetDetector detector = new FrontEnd.RussianHTMLCharsetDetector();

                byte[] bytes = EntityUtils.toByteArray(httpEntity);
                final Charset trueCharset = detector.detectActualCharset(bytes, enc);
                log.info("Downloaded {} bytes", bytes.length);
                return new String(bytes, trueCharset);
            } catch (IOException e) {
                throw new WebmasterException(UNABLE_TO_DOWNLOAD_URL_WITH_ZORA,
                        new WebmasterErrorResponse.SitaErrorResponse(getClass(), UNABLE_TO_DOWNLOAD_URL_WITH_ZORA, e), e);
            }
        }));

    }

    /**
     * Прокачивает документ через GO ZORA
     *
     * @param url
     * @return
     */
    @ExternalDependencyMethod("download-document")
    public byte[] downloadDocumentGoZora(String url) {
        return trackQuery(new JavaMethodWitness() {
        }, ALL_ERRORS_INTERNAL, () -> processEntityGoZora(url, httpEntity -> {
            try {
                byte[] bytes = EntityUtils.toByteArray(httpEntity);
                log.info("Downloaded {} bytes", bytes.length);
                return bytes;
            } catch (IOException e) {
                throw new WebmasterException(UNABLE_TO_DOWNLOAD_URL_WITH_ZORA,
                        new WebmasterErrorResponse.SitaErrorResponse(getClass(), UNABLE_TO_DOWNLOAD_URL_WITH_ZORA, e), e);
            }
        }));
    }

    /**
     * Поточно обрабатывает некий документ через прокси Zora. Полезно для случая прокачки больших документов, которые
     * целиком нам не нужны
     *
     * @param url             ссылка на документ
     * @param entityProcessor - обработчик результирующего документа
     * @param <T>
     * @return
     */
    @ExternalDependencyMethod("process-entity")
    public <T> T processEntityGoZora(String url, ThrowingFunction<HttpEntity, T, IOException> entityProcessor) {
        GoZoraRequest request = new GoZoraRequest(url, true, true, true);
        return goZoraService.processResponse(request, httpResponse -> entityProcessor.apply(httpResponse.getEntity()));
    }

    public void destroy() {
        IOUtils.closeQuietly(fastHttpClient);
    }
}
