package ru.yandex.wmtools.common.sita;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.wmtools.common.util.http.HttpUtils;

/**
 * @author aherman
 */
public class SitaUrlFetchJsonResponse {
    private static final Logger log = LoggerFactory.getLogger(SitaUrlFetchJsonResponse.class);

    private static final ObjectMapper OM = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    private static final ObjectWriter FILTERING_OBJECT_WRITER;
    private static final ObjectWriter PRETTY_FILTERING_OBJECT_WRITER;

    static {
        SimpleFilterProvider filterProvider = new SimpleFilterProvider();
        filterProvider.addFilter(SitaJson.TURLFETCHINGRESULT_FILTER,
                SimpleBeanPropertyFilter.serializeAllExcept("Document", "RobotsTxt"));
        FILTERING_OBJECT_WRITER = OM.disable(SerializationFeature.WRITE_NULL_MAP_VALUES).writer(filterProvider);
        PRETTY_FILTERING_OBJECT_WRITER = FILTERING_OBJECT_WRITER.withDefaultPrettyPrinter();
    }

    public static SitaUrlFetchResponse parse(Reader reader, boolean retainJsonResponse) throws IOException {
        SitaJson.TResponse response = OM.readValue(reader, SitaJson.TResponse.class);
        return parseJson(response, retainJsonResponse);
    }

    public static SitaUrlFetchExtendedResponse parseExtended(Reader reader, boolean retainJsonResponse) throws IOException {
        SitaJson.TResponse response = OM.readValue(reader, SitaJson.TResponse.class);
        return parseExtendedJson(response, retainJsonResponse);
    }

    static SitaUrlFetchResponse parseJson(SitaJson.TResponse response, boolean retainJsonResponse) throws IOException {
        log.debug("Sita response: {}", FILTERING_OBJECT_WRITER.writeValueAsString(response));

        if (response.Results == null || response.Results.length == 0) {
            log.error("TResponse.Results is empty");
            throw new SitaException("Empty Sita results");
        }

        SitaJson.TActionResult result = null;
        for (SitaJson.TActionResult actionRresult: response.Results) {
            if (actionRresult.Type != SitaJson.EActionType.AT_URL_FETCHING) {
                log.warn("Found unknown action result: {}", actionRresult.Type);
            } else {
                result = actionRresult;
                break;
            }
        }

        boolean hasErrors = false;
        if (result != null && result.Errors != null && result.Errors.length > 0) {
            for (SitaJson.TError error : result.Errors) {
                if (SitaJson.EErrorSource.SITA == error.Source && error.SitaError != null) {
                    if (SitaJson.ESitaErrorCode.INCOMPLETE_RESPONSE == error.SitaError.Code) {
                        throw new SitaIncompleteResponseException("Incomplete response");
                    }
                }
            }

            hasErrors = true;
        }
        if (response.Errors != null && response.Errors.length > 0) {
            for (SitaJson.TError error : response.Errors) {
                if (error.SitaError != null && error.SitaError.Code == SitaJson.ESitaErrorCode.TIMEOUT_EXCEEDED) {
                    throw new SitaIncompleteResponseException("Timeout exceeded");
                }
            }
        }

        String rawDocument = null;

        String httpHeader = null;
        Integer statusCode = null;
        String robotsTxt;
        HttpResponse parsedHttpHeaders = null;

        if (result == null || result.UrlFetchingResult == null) {
            log.error("Sita url fetch result not found");
            throw new SitaException("Sita url fetch result not found");
        } else {
            robotsTxt = result.UrlFetchingResult.RobotsTxt;
            if (!StringUtils.isEmpty(robotsTxt)) {
                byte[] robotsTxtBytes = robotsTxt.getBytes(SitaService.ISO8859_1);
                robotsTxt = removeBom(robotsTxtBytes, ByteOrderMark.UTF_8);
            }

            if (StringUtils.isNotEmpty(result.UrlFetchingResult.Document)) {
                rawDocument = result.UrlFetchingResult.Document;
                httpHeader = HttpUtils.getHttpHeader(rawDocument);
                try {
                    parsedHttpHeaders =
                            HttpUtils.parse(new ReaderInputStream(new StringReader(rawDocument), SitaService.ISO8859_1));
                    statusCode = parsedHttpHeaders.getStatusLine().getStatusCode();
                    parsedHttpHeaders.setEntity(null);
                } catch (HttpException e) {
                    log.error("Unable to parse http response", e);
                }
            }
        }

        SitaJson.TDiagInfo diagInfo = result.UrlFetchingResult.Times;
        long time = 0;
        if (diagInfo != null &&
                diagInfo.SpiderInfo != null &&
                diagInfo.SpiderInfo.FinishReceive != null &&
                diagInfo.SpiderInfo.StartSend != null) {
            time = (diagInfo.SpiderInfo.FinishReceive - diagInfo.SpiderInfo.StartSend)/1000L;
        }


        SitaJson.TUrlFetchingResult urlFetchingResult = result.UrlFetchingResult;
        SitaUrlFetchResponse sitaUrlFetchResponse = new SitaUrlFetchResponse(
                urlFetchingResult.IsUrlAllowed,
                urlFetchingResult.Ip4,
                urlFetchingResult.HttpCode,
                urlFetchingResult.RedirTarget,
                rawDocument,
                robotsTxt,
                httpHeader,
                parsedHttpHeaders,
                statusCode,
                hasErrors,
                time,
                urlFetchingResult.getReadableEncoding(),
                urlFetchingResult.getReadableLanguage(),
                urlFetchingResult.getReadableMimeType()
        );

        if (retainJsonResponse) {
            sitaUrlFetchResponse.setResponseJson(PRETTY_FILTERING_OBJECT_WRITER.writeValueAsString(response));
        }
        return sitaUrlFetchResponse;
    }

    static SitaUrlFetchExtendedResponse parseExtendedJson(SitaJson.TResponse response, boolean retainJsonResponse) throws IOException {
        log.debug("Sita response: {}", FILTERING_OBJECT_WRITER.writeValueAsString(response));

        if (response.Results == null || response.Results.length == 0) {
            log.error("TResponse.Results is empty");
            SitaUrlFetchExtendedResponse errorResult = new SitaUrlFetchExtendedResponse(
                    SitaUrlFetchExtendedResponse.SitaResponseError.EMPTY_RESULT);
            if (retainJsonResponse) {
                errorResult.setResponseJson(PRETTY_FILTERING_OBJECT_WRITER.writeValueAsString(response));
            }
            return errorResult;
        }

        SitaJson.TActionResult result = null;
        for (SitaJson.TActionResult actionRresult: response.Results) {
            if (actionRresult.Type != SitaJson.EActionType.AT_URL_FETCHING) {
                log.warn("Found unknown action result: {}", actionRresult.Type);
            } else {
                result = actionRresult;
                break;
            }
        }

        boolean hasErrors = false;
        if (result != null && result.Errors != null && result.Errors.length > 0) {
            for (SitaJson.TError error : result.Errors) {
                if (SitaJson.EErrorSource.SITA == error.Source && error.SitaError != null) {
                    if (SitaJson.ESitaErrorCode.INCOMPLETE_RESPONSE == error.SitaError.Code) {
                        SitaUrlFetchExtendedResponse errorResult = new SitaUrlFetchExtendedResponse(
                                SitaUrlFetchExtendedResponse.SitaResponseError.INCOMPLETE_RESPONSE);
                        if (retainJsonResponse) {
                            errorResult.setResponseJson(PRETTY_FILTERING_OBJECT_WRITER.writeValueAsString(response));
                        }
                        return errorResult;
                    }
                }
            }

            hasErrors = true;
        }
        if (response.Errors != null && response.Errors.length > 0) {
            for (SitaJson.TError error : response.Errors) {
                if (error.SitaError != null && error.SitaError.Code == SitaJson.ESitaErrorCode.TIMEOUT_EXCEEDED) {
                    SitaUrlFetchExtendedResponse errorResult = new SitaUrlFetchExtendedResponse(
                            SitaUrlFetchExtendedResponse.SitaResponseError.TIMEOUT_EXCEEDED);
                    if (retainJsonResponse) {
                        errorResult.setResponseJson(PRETTY_FILTERING_OBJECT_WRITER.writeValueAsString(response));
                    }
                    return errorResult;
                }
            }
        }

        String rawDocument = null;

        String httpHeader = null;
        Integer statusCode = null;
        String robotsTxt;
        HttpResponse parsedHttpHeaders = null;

        if (result == null || result.UrlFetchingResult == null) {
            log.error("Sita url fetch result not found");
            throw new SitaException("Sita url fetch result not found");
        } else {
            robotsTxt = result.UrlFetchingResult.RobotsTxt;
            if (!StringUtils.isEmpty(robotsTxt)) {
                byte[] robotsTxtBytes = robotsTxt.getBytes(SitaService.ISO8859_1);
                robotsTxt = removeBom(robotsTxtBytes, ByteOrderMark.UTF_8);
            }

            if (StringUtils.isNotEmpty(result.UrlFetchingResult.Document)) {
                rawDocument = result.UrlFetchingResult.Document;
                httpHeader = HttpUtils.getHttpHeader(rawDocument);
                try {
                    parsedHttpHeaders =
                            HttpUtils.parse(new ReaderInputStream(new StringReader(rawDocument), SitaService.ISO8859_1));
                    statusCode = parsedHttpHeaders.getStatusLine().getStatusCode();
                    parsedHttpHeaders.setEntity(null);
                } catch (HttpException e) {
                    log.error("Unable to parse http response", e);
                }
            }
        }

        SitaJson.TDiagInfo diagInfo = result.UrlFetchingResult.Times;
        long time = 0;
        if (diagInfo != null &&
                diagInfo.SpiderInfo != null &&
                diagInfo.SpiderInfo.FinishReceive != null &&
                diagInfo.SpiderInfo.StartSend != null) {
            time = (diagInfo.SpiderInfo.FinishReceive - diagInfo.SpiderInfo.StartSend)/1000L;
        }


        SitaJson.TUrlFetchingResult urlFetchingResult = result.UrlFetchingResult;
        SitaUrlFetchExtendedResponse sitaUrlFetchResponse = new SitaUrlFetchExtendedResponse(
                urlFetchingResult.IsUrlAllowed,
                urlFetchingResult.Ip4,
                urlFetchingResult.HttpCode,
                urlFetchingResult.RedirTarget,
                rawDocument,
                robotsTxt,
                httpHeader,
                parsedHttpHeaders,
                statusCode,
                hasErrors,
                time,
                urlFetchingResult.getReadableEncoding(),
                urlFetchingResult.getReadableLanguage(),
                urlFetchingResult.getReadableMimeType()
        );

        if (retainJsonResponse) {
            sitaUrlFetchResponse.setResponseJson(PRETTY_FILTERING_OBJECT_WRITER.writeValueAsString(response));
        }
        return sitaUrlFetchResponse;
    }

    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, SitaService.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;
    }
}
