package ru.yandex.webmaster3.worker.mobile;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
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.HttpPost;
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.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

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.metrics.externals.AbstractExternalAPIService;
import ru.yandex.webmaster3.core.metrics.externals.ExternalDependencyMethod;
import ru.yandex.webmaster3.core.mobile.data.ScreenshotResolution;
import ru.yandex.webmaster3.core.security.tvm.TVMTokenService;
import ru.yandex.webmaster3.core.solomon.HandleCommonMetricsService;
import ru.yandex.webmaster3.core.solomon.Indicators;
import ru.yandex.webmaster3.core.solomon.SolomonSensor;
import ru.yandex.webmaster3.core.solomon.metric.SolomonCounter;
import ru.yandex.webmaster3.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.core.solomon.metric.SolomonMetricConfiguration;
import ru.yandex.webmaster3.core.solomon.metric.SolomonMetricRegistry;
import ru.yandex.webmaster3.core.util.JavaMethodWitness;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.storage.mobile.data.MobileAuditResult;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

import static org.joda.time.DateTimeConstants.SECONDS_PER_MINUTE;

/**
 * @author avhaliullin
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class MobileAuditService extends AbstractExternalAPIService {
    private static final int TIMEOUT_SECONDS = 15 * 60;
    private static final int TIMEOUT_MS = TIMEOUT_SECONDS * 1000;

    private static final String SCREENSHOT_MODE_VIEWPORT_ONLY = "ModeViewportOnly";

    private static final ObjectMapper OM = new ObjectMapper()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE);

    private final SolomonMetricConfiguration solomonMetricConfiguration = new SolomonMetricConfiguration();
    private final Map<Integer, SolomonCounter> metrics = new ConcurrentHashMap<>();

    private final CloseableHttpClient client = HttpClientBuilder.create()
            .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
            .setDefaultRequestConfig(
            RequestConfig.custom()
                    .setConnectTimeout(HttpConstants.DEFAULT_CONNECT_TIMEOUT)
                    .setSocketTimeout(TIMEOUT_MS)
                    .build()
    ).build();

    @Value("${webmaster3.worker.mobileAudit.url}")
    private final String mobileCheckerServiceUrl;
    @Value("${webmaster3.worker.mobileAudit.user}")
    private final String user;
    private final HandleCommonMetricsService handleCommonMetricsService;
    private final SolomonMetricRegistry solomonMetricRegistry;
    private final TVMTokenService goZoraTvmTokenService;

    @ExternalDependencyMethod("urlMobileCheckGozora")
    public MobileAuditResult checkUrl(URL url, ScreenshotResolution resolution) {
        long startTime = System.currentTimeMillis();
        log.info("Requested mobile audit for url {}", url);
        CheckRequest request = new CheckRequest(user,
                goZoraTvmTokenService.getToken(),
                url.toExternalForm(),
                new Options(
                        new OutputFormat(true),
                        new ViewPortSize(resolution.getWidth(), resolution.getHeight()),
                        SCREENSHOT_MODE_VIEWPORT_ONLY,
                        true
                ),
                TIMEOUT_SECONDS
        );

        try {
            String jsonString = OM.writeValueAsString(request);
//            log.info("Requesting gorotor.zora with " + jsonString);
            HttpPost post = new HttpPost(mobileCheckerServiceUrl);
            post.setEntity(new StringEntity(jsonString, ContentType.APPLICATION_JSON));

            return trackQuery(new JavaMethodWitness() {}, ALL_ERRORS_INTERNAL,
                    () -> RetryUtils.query(RetryUtils.instantRetry(3), () -> mobileAuditCheckUrl(url, post)));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException("Unknown: failed to convert mobile audit request to JSON", e);
        } finally {
            log.info("Gorotor Mobile audit for {} finished in {}ms", url, System.currentTimeMillis() - startTime);
        }
    }

    private MobileAuditResult mobileAuditCheckUrl(URL url, HttpPost post) {
        log.info("Url to check: {}", url);
        try (CloseableHttpResponse res = client.execute(post)) {
            int httpCode = res.getStatusLine().getStatusCode();
            sendMobileAuditResultHttpCodeSolomon(httpCode);

            log.info("Response code for checking {} is {}", url, httpCode);
            if (httpCode != 200) {
                StringBuilder msg = new StringBuilder("Mobile audit service returned code " + httpCode);
                HttpEntity entity = res.getEntity();
                if (entity != null) {
                    BufferedReader br = new BufferedReader(new InputStreamReader(entity.getContent()));
                    String line;
                    while ((line = br.readLine()) != null) {
                        msg.append("\n").append(line);
                    }
                }

                log.error(msg.toString());
                throw new WebmasterException(msg.toString(),
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null));
            }

            String respStr = IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8);
            TCheckResponseRoot cr;
            try {
                cr = OM.readValue(respStr, TCheckResponseRoot.class);
            } catch (Exception e) {
                log.error("Failed to parse response: {}", respStr);
                throw e;
            }

            TResponse response = cr.Response;
            if (response.Error != null) {
                log.info("Got mobile check error: {}", response.Error);
                if (response.Error.HttpCode != null) {
                    var mobileAuditResult = new MobileAuditResult.FetchFailedError(response.Error.HttpCode);
                    sendMobileAuditResultTypeToSolomon(mobileAuditResult);
                    return mobileAuditResult;
                } else {
                    throw new WebmasterException("Mobile audit check returned error, message: \"" + response.Error.Message + "\"",
                            new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null));
                }
            } else {
                log.info("No mobile check error");
            }

            TMobileFriendlyConditions conditions = response.MobileFriendlyConditions;

            MobileAuditResult.Success.Indicators indicators = new MobileAuditResult.Success.Indicators(
                        conditions.HasViewPort,
                        //Инвертируем эти условия для единообразия - чтобы true всегда означало "хорошо"
                        !(conditions.HasHorizontalOversize && conditions.HasHorizontalScrollingComputed),
                        !conditions.HasFlash, !conditions.HasApplet,
                        !conditions.HasSilverlight, !conditions.TooMuchSmallText,
                        true, false);

            var mobileAuditResult = new MobileAuditResult.Success(
                    response.MobileFriendly,
                    response.PngImage,
                    indicators,
                    response.TargetUrl
            );

            sendMobileAuditResultTypeToSolomon(mobileAuditResult);
            return mobileAuditResult;
        } catch (IOException e) {
            throw new WebmasterException("Mobile audit check failed",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), null), e);
        }
    }

    private void sendMobileAuditResultTypeToSolomon(MobileAuditResult mobileAuditResult) {
        log.info("Sending MobileAuditResultType to Solomon type = {}", mobileAuditResult.getType());
        List<SolomonSensor> sensors = new ArrayList<>();
        sensors.add(SolomonSensor.createAligned(System.currentTimeMillis(), SECONDS_PER_MINUTE, mobileAuditResult.getType().value())
                .withLabel(SolomonSensor.LABEL_CATEGORY, "url_mobile_audit")
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, "mobile_audit_result"));
        handleCommonMetricsService.handle(sensors, 1);
    }

    private void sendMobileAuditResultHttpCodeSolomon(int code) {
        metrics.computeIfAbsent(code, ign ->
                {
                    var key = SolomonKey.create(SolomonSensor.LABEL_CATEGORY, "url_mobile_audit")
                            .withLabel(SolomonSensor.LABEL_SECTION, "rotor")
                            .withLabel("http_code", String.valueOf(code))
                            .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.COUNT);

                    return solomonMetricRegistry.createCounter(solomonMetricConfiguration, key);
                }
        ).update();
    }

    public static class TCheckResponseRoot {
        public TResponse Response;
    }

    public static class TResponse {
        public String PngImage;
        public boolean MobileFriendly;
        public TMobileFriendlyConditions MobileFriendlyConditions;
        public TError Error;
        public String TargetUrl;
    }

    @ToString
    public static class TError {
        public String Message;
        public Integer HttpCode;
    }

    public static class TMobileFriendlyConditions {
        public boolean HasViewPort;
        public boolean HasHorizontalOversize;
        public boolean HasHorizontalScrollingComputed;
        public boolean HasFlash;
        public boolean HasApplet;
        public boolean HasSilverlight;
        public boolean TooMuchSmallText;
    }

    public record ViewPortSize(@JsonProperty("Width") int width,
                               @JsonProperty("Height") int height) { }

    public record OutputFormat(@JsonProperty("Png") boolean png) { }

    public record Options(@JsonProperty("OutputFormat") OutputFormat outputFormat,
                          @JsonProperty("ViewPortSize") ViewPortSize viewPortSize,
                          @JsonProperty("ScreenshotMode") String screenshotMode,
                          @JsonProperty("EnableImages") boolean enableImages) { }

    public record CheckRequest(@JsonProperty("Source") String source,
                               @JsonProperty("TvmServiceTicket") String tvmServiceTicket,
                               @JsonProperty("Url") String url,
                               @JsonProperty("Options") Options options,
                               @JsonProperty("Timeout") int timeout) { }
}
