package ru.yandex.webmaster3.storage.sitemap.service;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Optional;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.HttpResponsePart;
import ru.yandex.webmaster3.core.http.FileParameter;
import ru.yandex.webmaster3.core.http.FileParameterTransfer;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.GzipUtils;
import ru.yandex.webmaster3.core.util.UrlUtils;
import ru.yandex.webmaster3.core.zora.data.response.ZoraUrlFetchResponse;
import ru.yandex.webmaster3.storage.sitemap.service.exception.AnalysisSitemapActionException;
import ru.yandex.webmaster3.storage.sitemap.service.exception.AnalysisSitemapIncorrectResponseException;
import ru.yandex.webmaster3.storage.sitemap.service.model.AnalyzeSitemapErrorType;
import ru.yandex.webmaster3.storage.sitemap.service.model.AnalyzeSitemapInputData;
import ru.yandex.webmaster3.storage.sitemap.service.model.SitemapAnalysisExtendedResult;
import ru.yandex.webmaster3.storage.util.SitemapAnalysisResult;
import ru.yandex.webmaster3.storage.util.W3DispatcherHttpService;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.util.URLUtil;

/**
 * @author: ishalaru
 * DATE: 14.05.2019
 * Service for analyze sitema
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class AnalyzeSitemapActionService {
    private final SitemapValidatorService sitemapValidatorService;
    private final W3DispatcherHttpService w3dispatcherHttpService;

    public static FileParameterTransfer convert(FileParameter fileParameter) {
        FileParameterTransfer fileParameterTransfer = null;

        if (fileParameter != null) {
            String value = FileParameterTransfer.convertToString(fileParameter.getRawData(SitemapValidatorService.MAX_SITEMAP_LENGTH + 1));
            fileParameterTransfer = new FileParameterTransfer(value, fileParameter.getContentType(),
                    fileParameter.getName(), fileParameter.getFileName(), fileParameter.getSize());
        }
        return fileParameterTransfer;
    }

    public SitemapAnalysisExtendedResult process(AnalyzeSitemapInputData inputData) {

        DataAggregatorPojo data;

        if (inputData.getSitemapText() != null) {
            data = processSitemapText(inputData.getSitemapText());
        } else if (inputData.getSitemapUrl() != null) {
            data = processSitemapURL(inputData.getSitemapUrl());
        } else if (inputData.getSitemapFile() != null) {
            data = processSitemapFile(inputData.getSitemapFile());
        } else {
            throw new WebmasterException("No sitemap provided",
                    new WebmasterErrorResponse.IllegalParameterValueResponse(this.getClass(), "sitemapText", null));
        }

        if (data.getSitemapData() == null) {
            throw new WebmasterException("No sitemap provided",
                    new WebmasterErrorResponse.IllegalParameterValueResponse(this.getClass(), "sitemapText", null));
        }
        if (data.getSitemapSize() > SitemapValidatorService.MAX_SITEMAP_LENGTH) {
            throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_IS_TOO_LONG);
        }
        if (data.getSitemapSize() == 0) {
            throw new AnalysisSitemapIncorrectResponseException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_IS_EMPTY, data.getHttpResponse());
        }

        final SitemapAnalysisResult result;
        try {
            result = w3dispatcherHttpService.analyzeSitemapBytes(data.getSitemapData());
        } catch (InternalException | UnsupportedEncodingException | UserException e) {
            throw new WebmasterException("Unable to parse sitemap url",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(this.getClass(),
                            "Unable to read sitemap data"), e);
        }

        return new SitemapAnalysisExtendedResult(result, data.getSitemapSize(), data.getSitemapFileName());
    }

    private DataAggregatorPojo processSitemapText(String text) {
        long sitemapSize = 0;
        ByteArrayOutputStream baos = new ByteArrayOutputStream(16 * 1024);
        try {
            sitemapSize = toDataArray(text, baos);
        } catch (IOException e) {
            throw new WebmasterException("Unable to parse sitemap url",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(this.getClass(),
                            "Unable to read sitemap data"), e);
        } catch (IllegalArgumentException e) {
            throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_IS_TOO_LONG);
        }
        return new DataAggregatorPojo(null, baos.toByteArray(), sitemapSize, null);

    }

    private DataAggregatorPojo processSitemapURL(String sitemapUrl) {
        final URL url;
        try {
            url = UrlUtils.prepareUrl(sitemapUrl, true);
        } catch (IllegalArgumentException e) {
            throw new WebmasterException("Unable to parse sitemap url",
                    new WebmasterErrorResponse.IllegalParameterValueResponse(this.getClass(), "sitemapUrl",
                            sitemapUrl));
        }

        if (URLUtil.isHomePage(url)) {
            throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_IS_HOMEPAGE);
        }

        ZoraUrlFetchResponse urlFetchResponse = downloadSitemap(url);
        boolean tooBigContentLength = Optional.ofNullable(urlFetchResponse.getHttpResponse())
                .map(HttpResponse::getEntity)
                .map(HttpEntity::getContentLength)
                .filter(cl -> cl > SitemapValidatorService.MAX_SITEMAP_LENGTH)
                .isPresent();
        if (tooBigContentLength) {
            throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_IS_TOO_LONG);
        }
        HttpResponsePart httpResponse = HttpResponsePart.createFromZoraResponse(urlFetchResponse, null);
        long sitemapSize = 0;
        ByteArrayOutputStream baos = new ByteArrayOutputStream(64 * 1024);
        try {
            sitemapSize = toDataArray(urlFetchResponse.getDocumentContentStream(), baos);
        } catch (IOException | InternalException e) {
            throw new WebmasterException("Unable to parse sitemap url",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(this.getClass(),
                            "Unable to read sitemap data"), e);
        }
        return new DataAggregatorPojo(url.toString(), baos.toByteArray(), sitemapSize, httpResponse);
    }

    private DataAggregatorPojo processSitemapFile(FileParameterTransfer fileParameter) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(64 * 1024);
        String sitemapFileName = fileParameter.getFileName();
        log.debug("fileName = {}", sitemapFileName);

        long sitemapSize = 0;
        try (InputStream inputStream = fileParameter.getInputStream();) {
            sitemapSize = toDataArray(inputStream, baos);
        } catch (IOException e) {
            log.error("Unable to load sitemap from file", e);
            throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__UNABLE_TO_DOWNLOAD_SITEMAP);
        }
        return new DataAggregatorPojo(sitemapFileName, baos.toByteArray(), sitemapSize, null);
    }

    private static long toDataArray(String sitemapContent, ByteArrayOutputStream baos) throws IOException {
        InputStream contentInputStream = new ReaderInputStream(new StringReader(sitemapContent), Charsets.UTF_8);
        return copyDataToArray(contentInputStream, baos);
    }

    private static long toDataArray(InputStream contentInputStream, ByteArrayOutputStream baso) throws IOException {
        contentInputStream = GzipUtils.unGzip(new BufferedInputStream(contentInputStream, 8));
        return copyDataToArray(contentInputStream, baso);
    }

    private static long copyDataToArray(InputStream contentInputStream, ByteArrayOutputStream baos) throws IOException {
        baos.write(W3DispatcherHttpService.SITEMAP_REQUEST_DATA_PARAM.getBytes());
        BOMInputStream bomInputStream = new BOMInputStream(contentInputStream);
        return IOUtils.copyLarge(bomInputStream, baos, 0, SitemapValidatorService.MAX_SITEMAP_LENGTH + 1L);
    }

    /**
     * Downloads sitemap, copy sitemap content to builder.
     *
     * @param url sitemap url to download
     * @return error response in case of error or null if it is OK
     * @throws InternalException
     */
    private ZoraUrlFetchResponse downloadSitemap(final URL url) throws AnalysisSitemapActionException {
        SitemapValidatorService.SitemapDownloadResult sitemapDownloadResult =
                sitemapValidatorService.downloadSitemapAndCheck(url);

        if (sitemapDownloadResult.getError() != SitemapValidatorService.SitemapDownloadError.NO_ERROR) {
            HttpResponsePart httpResponse = null;
            if (sitemapDownloadResult.getUrlFetchResponse() != null) {
                httpResponse =
                        HttpResponsePart.createFromZoraResponse(sitemapDownloadResult.getUrlFetchResponse(), null);
            }
            switch (sitemapDownloadResult.getError()) {
                case ILLEGAL_HTTP_CODE:
                    throw new AnalysisSitemapIncorrectResponseException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__ILLEGAL_HTTP_CODE, httpResponse);

                case SITEMAP_IS_FORBIDDEN_IN_ROBOTS_TXT:
                    throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_FORBIDDEN_IN_ROBOTS_TXT);

                case SITEMAP_TOO_LONG:
                    throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__SITEMAP_IS_TOO_LONG);

                case UNABLE_TO_DOWNLOAD_SITEMAP:
                    throw new AnalysisSitemapActionException(AnalyzeSitemapErrorType.ANALYZE_SITEMAP__UNABLE_TO_DOWNLOAD_SITEMAP);
                default:
                    throw new WebmasterException("Unknown sitemap download error: " + sitemapDownloadResult.getError(),
                            new WebmasterErrorResponse.InternalUnknownErrorResponse(this.getClass(),
                                    "Unknown sitemap download error: " + sitemapDownloadResult.getError()));
            }
        }

        return sitemapDownloadResult.getUrlFetchResponse();
    }

    public static class DataAggregatorPojo {
        private final String sitemapFileName;
        private final byte[] sitemapData;
        private final long sitemapSize;
        private final HttpResponsePart httpResponse;

        public DataAggregatorPojo(String sitemapFileName, byte[] sitemapData, long sitemapSize, HttpResponsePart httpResponse) {
            this.sitemapFileName = sitemapFileName;
            this.sitemapData = sitemapData;
            this.sitemapSize = sitemapSize;
            this.httpResponse = httpResponse;
        }

        public String getSitemapFileName() {
            return sitemapFileName;
        }

        public byte[] getSitemapData() {
            return sitemapData;
        }

        public long getSitemapSize() {
            return sitemapSize;
        }

        public HttpResponsePart getHttpResponse() {
            return httpResponse;
        }
    }
}
