package ru.yandex.wmconsole.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import ru.yandex.webmaster.common.urltree.YandexSearchShard;
import ru.yandex.wmconsole.data.MainMirrorStateEnum;
import ru.yandex.wmconsole.data.info.SerpLinkInfo;
import ru.yandex.wmconsole.data.info.XMLSerpLinkInfo;
import ru.yandex.wmconsole.util.WwwUtil;
import ru.yandex.wmconsole.util.XMLSearchRequests;
import ru.yandex.wmtools.common.Constants;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.data.xmlsearch.XmlSearchRequest;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.servantlet.AbstractServantlet;
import ru.yandex.wmtools.common.service.IService;
import ru.yandex.wmtools.common.service.XPathXmlSearchResultParser;
import ru.yandex.wmtools.common.service.XmlSearchService;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

/**
 * @author Andrey Mima (amima@yandex-team.ru)
 */
public class SerpLinksService implements IService, Constants {
    private static final Logger log = LoggerFactory.getLogger(SerpLinksService.class);

    private static final String REQUEST_DELIMITER = "?";

    private static final String PARAM_ACTION = "action";
    private static final String PARAM_HOST = "host";
    private static final String PARAM_PAGE = "page";
    private static final String PARAM_NAME = "name";

    private static final String TAG_HOST = "host";
    private static final String TAG_PAGE = "page";
    private static final String TAG_NAME = "name";
    private static final String ATTR_URL = "url";
    private static final String ATTR_SORTBY = "sortby";
    private static final String ATTR_BLOCK = "block";
    private static final String ATTR_WEIGHT = "weight";
    private static final String ATTR_HIDING_REASON = "hiding-reason";

    private String host;
    private String path;
    private XmlSearchService xmlSearchService;
    private XmlSearchService uaXmlSearchService;
    private XmlSearchService comXmlSearchService;
    private XmlSearchService trXmlSearchService;

    private CloseableHttpClient httpClient;

    private enum Action {
        VIEW("view"),
        EDIT("edit"),
        SECTIONS("sections"),
        INFO("info");

        private final String value;

        private Action(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    private enum Edit {
        BLOCK("block"),
        REMOVE("remove"),
        SORT("sortby");

        private final String value;

        private Edit(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    public enum Sort {
        ALPHA("alpha"),
        WEIGHT("weight");

        private final String value;

        private Sort(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    public void init() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(2000)
                .setConnectTimeout(5000)
                .setSocketTimeout(20000)
                .build();

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

    public void destroy() {
        try {
            httpClient.close();
        } catch (IOException e) {
            log.error("Unable to close httpClient", e);
        }
    }

    public boolean isSerpLinksPresent(String hostName) throws InternalException {
        Document responce = serpLinksRequest(Action.VIEW, new String[]{PARAM_HOST}, new String[]{hostName});
        NodeList upLevel = responce.getDocumentElement().getFirstChild().getChildNodes();
        return upLevel.getLength() != 0;
    }

    public List<XMLSerpLinkInfo> getXMLSearchSerpLinkInfo(String sectionName, YandexSearchShard searchShard) throws InternalException, UserException {
        URL sectionUrl = AbstractServantlet.prepareUrl(sectionName, true);
        String sectionNameWithScheme = sectionUrl.toString();
        List<XMLSerpLinkInfo> result = new ArrayList<XMLSerpLinkInfo>();
        XmlSearchRequest request = XMLSearchRequests.createSerpLinks(sectionName);
        final String XML_SERPLINKS_XPATH =
                "/yandexsearch/response/results/grouping/group/doc[url='%1$s/' or url='%1$s']/properties/Snippet";
        final String path = String.format(XML_SERPLINKS_XPATH, sectionNameWithScheme);

        final XmlSearchService actualXmlSearchService;
        switch (searchShard) {
            case UA:
                actualXmlSearchService = uaXmlSearchService;
                break;
            case COM:
                actualXmlSearchService = comXmlSearchService;
                break;
            case COM_TR:
                actualXmlSearchService = trXmlSearchService;
                break;
            default:
                actualXmlSearchService = xmlSearchService;
                break;
        }

        Node serpLinks = actualXmlSearchService.callXMLSearch(request, new XPathXmlSearchResultParser(path));
        if (serpLinks != null) {
            String serpLinksJson = serpLinks.getTextContent();
            if (serpLinksJson == null) {
                return result;
            } else {
                log.debug(serpLinksJson);
            }
            JSONObject jsonSnippet;
            try {
                jsonSnippet = new JSONObject(serpLinksJson);
                Object arrObj = jsonSnippet.has("sitelinks") ? jsonSnippet.get("sitelinks") : null;
                if (arrObj instanceof JSONArray) {
                    JSONArray jsonSerpLinks = (JSONArray) arrObj;
                    for (int i = 0; i < jsonSerpLinks.length(); i++) {
                        JSONArray link = jsonSerpLinks.getJSONArray(i);
                        if (link.length() != 2) {
                            throw new InternalException(
                                    InternalProblem.PROCESSING_ERROR, "Bad JSONArray length " + serpLinksJson);
                        }
                        // обходим баг в XML-отчете для https%3A//
                        String rawUrl = link.getString(0);
                        if (rawUrl.startsWith("https%3A//")) {
                            rawUrl = rawUrl.replaceFirst("https%3A//", "https://");
                        } else if (rawUrl.startsWith("http%3A//")) {
                            rawUrl = rawUrl.replaceFirst("http%3A//", "http://");
                        }
                        String url = StringEscapeUtils.escapeXml10(AbstractServantlet.prepareUrl(rawUrl, true).toString());
                        String name = StringEscapeUtils.escapeXml10(link.getString(1));
                        result.add(new XMLSerpLinkInfo(name, url));
                    }
                }
            } catch (JSONException e) {
                throw new InternalException(InternalProblem.PROCESSING_ERROR, "Can't parse json " + serpLinksJson, e);
            }
        }
        return result;
    }

    public SerpLinkInfo getSerpLinkInfo(String hostName) throws InternalException {
        Document response = serpLinksRequest(Action.INFO, new String[]{PARAM_HOST}, new String[]{hostName});
        boolean isAlphaSort = response.getDocumentElement().getFirstChild().getAttributes().getNamedItem(ATTR_SORTBY) != null;
        NodeList upLevel = response.getDocumentElement().getFirstChild().getChildNodes();
        List<SerpLinkInfo.SerpLinksPage> pages = new ArrayList<SerpLinkInfo.SerpLinksPage>();
        for (int i = 0; i < upLevel.getLength(); i++) {
            Node pageLevelNode = upLevel.item(i);
            if (TAG_PAGE.equals(pageLevelNode.getNodeName())) {
                SerpLinkInfo.HideStateEnum hideState = SerpLinkInfo.HideStateEnum.NOT_HIDDEN;
                Node hidingReasonAttr = pageLevelNode.getAttributes().getNamedItem(ATTR_HIDING_REASON);
                if (hidingReasonAttr != null) {
                    hideState = SerpLinkInfo.HideStateEnum.getByValue(hidingReasonAttr.getNodeValue());
                }
                SerpLinkInfo.SerpLinksPage serpLink = new SerpLinkInfo.SerpLinksPage(
                        processIDN(urlDecode(pageLevelNode.getAttributes().getNamedItem(ATTR_URL).getNodeValue())),
                        null, pageLevelNode.getAttributes().getNamedItem(ATTR_BLOCK) != null,
                        Float.valueOf(pageLevelNode.getAttributes().getNamedItem(ATTR_WEIGHT).getNodeValue()),
                        hideState, SerpLinkInfo.ModificationStatusEnum.NOT_CHANGED);
                NodeList secondLevel = pageLevelNode.getChildNodes();
                for (int j = 0; j < secondLevel.getLength(); j++) {
                    Node nameLevelNode = secondLevel.item(j);
                    if (TAG_NAME.equals(nameLevelNode.getNodeName())) {
                        String weight = nameLevelNode.getAttributes().getNamedItem(ATTR_WEIGHT).getNodeValue();
                        String name = nameLevelNode.getTextContent();
                        serpLink.addPageName(new SerpLinkInfo.SerpLinksPageName(name, Float.valueOf(weight), SerpLinkInfo.ModificationStatusEnum.NOT_CHANGED));
                    }
                }
                pages.add(serpLink);
            }
        }

        return new SerpLinkInfo(false, isAlphaSort, pages);
    }

    public List<String> getSections(final String hostName) throws InternalException {
        Document response = serpLinksRequest(Action.SECTIONS, new String[]{PARAM_HOST}, new String[]{hostName});
        NodeList list = response.getDocumentElement().getChildNodes();
        List<String> results = new LinkedList<String>();
        for (int i = 0; i < list.getLength(); i++) {
            Node host = list.item(i);
            if (TAG_HOST.equals(host.getNodeName())) {
                String url = host.getAttributes().getNamedItem(ATTR_URL).getNodeValue();
                results.add(WwwUtil.getHostName(url, MainMirrorStateEnum.WITHOUT_WWW));
            }
        }
        // Искусственно добавляем корень сайта в список разделов, так как в новой базе сайтлинки для корня могут
        // отсутствовать, а в старой -- присутствовать. Для корня всегда хотим показывать текущее состояние в поиске.

        if (!results.contains(WwwUtil.getHostName(hostName, MainMirrorStateEnum.WITHOUT_WWW))) {
            results.add(WwwUtil.getHostName(hostName, MainMirrorStateEnum.WITHOUT_WWW));
        }
        return results;
    }

    private String processIDN(String urlString) {
        try {
            URL url = SupportedProtocols.getURL(urlString);
            String decodedHost = IDN.toUnicode(url.getHost());
            if (!decodedHost.equals(url.getHost())) {
                return urlString.replace(url.getHost(), decodedHost);
            }
        } catch (MalformedURLException e) {
            // ignore
        } catch (URISyntaxException e) {
            // ignore
        } catch (SupportedProtocols.UnsupportedProtocolException e) {
            // ignore
        }
        return urlString;
    }

    private String urlDecode(final String urlString) {
        try {
            final String decodedUrl = URLDecoder.decode(urlString, "UTF-8");
            // check syntax
            if (!AbstractServantlet.isValid(SupportedProtocols.getURL(decodedUrl))) {
                return urlString;
            }
            return decodedUrl;
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to decode " + urlString + " because unsupported encoding exception raised", e);
            return urlString;
        } catch (MalformedURLException e) {
            log.error("Unable to decode " + urlString + " because malform url exception raised", e);
            return urlString;
        } catch (SupportedProtocols.UnsupportedProtocolException e) {
            log.error("Unable to decode " + urlString + " because unsupported protocol exception", e);
            return urlString;
        } catch (URISyntaxException e) {
            log.error("Unable to decode " + urlString + " because URI syntax exception raised", e);
            return urlString;
        }
    }

    public void blockAll(String hostName) throws InternalException {
        serpLinksRequest(Action.EDIT, new String[]{PARAM_HOST, Edit.BLOCK.getValue()}, new String[]{hostName, "true"});
    }

    public void unblockAll(String hostName) throws InternalException {
        serpLinksRequest(Action.EDIT, new String[]{PARAM_HOST, Edit.BLOCK.getValue()}, new String[]{hostName, "false"});
    }

    public void remove(String hostName, Collection<String> urlsToBlock) throws InternalException {
        for (String url : urlsToBlock) {
            remove(hostName, url);
        }
    }

    public void unremove(String hostName, Collection<String> urlsToUnblock) throws InternalException {
        for (String url : urlsToUnblock) {
            unremove(hostName, url);
        }
    }

    public void sort(String hostName, Sort sortType) throws InternalException {
        serpLinksRequest(Action.EDIT, new String[]{PARAM_HOST, Edit.SORT.getValue()}, new String[]{hostName, sortType.getValue()});
    }

    public void remove(String hostName, String page) throws InternalException {
        serpLinksRequest(Action.EDIT, new String[]{PARAM_HOST, PARAM_PAGE, Edit.REMOVE.getValue(), "check"}, new String[]{hostName, page, "true", "strict"});
    }

    public void unremove(String hostName, String page) throws InternalException {
        serpLinksRequest(Action.EDIT, new String[]{PARAM_HOST, PARAM_PAGE, Edit.REMOVE.getValue(), "check"}, new String[]{hostName, page, "false", "strict"});
    }

    public void rename(String hostName, String page, String name) throws InternalException {
        try {
            name = URLEncoder.encode(name, "cp1251");
            serpLinksRequest(Action.EDIT, new String[]{PARAM_HOST, PARAM_PAGE, PARAM_NAME, "check"}, new String[]{hostName, page, name, "strict"});
        } catch (UnsupportedEncodingException e) {
            //ignore
        }
    }

    private Document serpLinksRequest(Action action, String[] params, String[] values) throws InternalException {
        if (params != null && values != null && params.length != values.length) {
            //todo use runtime exception here?
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Array assertion problem");
        }

        String request = PARAM_ACTION + "=" + action.getValue();

        if (params != null && values != null) {
            for (int i = 0; i < params.length; i++) {
                String param = params[i];
                String value = values[i];
                request += "&" + param + "=" + value;
            }
        }

        return requestHttpGet(request);
    }

    private Document requestHttpGet(String request) throws InternalException {
        String httpGetRequest = HTTP_PREFIX + host + path + REQUEST_DELIMITER + request;
        return getDocumentNew(httpGetRequest);
    }

    private Document getDocumentNew(String httpGetRequest) throws InternalException {
        try (CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(httpGetRequest))) {
            StatusLine statusLine = httpResponse.getStatusLine();
            if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
                log.error("Serplinks http status returned not 200, but " + statusLine);
                throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "Serplinks http get status not 200");
            }
            HttpEntity entity = httpResponse.getEntity();
            if (entity == null) {
                log.error("Bad serplinks xml responce");
                throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "Bad serplinks xml responce");
            }
            DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            try (InputStream content = entity.getContent()) {
                return documentBuilder.parse(content);
            }
        } catch (ClientProtocolException e) {
            log.error("Http get request error", e);
            throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "Http get request error", e);
        } catch (IOException e) {
            log.error("Unable to read response", e);
            throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "Unable to read response", e);
        } catch (ParserConfigurationException e) {
            log.error("Unable to create xml parser", e);
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Unable to create xml parser", e);
        } catch (SAXException e) {
            log.error("Bad serplinks xml responce");
            throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "Bad serplinks xml responce");
        }
    }

    @Required
    public void setHost(String host) {
        this.host = host;
    }

    @Required
    public void setPath(String path) {
        this.path = path;
    }

    @Required
    public void setXmlSearchService(XmlSearchService xmlSearchService) {
        this.xmlSearchService = xmlSearchService;
    }

    @Required
    public void setUaXmlSearchService(XmlSearchService uaXmlSearchService) {
        this.uaXmlSearchService = uaXmlSearchService;
    }

    @Required
    public void setComXmlSearchService(XmlSearchService comXmlSearchService) {
        this.comXmlSearchService = comXmlSearchService;
    }

    @Required
    public void setTrXmlSearchService(XmlSearchService trXmlSearchService) {
        this.trXmlSearchService = trXmlSearchService;
    }
}
