package ru.yandex.wmconsole.service;


import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.ErrorSeverity;
import ru.yandex.wmconsole.data.MainPageErrorEnum;
import ru.yandex.wmconsole.data.SeverityEnum;
import ru.yandex.wmconsole.data.UrlErrorsWithCodeInfoComparator;
import ru.yandex.wmconsole.data.UserErrorOptions;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.ErrorCountInfo;
import ru.yandex.wmconsole.data.info.ErrorUrlInfo;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.info.MainPageErrorInfo;
import ru.yandex.wmconsole.data.info.ShortHostInfo;
import ru.yandex.wmconsole.data.info.UrlErrorsWithCodeInfo;
import ru.yandex.wmconsole.data.info.UserOptionsInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
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.error.UserProblem;
import ru.yandex.wmtools.common.service.AbstractDbService;
import ru.yandex.wmtools.common.util.SqlUtil;
import ru.yandex.wmtools.common.util.TimeFilter;

/**
 * Created by IntelliJ IDEA.
 * User: senin
 * Date: 20.03.2007
 * Time: 16:59:17
 */
public class ErrorInfoService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(ErrorInfoService.class);

    private static final String FIELD_ERROR_CODE = "error_code";
    private static final String FIELD_ERROR_COUNT = "error_count";
    private static final String FIELD_BANNED_COUNT = "banned_count";
    private static final String FIELD_ERROR_COUNT_TREND = "error_count_trend";
    public static final String FIELD_URL = "url";
    public static final String FIELD_INDEXED_ON = "indexed_on";
    public static final String FIELD_MODIFIED_ON = "modified_on";
    public static final String FIELD_HAS_INTERNAL_LINKS = "has_internal_links";
    public static final String FIELD_HAS_EXTERNAL_LINKS = "has_external_links";
    private static final String FIELD_SITEMAP_ID = "sitemap_id";
    private static final String FIELD_SEVERITY = "severity";
    private static final String FIELD_OPTIONS_UPDATED_ON = "options_updated_on";
    private static final String FIELD_CUR_CODE = "cur_code";
    private static final String FIELD_CUR_DATE = "cur_date";
    private static final String FIELD_PROD_CODE = "prod_code";
    private static final String FIELD_PROD_DATE = "prod_date";

    private HostDbHostInfoService hostDbHostInfoService;
    private UserOptionsService userOptionsService;
    private UsersHostsService usersHostsService;

    private static final String SELECT_GROUPED_CODES_QUERY =
            "SELECT " +
                    "    code AS " + FIELD_ERROR_CODE + ", " +
                    "    sum(count) AS " + FIELD_ERROR_COUNT + ", " +
                    "    sum(cet.banned_count) AS " + FIELD_BANNED_COUNT + ", " +
                    "    sum(count_trend) AS " + FIELD_ERROR_COUNT_TREND + " " +
                    "FROM " +
                    "    tbl_code_agr_error_trees cet " +
                    "JOIN "+
                    "    tbl_url_trees ut "+
                    "ON cet.node_id = ut.id " +
                    "WHERE " +
                    "    cet.host_id = ? " +
                    "    AND ut.parent_id IS NULL " +  // получаем агрегированное число ошибок для корня сайта
                    "    AND ut.shard_id = " + YandexSearchShard.RU.value() +
                    "    AND code IN (%s) " +
                    "GROUP BY " + FIELD_ERROR_CODE + " ";

    private static final String SELECT_GROUPED_CODES_FROM_SITEMAP_QUERY =
            "SELECT " +
                    "    code AS " + FIELD_ERROR_CODE + ", " +
                    "    count AS " + FIELD_ERROR_COUNT + " " +
                    "FROM " +
                    "    tbl_code_error_sitemap " +
                    "WHERE " +
                    "    sitemap_id = ? " +
                    "GROUP BY " + FIELD_ERROR_CODE + " ";

    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_MODIFIED_ON + ", " +
                    "    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.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_BY_CODE_COUNT_QUERY =
            "SELECT " +
                    "    count(*) " +
                    "FROM " +
                    "    tbl_error_urls eu " +
                    "WHERE " +
                    "    eu.host_id = ? " +
                    "AND " +
                    "    eu.code IN (%1$s) " +
                    " %2$s " +
                    " %3$s " +
                    " %4$s ";

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

    private static final String SELECT_ERROR_URLS_FOR_SITEMAP_BY_CODE_QUERY =
            "SELECT " +
                    "    eu.url AS " + FIELD_URL + ", " +
                    "    eu.code AS " + FIELD_ERROR_CODE + ", " +
                    "    unix_timestamp(eu.modified_on) AS " + FIELD_MODIFIED_ON + ", " +
                    "    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 " +
                    "JOIN " +
                    "    tbl_error_urls_sitemaps eus " +
                    "USING (host_id, url_id) " +
                    "WHERE " +
                    "    eu.host_id = ? " +
                    "AND " +
                    "    eus.sitemap_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_FOR_SITEMAP_BY_CODE_COUNT_QUERY =
            "SELECT " +
                    "    count(*) " +
                    "FROM " +
                    "    tbl_error_urls eu " +
                    "JOIN " +
                    "    tbl_error_urls_sitemaps eus " +
                    "USING (host_id, url_id)" +
                    "WHERE " +
                    "    eu.host_id = ? " +
                    "AND " +
                    "    eus.sitemap_id = ? " +
                    "AND " +
                    "    eu.code IN (%1$s) " +
                    " %2$s " +
                    " %3$s " +
                    " %4$s ";

    private static final String SELECT_ERROR_URLS_FOR_CODE_COUNT_QUERY =
            "SELECT " +
                    "    sum(count) " +
                    " FROM " +
                    "    tbl_code_agr_error_trees cet " +
                    " JOIN "+
                    "    tbl_url_trees ut "+
                    " ON cet.node_id = ut.id " +
                    " WHERE " +
                    "    cet.host_id = ? " +
                    " AND ut.parent_id IS NULL " +
                    " AND ut.shard_id = " + YandexSearchShard.RU.value() +
                    " AND code IN (%1$s) ";

    private static final String SELECT_ERROR_URLS_COUNT_FOR_CODE_AND_NODE_QUERY =
            "SELECT " +
                    "    sum(count) " +
                    "FROM " +
                    "    tbl_code_agr_error_trees cet " +
                    "WHERE " +
                    "    cet.host_id = ? " +
                    "AND cet.node_id = ? " +
                    "AND cet.code IN (%1$s) ";

    private static final String SELECT_ERROR_INDEX_PAGE_QUERY =
            "SELECT " +
                    "    eu.url AS " + FIELD_URL + ", " +
                    "    eu.code AS " + FIELD_ERROR_CODE + ", " +
                    "    unix_timestamp(eu.modified_on) AS " + FIELD_MODIFIED_ON + ", " +
                    "    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 eus " +
                    "USING(host_id, url_id) " +
                    "WHERE " +
                    "    eu.url = ? AND " +
                    "    eu.host_id = ? " +
                    "GROUP BY " +
                    "    eu.host_id, eu.url_id";

    private static final String SELECT_ERROR_OPTIONS_FOR_USER_QUERY =
            "SELECT " +
                    "    error_code AS " + FIELD_ERROR_CODE + ", " +
                    "    severity AS " + FIELD_SEVERITY + " " +
                    "FROM " +
                    "    tbl_user_error_options " +
                    "WHERE " +
                    "    user_id = ? ";

    private static final String REPLACE_ERROR_OPTIONS_FOR_USER_UPDATE_QUERY =
            "REPLACE INTO " +
                    "    tbl_user_error_options " +
                    "    (user_id, error_code, severity) " +
                    "VALUES " +
                    "    %s";

    private static final String SELECT_ERROR_OPTIONS_FOR_USER_AND_HOST_QUERY =
            "SELECT " +
                    "    error_code AS " + FIELD_ERROR_CODE + ", " +
                    "    severity AS " + FIELD_SEVERITY + " " +
                    "FROM " +
                    "    tbl_user_host_error_options " +
                    "WHERE " +
                    "    user_id = ? AND host_id = ? ";

    private static final String REPLACE_ERROR_OPTIONS_FOR_USER_AND_HOST_UPDATE_QUERY =
            "REPLACE INTO " +
                    "    tbl_user_host_error_options " +
                    "    (user_id, host_id, error_code, severity) " +
                    "VALUES " +
                    "    %s";

    private static final String DELETE_ERROR_OPTIONS_RESTORE_DEFAULTS_UPDATE_QUERY =
            "DELETE FROM " +
                    "    tbl_user_error_options " +
                    "WHERE " +
                    "    user_id = ? " +
                    "AND " +
                    "    error_code IN (%s) ";

    private static final String DELETE_ERROR_OPTIONS_RESTORE_DEFAULTS_FOR_USER_AND_HOST_UPDATE_QUERY =
            "DELETE FROM " +
                    "    tbl_user_host_error_options " +
                    "WHERE " +
                    "    user_id = ? " +
                    "AND " +
                    "    host_id = ? " +
                    "AND " +
                    "    error_code IN (%s) ";

    private static final String UPDATE_OPTIONS_UPDATED_ON_QUERY =
            "UPDATE " +
                    "    tbl_users " +
                    "SET " +
                    "    options_updated_on = NOW() " +
                    "WHERE" +
                    "    user_id = ? ";

    private static final String SELECT_OPTIONS_UPDATED_ON_QUERY =
            "SELECT " +
                    "    options_updated_on AS " + FIELD_OPTIONS_UPDATED_ON + " " +
                    "FROM " +
                    "    tbl_users " +
                    "WHERE " +
                    "    user_id = ? ";

    private static final String DELETE_USER_ERROR_OPTIONS_QUERY =
            "DELETE FROM tbl_user_error_options WHERE user_id = ?";
    private static final String DELETE_USER_HOST_ERROR_OPTIONS_QUERY =
            "DELETE FROM tbl_user_host_error_options WHERE user_id = ?";

    private static final String SELECT_MAIN_PAGE_ERROR_QUERY =
            "SELECT " +
                    "   prod_code AS " + FIELD_PROD_CODE + ", " +
                    "   prod_time AS " + FIELD_PROD_DATE + ", " +
                    "   cur_code AS " + FIELD_CUR_CODE + ", " +
                    "   cur_time AS " + FIELD_CUR_DATE + " " +
                    "FROM " +
                    "   tbl_host_main_page_info " +
                    "WHERE " +
                    "   host_id = ? " +
                    "ORDER BY prod_time, cur_time " +
                    "LIMIT 1";

    private static final ParameterizedRowMapper<ErrorSeverity> errorSeverityMapper = new ParameterizedRowMapper<ErrorSeverity>() {
        @Override
        public ErrorSeverity mapRow(ResultSet rs, int rowNum) throws SQLException {
            int code = rs.getInt(FIELD_ERROR_CODE);
            SeverityEnum severity = SeverityEnum.R.fromValueOrNull(rs.getInt(FIELD_SEVERITY));
            return new ErrorSeverity(code, severity);
        }
    };

    private static class MainPageErrorMapper implements ParameterizedRowMapper<MainPageErrorInfo> {
        private final UserErrorOptions userErrorOptions;

        private MainPageErrorMapper(UserErrorOptions userErrorOptions) {
            this.userErrorOptions = userErrorOptions;
        }

        @Override
        public MainPageErrorInfo mapRow(ResultSet resultSet, int i) throws SQLException {
            return new MainPageErrorInfo(
                    SqlUtil.getIntNullable(resultSet, FIELD_PROD_CODE),
                    SqlUtil.safeGetTimestamp(resultSet, FIELD_PROD_DATE),
                    SqlUtil.getIntNullable(resultSet, FIELD_CUR_CODE),
                    SqlUtil.safeGetTimestamp(resultSet, FIELD_CUR_DATE),
                    userErrorOptions);
        }
    }

    private List<UrlErrorsWithCodeInfo> getErrorInfoGroups(BriefHostInfo hostInfo, UserErrorOptions userErrorOptions)
            throws InternalException {
        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());
        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(
                String.format(SELECT_GROUPED_CODES_QUERY,
                        SqlUtil.getCommaSeparatedList(UrlErrorGrouper.expandGroups(userErrorOptions.getNotOkSeverityCodesWithGroups()))),
                new GroupedUrlErrorWithTrendMapper(userErrorOptions), hostDbHostInfo.getHostDbHostId());
    }

    private List<UrlErrorsWithCodeInfo> getErrorInfoGroupsForSitemap(BriefHostInfo hostInfo, UserErrorOptions userErrorOptions, long sitemapId)
            throws InternalException {
        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());
        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(SELECT_GROUPED_CODES_FROM_SITEMAP_QUERY,
                new GroupedUrlErrorMapper(userErrorOptions), sitemapId);
    }

    public UserErrorOptions getUserErrorOptions(Long userId, @Nullable Long hostId) throws InternalException {
        UserOptionsInfo info = userOptionsService.getUserOptions(userId);
        return getUserErrorOptions(userId, hostId, info.isUseGlobalErrorOptions());
    }

    public UserErrorOptions getUserErrorOptions(Long userId, @Nullable Long hostId, boolean useGlobalErrorOptions) throws InternalException {
        if (hostId != null && !useGlobalErrorOptions) {
            return getUserHostErrorOptionsImpl(userId, hostId);
        } else {
            return getUserErrorOptionsImpl(userId);
        }
    }

    private UserErrorOptions getUserHostErrorOptionsImpl(Long userId, Long hostId) throws InternalException {
        List<ErrorSeverity> errorSeverities = getJdbcTemplate(new WMCPartition(null, userId)).query(
                SELECT_ERROR_OPTIONS_FOR_USER_AND_HOST_QUERY, errorSeverityMapper, userId, hostId);

        UserErrorOptions result = new UserErrorOptions(userId);
        for (ErrorSeverity errorSeverity : errorSeverities) {
            SeverityEnum severity = errorSeverity.getSeverity();
            if (SeverityEnum.ERROR.equals(severity) ||
                    SeverityEnum.WARNING.equals(severity) ||
                    SeverityEnum.INFO.equals(severity) ||
                    UrlErrorGrouper.isIgnored(errorSeverity.getCode())) {
                // Если попадаются старые настройки ERROR, WARNING, INFO, то мы их игнорируем, используем настройки по умолчанию
                continue;
            }
            result.addErrorSeverity(errorSeverity);
        }
        return result;
    }

    private UserErrorOptions getUserErrorOptionsImpl(Long userId) throws InternalException {
        List<ErrorSeverity> errorSeverities = getJdbcTemplate(WMCPartition.nullPartition()).query(
                SELECT_ERROR_OPTIONS_FOR_USER_QUERY, errorSeverityMapper, userId);

        UserErrorOptions result = new UserErrorOptions(userId);
        for (ErrorSeverity errorSeverity : errorSeverities) {
            SeverityEnum severity = errorSeverity.getSeverity();
            if (SeverityEnum.ERROR.equals(severity) ||
                    SeverityEnum.WARNING.equals(severity) ||
                    SeverityEnum.INFO.equals(severity) ||
                    UrlErrorGrouper.isIgnored(errorSeverity.getCode())) {
                // Если попадаются старые настройки ERROR, WARNING, INFO, то мы их игнорируем, используем настройки по умолчанию
                continue;
            }
            result.addErrorSeverity(errorSeverity);
        }
        return result;
    }

    public void updateUserErrorOptions(UserErrorOptions difference, Long hostId, boolean useGlobalErrorOptions) throws InternalException, UserException {
        if (useGlobalErrorOptions) {
            updateUserErrorOptionsImpl(difference);
        } else {
            updateUserHostErrorOptionsImpl(difference, hostId);
        }
    }

    private void updateUserErrorOptionsImpl(UserErrorOptions difference) throws InternalException {
        if ((difference == null) || (difference.getAllSeverities() == null) ||
                (difference.getAllSeverities().size() == 0)) {
            // nothing to update
            return;
        }

        StringBuilder diffStr = new StringBuilder();
        boolean first = true;
        for (Map.Entry<Integer, SeverityEnum> entry : difference.getAllSeverities().entrySet()) {
            if (!first) {
                diffStr.append(",");
            }
            diffStr.append("(").append(difference.getUserId()).append(", ").append(entry.getKey()).append(", \"")
                    .append(entry.getValue().getValue()).append("\")");
            first = false;
        }

        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_OPTIONS_UPDATED_ON_QUERY, difference.getUserId());

        String query = String.format(REPLACE_ERROR_OPTIONS_FOR_USER_UPDATE_QUERY, diffStr.toString());
        getJdbcTemplate(WMCPartition.nullPartition()).update(query);
    }

    private void updateUserHostErrorOptionsImpl(UserErrorOptions difference, Long hostId) throws InternalException, UserException {
        if ((difference == null) || (difference.getAllSeverities() == null) ||
                (difference.getAllSeverities().size() == 0)) {
            // nothing to update
            return;
        }

        if (hostId == null) {
            throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "invalid value of parameter host", "null");
        }

        StringBuilder diffStr = new StringBuilder();
        boolean first = true;
        for (Map.Entry<Integer, SeverityEnum> entry : difference.getAllSeverities().entrySet()) {
            if (!first) {
                diffStr.append(",");
            }
            diffStr.append("(")
                    .append(difference.getUserId())
                    .append(", ")
                    .append(hostId)
                    .append(", ")
                    .append(entry.getKey())
                    .append(", \"")
                    .append(entry.getValue().getValue()).append("\")");
            first = false;
        }

        getJdbcTemplate(new WMCPartition(null, difference.getUserId())).update(UPDATE_OPTIONS_UPDATED_ON_QUERY, difference.getUserId());

        String query = String.format(REPLACE_ERROR_OPTIONS_FOR_USER_AND_HOST_UPDATE_QUERY, diffStr.toString());
        getJdbcTemplate(new WMCPartition(null, difference.getUserId())).update(query);
    }

    public void removeUserErrorOptions(Long userId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(
                DELETE_USER_ERROR_OPTIONS_QUERY, userId);
    }

    public void removeUserHostErrorOptions(Long userId) throws InternalException {
        getJdbcTemplate(new WMCPartition(null, userId)).update(
                DELETE_USER_HOST_ERROR_OPTIONS_QUERY, userId);
    }

    public void copyUserErrorOptions(final Long userId) throws InternalException {
        final List<ShortHostInfo> hosts = usersHostsService.getAllHostsVerifiedByUser(userId);
        final UserErrorOptions errorOptions = getUserErrorOptions(userId, null, true);
        final UserErrorOptions defaultOptions = new UserErrorOptions(userId, true);
        final UserErrorOptions difference = defaultOptions.whatsNew(errorOptions);
        if ((difference == null) || (difference.getAllSeverities() == null) ||
                (difference.getAllSeverities().size() == 0)) {
            // nothing to update
            return;
        }

        StringBuilder diffStr = new StringBuilder();
        boolean first = true;
        for (ShortHostInfo hostInfo : hosts) {
            long hostId = hostInfo.getId();
            for (Map.Entry<Integer, SeverityEnum> entry : difference.getAllSeverities().entrySet()) {
                if (!first) {
                    diffStr.append(",");
                }
                diffStr.append("(")
                        .append(userId)
                        .append(", ")
                        .append(hostId)
                        .append(", ")
                        .append(entry.getKey())
                        .append(", \"")
                        .append(entry.getValue().getValue()).append("\")");
                first = false;
            }
        }

        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_OPTIONS_UPDATED_ON_QUERY, difference.getUserId());
        String query = String.format(REPLACE_ERROR_OPTIONS_FOR_USER_AND_HOST_UPDATE_QUERY, diffStr.toString());
        getJdbcTemplate(new WMCPartition(null, userId)).update(query);
        log.debug(new WMCPartition(null, userId).getReadySqlString("select * from tbl_user_host_error_options"));
    }

    public void restoreDefaults(long userId, Long hostId, List<Integer> codes) throws InternalException {
        if ((codes == null) || (codes.size() == 0)) {
            return;
        }
        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_OPTIONS_UPDATED_ON_QUERY, userId);
        if (hostId == null) {
            String queryString = String.format(DELETE_ERROR_OPTIONS_RESTORE_DEFAULTS_UPDATE_QUERY,
                    SqlUtil.getCommaSeparatedList(codes));
            getJdbcTemplate(WMCPartition.nullPartition()).update(queryString, userId);
        } else {
            String queryString = String.format(DELETE_ERROR_OPTIONS_RESTORE_DEFAULTS_FOR_USER_AND_HOST_UPDATE_QUERY,
                    SqlUtil.getCommaSeparatedList(codes));
            getJdbcTemplate(new WMCPartition(null, userId)).update(queryString, userId, hostId);
        }
    }

    /**
     * Возвращает отсортированный и упорядоченный список групп ошибок для хоста с учетом настроек пользователя
     */
    public Collection<UrlErrorsWithCodeInfo> getOrderedErrorGroupsList(long userId, BriefHostInfo hostInfo,
                                                                       OrderByClause order) throws InternalException {
        List<UrlErrorsWithCodeInfo> list = getErrorGroupsList(hostInfo, getUserErrorOptions(userId, hostInfo.getId()));
        Collections.sort(list, new UrlErrorsWithCodeInfoComparator(order));
        return list;
    }

    public Collection<UrlErrorsWithCodeInfo> getOrderedErrorGroupsListForSitemap(long userId, BriefHostInfo hostInfo,
                                                                                 OrderByClause order, long sitemapId) throws InternalException {
        List<UrlErrorsWithCodeInfo> list = getErrorGroupsListForSitemap(hostInfo, getUserErrorOptions(userId, hostInfo.getId()), sitemapId);
        Collections.sort(list, new UrlErrorsWithCodeInfoComparator(order));
        return list;
    }

    /**
     * Возвращает список групп ошибок и число ошибок в каждой группе с учетом настроек пользователя
     */
    public List<UrlErrorsWithCodeInfo> getErrorGroupsList(BriefHostInfo hostInfo, UserErrorOptions userErrorOptions)
            throws InternalException {
        return UrlErrorGrouper.getInstance().mapErrorsToErrorGroups(
                getErrorInfoGroups(hostInfo, userErrorOptions), userErrorOptions);
    }

    public List<UrlErrorsWithCodeInfo> getErrorGroupsListForSitemap(BriefHostInfo hostInfo, UserErrorOptions userErrorOptions, long sitemapId)
            throws InternalException {
        return UrlErrorGrouper.getInstance().mapErrorsToErrorGroups(
                getErrorInfoGroupsForSitemap(hostInfo, userErrorOptions, sitemapId), userErrorOptions);
    }

    public int[] getPrimitiveCodesForUserAndHost(long userId, long hostId) throws InternalException {
        UserErrorOptions userErrorOptions = getUserErrorOptions(userId, hostId);
        List<Integer> groupedErrors = userErrorOptions.getNotOkSeverityCodesWithGroups();

        List<Integer> primitiveErrors = new ArrayList<Integer>();
        for (int code : groupedErrors) {
            UrlErrorOrGroup errorOrGroup = new UrlErrorOrGroup(code);
            if (errorOrGroup.isGroup()) {
            } else {
                primitiveErrors.add(code);
            }
        }

        int[] primitive = new int[primitiveErrors.size()];

        int offset = 0;
        for (Integer error : primitiveErrors) {
            primitive[offset++] = error;
        }

        return primitive;
    }

    public List<ErrorUrlInfo> getAllErrors(long userId, BriefHostInfo hostInfo, int[] primitiveCodes)
            throws InternalException {
        return internalGetUrlsWithErrorsByCode(null, null, userId, hostInfo, primitiveCodes, TimeFilter.createNull(),
                false, false, null);
    }

    public List<ErrorUrlInfo> getUrlsWithErrorsByCode(
            Pager pager, OrderByClause order, long userId, BriefHostInfo hostInfo,
            UrlErrorOrGroup error, final TimeFilter timeFilter, Boolean mustHaveIntLinks, Boolean mustHaveExtLinks,
            Long sitemapId) throws InternalException {
        return internalGetUrlsWithErrorsByCode(pager, order, userId, hostInfo, error.getPrimitiveCodes(), timeFilter,
                mustHaveIntLinks, mustHaveExtLinks, sitemapId);
    }

    private List<ErrorUrlInfo> internalGetUrlsWithErrorsByCode(
            Pager pager, OrderByClause order, long userId, BriefHostInfo hostInfo,
            int[] primitiveErrors, final TimeFilter timeFilter,
            Boolean mustHaveIntLinks, Boolean mustHaveExtLinks, Long sitemapId) throws InternalException {
        int codeCount = primitiveErrors.length;

        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());
        String urlsByCodeCount;
        String urlsByCode;

        Object[] params;

        if (sitemapId == null) {
            urlsByCodeCount = String.format(
                    SELECT_ERROR_URLS_BY_CODE_COUNT_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : ""
            );

            urlsByCode = String.format(
                    SELECT_ERROR_URLS_BY_CODE_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : "",
                    "%1$s",
                    "%2$s"
            );
            params = SqlUtil.prepareFirstParams(primitiveErrors, hostDbHostInfo.getHostDbHostId());
        } else {
            urlsByCodeCount = String.format(
                    SELECT_ERROR_URLS_FOR_SITEMAP_BY_CODE_COUNT_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, "eu." + FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : ""
            );

            urlsByCode = String.format(
                    SELECT_ERROR_URLS_FOR_SITEMAP_BY_CODE_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : "",
                    "%1$s",
                    "%2$s"
            );
            params = SqlUtil.prepareFirstParams(primitiveErrors, hostDbHostInfo.getHostDbHostId(), sitemapId);
        }

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

    /**
     * Returns null, if index page does not contain error,
     * and error info otherwise.
     *
     * @param userId   q...
     * @param hostInfo host id @return Returns null, if index page does not contain error,
     *                 and error info otherwise.
     * @return ...
     * @throws InternalException Thrown when errors occures in operations with DB.
     */
    public ErrorUrlInfo getIndexPageError(Long userId, BriefHostInfo hostInfo) throws InternalException {
        HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName());
        List<ErrorUrlInfo> errorUrlInfos = getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(
                SELECT_ERROR_INDEX_PAGE_QUERY, new ErrorInfoMapper(getUserErrorOptions(userId, hostInfo.getId())),
                "/", hostDbHostInfo.getHostDbHostId());

        if (errorUrlInfos.size() == 0) {
            // no errors for index page
            return null;
        }
        ErrorUrlInfo result = errorUrlInfos.get(0);
        if (SeverityEnum.OK.equals(result.getSeverity())) {
            // not an error for this user
            return null;
        }

        return errorUrlInfos.get(0);
    }

    public MainPageErrorInfo getMainPageError(Long userId, BriefHostInfo briefHostInfo) throws InternalException {
        final UserErrorOptions userErrorOptions = getUserErrorOptions(userId, briefHostInfo.getId());
        final HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(briefHostInfo.getName());
        final List<MainPageErrorInfo> errors = getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(
                SELECT_MAIN_PAGE_ERROR_QUERY, new MainPageErrorMapper(userErrorOptions), hostDbHostInfo.getHostDbHostId());
        if (errors.size() == 0) {
            return null;
        }

        final MainPageErrorInfo errorInfo = errors.iterator().next();
        if (errorInfo.getMainPageError() == MainPageErrorEnum.NONE) {
            return null;
        } else {
            return errorInfo;
        }
    }

    private Integer getErrorUrlsForCodeCount(BriefHostInfo hostInfo, int[] primitive) throws InternalException {
        int codeCount = primitive.length;

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

        String queryString = String.format(
                SELECT_ERROR_URLS_FOR_CODE_COUNT_QUERY,
                SqlUtil.createQuestionMarks(codeCount)
        );

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

        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).queryForInt(queryString, params);
    }

    private Integer getRealErrorUrlsCountForCodeAndNode(BriefHostInfo hostInfo, long nodeId, int[] primitive) throws InternalException {
        int codeCount = primitive.length;

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

        String queryString = String.format(
                SELECT_ERROR_URLS_COUNT_FOR_CODE_AND_NODE_QUERY,
                SqlUtil.createQuestionMarks(codeCount)
        );

        Object[] params = SqlUtil.prepareFirstParams(primitive, hostDbHostInfo.getHostDbHostId(), nodeId);

        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).queryForInt(queryString, params);
    }

    private Integer getShownErrorUrlsForCodeCount(BriefHostInfo hostInfo, int[] primitive,
                                                  TimeFilter timeFilter, boolean mustHaveIntLinks,
                                                  boolean mustHaveExtLinks, Long sitemapId)
            throws InternalException {
        int codeCount = primitive.length;

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

        Object[] params;

        String queryString;
        if (sitemapId == null) {
            queryString = String.format(
                    SELECT_ERROR_URLS_BY_CODE_COUNT_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : ""
            );
            params = SqlUtil.prepareFirstParams(primitive, hostDbHostInfo.getHostDbHostId());
        } else {
            queryString = String.format(
                    SELECT_ERROR_URLS_FOR_SITEMAP_BY_CODE_COUNT_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : ""
            );
            params = SqlUtil.prepareFirstParams(primitive, hostDbHostInfo.getHostDbHostId(), sitemapId);
        }

        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).queryForInt(queryString, params);
    }

    private Integer getShownErrorUrlsCountForCodeAndNode(BriefHostInfo hostInfo, int[] primitive, long nodeId,
                                                  TimeFilter timeFilter, boolean mustHaveIntLinks,
                                                  boolean mustHaveExtLinks, Long sitemapId)
            throws InternalException {
        int codeCount = primitive.length;

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

        Object[] params;

        String queryString;
        if (sitemapId == null) {
            queryString = String.format(
                    SELECT_SHOWN_ERROR_URLS_COUNT_BY_CODE_AND_NODE_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : ""
            );
            params = SqlUtil.prepareFirstParams(primitive, hostDbHostInfo.getHostDbHostId(), nodeId);
        } else {
            queryString = String.format(
                    SELECT_ERROR_URLS_FOR_SITEMAP_BY_CODE_COUNT_QUERY,
                    SqlUtil.createQuestionMarks(codeCount),
                    SqlUtil.getTimeFilterPart(timeFilter, FIELD_MODIFIED_ON),
                    mustHaveIntLinks ? "AND eu.has_int_links = 1" : "",
                    mustHaveExtLinks ? "AND eu.has_ext_links = 1" : ""
            );
            params = SqlUtil.prepareFirstParams(primitive, hostDbHostInfo.getHostDbHostId(), sitemapId);
        }

        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).queryForInt(queryString, params);
    }

    public ErrorCountInfo calcErrorCountInfo(BriefHostInfo hostInfo, UrlErrorOrGroup error, TimeFilter timeFilter,
                                             Boolean mustHaveIntLinks, Boolean mustHaveExtLinks, Long sitemapId) throws InternalException {
        return calcErrorCountInfo(hostInfo, error.getPrimitiveCodes(), timeFilter, mustHaveIntLinks, mustHaveExtLinks,
                sitemapId);
    }

    public ErrorCountInfo calcErrorCountInfo(BriefHostInfo hostInfo, int[] primitive, TimeFilter timeFilter,
                                             Boolean mustHaveIntLinks, Boolean mustHaveExtLinks, Long sitemapId) throws InternalException {
        Integer realCount = getErrorUrlsForCodeCount(hostInfo, primitive);
        Integer shownCount = getShownErrorUrlsForCodeCount(hostInfo, primitive, timeFilter, mustHaveIntLinks,
                mustHaveExtLinks, sitemapId);
        return new ErrorCountInfo(realCount, shownCount);
    }

    public ErrorCountInfo calcErrorCountInfoForCodeAndNode(BriefHostInfo hostInfo, UrlErrorOrGroup error, long nodeId, TimeFilter timeFilter,
                                             Boolean mustHaveIntLinks, Boolean mustHaveExtLinks, Long sitemapId) throws InternalException {
        return calcErrorCountInfoForCodeAndNode(hostInfo, error.getPrimitiveCodes(), nodeId, timeFilter, mustHaveIntLinks, mustHaveExtLinks,
                sitemapId);
    }

    public ErrorCountInfo calcErrorCountInfoForCodeAndNode(BriefHostInfo hostInfo, int[] primitive, long nodeId, TimeFilter timeFilter,
                                             Boolean mustHaveIntLinks, Boolean mustHaveExtLinks, Long sitemapId) throws InternalException {
        Integer realCount = getRealErrorUrlsCountForCodeAndNode(hostInfo, nodeId, primitive);
        Integer shownCount = getShownErrorUrlsCountForCodeAndNode(hostInfo, primitive, nodeId, timeFilter, mustHaveIntLinks,
                mustHaveExtLinks, sitemapId);
        return new ErrorCountInfo(realCount, shownCount);
    }

    public Date getOptionsUpdatedOnForUser(Long userId) throws InternalException {
        return getJdbcTemplate(WMCPartition.nullPartition()).queryForObject(SELECT_OPTIONS_UPDATED_ON_QUERY,
                new ParameterizedRowMapper<Date>() {
                    @Override
                    public Date mapRow(ResultSet rs, int i) throws SQLException {
                        return SqlUtil.safeGetTimestamp(rs, FIELD_OPTIONS_UPDATED_ON);
                    }
                }, userId);
    }

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

    @Required
    public void setUserOptionsService(UserOptionsService userOptionsService) {
        this.userOptionsService = userOptionsService;
    }

    @Required
    public void setUsersHostsService(UsersHostsService usersHostsService) {
        this.usersHostsService = usersHostsService;
    }
}
