package ru.yandex.wmconsole.service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import ru.yandex.common.framework.pager.Pager;
import ru.yandex.common.util.db.OrderByClause;
import ru.yandex.webmaster.common.urltree.YandexSearchShard;
import ru.yandex.wmconsole.data.Node;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.ErrorUrlInfo;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.info.TreeInfo;
import ru.yandex.wmconsole.data.info.TreeNodeInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmconsole.util.UrlErrorGrouper;
import ru.yandex.wmconsole.util.UrlErrorOrGroup;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.util.SqlUtil;
import ru.yandex.wmtools.common.util.TimeFilter;

/**
 * Created by IntelliJ IDEA.
 * User: senin
 * Date: 26.07.2007
 * Time: 10:09:24
 */

public class ErrorTreeService extends TreeService {
    public static final String FIELD_HOST_ID = "host_id";
    public static final String FIELD_ERROR_NODE_ID = "error_node_id";
    public static final String FIELD_PARENT_ERROR_NODE_ID = "parent_error_node_id";
    public static final String FIELD_ERROR_NODE_NAME = "error_node_name";
    public static final String FIELD_ERROR_NODE_URLS = "error_node_urls";
    public static final String FIELD_URL = "url";
    public static final String FIELD_ERROR_CODE = "error_code";
    public static final String FIELD_INDEXED_ON = "indexed_on";
    public static final String FIELD_MOD_TIME = "modified_on";
    public static final String FIELD_CHILDREN_COUNT = "children_count";
    public static final String FIELD_HAS_INTERNAL_LINKS = "has_internal_links";
    public static final String FIELD_HAS_EXTERNAL_LINKS = "has_external_links";
    public static final String FIELD_HAS_INT_LINKS = "has_int_links";
    public static final String FIELD_HAS_EXT_LINKS = "has_ext_links";
    public static final String FIELD_SITEMAP_ID = "sitemap_id";

    private static final String SELECT_ERROR_NODES_INFO_QUERY =
            "SELECT " +
                    "    et.id AS " + FIELD_ERROR_NODE_ID + ", " +
                    "    et.parent_id AS " + FIELD_PARENT_ERROR_NODE_ID + ", " +
                    "    et.name AS " + FIELD_ERROR_NODE_NAME + ", " +
                    "    (SELECT " +
                    "         SUM(cet.count) " +
                    "     FROM " +
                    "         tbl_code_agr_error_trees cet " +
                    "     WHERE " +
                    "         cet.node_id = et.id " +
                    "     AND " +
                    "         code IN (%1$s) " +
                    "     GROUP BY " +
                    "         cet.node_id) AS " + FIELD_ERROR_NODE_URLS + ", " +
                    "    et.has_int_links AS " + FIELD_HAS_INT_LINKS + ", " +
                    "    et.has_ext_links AS " + FIELD_HAS_EXT_LINKS + ", " +
                    "    (SELECT " +
                    "         count(*) " +
                    "     FROM " +
                    "         tbl_url_trees et1 " +
                    "     WHERE " +
                    "         et1.parent_id = et.id " +
                    "         AND et1.shard_id = " + YandexSearchShard.RU.value() +
                    "    ) AS " + FIELD_CHILDREN_COUNT + " " +
                    " FROM " +
                    "    tbl_url_trees et " +
                    " WHERE " +
                    "    et.host_id = ? " +
                    "    AND et.shard_id = " + YandexSearchShard.RU.value();

    private static final String SELECT_ERROR_URLS_BY_CODE_COUNT_QUERY =
            "SELECT " +
                    "    count(*) " +
                    "FROM " +
                    "    tbl_error_urls " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    node_id = ? " +
                    "AND " +
                    "    code IN (%1$s) " +
                    " %2$s " +
                    " %3$s " +
                    " %4$s ";

    private static final String SELECT_ERROR_URLS_NODES_BY_CODE_COUNT_QUERY =
            "SELECT " +
                    "    count(*) " +
                    "FROM " +
                    "    tbl_error_urls " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    node_id IN (%1$s) " +
                    "AND " +
                    "    code IN (%2$s) " +
                    " %3$s " +
                    " %4$s " +
                    " %5$s ";

    private static final String SELECT_ERROR_URLS_BY_CODE_QUERY =
            "SELECT " +
                    "    eu.url AS " + FIELD_URL + ", " +
                    "    eu.code AS " + FIELD_ERROR_CODE + ", " +
                    "    unix_timestamp(eu.modified_on) AS " + FIELD_MOD_TIME + ", " +
                    "    unix_timestamp(eu.indexed_on) AS " + FIELD_INDEXED_ON + ", " +
                    "    eu.has_int_links AS " + FIELD_HAS_INTERNAL_LINKS + ", " +
                    "    eu.has_ext_links AS " + FIELD_HAS_EXTERNAL_LINKS + ", " +
                    "    MIN(eus.sitemap_id) AS " + FIELD_SITEMAP_ID + " " +
                    "FROM " +
                    "    tbl_error_urls eu " +
                    "LEFT JOIN " +
                    "    tbl_error_urls_sitemaps eus " +
                    "USING(host_id, url_id) " +
                    "WHERE " +
                    "    eu.host_id = ? " +
                    "AND " +
                    "    eu.node_id = ? " +
                    "AND " +
                    "    eu.code IN (%1$s) " +
                    " %2$s " +
                    " %3$s " +
                    " %4$s " +
                    "GROUP BY " +
                    "    eu.host_id, eu.url_id " +
                    " %5$s " +
                    " %6$s ";

    private static final String SELECT_ERROR_URLS_NODES_BY_CODE_QUERY =
            "SELECT " +
                    "    eu.url AS " + FIELD_URL + ", " +
                    "    eu.code AS " + FIELD_ERROR_CODE + ", " +
                    "    unix_timestamp(eu.modified_on) AS " + FIELD_MOD_TIME + ", " +
                    "    unix_timestamp(eu.indexed_on) AS " + FIELD_INDEXED_ON + ", " +
                    "    eu.has_int_links AS " + FIELD_HAS_INTERNAL_LINKS + ", " +
                    "    eu.has_ext_links AS " + FIELD_HAS_EXTERNAL_LINKS + ", " +
                    "    MIN(eus.sitemap_id) AS " + FIELD_SITEMAP_ID + " " +
                    "FROM " +
                    "    tbl_error_urls eu " +
                    "LEFT JOIN " +
                    "    tbl_error_urls_sitemaps eus " +
                    "USING(host_id, url_id) " +
                    "WHERE " +
                    "    eu.host_id = ? " +
                    "AND " +
                    "    eu.node_id IN (%1$s) " +
                    "AND " +
                    "    eu.code IN (%2$s) " +
                    " %3$s " +
                    " %4$s " +
                    " %5$s " +
                    "GROUP BY " +
                    "   eu.host_id, eu.url_id " +
                    " %6$s " +
                    " %7$s ";

    private ErrorInfoService errorInfoService;
    private HostDbHostInfoService hostDbHostInfoService;

    private static final ParameterizedRowMapper<TreeNodeInfo> errorTreeNodeBriefInfoMapper = new ParameterizedRowMapper<TreeNodeInfo>() {
        @Override
        public TreeNodeInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
            long id = rs.getLong(FIELD_ERROR_NODE_ID);
            Long parentId = rs.getLong(FIELD_PARENT_ERROR_NODE_ID);
            if (rs.wasNull()) {
                parentId = null;
            }
            String name = rs.getString(FIELD_ERROR_NODE_NAME);

            Long urls = rs.getLong(FIELD_ERROR_NODE_URLS);
            if (rs.wasNull()) {
                urls = 0L;
            }

            boolean hasIntLinks = rs.getBoolean(FIELD_HAS_INT_LINKS);
            boolean hasExtLinks = rs.getBoolean(FIELD_HAS_EXT_LINKS);
            int childrenCnt = rs.getInt(FIELD_CHILDREN_COUNT);
            return new TreeNodeInfo(id, parentId, name, urls, null,  hasIntLinks, hasExtLinks, childrenCnt, null, null);
        }
    };

    @Override
    protected TreeNodeInfo createTreeNodeInfo(TreeNodeInfo info) {
        return new TreeNodeInfo(info);
    }

    protected List<TreeNodeInfo> getTreeNodesListByQuery(HostDbHostInfo hostDbHostInfo, String query) throws InternalException {
        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(query, errorTreeNodeBriefInfoMapper, hostDbHostInfo.getHostDbHostId());
    }

    /**
     * Returns list of error pages for given node of host, for given error code, for given user (UserErrorOptions
     * used), ordered, paged and filtered, if needed.
     *
     * @param userId        User, for which to get information (UserErrorOptions will be used).
     * @param hostInfo      Host, for which to get information.
     * @param errorNodeId   Error tree node for given host.
     * @param error         Error code or error group code.
     * @param pager         Pager, used to filter results by range.
     * @param order         Order, in which the results will be sorted.
     * @param timeFilter    Time filtering values.
     * @return Returns list of objects, storing information about error pages.
     * @throws InternalException Thrown if something's gone wrong. See InternalException.getProblem() for details.
     */
    public List<ErrorUrlInfo> getNodeErrorUrlsByCode(
            long userId, BriefHostInfo hostInfo, long errorNodeId, UrlErrorOrGroup error, Pager pager,
            OrderByClause order, final TimeFilter timeFilter, Boolean mustHaveIntLinks, Boolean mustHaveExtLinks) throws InternalException {
        int codeCount = error.getPrimitiveCodesCount();

        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());

        String urlsByCodeCount = String.format(
                SELECT_ERROR_URLS_BY_CODE_COUNT_QUERY,
                SqlUtil.createQuestionMarks(codeCount),
                SqlUtil.getTimeFilterPart(timeFilter, FIELD_MOD_TIME),
                mustHaveIntLinks ? "AND has_int_links = 1" : "",
                mustHaveExtLinks ? "AND has_ext_links = 1" : ""
        );

        String urlsByCode = String.format(
                SELECT_ERROR_URLS_BY_CODE_QUERY,
                SqlUtil.createQuestionMarks(codeCount),
                SqlUtil.getTimeFilterPart(timeFilter, FIELD_MOD_TIME),
                mustHaveIntLinks ? "AND has_int_links = 1" : "",
                mustHaveExtLinks ? "AND has_ext_links = 1" : "",
                "%1$s",
                "%2$s"
        );

        Object[] params = SqlUtil.prepareFirstParams(error.getPrimitiveCodes(), hostDbHostInfo.getHostDbHostId(), errorNodeId);

        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).select(
                urlsByCodeCount,
                urlsByCode, new ErrorInfoMapper(errorInfoService.getUserErrorOptions(userId, hostInfo.getId())), order, pager, params);
    }

    public List<ErrorUrlInfo> getNodesErrorUrlsByCode(
            long userId, BriefHostInfo hostInfo, List<Long> errorNodeIds, UrlErrorOrGroup error, Pager pager,
            OrderByClause order, final TimeFilter timeFilter, Boolean mustHaveIntLinks, Boolean mustHaveExtLinks) throws InternalException {
        int codeCount = error.getPrimitiveCodesCount();

        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());

        String urlsByCodeCount = String.format(
                SELECT_ERROR_URLS_NODES_BY_CODE_COUNT_QUERY,
                SqlUtil.getCommaSeparatedList(errorNodeIds),
                SqlUtil.createQuestionMarks(codeCount),
                SqlUtil.getTimeFilterPart(timeFilter, FIELD_MOD_TIME),
                mustHaveIntLinks ? "AND has_int_links = 1" : "",
                mustHaveExtLinks ? "AND has_ext_links = 1" : ""
        );

        String urlsByCode = String.format(
                SELECT_ERROR_URLS_NODES_BY_CODE_QUERY,
                SqlUtil.getCommaSeparatedList(errorNodeIds),
                SqlUtil.createQuestionMarks(codeCount),
                SqlUtil.getTimeFilterPart(timeFilter, FIELD_MOD_TIME),
                mustHaveIntLinks ? "AND has_int_links = 1" : "",
                mustHaveExtLinks ? "AND has_ext_links = 1" : "",
                "%1$s",
                "%2$s"
        );

        Object[] params = SqlUtil.prepareFirstParams(error.getPrimitiveCodes(), hostDbHostInfo.getHostDbHostId());

        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).select(
                urlsByCodeCount,
                urlsByCode, new ErrorInfoMapper(errorInfoService.getUserErrorOptions(userId, hostInfo.getId())), order, pager, params);
    }

    /**
     * Строит дерево структуры сайта для разделов, содержащих ошибки
     *
     * @param userId
     * @param hostInfo
     * @param selectedNodeId        узел дерева
     * @param errorCode             код ошибки
     * @return
     * @throws InternalException
     * @throws UserException
     */
    public TreeInfo getErrorTreeInfoByCode(long userId, BriefHostInfo hostInfo, Long selectedNodeId, Integer errorCode) throws InternalException, UserException {
        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());

        Date updatedOn = getHostInfoService().getUpdatedOn(hostDbHostInfo);
        if (updatedOn == null) {
            // no data for this host is available!
            throw new UserException(WMCUserProblem.NO_DATA_FOR_HOST, "updated_on is null for this host -> no data for host is available");
        }
        Date optionsUpdatedOn = errorInfoService.getOptionsUpdatedOnForUser(userId);
        if ((optionsUpdatedOn != null) && (updatedOn.before(optionsUpdatedOn))) {
            updatedOn = optionsUpdatedOn;
        }
        String query = String.format(SELECT_ERROR_NODES_INFO_QUERY,
                SqlUtil.getCommaSeparatedList(UrlErrorGrouper.expandGroups(Collections.singletonList(errorCode))));
        List<TreeNodeInfo> list = getTreeNodesListByQuery(hostDbHostInfo, query);
        if (list.isEmpty()) {
            return null;
        }

        HashMap<Long, Node> id2node = new HashMap<Long, Node>();
        Node rootNode = convertListToTree(list, id2node);

        // Расставляет childrenCount и устанавливает 0 вместо null
        considerUrlsInSubTree(rootNode);

        return new TreeInfo(selectedNodeId, rootNode, id2node, updatedOn);
    }

    /*
     * Выставляет childrenCount в зависимости от числа ошибок
     */
    private long considerUrlsInSubTree(Node node) {
        long res = 0;
        for (Node child : node.getChildren()) {
            res += considerUrlsInSubTree(child);
        }

        if (res == 0) {
            node.getInfo().setHasChildren(false);
        }

        if (node.getInfo().getUrlsInSubtree() == null) {
            node.getInfo().setUrlsInSubtree(0L);
        }

        return node.getInfo().getUrlsInSubtree();
    }

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