package ru.yandex.wmconsole.viewer.originals;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;

import ru.yandex.webmaster.common.util.xml.SimpleXmlBuilder;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.error.ClientException;
import ru.yandex.wmconsole.error.ClientProblem;
import ru.yandex.wmconsole.util.PageUtils;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

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

    private static final int DEFAULT_PAGE_SIZE = 10;

    private static final int DEFAULT_MIN_TEXT_LENGTH = 500;
    private static final int DEFAULT_MAX_TEXT_LENGTH = 32000;

    private URI originalsApiUrl;
    private boolean prettyPrint = false;

    private CloseableHttpClient httpClient;
    private int connectionTTLSeconds = 10;
    private int socketTimeoutMillis = (int) TimeUnit.SECONDS.toMillis(5);
    private int connectionTimeoutMillis = (int) TimeUnit.SECONDS.toMillis(1);


    public void init() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectionTimeoutMillis)
                .setSocketTimeout(socketTimeoutMillis)
                .setRedirectsEnabled(false)
                .build();

        ConnectionConfig connectionConfig = ConnectionConfig.custom()
                .setCharset(StandardCharsets.UTF_8)
                .build();

        httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionTimeToLive(connectionTTLSeconds, TimeUnit.SECONDS)
                .setDefaultConnectionConfig(connectionConfig)
                .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
                .setMaxConnTotal(16)
                .setMaxConnPerRoute(16)
                .build();
    }

    public void destroy() {
        IOUtils.closeQuietly(httpClient);
    }

    public OriginalsResponse listTexts(BriefHostInfo briefHostInfo, PageUtils.Pager pager) throws ClientException {
        String hostName = briefHostInfo.getName();
        int pageFrom = getFrom(pager);
        UriComponents requestUri = createRequestUri(hostName, "getReport")
                .queryParam("from", pageFrom)
                .build();
        String requestXml = createListTextsXmlRequest(hostName, pageFrom, DEFAULT_PAGE_SIZE);

        HttpPost post = getHttpPost(requestUri, requestXml);
        try (CloseableHttpResponse httpResponse = httpClient.execute(post)) {
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                log.error("Unable to load original texts: host=" + briefHostInfo.getName() + ", status=" + statusCode);
                throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to load original text");
            }

            OriginalsResponse originalsResponse = parseOriginalTextsXml(httpResponse);
            if (originalsResponse.isErrorState()) {
                log.error("Unable to load original texts: host=" + briefHostInfo.getName() + ", errorState=true");
                throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to load original text");
            }
            return originalsResponse;
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to load original texts: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to load original text", e);
        } catch (ParserConfigurationException e) {
            log.error("Unable to load original texts: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to load original text", e);
        } catch (SAXException e) {
            log.error("Unable to load original texts: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to load original text", e);
        } catch (IOException e) {
            log.error("Unable to load original texts: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to load original text", e);
        }
    }

    private OriginalsResponse parseOriginalTextsXml(HttpResponse httpResponse)
            throws IOException, ParserConfigurationException, SAXException
    {
        WswDataHandler handler = new WswDataHandler();
        parseHttpContent(httpResponse, handler);
        return handler.getResponse();
    }

    private void parseHttpContent(HttpResponse httpResponse, DefaultHandler2 handler) throws
            IOException, ParserConfigurationException, SAXException
    {
        HttpEntity entity = httpResponse.getEntity();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        entity.writeTo(byteArrayOutputStream);
        String responseXmlStr = new String(byteArrayOutputStream.toByteArray());
        SAXParserFactory parserFactory = SAXParserFactory.newInstance();
        parserFactory.setNamespaceAware(true);
        parserFactory.setValidating(false);
        parserFactory.setXIncludeAware(false);

        SAXParser parser = parserFactory.newSAXParser();
        parser.parse(new InputSource(new StringReader(responseXmlStr)), handler);
    }

    public OriginalsResponse addText(BriefHostInfo briefHostInfo, String text) throws ClientException {
        UriComponents requestUri = createRequestUri(briefHostInfo.getName(), "saveData").build();
        String requestXml = createSaveTextXmlRequest(briefHostInfo, text);

        HttpPost post = getHttpPost(requestUri, requestXml);

        try (CloseableHttpResponse httpResponse = httpClient.execute(post)) {
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                log.error("Unable to save original text: host=" + briefHostInfo.getName() + ", status=" + statusCode);
                throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to save original text");
            }

            OriginalsResponse originalsResponse = parseOriginalTextsXml(httpResponse);
            switch (originalsResponse.getError()) {
                case TEXT_ALREADY_ADDED:
                    log.error("Unable to save original text: host=" + briefHostInfo.getName() + ", text already added");
                    throw new ClientException(ClientProblem.ORIGINALS_TEXT_ALREADY_ADDED, "Original text already added");

                case UNKNOWN_ERROR:
                    log.error("Unable to save original text due to error: host=" + briefHostInfo.getName());
                    throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to save original text");
            }

            return originalsResponse;
        } catch (SAXException e) {
            log.error("Unable to save original text: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to save original text", e);
        } catch (ParserConfigurationException e) {
            log.error("Unable to save original text: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to save original text", e);
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to save original text: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to save original text", e);
        } catch (IOException e) {
            log.error("Unable to save original text: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to save original text", e);
        }
    }

    public OriginalTextLimits getLimits(BriefHostInfo briefHostInfo) throws ClientException {
        UriComponents requestUri = createRequestUri(briefHostInfo.getName(), "getSettings").build();
        HttpPost httpPost = getHttpPost(requestUri, "");
        try (CloseableHttpResponse httpResponse = httpClient.execute(httpPost)) {
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                log.error("Unable to get settings: host=" + briefHostInfo.getName() + ", status=" + statusCode);
                throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to get settings");
            }
            return parseLimits(httpResponse);
        } catch (ClientProtocolException e) {
            log.error("Unable to get settings: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to get settings", e);
        } catch (IOException e) {
            log.error("Unable to get settings: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to get settings", e);
        } catch (ParserConfigurationException e) {
            log.error("Unable to get settings: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to get settings", e);
        } catch (SAXException e) {
            log.error("Unable to get settings: host=" + briefHostInfo.getName(), e);
            throw new ClientException(ClientProblem.INTERNAL_ERROR, "Unable to get settings", e);
        }
    }

    protected OriginalTextLimits parseLimits(HttpResponse httpResponse) throws ParserConfigurationException,
            SAXException, IOException
    {
        SettingsParser handler = new SettingsParser();
        parseHttpContent(httpResponse, handler);
        return handler.getLimits();
    }

    public void deleteText(BriefHostInfo briefHostInfo, String baseId) throws IOException {
        UriComponents requestUri = createRequestUri(briefHostInfo.getName(), "deleteTask").build();
        String requestXml = createDeleteTextXmlRequest(briefHostInfo, baseId);

        HttpPost post = getHttpPost(requestUri, requestXml);
        try (CloseableHttpResponse response = httpClient.execute(post)) {
            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                throw new RuntimeException("Unable to delete original text: " + briefHostInfo.getName());
            }
            EntityUtils.consume(response.getEntity());
        }
    }

    private HttpPost getHttpPost(UriComponents requestUri, String requestXml) {
        HttpPost post = new HttpPost(requestUri.toUri());
        NameValuePair nameValuePair = new BasicNameValuePair("wsw-fields", requestXml);
        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(Collections.singletonList(nameValuePair), StandardCharsets.UTF_8);
        post.setEntity(formEntity);
        return post;
    }

    private UriComponentsBuilder createRequestUri(String hostName, String operation) {
        return UriComponentsBuilder.fromUri(originalsApiUrl)
                .queryParam("operation", operation)
                .queryParam("hosts", hostName)
                .queryParam("host", hostName)
                .queryParam("from-service", "WEBMASTER_API");
    }

    private int getFrom(PageUtils.Pager pager) {
        return pager.toRangeStart() + 1;
    }

    private String createListTextsXmlRequest(String host, int pageFrom, int pageSize) {
        SimpleXmlBuilder xml = new SimpleXmlBuilder(prettyPrint);
        xml.open("wsw-fields").attribute("pager-from", pageFrom).attribute("pager-items", pageSize);

        xml.open("wsw-field").attribute("name", "host");
        xml.element("wsw-value", host);
        xml.close(); //wsw-field

        xml.close(); //wsw-fields

        return xml.finish();
    }

    private String createSaveTextXmlRequest(BriefHostInfo briefHostInfo, String text) {
        SimpleXmlBuilder xml = new SimpleXmlBuilder(prettyPrint);
        xml.open("wsw-fields").attribute("pager-items", 1);

        xml.open("wsw-field").attribute("name", "host");
        xml.element("wsw-value", briefHostInfo.getName());
        xml.close(); // wsw-field

        xml.open("wsw-field")
                .attribute("name", "Original_text")
                .attribute("new", true);
        xml.element("wsw-value", text);
        xml.close(); // wsw-field

        xml.close(); // wsw-fields
        return xml.finish();
    }

    private String createDeleteTextXmlRequest(BriefHostInfo briefHostInfo, String baseId) {
        SimpleXmlBuilder xml = new SimpleXmlBuilder(prettyPrint);
        xml.open("wsw-fields");

        xml.open("wsw-field").attribute("name", "host");
        xml.element("wsw-value", briefHostInfo.getName());
        xml.close(); // wsw-field

        xml.open("wsw-field")
                .attribute("name", "Original_text")
                .attribute("deleted", true)
                .attribute("base-id", baseId);
        xml.element("wsw-value", "");
        xml.close(); // wsw-field

        xml.close(); // wsw-fields
        return xml.finish();
    }

    private static class WswDataHandler extends DefaultHandler2 {
        private boolean addData = false;
        private int totalTexts = 0;

        private List<OriginalText> originalTexts = new ArrayList<OriginalText>(DEFAULT_PAGE_SIZE);

        private boolean inOriginalTextField = false;
        private String currentBaseId = null;
        private StringBuilder textBuilder = null;
        private OriginalsResponse.OriginalsError errorState = OriginalsResponse.OriginalsError.UNKNOWN_ERROR;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException
        {
            if ("wsw-error".equals(localName)) {
                String errorCode = attributes.getValue("code");
                if ("text-already-added".equals(errorCode)) {
                    errorState = OriginalsResponse.OriginalsError.TEXT_ALREADY_ADDED;
                } else {
                    errorState = OriginalsResponse.OriginalsError.UNKNOWN_ERROR;
                }
            } else if ("wsw-data".equals(localName)) {
                addData = Boolean.parseBoolean(attributes.getValue("add-data"));
                errorState = OriginalsResponse.OriginalsError.NO_ERROR;
            } else if ("wsw-fields".equals(localName)) {
                String pageName = attributes.getValue("page");
                if (!"Originals-submission-form".equals(pageName)) {
                    return;
                }
                totalTexts = Integer.parseInt(attributes.getValue("pager-count"));
            } else if ("wsw-field".equals(localName)) {
                String pageName = attributes.getValue("name");
                if (!"Original_text".equals(pageName)) {
                    return;
                }

                String baseId = attributes.getValue("base-id");
                if (!StringUtils.isEmpty(baseId)) {
                    currentBaseId = baseId;
                }
            } else if ("wsw-value".equals(localName)) {
                if (currentBaseId != null) {
                    textBuilder = new StringBuilder();
                    inOriginalTextField = true;
                }
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if ("wsw-value".equals(localName)) {
                if (inOriginalTextField && textBuilder != null) {
                    String text = textBuilder.toString();
                    originalTexts.add(new OriginalText(currentBaseId, text));
                }
                inOriginalTextField = false;
                textBuilder = null;
            } else if ("wsw-field".equals(localName)) {
                currentBaseId = null;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (inOriginalTextField) {
                textBuilder.append(ch, start, length);
            }
        }

        public OriginalsResponse getResponse() {
            return new OriginalsResponse(totalTexts, addData, originalTexts, errorState);
        }
    }

    private static class SettingsParser extends DefaultHandler2 {
        private State state = State.UNKNOWN;
        private int minTextLength;
        private int maxTextLength;
        private int textsAdded;
        private int textsCurrentLimit;
        private StringBuilder value;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws
                SAXException
        {
            if (!"wsw-value".equals(localName)) {
                return;
            }

            state = State.getState(attributes.getValue("name"));
            if (state != State.UNKNOWN) {
                value = new StringBuilder();
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (state == State.UNKNOWN || value == null) {
                return;
            }
            value.append(ch, start, length);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (state == State.UNKNOWN || value == null) {
                return;
            }
            String valueStr = StringUtils.trimToEmpty(value.toString());
            try {
                switch (state) {
                    case MIN_TEXT_LENGTH:
                        minTextLength = Integer.parseInt(valueStr);
                        break;
                    case MAX_TEXT_LENGTH:
                        maxTextLength = Integer.parseInt(valueStr);
                        break;
                    case TEXTS_ADDED:
                        textsAdded = Integer.parseInt(valueStr);
                        break;
                    case TEXTS_CURRENT_LIMIT:
                        textsCurrentLimit = Integer.parseInt(valueStr);
                        break;
                }
            } catch (NumberFormatException e) {
                log.error("Unable to parse limits: state=" + state + " value=" + valueStr, e);
            }
            value = null;
            state = State.UNKNOWN;
        }

        public OriginalTextLimits getLimits() {
            if (minTextLength < 0 || maxTextLength < 0 || maxTextLength <= minTextLength) {
                return new OriginalTextLimits(DEFAULT_MIN_TEXT_LENGTH, DEFAULT_MAX_TEXT_LENGTH, textsAdded, textsCurrentLimit);
            }
            return new OriginalTextLimits(minTextLength, maxTextLength, textsAdded, textsCurrentLimit);
        }

        private static enum State {
            UNKNOWN,
            MIN_TEXT_LENGTH,
            MAX_TEXT_LENGTH,
            TEXTS_ADDED,
            TEXTS_CURRENT_LIMIT,
            ;

            public static State getState(String name) {
                if (StringUtils.isEmpty(name)) {
                    return UNKNOWN;
                }

                switch(name) {
                    case "min-text-length": return MIN_TEXT_LENGTH;
                    case "max-text-length": return MAX_TEXT_LENGTH;
                    case "texts-added": return TEXTS_ADDED;
                    case "texts-current-limit": return TEXTS_CURRENT_LIMIT;
                }
                return UNKNOWN;
            }
        }
    }

    @Required
    public void setOriginalsApiUrl(URI originalsApiUrl) {
        this.originalsApiUrl = originalsApiUrl;
    }

    public void setPrettyPrint(boolean prettyPrint) {
        this.prettyPrint = prettyPrint;
    }

    public void setConnectionTTLSeconds(int connectionTTLSeconds) {
        this.connectionTTLSeconds = connectionTTLSeconds;
    }

    public void setSocketTimeoutMillis(int socketTimeoutMillis) {
        this.socketTimeoutMillis = socketTimeoutMillis;
    }

    public void setConnectionTimeoutMillis(int connectionTimeoutMillis) {
        this.connectionTimeoutMillis = connectionTimeoutMillis;
    }
}
