package ru.yandex.wmconsole.service;

import java.net.IDN;
import java.net.URL;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeSet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.transaction.TransactionStatus;

import ru.yandex.common.framework.pager.Pager;
import ru.yandex.webmaster.common.util.HostnamePart;
import ru.yandex.webmaster.common.util.HostnamePartUtils;
import ru.yandex.wmconsole.data.HostRegionChangeRequestStateEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.info.RegionModerationHistoryInfo;
import ru.yandex.wmconsole.data.info.UserHostRegionInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.dao.TblRegionModerationDao;
import ru.yandex.wmconsole.service.dao.TblVerifiedHostsRegionsDao;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmtools.common.data.RegionTypeEnum;
import ru.yandex.wmtools.common.data.info.HostRegionInfo;
import ru.yandex.wmtools.common.data.info.HostRegionsInfo;
import ru.yandex.wmtools.common.data.info.RegionInfo;
import ru.yandex.wmtools.common.data.info.WMUserInfo;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.servantlet.AbstractServantlet;
import ru.yandex.wmtools.common.service.AbstractDbService;
import ru.yandex.wmtools.common.service.IUserInfoService;
import ru.yandex.wmtools.common.service.RegionsTreeCacheService;
import ru.yandex.wmtools.common.util.ServiceTransactionCallbackWithoutResult;
import ru.yandex.wmtools.common.util.SqlUtil;
import ru.yandex.wmtools.common.util.TimeFilter;
import ru.yandex.wmtools.common.util.URLUtil;

/**
 * Date: May 13, 2009
 *
 * @author yakushev
 */
public class HostRegionService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(HostRegionService.class);

    public static final int NO_REGION = 0;
    public static final int UNDEFINED_REGION = 29;

    private static final int NUMBER_OF_WAITING_TO_SHOW = 100;

    private static final String FIELD_ID = "id";
    private static final String FIELD_REGION_ID = "region_id";
    private static final String FIELD_AUTO_REGION_ID = "auto_region_id";
    private static final String FIELD_TYPE_ID = "type_id";
    private static final String FIELD_STATE_ID = "state";
    private static final String FIELD_MODERATOR_ID = "moderator";
    private static final String FIELD_HOST_ID = "host";
    private static final String FIELD_USER_ID = "user";
    private static final String FIELD_UPDATE_DAY = "update_day";
    private static final String FIELD_CHECK_DAY = "check_day";
    private static final String FIELD_PERFORMED_ON = "performed_on";
    private static final String FIELD_CONFIRMED = "confirmed";
    private static final String FIELD_HOSTNAME = "hostname";
    private static final String FIELD_URL = "url";
    private static final String FIELD_COMMENT = "comment";
    private static final String FIELD_COUNT = "count";
    private static final String FIELD_IS_VISIBLE = "is_visible";
    private static final String FIELD_APPLIED = "applied";

    private static final String SELECT_CLASSIFICATION_INFO_FOR_HOST_QUERY =
            "SELECT " +
                    "    hr.region_id as " + FIELD_REGION_ID + ", " +
                    "    hr.visible as " + FIELD_IS_VISIBLE + ", " +
                    "    hr.type_id as " + FIELD_TYPE_ID + " " +
                    "FROM " +
                    "    tbl_hosts_regions hr " +
                    "WHERE " +
                    "    hr.host_id = ? " +
                    "ORDER BY " +
                    "    hr.region_id";

    private static final String SELECT_VISIBLE_CLASSIFICATION_INFO_FOR_HOST_QUERY =
            "SELECT " +
                    "    hr.region_id as " + FIELD_REGION_ID + ", " +
                    "    hr.visible as " + FIELD_IS_VISIBLE + ", " +
                    "    hr.type_id as " + FIELD_TYPE_ID + " " +
                    "FROM " +
                    "    tbl_hosts_regions hr " +
                    "WHERE " +
                    "    hr.host_id = ? " +
                    "AND " +
                    "    hr.visible = 1 " +
                    "ORDER BY " +
                    "    hr.region_id";

    private static final String SELECT_REGION_MODERATION_COUNT_QUERY =
            "SELECT " +
                    "    count(*) " +
                    "FROM " +
                    "    tbl_region_moderation rm " +
                    "LEFT JOIN " +
                    "    tbl_user_hosts_regions uhr " +
                    "ON " +
                    "    uhr.host_id = rm.host_id " +
                    "WHERE " +
                    "    %1$s " +
                    "    %2$s " +
                    "    %3$s " +
                    "    %4$s ";

    private static final String SELECT_REGION_MODERATION_QUERY =
            "SELECT " +
                    "    rm.region_id as " + FIELD_REGION_ID + ", " +
                    "    rm.auto_region_id as " + FIELD_AUTO_REGION_ID + ", " +
                    "    rm.url as " + FIELD_URL + ", " +
                    "    rm.id as " + FIELD_ID + ", " +
                    "    rm.moderator_id as " + FIELD_MODERATOR_ID + ", " +
                    "    rm.host_id as " + FIELD_HOST_ID + ", " +
                    "    rm.hostname as " + FIELD_HOSTNAME + ", " +
                    "    rm.performed_on as " + FIELD_PERFORMED_ON + ", " +
                    "    rm.state_id as " + FIELD_STATE_ID + ", " +
                    "    uhr.host_id as " + FIELD_APPLIED + " " +
                    "FROM " +
                    "    tbl_region_moderation rm " +
                    "LEFT JOIN " +
                    "    tbl_user_hosts_regions uhr " +
                    "ON " +
                    "    uhr.host_id = rm.host_id " +
                    "WHERE " +
                    "    %1$s " +
                    "    %2$s " +
                    "    %3$s " +
                    "    %4$s " +
                    "ORDER BY " +
                    "    rm.performed_on DESC " +
                    "    %5$s ";

    private static final String SELECT_USER_HOST_REGION_INFO_QUERY =
            "SELECT " +
                    "    uhr.region_id as " + FIELD_REGION_ID + ", " +
                    "    uhr.state_id as " + FIELD_STATE_ID + ", " +
                    "    uhr.host_id as " + FIELD_HOST_ID + ", " +
                    "    uhr.user_id as " + FIELD_USER_ID + ", " +
                    "    uhr.update_day as " + FIELD_UPDATE_DAY + ", " +
                    "    uhr.check_day as " + FIELD_CHECK_DAY + ", " +
                    "    uhr.url as " + FIELD_URL + ", " +
                    "    uhr.comment as " + FIELD_COMMENT + ", " +
                    "    uhr.moderator_id as " + FIELD_MODERATOR_ID + ", " +
                    "    uhr.confirmed as " + FIELD_CONFIRMED + ", " +
                    "    h.name as " + FIELD_HOSTNAME + " " +
                    "FROM " +
                    "    tbl_user_hosts_regions uhr " +
                    "LEFT JOIN " +
                    "    tbl_hosts h " +
                    "ON " +
                    "    uhr.host_id = h.host_id " +
                    "WHERE " +
                    "    uhr.host_id = ?";

    private static final String DELETE_USER_HOST_REGION_INFO_QUERY =
            "DELETE " +
                    "FROM " +
                    "    tbl_user_hosts_regions " +
                    "WHERE " +
                    "    host_id = ?";

    private static final String SELECT_AUTO_REGION_INFO_QUERY =
            "SELECT " +
                    "    region_id as " + FIELD_REGION_ID + " " +
                    "FROM " +
                    "    tbl_hosts_regions hr " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    type_id = " + RegionTypeEnum.AUTO.getId() + " " +
                    "LIMIT 1";

    private static final String SELECT_WAITING_LIST_QUERY =
            "SELECT " +
                    "    uhr.region_id as " + FIELD_REGION_ID + ", " +
                    "    uhr.state_id as " + FIELD_STATE_ID + ", " +
                    "    uhr.host_id as " + FIELD_HOST_ID + ", " +
                    "    uhr.user_id as " + FIELD_USER_ID + ", " +
                    "    uhr.update_day as " + FIELD_UPDATE_DAY + ", " +
                    "    uhr.check_day as " + FIELD_CHECK_DAY + ", " +
                    "    uhr.url as " + FIELD_URL + ", " +
                    "    uhr.comment as " + FIELD_COMMENT + ", " +
                    "    uhr.moderator_id as " + FIELD_MODERATOR_ID + ", " +
                    "    uhr.confirmed as " + FIELD_CONFIRMED + ", " +
                    "    h.name as " + FIELD_HOSTNAME + " " +
                    "FROM " +
                    "    tbl_user_hosts_regions uhr " +
                    "JOIN " +
                    "    tbl_hosts h " +
                    "ON " +
                    "    uhr.host_id = h.host_id " +
                    "WHERE " +
                    "    uhr.state_id = ? " +
                    "    %1$s " +
                    "    %2$s " +
                    "    %3$s " +
                    "ORDER BY " +
                    "    uhr.update_day ASC " +
                    "LIMIT " + NUMBER_OF_WAITING_TO_SHOW;

    private static final String SELECT_WAITING_LIST_COUNT_QUERY =
            "SELECT " +
                    "    count(*) as " + FIELD_COUNT + " " +
                    "FROM " +
                    "    tbl_user_hosts_regions uhr " +
                    "JOIN " +
                    "    tbl_hosts h " +
                    "ON " +
                    "    uhr.host_id = h.host_id " +
                    "WHERE " +
                    "    uhr.state_id = ? " +
                    "    %1$s " +
                    "    %2$s " +
                    "    %3$s ";

    private static final String UPDATE_CONFIRM_USER_HOST_REGION_REFUSE_QUERY =
            "UPDATE " +
                    "    tbl_user_hosts_regions " +
                    "SET " +
                    "    confirmed = 1 " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    user_id = ? " +
                    "AND " +
                    "    state_id = " + HostRegionChangeRequestStateEnum.DENIED.getValue();

    private static final String UPDATE_RESOLVE_WAITING_UPDATE_QUERY =
            "UPDATE " +
                    "    tbl_user_hosts_regions " +
                    "SET " +
                    "    state_id = ?, " +
                    "    check_day = NOW(), " +
                    "    moderator_id = ?, " +
                    "    comment = ? " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    state_id IN ( " + HostRegionChangeRequestStateEnum.WAITING.getValue() + ", " + HostRegionChangeRequestStateEnum.DELAYED.getValue() + ") ";

    private static final String UPDATE_RETURN_TO_MODERATION_QUERY =
            "UPDATE " +
                    "    tbl_user_hosts_regions " +
                    "SET " +
                    "    state_id = " + HostRegionChangeRequestStateEnum.WAITING.getValue() + ", " +
                    "    check_day = NOW() " +
                    "WHERE " +
                    "    host_id = ? ";

    private static final String UPDATE_MODERATOR_ISSUE_QUERY =
            "UPDATE " +
                    "    tbl_user_hosts_regions " +
                    "SET " +
                    "    state_id = ?, " +
                    "    check_day = NOW(), " +
                    "    region_id = ?, " +
                    "    moderator_id = ?, " +
                    "    comment = ? " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    state_id IN ( " + HostRegionChangeRequestStateEnum.WAITING.getValue() + ", " + HostRegionChangeRequestStateEnum.DELAYED.getValue() + ") ";

    private static final String UPDATE_DELAY_WAITING_QUERY =
            "UPDATE " +
                    "    tbl_user_hosts_regions " +
                    "SET " +
                    "    state_id = ?, " +
                    "    check_day = NOW(), " +
                    "    moderator_id = ? " +
                    "WHERE " +
                    "    host_id = ? " +
                    "AND " +
                    "    state_id = " + HostRegionChangeRequestStateEnum.WAITING.getValue();

    private static final String INSERT_SET_USER_HOST_REGION_QUERY =
            "INSERT INTO " +
                    "    tbl_user_hosts_regions (host_id, region_id, state_id, user_id, update_day, check_day, url) " +
                    "VALUES " +
                    "    (?, ?, ?, ?, NOW(), ?, ?) " +
                    "ON DUPLICATE KEY UPDATE " +
                    "    region_id = ?, " +
                    "    state_id = ?, " +
                    "    user_id = ?, " +
                    "    update_day = NOW(), " +
                    "    check_day = ?, " +
                    "    moderator_id = NULL, " +
                    "    confirmed = 0, " +
                    "    url = ? ";

    private HostDbHostInfoService hostDbHostInfoService;
    private IUserInfoService userInfoService;
    private RegionsTreeCacheService regionsTreeCacheService;
    private TblRegionModerationDao tblRegionModerationDao;
    private TblVerifiedHostsRegionsDao tblVerifiedHostsRegionsDao;


    private final ParameterizedRowMapper<HostRegionInfo> CLASSIFICATION_INFO_ROW_MAPPER =
            new ParameterizedRowMapper<HostRegionInfo>()
    {
        @Override
        public HostRegionInfo mapRow(ResultSet rs, int i) throws SQLException {
            int regionId = rs.getInt(FIELD_REGION_ID);
            int visible = rs.getInt(FIELD_IS_VISIBLE);
            RegionInfo regionInfo = getRegionInfo(regionId);
            RegionTypeEnum type = RegionTypeEnum.getByValue(rs.getInt(FIELD_TYPE_ID));
            return new HostRegionInfo(regionInfo, type, visible > 0);
        }
    };

    private final ParameterizedRowMapper<RegionModerationHistoryInfo> REGION_MODERATION_HISTORY_INFO_ROW_MAPPER =
            new ParameterizedRowMapper<RegionModerationHistoryInfo>()
    {
        @Override
        public RegionModerationHistoryInfo mapRow(ResultSet rs, int i) throws SQLException {
            WMUserInfo moderator = null;
            try {
                moderator = userInfoService.getUserInfo(rs.getLong(FIELD_MODERATOR_ID));
            } catch (UserException e) {
                log.warn("Failed to get user info inside row mapper", e);
            } catch (InternalException e) {
                log.warn("Failed to get user info inside row mapper", e);
            }

            long id = rs.getLong(FIELD_ID);
            long hostId = rs.getLong(FIELD_HOST_ID);
            String hostname = rs.getString(FIELD_HOSTNAME);
            Date performedOn = rs.getTimestamp(FIELD_PERFORMED_ON);
            HostRegionChangeRequestStateEnum state = HostRegionChangeRequestStateEnum.R.fromValueOrNull(
                    rs.getInt(FIELD_STATE_ID));
            String url = rs.getString(FIELD_URL);

            final RegionInfo region;
            int regionId = rs.getInt(FIELD_REGION_ID);
            if (rs.wasNull()) {
                region = null;
            } else {
                region = getRegionInfo(regionId);
            }

            final RegionInfo autoRegion;
            int autoRegionId = rs.getInt(FIELD_AUTO_REGION_ID);
            if (rs.wasNull()) {
                autoRegion = null;
            } else {
                autoRegion = getRegionInfo(autoRegionId);
            }

            boolean applied = false;
            rs.getLong(FIELD_APPLIED);
            if (rs.wasNull()) {
                applied = true;
            }

            return new RegionModerationHistoryInfo(id, moderator, hostId, hostname, performedOn, state, region,
                    autoRegion, url, applied);
        }
    };

    private final ParameterizedRowMapper<UserHostRegionInfo> USER_HOST_REGION_INFO_ROW_MAPPER =
            new ParameterizedRowMapper<UserHostRegionInfo>()
    {
        @Override
        public UserHostRegionInfo mapRow(ResultSet rs, int i) throws SQLException {
            long hostId = rs.getLong(FIELD_HOST_ID);
            long userId = rs.getLong(FIELD_USER_ID);

            int regionId = rs.getInt(FIELD_REGION_ID);
            RegionInfo regionInfo = getRegionInfo(regionId);
            HostRegionInfo hostRegionInfo = new HostRegionInfo(regionInfo, RegionTypeEnum.WMCONSOLE, true);

            HostRegionChangeRequestStateEnum state = HostRegionChangeRequestStateEnum.R.fromValueOrNull(rs.getInt(FIELD_STATE_ID));

            Date updateDay = rs.getTimestamp(FIELD_UPDATE_DAY);
            Date checkDay = rs.getTimestamp(FIELD_CHECK_DAY);

            WMUserInfo moderator = null;
            try {
                long moderatorId = rs.getLong(FIELD_MODERATOR_ID);
                if (!rs.wasNull()) {
                    moderator = userInfoService.getUserInfo(moderatorId);
                }
            } catch (UserException e) {
                log.warn("Failed to get user info inside row mapper", e);
            } catch (InternalException e) {
                log.warn("Failed to get user info inside row mapper", e);
            }

            String url = rs.getString(FIELD_URL);
            String comment = rs.getString(FIELD_COMMENT);
            if (rs.wasNull()) {
                comment = null;
            }

            boolean confirmed = rs.getBoolean(FIELD_CONFIRMED);

            String hostname = rs.getString(FIELD_HOSTNAME);
            return new UserHostRegionInfo(hostId, hostname, userId, hostRegionInfo, state, updateDay, checkDay,
                    moderator, confirmed, url, comment);
        }
    };

    private RegionInfo getRegionInfo(int regionId) {
        if (regionId == UNDEFINED_REGION) {
            return new RegionInfo(UNDEFINED_REGION, (byte)0, (byte)0, 0, (byte)0);
        } else if (regionId == NO_REGION) {
            return new RegionInfo(NO_REGION, (byte)0, (byte)0, 0, (byte)0);
        }
        try {
            return regionsTreeCacheService.getRegionInfo(regionId);
        } catch (InternalException e) {
            log.error("Unable  to find region=" + regionId, e);
            return new RegionInfo(regionId, (byte)0, (byte)0, 0, (byte)0);
        }
    }

    public HostRegionsInfo getRegionsForHost(final HostDbHostInfo hostDbHostInfo) throws InternalException {
        Collection<HostRegionInfo> regions = getGoodRegions(
                getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(
                        SELECT_CLASSIFICATION_INFO_FOR_HOST_QUERY,
                        CLASSIFICATION_INFO_ROW_MAPPER,
                        hostDbHostInfo.getHostDbHostId()
                )
                , true);

        return new HostRegionsInfo(regions);
    }

    public HostRegionsInfo getVisibleRegionsForHost(final HostDbHostInfo hostDbHostInfo) throws InternalException {
        Collection<HostRegionInfo> regions = getGoodRegions(
                getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).query(
                        SELECT_VISIBLE_CLASSIFICATION_INFO_FOR_HOST_QUERY,
                        CLASSIFICATION_INFO_ROW_MAPPER,
                        hostDbHostInfo.getHostDbHostId()
                )
                , true);

        int initialSize = regions.size();
        boolean hasCatalog;

        // if we have regions from ya.catalog, delete others.
        hasCatalog = leaveOnlyRegionsOfSpecifiedType(regions, RegionTypeEnum.CATALOG);

        // if we don't have regions from ya.catalog, but have user-specified regions, delete others.
        leaveOnlyRegionsOfSpecifiedType(regions, RegionTypeEnum.WMCONSOLE);

        // removing "empty" regions.
        List<HostRegionInfo> regionsToRemove = new ArrayList<HostRegionInfo>();
        for (HostRegionInfo info : regions) {
            if (info.getRegionInfo().isEmpty()) {
                regionsToRemove.add(info);
            }
        }
        regions.removeAll(regionsToRemove);

        if ((regions.size() == 0) && (initialSize > 0)) {
            if (!regionsToRemove.isEmpty() & !hasCatalog) {
                regions.add(new HostRegionInfo(RegionInfo.zeroRegion, regionsToRemove.get(0).getRegionType(), true));
            } else {
                regions.add(new HostRegionInfo(RegionInfo.zeroRegion, hasCatalog ? RegionTypeEnum.CATALOG : RegionTypeEnum.AUTO, true));
            }
        }

        return new HostRegionsInfo(regions);
    }

    private boolean leaveOnlyRegionsOfSpecifiedType(Collection<HostRegionInfo> regions, RegionTypeEnum regionType) {
        boolean hasRegionsOfSpecifiedType = false;
        for (HostRegionInfo info : regions) {
            if (regionType.equals(info.getRegionType())) {
                hasRegionsOfSpecifiedType = true;
                break;
            }
        }

        if (hasRegionsOfSpecifiedType) {
            Collection<HostRegionInfo> regionsToRemove = new ArrayList<HostRegionInfo>();
            for (HostRegionInfo info : regions) {
                if (!regionType.equals(info.getRegionType())) {
                    regionsToRemove.add(info);
                }
            }
            regions.removeAll(regionsToRemove);
        }

        return hasRegionsOfSpecifiedType;
    }

    private Collection<HostRegionInfo> getGoodRegions(final Collection<HostRegionInfo> regions, boolean useHideForCatalogOnly) throws InternalException {
        Collection<HostRegionInfo> goodRegions = new LinkedHashSet<HostRegionInfo>();

        for (HostRegionInfo region : regions) {
            goodRegions.add(findGoodRegionForRegion(region, !useHideForCatalogOnly || RegionTypeEnum.CATALOG.equals(region.getRegionType())));
        }

        return goodRegions;
    }

    private HostRegionInfo findGoodRegionForRegion(HostRegionInfo hostRegion, boolean useHide) throws InternalException {
        while ((hostRegion.getRegionInfo().getType() != 0) || (hostRegion.getRegionInfo().getHide() != 0 && useHide)) {
            if ((hostRegion.getRegionInfo().getId() == NO_REGION) || (hostRegion.getRegionInfo().getId() == UNDEFINED_REGION)) {
                // we have found magic regions and have to finish search.
                break;
            }

            if (hostRegion.getRegionInfo().getParent() == hostRegion.getRegionInfo().getId()) {
                // we have reached tree root
                break;
            }

            RegionInfo newRegion = regionsTreeCacheService.getRegionInfo(hostRegion.getRegionInfo().getParent());
            hostRegion = new HostRegionInfo(newRegion, hostRegion.getRegionType(), hostRegion.isVisible());
        }

        return hostRegion;
    }

    public void tryModifyRegion(final long userId, final BriefHostInfo hostInfo, final HostDbHostInfo hostDbHostInfo, final int region, final String url)
            throws UserException, InternalException {
        if (region != HostRegionService.UNDEFINED_REGION) {
            RegionInfo regionInfo = regionsTreeCacheService.getRegionInfo(region);
            if ((regionInfo.getGeotype() < RegionInfo.GEOTYPE_COUNTRY) || (regionInfo.getGeotype() > RegionInfo.GEOTYPE_CITY)) {
                throw new UserException(WMCUserProblem.REGION_IS_NOT_ALLOWED_TO_SET,
                        "Only regions in bounds [3 (country) .. 6 (city)] are allowed to be set, but user tried" +
                                " to set region " + regionInfo.getDefaultName() + " with geotype " + regionInfo.getGeotype(),
                        "region", Integer.toString(region));
            }
        }

        HostRegionsInfo hostRegionsInfo = getRegionsForHost(hostDbHostInfo);
        final boolean[] autoMatch = new boolean[1];

        for (HostRegionInfo hostRegionInfo : hostRegionsInfo.getRegions()) {
            if (RegionTypeEnum.CATALOG.equals(hostRegionInfo.getRegionType())) {
                if (hostRegionInfo.isVisible()) {
                    throw new UserException(WMCUserProblem.REGION_SET_IN_CATALOG, getClass().getName(), "Cannot set region, because it is already set in catalog.");
                }
            }

            if (RegionTypeEnum.AUTO.equals(hostRegionInfo.getRegionType())) {
                if (region == hostRegionInfo.getRegionInfo().getId()) {
                    autoMatch[0] = true;
                }
            }
        }

        final HostRegionChangeRequestStateEnum state = autoMatch[0] ? HostRegionChangeRequestStateEnum.VERIFIED : HostRegionChangeRequestStateEnum.WAITING;
        final Date checkDay = autoMatch[0] ? new Date() : null;

        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                getJdbcTemplate(WMCPartition.nullPartition()).update(INSERT_SET_USER_HOST_REGION_QUERY,
                        hostInfo.getId(),
                        region, state.getValue(), userId, checkDay, url,
                        region, state.getValue(), userId, checkDay, url);

                if (autoMatch[0]) {
                    tblVerifiedHostsRegionsDao.insertVerifiedHostsRegions(hostInfo, region);

                    tblRegionModerationDao.insertRegionModerationHistory(userId, hostInfo, HostRegionChangeRequestStateEnum.VERIFIED, url, region, (long) region);
                }
            }
        });
    }

    public void moderatorIssueRegion(final long moderatorId, final BriefHostInfo hostInfo, final int region, final String comment, final String url) throws UserException, InternalException {
        final String userUrl;
        if (url == null) {
            // write url, provided by user
            userUrl = getUserSetRegionForHost(hostInfo.getId()).getUrl();
        } else {
            userUrl = null;
        }
        final Long autoRegionId = getAutoRegionForHost(hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName()));

        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_MODERATOR_ISSUE_QUERY,
                        HostRegionChangeRequestStateEnum.MODERATOR_ISSUED.getValue(), region, moderatorId, comment, hostInfo.getId());
                tblRegionModerationDao.insertRegionModerationHistory(
                        moderatorId, hostInfo, HostRegionChangeRequestStateEnum.MODERATOR_ISSUED,
                        (url == null) ? userUrl : url, region, autoRegionId);
                tblVerifiedHostsRegionsDao.insertVerifiedHostsRegions(hostInfo, region);
            }
        });
    }

    public void deleteUserSetRegionForHost(final long hostId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(DELETE_USER_HOST_REGION_INFO_QUERY, hostId);
    }

    public UserHostRegionInfo getUserSetRegionForHost(final long hostId) throws InternalException {
        return getJdbcTemplate(WMCPartition.nullPartition()).safeQueryForObject(SELECT_USER_HOST_REGION_INFO_QUERY,
                USER_HOST_REGION_INFO_ROW_MAPPER, hostId);
    }

    public Long getAutoRegionForHost(final HostDbHostInfo hostDbHostInfo) throws InternalException {
        return getJdbcTemplate(new WMCPartition(hostDbHostInfo, null)).safeQueryForLong(SELECT_AUTO_REGION_INFO_QUERY, hostDbHostInfo.getHostDbHostId());
    }

    public void confirmUserHostRegionRefuse(long userId, HostDbHostInfo hostDbHostInfo) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_CONFIRM_USER_HOST_REGION_REFUSE_QUERY, hostDbHostInfo.getHostDbHostId(), userId);
    }

    public Collection<UserHostRegionInfo> getWaitingHosts(boolean delayed, String urlTemplate, TimeFilter timeFilter, Long moderatorId)
            throws InternalException {
        Collection<UserHostRegionInfo> res = new TreeSet<UserHostRegionInfo>();

        String hostName = urlTemplate;
        if (urlTemplate != null) {
            try {
                URL url = AbstractServantlet.prepareUrl(urlTemplate, true);
                hostName = URLUtil.getHostName(url, false);
            } catch (UserException e) {
                log.error("unable to parse urlTemplate");
            }
        }

        String waitingListQuery = String.format(SELECT_WAITING_LIST_QUERY,
                (urlTemplate != null) ? " AND ((uhr.url LIKE '%" + hostName + "%') OR (h.name LIKE '%" + hostName + "%')) " : " ",
                SqlUtil.getTimeFilterPart(timeFilter, FIELD_UPDATE_DAY),
                (moderatorId != null) ? " AND uhr.moderator_id = " + moderatorId : " ");

        Collection<UserHostRegionInfo> tempInfos = getJdbcTemplate(WMCPartition.nullPartition()).query(
                waitingListQuery, USER_HOST_REGION_INFO_ROW_MAPPER,
                delayed ? HostRegionChangeRequestStateEnum.DELAYED.getValue() : HostRegionChangeRequestStateEnum.WAITING.getValue());

        for (UserHostRegionInfo info : tempInfos) {
            HostDbHostInfo hostDbInfo = hostDbHostInfoService.getHostDbHostInfo(info.getHostName());
            Collection<HostRegionInfo> regions = getJdbcTemplate(new WMCPartition(hostDbInfo, null)).query(
                    SELECT_CLASSIFICATION_INFO_FOR_HOST_QUERY, CLASSIFICATION_INFO_ROW_MAPPER,
                    hostDbInfo.getHostDbHostId());
            Collection<HostRegionInfo> goodRegions = getGoodRegions(regions, true);

            res.add(new UserHostRegionInfo(info, goodRegions));
        }

        return res;
    }

    public int getWaitingHostsCount(boolean delayed, String urlTemplate, TimeFilter timeFilter, Long moderatorId)
            throws InternalException {

        String hostName = urlTemplate;
        if (urlTemplate != null) {
            try {
                URL url = AbstractServantlet.prepareUrl(urlTemplate, true);
                hostName = URLUtil.getHostName(url, false);
            } catch (UserException e) {
                log.error("unable to parse urlTemplate");
            }
        }

        String waitingListQuery = String.format(SELECT_WAITING_LIST_COUNT_QUERY,
                (urlTemplate != null) ? " AND ((uhr.url LIKE '%" + hostName + "%') OR (h.name LIKE '%" + hostName + "%')) " : " ",
                SqlUtil.getTimeFilterPart(timeFilter, FIELD_UPDATE_DAY),
                (moderatorId != null) ? " AND uhr.moderator_id = " + moderatorId : " ");

        return getJdbcTemplate(WMCPartition.nullPartition()).safeQueryForInt(waitingListQuery, delayed ? HostRegionChangeRequestStateEnum.DELAYED.getValue() : HostRegionChangeRequestStateEnum.WAITING.getValue());
    }

    public void resolveWaiting(final BriefHostInfo hostInfo, final HostRegionChangeRequestStateEnum state, final long moderatorId, final String comment)
            throws UserException, InternalException {
        final UserHostRegionInfo regionInfo = getUserSetRegionForHost(hostInfo.getId());
        final Long autoRegionId = getAutoRegionForHost(hostDbHostInfoService.getHostDbHostInfo(hostInfo.getName()));
        final int region = regionInfo.getUserClassificationInfo().getRegionInfo().getId();

        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_RESOLVE_WAITING_UPDATE_QUERY,
                        state.getValue(), moderatorId, comment, hostInfo.getId());
                tblRegionModerationDao.insertRegionModerationHistory(moderatorId, hostInfo, state, regionInfo.getUrl(), region, autoRegionId);

                if (state.isVerifiedState()) {
                    tblVerifiedHostsRegionsDao.insertVerifiedHostsRegions(hostInfo, region);
                } else if (state == HostRegionChangeRequestStateEnum.DENIED) {
                    tblVerifiedHostsRegionsDao.deleteVerifiedHostsRegions(hostInfo);
                }
            }
        });
    }

    public void delayWaiting(final long hostId, final long moderatorId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_DELAY_WAITING_QUERY,
                HostRegionChangeRequestStateEnum.DELAYED.getValue(), moderatorId, hostId);
    }

    public Collection<RegionModerationHistoryInfo> listRegionModerationHistory(final Long moderatorId,
            final Pager pager, final TimeFilter timeFilter, final HostRegionChangeRequestStateEnum state,
            String urlTemplate) throws InternalException
    {
        final String moderatorPlaceholder;
        if (moderatorId != null) {
            moderatorPlaceholder = "rm.moderator_id = ?";// + moderatorId;
        } else {
            moderatorPlaceholder = "null is ?";
        }

        HostnamePart hostnamePart = HostnamePartUtils.cleanHostnamePart(urlTemplate);

        final String stateClause = (state != null) ? " AND rm.state_id = " + state.getValue() + " " : " ";
        final String performedOnClause = SqlUtil.getTimeFilterPart(timeFilter, FIELD_PERFORMED_ON);

        final String countQuery = String.format(
                SELECT_REGION_MODERATION_COUNT_QUERY,
                moderatorPlaceholder,
                stateClause,
                performedOnClause,
                (hostnamePart != null) ? " AND (" + toHostnameLikeSql("rm.hostname", hostnamePart) + ") " : " "
        );

        final String selectQuery = String.format(
                SELECT_REGION_MODERATION_QUERY,
                moderatorPlaceholder,
                stateClause,
                performedOnClause,
                (hostnamePart != null) ? " AND (" + toHostnameLikeSql("rm.hostname", hostnamePart) + ") " : " ",
                "%1$s"
        );

        return getJdbcTemplate(WMCPartition.nullPartition()).pageableSelect(
                countQuery,
                selectQuery,
                REGION_MODERATION_HISTORY_INFO_ROW_MAPPER,
                pager,
                moderatorId
        );
    }

    static String toHostnameLikeSql(String columnName, HostnamePart hostnamePart) {
        StringBuilder sb = new StringBuilder();

        sb.append(columnName).append(" LIKE ");
        sb.append('\'');
        if (hostnamePart.isPrefix()) {
            if (hostnamePart.getProtocol() == HostnamePart.Protocol.HTTPS) {
                sb.append("https://");
            }
        } else {
            sb.append("%%");
        }
        if (hostnamePart.isIDN()) {
            sb.append(IDN.toASCII(hostnamePart.getHostnamePart()));
        } else {
            sb.append(hostnamePart.getHostnamePart());
            sb.append("%%");
        }
        sb.append('\'');
        return sb.toString();
    }

    public void returnToModeration(final BriefHostInfo hostInfo) throws UserException, InternalException {
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                int updatedRows = getJdbcTemplate(WMCPartition.nullPartition()).update(
                        UPDATE_RETURN_TO_MODERATION_QUERY, hostInfo.getId());
                if (updatedRows > 0) {
                    tblVerifiedHostsRegionsDao.deleteVerifiedHostsRegions(hostInfo);
                    tblRegionModerationDao.deleteRegionModeration(hostInfo.getId());
                }
            }
        });
    }

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

    @Required
    public void setUserInfoService(IUserInfoService userInfoService) {
        this.userInfoService = userInfoService;
    }

    @Required
    public void setRegionsTreeCacheService(RegionsTreeCacheService regionsTreeCacheService) {
        this.regionsTreeCacheService = regionsTreeCacheService;
    }

    @Required
    public void setTblRegionModerationDao(TblRegionModerationDao tblRegionModerationDao) {
        this.tblRegionModerationDao = tblRegionModerationDao;
    }

    @Required
    public void setTblVerifiedHostsRegionsDao(TblVerifiedHostsRegionsDao tblVerifiedHostsRegionsDao) {
        this.tblVerifiedHostsRegionsDao = tblVerifiedHostsRegionsDao;
    }
}
