package ru.yandex.webmaster3.storage.turbo.service.preview;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;

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

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.google.common.base.Charsets;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.http.HttpStatus;
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.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.HttpConstants;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.metrics.externals.AbstractExternalAPIService;
import ru.yandex.webmaster3.core.metrics.externals.ExternalDependencyMethod;
import ru.yandex.webmaster3.core.security.tvm.TVMTokenService;
import ru.yandex.webmaster3.core.turbo.TurboConstants;
import ru.yandex.webmaster3.core.turbo.model.TurboHostSettings;
import ru.yandex.webmaster3.core.turbo.model.app.TurboAppSettings;
import ru.yandex.webmaster3.core.turbo.model.desktop.TurboDesktopSettings;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedSettings;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedType;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.JavaMethodWitness;
import ru.yandex.webmaster3.core.util.WwwUtil;
import ru.yandex.webmaster3.core.util.XmlUtil;
import ru.yandex.webmaster3.core.util.xml.PositionalXMLReader;
import ru.yandex.webmaster3.storage.turbo.dao.TurboDomainsStateCHDao;
import ru.yandex.webmaster3.storage.turbo.dao.TurboPreviewCacheYDao;
import ru.yandex.webmaster3.storage.turbo.dao.app.TurboAppSettingsYDao;
import ru.yandex.webmaster3.storage.turbo.service.TurboDomainsStateService.TurboDomainState;
import ru.yandex.webmaster3.storage.turbo.service.TurboFeedRawStatsData;
import ru.yandex.webmaster3.storage.turbo.service.TurboFeedsService;
import ru.yandex.webmaster3.storage.turbo.service.settings.TurboSettingsService;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

import static ru.yandex.webmaster3.core.turbo.TurboConstants.PUB_DATE_FORMAT;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_CATEGORIES;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_CATEGORY;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_CATEGORY_ID;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_CHANNEL;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_DESCRIPTION;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_ITEM;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_LINK;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_OFFER;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_OFFERS;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_PUD_DATE;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_RSS;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_SHOP;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_TITLE;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_URL;
import static ru.yandex.webmaster3.core.turbo.TurboConstants.TAG_YML;

/**
 * Created by Oleg Bazdyrev on 02/10/2017.
 */
@Service("turboFeedPreviewService")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class TurboFeedPreviewService extends AbstractExternalAPIService {

    private static final Logger log = LoggerFactory.getLogger(TurboFeedPreviewService.class);
    private static final int socketTimeoutMs = 60000;
    private static final int connectTimeoutMs = HttpConstants.DEFAULT_CONNECT_TIMEOUT;
    private static final ObjectMapper OM = new ObjectMapper()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .registerModule(new ParameterNamesModule());
    private static final long DEFAULT_CONTENT_TTL = TimeUnit.DAYS.toSeconds(1L);

    @Value("${webmaster3.storage.turbo.feedPreview.serviceUrl}")
    private String serviceUrl;
    private CloseableHttpClient httpClient;

    private final TurboAppSettingsYDao turboAppSettingsYDao;
    private final TurboDomainsStateCHDao turboDomainsStateCHDao;
    private final TurboFeedsService turboFeedsService;
    private final TurboPreviewCacheYDao turboPreviewCacheYDao;
    private final TurboSettingsService turboSettingsService;
    private final TVMTokenService turboPreviewTvmTokenService;

    public void init() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(socketTimeoutMs)
                .setConnectTimeout(connectTimeoutMs)
                .build();

        httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionTimeToLive(30, TimeUnit.SECONDS)
                .build();
    }

    /**
     * Загружает фид в SAAS, Аватарницу и прочие интересные места, и возвращает готовую статистику обхода
     */
    public TurboFeedRawStatsData uploadFeed(WebmasterHostId hostId, String url, TurboFeedType type, byte[] feedData) {
        return uploadFeed(hostId, url, type, feedData, null, null, null, DEFAULT_CONTENT_TTL);
    }

    /**
     * Создает превью фида по его содержимому
     */
    public TurboFeedRawStatsData uploadFeed(WebmasterHostId hostId, String url, TurboFeedType type, byte[] feedData,
                                            TurboHostSettings settings, TurboDesktopSettings ds, TurboAppSettings as, long contentTtl) {
        final String domain = WwwUtil.cutWWWAndM(hostId);
        final HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(url);
        builder.append(type);
        builder.append(feedData);
        try {
            builder.append(OM.writeValueAsString(settings));
            builder.append(OM.writeValueAsString(as));
            builder.append(OM.writeValueAsString(ds));
        } catch (JsonProcessingException e) {
            throw new WebmasterException("JSON expression", new WebmasterErrorResponse.UnableToReadJsonRequestResponse(getClass(), e), e);
        }
        int hashCode = builder.toHashCode();
        // поищем в кэше
        TurboFeedRawStatsData result = turboPreviewCacheYDao.get(domain, hashCode);
        if (result == null) {
            result = uploadFeedInternal(hostId, url, type, feedData, settings, ds, as, contentTtl);
            if (result != null) {
                turboPreviewCacheYDao.insert(domain, hashCode, result);
            }
        } else {
            log.info("Using cached preview version for domain {} and hash {}", domain, hashCode);
        }
        return result;
    }

    @ExternalDependencyMethod("upload-feed")
    private TurboFeedRawStatsData uploadFeedInternal(WebmasterHostId hostId, String url, TurboFeedType type, byte[] feedData,
                                                     TurboHostSettings settings, TurboDesktopSettings ds, TurboAppSettings as, long contentTtl) {
        return trackQuery(new JavaMethodWitness() {}, ALL_ERRORS_INTERNAL, () -> {
            String params;
            try {
                // генерируем
                String domain = WwwUtil.cutWWWAndM(hostId);
                TurboDomainState domainState = turboDomainsStateCHDao.getDomainState(domain);
                List<TurboFeedSettings> feeds = turboFeedsService.getFeeds(domain);
                var request = new TurboFeedPreviewRequest(hostId, url, type, settings, ds, as, domainState, feeds);
                params = OM.writeValueAsString(request);
            } catch (JsonProcessingException e) {
                log.error("Json error", e);
                throw new WebmasterException("Json error",
                        new WebmasterErrorResponse.UnableToReadJsonRequestResponse(getClass(), e), e);
            }

            HttpPost post = new HttpPost(serviceUrl + "/preview");
            post.setEntity(MultipartEntityBuilder.create()
                    .addPart("params", new StringBody(params, ContentType.APPLICATION_JSON))
                    .addPart("feed_data", new ByteArrayBody(feedData, ContentType.APPLICATION_XML, "feed_data"))
                    .addPart("content_ttl", new StringBody(Long.toString(contentTtl), ContentType.TEXT_PLAIN))
                    .build()
            );
            log.info("Uploading feed to turbo-rss-preview");
            log.debug("Params: {}", params);
            try (CloseableHttpResponse response = httpClient.execute(post)) {
                int code = response.getStatusLine().getStatusCode();
                log.info("Service turbo-rss-preview return http code {}", code);
                String content = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
                if (code == HttpStatus.SC_OK || code == HttpStatus.SC_UNPROCESSABLE_ENTITY) {
                    // log.debug(content);
                    // ок, можно парсить результат
                    TurboFeedRawStatsData result = OM.readValue(content, TurboFeedRawStatsData.class);
                    return result;
                } else {
                    String msg = "Service turbo-rss-preview return http code " + code;
                    log.warn(content);
                    throw new WebmasterException(msg, new WebmasterErrorResponse.TurboErrorResponse(getClass(), null, msg));
                }
            } catch (ClientProtocolException e) {
                log.error("ClientProtocolException", e);
                throw new WebmasterException("ClientProtocolException",
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), "ClientProtocolException"), e);
            } catch (IOException e) {
                String msg = "Error when executing uploadFeed";
                log.error(msg, e);
                throw new WebmasterException(msg, new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), msg), e);
            }
        });
    }

    /**
     * Загружает настройки хоста при помощи спецручки сразу в SAAS
     * @param hostId
     * @param settings
     */
    @ExternalDependencyMethod("update-settins")
    public TurboHostSettingsUploadResponse uploadHostSettings(WebmasterHostId hostId, TurboHostSettings settings,
                                                              TurboDesktopSettings ds, TurboAppSettings as, boolean validateOnly) {
        log.info("Uploading settings for host {}", hostId);
        String params;
        try {
            String domain = WwwUtil.cutWWWAndM(hostId);
            if (settings == null) {
                // затянем турбо-настройки хоста, если их нет
                settings = turboSettingsService.getSettings(domain);
            }
            if (ds == null) {
                ds = turboSettingsService.getDesktopSettings(domain);
            }
            if (as == null) {
                as = turboAppSettingsYDao.getSettings(domain);
            }
            boolean hasYmlFeeds = turboFeedsService.getFeeds(domain).stream()
                    .anyMatch(tfs -> tfs.getType() == TurboFeedType.YML) || settings.getCommerceSettings() != null;
            // генерируем
            TurboDomainState domainState = turboDomainsStateCHDao.getDomainState(domain);
            List<TurboFeedSettings> feeds = turboFeedsService.getFeeds(domain);

            var request = new TurboHostSettingsUploadRequest(hostId, settings, ds, as, hasYmlFeeds, domainState, feeds);
            params = OM.writeValueAsString(request);
        } catch (WebmasterYdbException e) {
            log.error("Ydb error", e);
            throw new WebmasterException("Cassandra error",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        } catch (JsonProcessingException e) {
            log.error("Json error", e);
            throw new WebmasterException("Json error",
                    new WebmasterErrorResponse.UnableToReadJsonRequestResponse(getClass(), e), e);
        }
        return trackQuery(new JavaMethodWitness() {}, ALL_ERRORS_INTERNAL, () -> {

            log.debug("Request: {}", params);

            HttpPost post = new HttpPost(serviceUrl + "/host-data" + (validateOnly ? "?validate_only=1" : ""));
            post.setEntity(new StringEntity(params, ContentType.APPLICATION_JSON));
            // подпишем наш запрос
            post.addHeader(TVMTokenService.TVM2_TICKET_HEADER, turboPreviewTvmTokenService.getToken());

            try (CloseableHttpResponse response = httpClient.execute(post)) {
                int code = response.getStatusLine().getStatusCode();
                log.info("Service turbo-host-data return http code {}", code);
                if (code == HttpStatus.SC_OK || code == HttpStatus.SC_UNPROCESSABLE_ENTITY) {
                    String content = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
                    log.debug(content);
                    TurboHostSettingsUploadResponse result = OM.readValue(content, TurboHostSettingsUploadResponse.class);
                    return result;
                } else {
                    String msg = "Service turbo-host-data return http code " + code;
                    throw new WebmasterException(msg, new WebmasterErrorResponse.TurboErrorResponse(getClass(), null, msg));
                }
            } catch (ClientProtocolException e) {
                log.error("ClientProtocolException", e);
                throw new WebmasterException("ClientProtocolException",
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), "ClientProtocolException"), e);
            } catch (IOException e) {
                String msg = "Error when executing uploadHostSettings";
                log.error(msg, e);
                throw new WebmasterException(msg, new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), msg), e);
            }
        });
    }

    @Nullable
    public static byte[] createTurboRss(String content, WebmasterHostId hostId, MutableInt lineCorrection,
                                        String urlSuffix) {
        String previewLink = IdUtils.hostIdToUrl(hostId) + TurboConstants.SANDBOX_PREVIEW_PATH + urlSuffix;
        Document document;
        Element root = null;
        try {
            document = PositionalXMLReader.readXML(new ByteArrayInputStream(content.getBytes()));
            root = document.getDocumentElement();
        } catch (Exception e) {
            log.warn("Unable to parse xml. Interpreting as RSS item content", e);
            try {
                document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            } catch (ParserConfigurationException e1) {
                throw new RuntimeException(e1);
            }
        }
        // найдем корневой элемент
        Element rss;
        if (root != null && TAG_RSS.equals(root.getTagName())) {
            int startLineNumber = Integer.parseInt(root.getUserData("lineNumber").toString());
            log.info("Creating preview for full rss. Start line number: {}", startLineNumber);
            lineCorrection.setValue(startLineNumber - 2);
            rss = root;
        } else if (root != null && TAG_ITEM.equals(root.getTagName())) {
            int startLineNumber = Integer.parseInt(root.getUserData("lineNumber").toString());
            log.info("Creating preview only for item. Start line number: {}", startLineNumber);
            lineCorrection.setValue(startLineNumber - 3);
            // сгенерируем шапку
            rss = createRssTemplate(document);
            Element channel = createChannelTemplate(document, previewLink);
            rss.appendChild(channel);
            channel.appendChild(root.cloneNode(true));
        } else {
            // неизвестно, что - вставим как turbo:content
            log.error("Unknown root. Interpreting as turbo:content");
            lineCorrection.setValue(-1);
            // сгенерируем шапку (rss + channel)
            rss = createRssTemplate(document);
            Element channel = createChannelTemplate(document, previewLink);
            rss.appendChild(channel);
            Element item = createItemTemplate(document, previewLink, content);
            channel.appendChild(item);
        }
        // сериализуем в строку
        return XmlUtil.serializeDocument(rss);
    }

    @NotNull
    public static byte[] createTurboYml(String content, WebmasterHostId hostId, MutableInt lineCorrection,
                                        String urlSuffix) {
        Document document;
        Element root = null;
        try {
            document = PositionalXMLReader.readXML(new ByteArrayInputStream(content.getBytes()));
            root = document.getDocumentElement();
        } catch (Exception e) {
            log.warn("Unable to parse xml. Interpreting as yml_catalog", e);
            try {
                document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            } catch (ParserConfigurationException e1) {
                throw new RuntimeException(e1);
            }
        }
        // найдем корневой элемент
        Element yml;
        if (root != null && TAG_YML.equals(root.getTagName())) {
            int startLineNumber = Integer.parseInt(root.getUserData("lineNumber").toString());
            log.info("Creating preview for full yml. Start line number: {}", startLineNumber);
            lineCorrection.setValue(startLineNumber - 2);
            yml = root;
        } else if (root != null && TAG_OFFER.equals(root.getTagName())) {
            int startLineNumber = Integer.parseInt(root.getUserData("lineNumber").toString());
            log.info("Creating preview only for item. Start line number: {}", startLineNumber);
            lineCorrection.setValue(startLineNumber - 3);
            // сгенерируем шапку
            yml = createYmlTemplate(document);
            Element shop = document.createElement(TAG_SHOP);
            yml.appendChild(shop);
            shop.appendChild(createSimpleTextNode(document, TAG_URL, IdUtils.hostIdToUrl(hostId)));
            // фейковая категория, если надо
            NodeList offerCategories = root.getElementsByTagName(TAG_CATEGORY_ID);
            if (offerCategories.getLength() > 0) {
                String categoryId = getNodeTextContent(offerCategories.item(0));
                log.info("Found offer category {}", categoryId);
                Element categories = document.createElement(TAG_CATEGORIES);
                shop.appendChild(categories);
                Element category = document.createElement(TAG_CATEGORY);
                category.appendChild(document.createTextNode("Preview category"));
                category.setAttribute("id", categoryId);
                categories.appendChild(category);
            }

            Element offers = document.createElement(TAG_OFFERS);
            shop.appendChild(offers);
            offers.appendChild(document.createTextNode("\n"));
            offers.appendChild(root.cloneNode(true));
        } else {
            // неизвестно, что - вставим как turbo:content
            log.error("Unknown root. Interpreting as yml_catalog");
            lineCorrection.setValue(0);
            return content.getBytes();
        }
        // сериализуем в строку
        return XmlUtil.serializeDocument(yml);
    }

    private static Node createSimpleTextNode(Document document, String tagName, String value) {
        Element element = document.createElement(tagName);
        element.appendChild(document.createTextNode(value));
        return element;
    }

    private static String getNodeTextContent(Node node) {
        if (node == null || node.getChildNodes().getLength() == 0) {
            return null;
        }
        return node.getChildNodes().item(0).getTextContent();
    }

    private static Element createRssTemplate(Document document) {
        Element rss = document.createElement(TAG_RSS);
        rss.setAttribute("xmlns:yandex", "http://news.yandex.ru");
        rss.setAttribute("xmlns:turbo", "http://turbo.yandex.ru");
        rss.setAttribute("version", "2.0");
        return rss;
    }

    private static Element createYmlTemplate(Document document) {
        Element yml = document.createElement(TAG_YML);
        yml.setAttribute("date", "2017-02-05 17:22");
        return yml;
    }

    private static Element createChannelTemplate(Document document, String previewLink) {
        Element channel = document.createElement(TAG_CHANNEL);
        channel.appendChild(createSimpleTextNode(document, TAG_TITLE, "Turbo channel title example"));
        channel.appendChild(createSimpleTextNode(document, TAG_LINK, previewLink));
        channel.appendChild(createSimpleTextNode(document, TAG_DESCRIPTION, "Turbo channel description example"));
        channel.appendChild(document.createTextNode("\n")); // просто перевод строки для корректного подсчета позиции
        return channel;
    }

    private static Element createItemTemplate(Document document, String previewLink, String data) {
        Element item = document.createElement(TAG_ITEM);
        item.setAttribute("turbo", "true");
        item.appendChild(createSimpleTextNode(document, TAG_TITLE, "Turbo page title example"));
        item.appendChild(createSimpleTextNode(document, TAG_LINK, previewLink));
        item.appendChild(createSimpleTextNode(document, TAG_DESCRIPTION, "Turbo page description example"));
        item.appendChild(createSimpleTextNode(document, TAG_PUD_DATE, PUB_DATE_FORMAT.print(DateTime.now())));
        Element content = document.createElement("turbo:content");
        item.appendChild(content);
        CDATASection text = document.createCDATASection(data);
        content.appendChild(text);
        return item;
    }

}
