package ru.yandex.webmaster3.core.zora;

import NUrlChecker.Response.TUrlCheckResponse.TArchiveResult;
import NUrlChecker.Response.TUrlCheckResponse.TFetchResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.ByteOrderMark;
import org.apache.http.*;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.util.EntityUtils;
import org.joda.time.Instant;
import ru.yandex.webmaster3.core.util.MimeUtils;
import ru.yandex.webmaster3.core.zora.data.calc.ZoraCalcResult;
import ru.yandex.webmaster3.core.zora.data.common.ZoraIPAddress;
import ru.yandex.webmaster3.core.zora.data.fetch.ZoraFetchResult;
import ru.yandex.webmaster3.core.zora.data.fetch.ZoraFetchStatusEnum;
import ru.yandex.webmaster3.core.zora.data.pdfetch.ZoraPDFetchResult;
import ru.yandex.webmaster3.core.zora.data.response.*;
import ru.yandex.webmaster3.core.zora.go_data.response.GoZoraResponseFetchUrl;
import ru.yandex.wmtools.common.sita.SitaException;
import ru.yandex.wmtools.common.sita.YandexCharset;
import ru.yandex.wmtools.common.sita.YandexDocumentLanguage;
import ru.yandex.webmaster3.core.util.http.YandexMimeType;
import ru.yandex.wmtools.common.util.http.HttpUtils;
import ru.yandex.wmtools.common.util.http.YandexHttpStatus;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.List;
import java.util.Optional;


/**
 * @author avhaliullin
 */
@Slf4j
public class ZoraConversionUtil {
    private static final String YANDEX_PREFIX_HEADER = "x-ya";

    public static ZoraFetchResult verifyAndGetFirstFetchResult(ZoraRawPDFetchResponse rawResponse) {
        ZoraPDFetchResult pdFetchResult = rawResponse.getPdFetch();
        if (pdFetchResult.getStatus() != ZoraPDFetchStatus.Ok) {
            throw new SitaException("PDFetch error " + pdFetchResult.getStatus());
        }

        List<ZoraPDFetchResult.ResultItem> results = pdFetchResult.getResult().getInfos();
        if (results.isEmpty()) {
            throw new SitaException("Bad zora response: no PDFetch items found");
        }

        ZoraRawUrlFetchResponse urlFetchResponse = results.get(0).getDoc();
        if (urlFetchResponse.getStatus() != ZoraStatusEnum.ZS_OK) {
            throw new SitaException("Zora error " + urlFetchResponse.getStatus());
        }

        ZoraFetchResult fetchResult = urlFetchResponse.getFetch();
        if (fetchResult == null) {
            throw new SitaException("Empty Zora results");
        }

        return fetchResult;
    }

    public static ZoraUrlFetchResponse toUrlFetchResponse(ZoraRawPDFetchResponse zoraResponse) {
        if (zoraResponse.getPdFetch() == null) {
            try {
                log.error("Bad response from Zora: {}", OfflineZoraService.OM.writeValueAsString(zoraResponse));
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize response", e);
            }
            throw new NullPointerException("PDFetch is null!");
        }
        ZoraPDFetchStatus status = zoraResponse.getPdFetch().getStatus();
        switch (status) {
            case Timeout:
                return ZoraUrlFetchResponse.builder().extendedHttpStatus(YandexHttpStatus.HTTP_1039_TIMEDOUT_WHILE_BYTES_RECEIVING).build();
            case Ok:
                List<ZoraPDFetchResult.ResultItem> results = zoraResponse.getPdFetch().getResult().getInfos();
                if (results.isEmpty()) {
                    throw new RuntimeException("Bad zora response: no PDFetch items found");
                }
                ZoraPDFetchResult.ResultItem resultItem = results.get(0);
                return toUrlFetchResponse(resultItem.getDoc(), resultItem.isUrlAllowed());
            case Error:
                log.error("zora bad response - {}", zoraResponse.getPdFetch().getErrorStr());
            default:
                throw new RuntimeException("PDFetch error " + zoraResponse.getPdFetch().getStatus());
        }
    }

    public static ZoraUrlFetchResponse toUrlFetchResponse(ZoraRawUrlFetchResponse zoraResponse) {
        return toUrlFetchResponse(zoraResponse, true);
    }

    private static ZoraUrlFetchResponse toUrlFetchResponse(ZoraRawUrlFetchResponse zoraResponse, boolean allowedInRobots) {
        if (zoraResponse.getStatus() != ZoraStatusEnum.ZS_OK) {
            throw new SitaException("Zora error " + zoraResponse.getStatus());
        }
        if (zoraResponse.getFetch() == null) {
            throw new SitaException("Empty Zora results");
        }

        String httpHeader = null;
        String robotsTxt;
        HttpResponse parsedHttpResponse = null;

        robotsTxt = Optional.ofNullable(zoraResponse.getFetch().getRobotsTxt())
                .map(bytes -> removeBom(bytes, ByteOrderMark.UTF_8))
                .orElse(null);

        byte[] rawResponse = zoraResponse.getFetch().getHttpResponse();
        if (rawResponse != null && rawResponse.length > 0) {
            String rawDocument = new String(rawResponse, StandardCharsets.ISO_8859_1);
            httpHeader = HttpUtils.getHttpHeader(rawDocument); //TODO: ?
            try {
                parsedHttpResponse = HttpUtils.parse(new ByteArrayInputStream(rawResponse));
                HttpEntity entity = new BufferedHttpEntity(parsedHttpResponse.getEntity());
                EntityUtils.consume(entity);
                parsedHttpResponse.setEntity(entity);
            } catch (HttpException | IOException e) {
                log.error("Unable to parse http response", e);
            }
        }

        Long time = null;
        ZoraFetchResult.Trace trace = zoraResponse.getFetch().getTrace();
        List<ZoraTraceEntry> traceEntries = trace == null? null : trace.getEntries();
        if (CollectionUtils.isNotEmpty(traceEntries)) {
            Instant startDownload = null;
            Instant finishDownload = null;
            for (ZoraTraceEntry entry : traceEntries) {
                if ("fetcher".equals(entry.getComponent())) {
                    if ("sending response".equals(entry.getMessage())) {
                        finishDownload = new Instant(entry.getInstant().getMicroSeconds() / 1000L);
                    } else if ("start download".equals(entry.getMessage())) {
                        startDownload = new Instant(entry.getInstant().getMicroSeconds() / 1000L);
                    }
                }
            }
            if (startDownload != null && finishDownload != null) {
                time = finishDownload.getMillis() - startDownload.getMillis();
            }
        } else {
            log.warn("No trace provided");
        }

        String ipAddress = null;
        if (zoraResponse.getFetch().getIp() != null) {
            ZoraIPAddress ip = zoraResponse.getFetch().getIp();
            ByteBuffer bb;
            switch (ip.getType()) {
                case Ipv4:
                    bb = ByteBuffer.allocate(4);
                    bb.asIntBuffer().put(0, ip.getLow().intValue());
                    break;
                case Ipv6:
                    bb = ByteBuffer.allocate(16);
                    bb.asLongBuffer().put(0, ip.getHigh().longValue())
                            .put(1, ip.getLow().longValue());
                    break;
                case Unset:
                    bb = null;
                    break;
                default:
                    throw new RuntimeException("Unkown ip type " + ip.getType());
            }
            try {
                if (bb != null) {
                    InetAddress address = InetAddress.getByAddress(bb.array());
                    ipAddress = address.getHostAddress();
                }
            } catch (UnknownHostException e) {
                log.error("Failed to parse IP", e);
            }
        }

        YandexHttpStatus extHttpCode = getYandexHttpStatus(zoraResponse);

        YandexCharset charset = YandexCharset.UNKNOWN;
        YandexDocumentLanguage language = YandexDocumentLanguage.UNKNOWN;
        YandexMimeType mimeType = YandexMimeType.UNKNOWN;

        ZoraCalcResult calc = zoraResponse.getCalc();
        ZoraCalcResult.Result calcResult = null;
        if (calc != null) {
            calcResult = calc.getResult();
        }

        if (calcResult != null) {
            charset = YandexCharset.R.fromValueOrUnknown(calcResult.getEncoding());
            language = YandexDocumentLanguage.R.fromValueOrUnknown(calcResult.getLanguage());
            mimeType = YandexMimeType.R.fromValueOrUnknown(calcResult.getConvMimeType());
        }

        Charset charsetJava = StandardCharsets.UTF_8;
        if (charset.getCharsetName() != null) {
            try {
                charsetJava = Charset.forName(charset.getCharsetName());
            } catch (UnsupportedCharsetException | IllegalCharsetNameException e) {
                log.warn("Unknown charset, fallback to UTF-8: " + charset);
            }
        }

        ZoraSslCertErrorEnum certError = null;
        if (zoraResponse.getFetch().isHasSslCertErrors()) {

            certError = ZoraSslCertErrorEnum.getEnumByString(zoraResponse.getFetch().getSslCertErrorsExplanation());
            log.debug("certErrorsStr - {}, certErrorEnum - {}", zoraResponse.getFetch().getSslCertErrorsExplanation(), certError);
        }

        String httpLocation = zoraResponse.getFetch().getHttpLocation();
        if (httpLocation == null && parsedHttpResponse != null) {
            Header locationHeader = parsedHttpResponse.getFirstHeader("Location");
            if (locationHeader != null) {
                httpLocation = locationHeader.getValue();
            }
        }

        return ZoraUrlFetchResponse.builder()
                .isAllowedInRobotsTxt(allowedInRobots)
                .ipAddress(ipAddress)
                .redirTarget(httpLocation)
                .robotsTxtContent(robotsTxt)
                .originalHttpHeader(httpHeader)
                .httpResponse(parsedHttpResponse)
                .serverHttpStatusCode(zoraResponse.getFetch().getHttpStatus())
                .extendedHttpStatus(extHttpCode)
                .responseTime(time)
                .charset(charset)
                .charsetJava(charsetJava)
                .language(language)
                .mimeType(mimeType)
                .sslErrorExplanation(certError).build();
    }

    public static ZoraUrlFetchResponse toUrlFetchResponse(GoZoraResponseFetchUrl zoraResponse) {
        YandexHttpStatus extHttpCode = YandexHttpStatus.parseCode(zoraResponse.getHttpCode());
        YandexCharset charset = YandexCharset.UNKNOWN;
        Charset charsetJava = zoraResponse.getCharset() != null ? zoraResponse.getCharset() : StandardCharsets.UTF_8;
        YandexMimeType mimeType = MimeUtils.typeByName(zoraResponse.getMimeType());
        YandexDocumentLanguage language = YandexDocumentLanguage.UNKNOWN;

        if (zoraResponse.getResponse() != null) {
            HeaderIterator headerIterator = zoraResponse.getResponse().headerIterator();
            while (headerIterator.hasNext()) {
                Header header = headerIterator.nextHeader();
                if (header.getName().toLowerCase().startsWith(YANDEX_PREFIX_HEADER)) {
                    headerIterator.remove();
                }
            }
        }

        return ZoraUrlFetchResponse.builder()
                .isAllowedInRobotsTxt(true)
                .httpResponse(zoraResponse.getResponse())
                .serverHttpStatusCode(zoraResponse.getHttpCode())
                .extendedHttpStatus(extHttpCode)
                .responseTime(zoraResponse.getResponseTime())
                .charset(charset)
                .charsetJava(charsetJava)
                .language(language)
                .mimeType(mimeType)
                .build();
    }

    public static ZoraUrlFetchResponse toUrlFetchResponse(TFetchResult fetchResult) {
        String httpHeader = null;
        String robotsTxt;
        HttpResponse parsedHttpResponse = null;
        ContentType contentType = null;

        robotsTxt = Optional.ofNullable(fetchResult.getRobotsTxt().toByteArray())
                .map(bytes -> removeBom(bytes, ByteOrderMark.UTF_8))
                .orElse(null);

        byte[] rawResponse = fetchResult.getHttpResponse().toByteArray();
        if (rawResponse != null && rawResponse.length > 0) {
            String rawDocument = new String(rawResponse, StandardCharsets.ISO_8859_1);
            httpHeader = HttpUtils.getHttpHeader(rawDocument); //TODO: ?
            try {
                parsedHttpResponse = HttpUtils.parse(new ByteArrayInputStream(rawResponse));
                HttpEntity entity = new BufferedHttpEntity(parsedHttpResponse.getEntity());
                EntityUtils.consume(entity);
                parsedHttpResponse.setEntity(entity);
                contentType = GoZoraResponseFetchUrl.parseContentType(parsedHttpResponse);
            } catch (HttpException | IOException e) {
                log.error("Unable to parse http response", e);
            }
        }

        long fetchTime = fetchResult.getTimeSpent();
        YandexHttpStatus extHttpCode = YandexHttpStatus.parseCode(fetchResult.getExtHttpCode());

        Charset charsetJava = contentType != null ? contentType.getCharset() : null;
        if (charsetJava == null) {
            charsetJava = StandardCharsets.UTF_8;
        }

        String mimeTypeStr = contentType != null ? contentType.getMimeType() : null;
        YandexCharset charset = YandexCharset.UNKNOWN;
        YandexMimeType mimeType = MimeUtils.typeByName(mimeTypeStr);
        YandexDocumentLanguage language = YandexDocumentLanguage.UNKNOWN;


//        ZoraSslCertErrorEnum certError = null;
//        if (zoraResponse.getFetch().isHasSslCertErrors()) {
//            certError = ZoraSslCertErrorEnum.getEnumByString(zoraResponse.getFetch().getSslCertErrorsExplanation());
//            log.debug("certErrorsStr - {}, certErrorEnum - {}", zoraResponse.getFetch().getSslCertErrorsExplanation(), certError);
//        }

        String httpLocation = null;
        if (parsedHttpResponse != null) {
            Header locationHeader = parsedHttpResponse.getFirstHeader("Location");
            if (locationHeader != null) {
                httpLocation = locationHeader.getValue();
            }
        }

        if (parsedHttpResponse != null) {
            Header sslErrorsHeader = parsedHttpResponse.getFirstHeader("X-Yandex-Gozora-Error-Description");
            if (sslErrorsHeader != null) {
                log.info("SSL errors header: {}", sslErrorsHeader.getValue());
            }
        }

        return ZoraUrlFetchResponse.builder()
                .isAllowedInRobotsTxt(fetchResult.getAllowedByRobotsTxt())
                .redirTarget(httpLocation)
                .robotsTxtContent(robotsTxt)
                .originalHttpHeader(httpHeader)
                .httpResponse(parsedHttpResponse)
                .serverHttpStatusCode(fetchResult.getHttpCode())
                .extendedHttpStatus(extHttpCode)
                .responseTime(fetchTime)
                .charset(charset)
                .charsetJava(charsetJava)
                .language(language)
                .mimeType(mimeType)
                .sslErrorExplanation(null)
                .build();

    }

    public static YandexHttpStatus getYandexHttpStatus(ZoraFetchResult fetchResult) {
        YandexHttpStatus extHttpCode;
        ZoraFetchStatusEnum status = fetchResult.getStatus();
        if (status != ZoraFetchStatusEnum.FS_OK) {
            extHttpCode = YandexHttpStatus.parseCode(status == null ? 0 : status.value());
            if (extHttpCode == YandexHttpStatus.UNKNOWN) {
                log.warn("Unknown zora fetch status " + status);
            }
        } else /*Calc check*/ {
            Integer httpStatus = fetchResult.getHttpStatus();
            if (httpStatus == null) {
                throw new RuntimeException("Null http status");
            }
            extHttpCode = YandexHttpStatus.parseCode(httpStatus);
        }
        return extHttpCode;
    }

    public static YandexHttpStatus getYandexHttpStatus(ZoraRawUrlFetchResponse fetchResponse) {
        YandexHttpStatus extHttpCode = getYandexHttpStatus(fetchResponse.getFetch());
        ZoraCalcResult calcResult = fetchResponse.getCalc();
        if (calcResult != null && calcResult.getResult() != null) {
            int extCode = calcResult.getResult().getExtHttpCode();
            if (extCode > 0) {
                YandexHttpStatus calcStatus = YandexHttpStatus.parseCode(extCode);
                if (calcStatus != YandexHttpStatus.UNKNOWN) {
                    extHttpCode = calcStatus;
                }
            }
        }

        return extHttpCode;
    }

    private static String removeBom(byte[] robotsTxtBytes, ByteOrderMark bom) {
        String robotsTxt;
        int startPosition = 0;
        if (startWith(robotsTxtBytes, bom)) {
            startPosition = bom.length();
        }

        robotsTxt = new String(robotsTxtBytes, startPosition, robotsTxtBytes.length - startPosition, StandardCharsets.UTF_8);
        return robotsTxt;
    }

    private static boolean startWith(byte[] array, ByteOrderMark bom) {
        if (array.length < bom.length()) {
            return false;
        }
        for (int i = 0; i < bom.length(); i++) {
            if (array[i] != (byte) bom.get(i)) {
                return false;
            }
        }
        return true;
    }
}
