package ru.yandex.autotests.directapi.apiclient.version5;

import java.io.StringReader;
import java.nio.charset.Charset;

import javax.ws.rs.core.Response;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import com.yandex.direct.api.v5.reports.ApiError;
import com.yandex.direct.api.v5.reports.ReportDefinition;
import com.yandex.direct.api.v5.reports.ReportDownloadError;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 ru.yandex.autotests.direct.utils.config.DirectTestRunProperties;
import ru.yandex.autotests.directapi.apiclient.HttpMessage;
import ru.yandex.autotests.directapi.apiclient.ReportsResponseHandler;
import ru.yandex.autotests.directapi.apiclient.RequestHeader;
import ru.yandex.autotests.directapi.apiclient.config.ConnectionConfig;
import ru.yandex.autotests.directapi.apiclient.config.ProtocolType;
import ru.yandex.autotests.directapi.apiclient.errors.Api5JsonError;
import ru.yandex.autotests.directapi.apiclient.errors.Api5XmlError;
import ru.yandex.autotests.directapi.darkside.model.ScriptParams;
import ru.yandex.autotests.directapi.darkside.model.Scripts;
import ru.yandex.autotests.directapi.darkside.steps.DarkSideSteps;
import ru.yandex.autotests.directapi.exceptions.DirectAPIException;
import ru.yandex.autotests.directapi.exceptions.TimeoutException;
import ru.yandex.autotests.directapi.model.api5.Action;
import ru.yandex.autotests.directapi.model.api5.reports.ReportProcessingMode;
import ru.yandex.autotests.directapi.model.api5.reports.ReportResponse;
import ru.yandex.autotests.httpclient.lite.core.config.HttpClientConnectionConfig;
import ru.yandex.autotests.httpclient.lite.core.config.HttpClientFactory;
import ru.yandex.autotests.irt.testutils.json.JsonUtils;
import ru.yandex.qatools.allure.annotations.Attachment;

/**
 * Created by pavryabov on 01.06.16.
 */
public class ReportsClient extends BaseApiV5Client {

    private static final Response.Status HTTP_OK = Response.Status.OK;
    private static final Response.Status HTTP_OFFLINE_REPORT_CREATED = Response.Status.CREATED;
    private static final Response.Status HTTP_OFFLINE_REPORT_PROCESSING = Response.Status.ACCEPTED;

    public static Log log = LogFactory.getLog(ReportsClient.class);

    private BaseApiV5Request request;

    public ReportsClient(ConnectionConfig connectionConfig, RequestHeader requestHeader, BaseApiV5Request request) {
        super(connectionConfig, requestHeader);
        this.request = request;
    }

    @Override
    public <T> MethodInvocationResult<T> invokeMethodEx(ServiceNames serviceName, String login, Action methodName,
            Object params)
    {
        return new MethodInvocationResult<T>(
                (T) sendRequest(serviceName, login, params, null, null), false);
    }

    public <T> T invokeMethod(ServiceNames serviceName, String login, Action methodName, Object params,
            ReportProcessingMode processingMode, Integer timeout)
    {
        return (T) sendRequest(serviceName, login, params, processingMode, timeout);
    }

    @Override
    protected ProtocolType getProtocolType() {
        return request.getProtocolType();
    }

    public ReportResponse sendRequest(ServiceNames serviceName, String login, Object params,
            ReportProcessingMode requestedProcessingMode, Integer timeout)
    {

        if (requestedProcessingMode == null) {
            requestedProcessingMode = requestHeader.getProcessingMode();
        }

        ProtocolType protocolType = getProtocolType();

        request.setHeader(
                getHttpHeaders(
                        login != null ? login : requestHeader.getClientLogin(),
                        requestedProcessingMode));
        request.setParameters(params);

        String endpoint = connectionConfig.getEndPointForV5(protocolType, serviceName.toString());

        HttpMessage message = new HttpMessage();
        message.setUrl(endpoint);
        message.setHeaderMap(request.getHeader());
        message.setRequest(request.toString());

        String response;
        ReportProcessingMode actualProcessingMode = null;
        try {
            HttpClientConnectionConfig clientConnectionConfig =
                    ConnectionConfig.getHttpClientConnectionConfig(endpoint);
            if (timeout != null) {
                clientConnectionConfig.timeout(timeout);
            }
            CloseableHttpClient httpClient = HttpClientFactory.getHttpClient(clientConnectionConfig);

            HttpPost httpRequest = new HttpPost(endpoint);
            StringEntity entity = new StringEntity(request.toString(),
                    ContentType.create("text/plain", Charset.forName("UTF-8")));
            httpRequest.setEntity(entity);
            httpRequest.setHeaders(toHeadersArray(request.getHeader()));

            response = httpClient.execute(httpRequest, new ReportsResponseHandler(message))
                .getResponseObject();

            Response.Status status = message.getStatus();

            if (status == Response.Status.CREATED || status == Response.Status.ACCEPTED) {
                if (requestedProcessingMode == ReportProcessingMode.ONLINE) {
                    throw new DirectAPIException("Online-отчет начал строиться в режиме offline");
                }

                actualProcessingMode = ReportProcessingMode.OFFLINE;

                runReportBuilderScript(login, (ReportDefinition) params);

                response = httpClient.execute(httpRequest, new ReportsResponseHandler(message))
                        .getResponseObject();
                status = message.getStatus();

                if (status != Response.Status.OK) {
                    throw new DirectAPIException("не удалось построить отчёт");
                }

                if (response == null) {
                    throw new DirectAPIException("Получен пустой ответ от сервера");
                }

                log.debug("Report built in offline mode");
            } else if (status == Response.Status.OK) {
                actualProcessingMode = ReportProcessingMode.ONLINE;
                if (response == null) {
                    throw new DirectAPIException("Получен пустой ответ от сервера");
                }
                if (requestedProcessingMode == ReportProcessingMode.OFFLINE) {
                    if (timeout == null) {
                        // явно задали получение ранее сгенерированного отчета
                        log.debug("Got previously generated report");
                    } else {
                        throw new DirectAPIException("Отчет построился в online-режиме или получен уже готовый отчет");
                    }
                } else {
                    log.debug("Report built in online mode");
                }
            }

            message.setResponse(response);
            traceHttpMessage(message, protocolType);
            if (!response.isEmpty()) {
                response = getResponseResult(response);
                checkFaultPresence(response, message, protocolType);
            }
        } catch (Api5XmlError xmlError) {
            throw xmlError;
        } catch (Api5JsonError jsonError) {
            throw jsonError;
        } catch (DirectAPIException knownException) {
            traceHttpMessage(message, protocolType);
            throw new DirectAPIException("Неверный ответ сервера: " + knownException.getMessage());
        } catch (TimeoutException timeoutException) {
            traceHttpMessage(message, protocolType);
            throw timeoutException;
        } catch (Exception e) {
            traceHttpMessage(message, protocolType);
            throw new DirectAPIException("Неизвестная ошибка при выполнении запроса сервиса Reports", e);
        }
        return new ReportResponse(actualProcessingMode, response, message.getResponseHeaderMap());
    }

    private void runReportBuilderScript(String login, ReportDefinition reportDefinition) {
        String requestLogin = login != null ? login
                : requestHeader.getClientLogin() != null ? requestHeader.getClientLogin()
                        : requestHeader.getFakeLogin() != null ? requestHeader.getFakeLogin()
                                : requestHeader.getLogin();

        DarkSideSteps darkSideSteps = new DarkSideSteps(DirectTestRunProperties.getInstance());
        int shard = darkSideSteps.getDirectJooqDbSteps().shardingSteps().getShardByLogin(requestLogin);
        darkSideSteps.getRunScriptSteps().runScript(Scripts.API_REPORTS_BUILDER, new ScriptParams()
                .withShardId(shard)
                .withUniq(1)
                .withLogin(requestLogin)
                .withCustomParam("--report-name", "\"" + reportDefinition.getReportName() + "\""));
    }

    private Long getRetryInHeaderValue(HttpMessage message) {
        return Long.parseLong(
                message.getResponseHeaderMap().get(ReportsResponseHandler.RETRY_IN_HEADER)
        );
    }

    private Long getReportsInQueueHeaderValue(HttpMessage message) {
        return Long.parseLong(
                message.getResponseHeaderMap().get(ReportsResponseHandler.REPORTS_IN_QUEUE_HEADER)
        );
    }

    public String traceHttpMessage(HttpMessage message, ProtocolType protocolType) {
        if (protocolType.equals(ProtocolType.JSON)) {
            return traceHttpMessageJson(message);
        }
        if (protocolType.equals(ProtocolType.XML)) {
            return traceHttpMessageXml(message);
        }
        return null;
    }

    @Attachment(value = "[XML/CURL]: Request", type = "text/html")
    public String traceHttpMessageXml(HttpMessage message) {
        return super.traceHttpMessage(message, ProtocolType.XML);
    }

    @Attachment(value = "[JSON/CURL]: Request", type = "text/html")
    public String traceHttpMessageJson(HttpMessage message) {
        return super.traceHttpMessage(message, ProtocolType.JSON);
    }

    public String getResponseResult(String response) {
        return response.replace(":null", ":\"null\"");
    }


    /**
     * Look http message for error codes and throw AxisError if found one
     *
     * @param response
     */
    @Override
    protected void checkJsonFaultPresence(String response) {
        try {
            Api5JsonErrorContainer responseDeserialized = JsonUtils.getObject(response, Api5JsonErrorContainer.class);
            Api5JsonError error = responseDeserialized.getError();
            if (error.getErrorCode() != null) {
                throw error;
            }
        } catch (JsonSyntaxException e) {
            log.debug("Non parsable response error");
        }
    }

    private void checkFaultPresence(String response, HttpMessage message, ProtocolType protocolType) {
        final String contentType = message.getResponseHeaderMap().get("Content-Type");
        if (protocolType.equals(ProtocolType.JSON) && contentType.equals("text/json")) {
            checkJsonFaultPresence(response);
        } else if (protocolType.equals(ProtocolType.XML) && contentType.equals("text/xml")) {
            try {
                JAXBContext jaxbContext = JAXBContext.newInstance(ReportDownloadError.class);
                Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

                StringReader reader = new StringReader(response);
                ReportDownloadError error = (ReportDownloadError) unmarshaller.unmarshal(reader);

                ApiError apiError = error.getApiError();
                if (apiError != null && apiError.getErrorCode() != 0) {
                    Api5XmlError api5XmlError = new Api5XmlError();
                    api5XmlError.setErrorCode(apiError.getErrorCode());
                    api5XmlError.setErrorMessage(apiError.getErrorMessage());
                    api5XmlError.setErrorDetail(apiError.getErrorDetail());
                    throw api5XmlError;
                }
            } catch (JAXBException e) {
                log.debug("Non parsable response error");
            }
        }
    }

    /*
        Class for unwrapping error from api5 reports failure response
     */
    private static class Api5JsonErrorContainer {
        @SerializedName("error")
        private Api5JsonError error;

        public Api5JsonError getError() {
            return error;
        }

        public void setError(Api5JsonError error) {
            this.error = error;
        }
    }

}
