package ru.yandex.wmconsole.servantlet.xmlsearch;

import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URL;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import com.Ostermiller.util.CircularCharBuffer;
import org.jdom.Document;
import org.jdom.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import ru.yandex.common.framework.core.ServRequest;
import ru.yandex.common.framework.core.ServResponse;
import ru.yandex.common.framework.pager.Pager;
import ru.yandex.webmaster.common.urltree.YandexSearchShard;
import ru.yandex.wmconsole.data.LastDaysFilterEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.wrappers.OfflineLinksAvailableWrapper;
import ru.yandex.wmconsole.data.wrappers.OfflineLinksInfoWrapper;
import ru.yandex.wmconsole.servantlet.WMCAuthorizedHostOperationServantlet;
import ru.yandex.wmconsole.service.*;
import ru.yandex.wmconsole.service.dao.TblUrlTreesDao;
import ru.yandex.wmconsole.util.HostElementWrapper;
import ru.yandex.wmconsole.util.WwwUtil;
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.error.UserProblem;
import ru.yandex.wmtools.common.service.IndexInfoService;
import ru.yandex.wmtools.common.service.XSLTXmlSearchResultParser;
import ru.yandex.wmtools.common.util.TimeFilter;

public abstract class AbstractXmlSearchServantlet extends WMCAuthorizedHostOperationServantlet {
    private static final Logger log = LoggerFactory.getLogger(AbstractXmlSearchServantlet.class);

    private static final String ALL_PAGES_PATH = "*";

    private static final String PARAM_PATH = "path";
    private static final String PARAM_LAST = "last";
    private static final String PARAM_NEW_OFFLINE = "new-offline";

    private OfflineLinksService offlineLinksService;
    private IndexInfoService indexInfoService;
    private IndexInfoService uaIndexInfoService;
    private IndexInfoService comIndexInfoService;
    private IndexInfoService trIndexInfoService;
    private HostDbHostInfoService hostDbHostInfoService;
    protected LinksCacheService linksCacheService;
    protected TblUrlTreesDao tblUrlTreesDao;
    private ConsistentMainMirrorService consistentMainMirrorService;

    @Override
    public void doProcess(ServRequest req, ServResponse res, long userId) throws UserException, InternalException {
        final BriefHostInfo briefHostInfo = getHostInfoAndVerify(req, userId);
        final HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(briefHostInfo.getName());
        // Выполняем запрос и кэшируем результат в tbl_links_cache
        doXmlSearch(req, res, userId, true, briefHostInfo, hostDbHostInfo);
    }

    /**
     * Может использоваться в классах-наследниках для отключения кэширования
     */
    protected void doXmlSearch(ServRequest req, ServResponse res, long userId,
                               boolean saveLinksToCache,
                               final BriefHostInfo briefHostInfo,
                               final HostDbHostInfo hostDbHostInfo) throws InternalException, UserException {
        TimeFilter timeFilter = prepareTimeFilter(getIntParam(req, PARAM_LAST));

        // Проверяем, что сайт проиндексирован обычным роботом или быстророботом
        checkIndexed(briefHostInfo);

        String path = req.getParam(PARAM_PATH, true);
        path = preparePath(briefHostInfo, path);

        Pager pager = createOutputStrategy(req).createDefaultPager();

        XmlSearchRequest request = createRequest(briefHostInfo.getName(), path, timeFilter, pager, req);
        YandexSearchShard searchShard = getYandexSearchShard(hostDbHostInfo);

        final IndexInfoService countryIndexInfoService;
        switch(searchShard) {
            case UA: countryIndexInfoService = uaIndexInfoService; break;
            case COM: countryIndexInfoService = comIndexInfoService; break;
            case COM_TR: countryIndexInfoService = trIndexInfoService; break;

            default: countryIndexInfoService = indexInfoService;
        }

        Document response = countryIndexInfoService.callXMLSearch(request);

        if (saveLinksToCache) {
            cacheToDbDataOfResultDocument(hostDbHostInfo, path, timeFilter, response, searchShard);
        }

        try {
            final String resultString = createResultString(response, getDocsCount(req, briefHostInfo, timeFilter, path));

            res.addData(new HostElementWrapper(null, briefHostInfo) {
                @Override
                protected void doToXml(StringBuilder result) {
                    super.doToXml(result);
                    result.append(resultString);
                }
            });

            Boolean newOffline = getBooleanParam(req, PARAM_NEW_OFFLINE, false);
            if (newOffline) {
                res.addData(new OfflineLinksInfoWrapper(offlineLinksService.getOfflineLinksInfo(userId, req.getUserId(), briefHostInfo, getLinkType())));
            } else {
                res.addData(new OfflineLinksAvailableWrapper(offlineLinksService.isNewAvailable(userId, req.getUserId(), briefHostInfo, getLinkType())));
            }
        } catch (IOException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "Cannot convert result in AbstractXmlSearchServantlet!", e);
        }
    }

    /**
     * Получить число документов.
     * Может использоваться в классах-наследниках для замены числа документов в аттрибуте count элемента docs
     *
     */
    protected Long getDocsCount(final ServRequest req, final BriefHostInfo briefHostInfo, TimeFilter timeFilter, String path) throws InternalException {
        return null;
    }

    protected YandexSearchShard getYandexSearchShard(HostDbHostInfo hostDbHostInfo) throws InternalException {
        return tblUrlTreesDao.getOptimumShardId(hostDbHostInfo);
    }

    protected String createResultString(Document response, Long docsCount) throws IOException, InternalException {
        CircularCharBuffer occb = new CircularCharBuffer(CircularCharBuffer.INFINITE_SIZE);
        XMLOutputter outputer = new XMLOutputter();
        outputer.output(response, occb.getWriter());
        occb.getWriter().close();
        XSLTXmlSearchResultParser parser = new XSLTXmlSearchResultParser(getXslFileName());

        Reader r = parser.parseResult(occb.getReader());
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        saxParserFactory.setNamespaceAware(true);
        saxParserFactory.setValidating(false);
        saxParserFactory.setXIncludeAware(false);

        XMLOutputFactory factory = XMLOutputFactory.newInstance();
        try {
            factory.setProperty("com.ctc.wstx.outputValidateStructure", false);
        } catch (IllegalArgumentException e) {
            log.warn("Unable to set property: " + e.getMessage());
        }
        StringWriter result = new StringWriter();
        try {
            XMLStreamWriter writer = factory.createXMLStreamWriter(result);
            SAXParser saxParser = saxParserFactory.newSAXParser();
            PunycodeRewritingHandler.Options options = new PunycodeRewritingHandler.Options();
            options.setAbsolute(isAbsolutePath());
            options.setNeedPunycodeDomain(needPunycodeDomain());
            options.setNeedPunycodeHost(needPunycodeHostName());
            options.setDocsCount(docsCount);
            saxParser.parse(new InputSource(r), new PunycodeRewritingHandler(writer, options));
        } catch (ParserConfigurationException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "ParserConfigurationException", e);
        } catch (SAXException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "SAXException", e);
        } catch (XMLStreamException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "XMLStreamException", e);
        }

        return result.toString();
    }

    protected String preparePath(BriefHostInfo briefHostInfo, String path) throws UserException, InternalException {
        if (path == null) {
            return ALL_PAGES_PATH;
        }

        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }

        if (path.startsWith("/")) {
            URL url = prepareUrl(briefHostInfo.getName() + path, true);

            return url.getPath() + (url.getQuery() != null && !url.getQuery().isEmpty() ? "?" + url.getQuery() : "");
        } else if (!("".equals(path) || "*".equals(path))) {
            URL url = prepareUrl(path, true);

            String urlHostName = SupportedProtocols.getCanonicalHostname(url);
            String mainMirrorUrlHostName = consistentMainMirrorService.getMainMirror(urlHostName);
            if (WwwUtil.equalsIgnoreWww(mainMirrorUrlHostName, urlHostName)) {
                urlHostName = mainMirrorUrlHostName;
            }
            if (!urlHostName.equalsIgnoreCase(briefHostInfo.getName())) {
                throw new UserException(UserProblem.INVALID_URL, "Invalid path: " + path);
            }

            return url.getPath() + (url.getQuery() != null && !url.getQuery().isEmpty() ? "?" + url.getQuery() : "");
        }

        return path;
    }

    protected TimeFilter prepareTimeFilter(Integer lastDays) {
        if (lastDays != null &&
                (LastDaysFilterEnum.ONE_WEEK.getValue() == lastDays || LastDaysFilterEnum.TWO_WEEKS.getValue() == lastDays)) {
            return TimeFilter.create(lastDays);
        } else {
            return TimeFilter.createNull();
        }
    }

    protected void cacheToDbDataOfResultDocument(HostDbHostInfo hostDbHostInfo, String path, TimeFilter timeFilter, Document response, YandexSearchShard searchShard) throws InternalException {
        if ((ALL_PAGES_PATH.equals(path)) && (timeFilter.getFromInMilliseconds() == null) && (timeFilter.getToInMilliseconds() == null)) {
            Long count = indexInfoService.extractLinksCount(response);
            linksCacheService.saveLinksCountToDbCache(hostDbHostInfo, count, getLinkType(), searchShard);
        }
    }

    protected boolean isAbsolutePath() {
        return true;
    }

    protected boolean needPunycodeDomain() {
        return false;
    }

    protected boolean needPunycodeHostName() {
        return false;
    }

    protected abstract String getXslFileName();

    protected abstract XmlSearchRequest createRequest(String hostName, String path, TimeFilter timeFilter, Pager pager, ServRequest req) throws UserException;

    protected abstract LinkType getLinkType();

    @Required
    public void setOfflineLinksService(OfflineLinksService offlineLinksService) {
        this.offlineLinksService = offlineLinksService;
    }

    @Required
    public void setIndexInfoService(IndexInfoService indexInfoService) {
        this.indexInfoService = indexInfoService;
    }

    public IndexInfoService getIndexInfoService() {
        return indexInfoService;
    }

    @Required
    public void setHostDbHostInfoService(HostDbHostInfoService hostDbHostInfoService) {
        this.hostDbHostInfoService = hostDbHostInfoService;
    }

    public HostDbHostInfoService getHostDbHostInfoService() {
        return hostDbHostInfoService;
    }

    @Required
    public void setLinksCacheService(LinksCacheService linksCacheService) {
        this.linksCacheService = linksCacheService;
    }

    @Required
    public void setTblUrlTreesDao(TblUrlTreesDao tblUrlTreesDao) {
        this.tblUrlTreesDao = tblUrlTreesDao;
    }

    @Required
    public void setUaIndexInfoService(IndexInfoService uaIndexInfoService) {
        this.uaIndexInfoService = uaIndexInfoService;
    }

    @Required
    public void setComIndexInfoService(IndexInfoService comIndexInfoService) {
        this.comIndexInfoService = comIndexInfoService;
    }

    @Required
    public void setTrIndexInfoService(IndexInfoService trIndexInfoService) {
        this.trIndexInfoService = trIndexInfoService;
    }

    @Required
    public void setConsistentMainMirrorService(ConsistentMainMirrorService consistentMainMirrorService) {
        this.consistentMainMirrorService = consistentMainMirrorService;
    }
}
