package ru.yandex.wmconsole.service;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import ru.yandex.wmconsole.data.info.*;
import ru.yandex.wmconsole.data.partition.WMCCopyPartition;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmtools.common.data.partition.IPartition;
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 java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;

/**
 * User: azakharov
 * Date: 24.12.12
 * Time: 20:10
 */
public class DbCopyService extends AbstractDbService {
    
    private static final String FIELD_TABLE_NAME = "TABLE_NAME";
    private static final String FIELD_COLUMN_NAME = "COLUMN_NAME";
    private static final String FIELD_REFERENCED_TABLE_NAME = "REFERENCED_TABLE_NAME";
    private static final String FIELD_REFERENCED_COLUMN_NAME = "REFERENCED_COLUMN_NAME";
    private static final String FIELD_EXTRA = "EXTRA";

    private static String HOST_ID_QUERY_STRING = "SELECT host_id FROM tbl_hosts WHERE name like ?";
    
    private static final String REFERENCING_TABLES_QUERY_STRING =
            "SELECT " +
                    "   TABLE_NAME AS " + FIELD_TABLE_NAME + ", " +
                    "   COLUMN_NAME AS "+ FIELD_COLUMN_NAME + ", " +
                    "   REFERENCED_TABLE_NAME AS " + FIELD_REFERENCED_TABLE_NAME + ", " +
                    "   REFERENCED_COLUMN_NAME AS "+ FIELD_REFERENCED_COLUMN_NAME + " " +
                    "FROM " +
                    "   INFORMATION_SCHEMA.KEY_COLUMN_USAGE "+
                    "WHERE " +
                    "   REFERENCED_TABLE_NAME = ? AND " +
                    "   TABLE_SCHEMA IN ('host_03', 'host_04', 'host_05', 'host_06', "+
                    "                    'host_07', 'host_08', 'host_09', 'host_10')";

    private static final String DESIRED_REFERENCING_TABLES_QUERY_STRING =
            "SELECT " +
                    "   TABLE_NAME AS " + FIELD_TABLE_NAME + ", " +
                    "   COLUMN_NAME AS "+ FIELD_COLUMN_NAME + ", " +
                    "   REFERENCED_TABLE_NAME AS " + FIELD_REFERENCED_TABLE_NAME + ", " +
                    "   REFERENCED_COLUMN_NAME AS "+ FIELD_REFERENCED_COLUMN_NAME + " " +
                    "FROM " +
                    "   INFORMATION_SCHEMA.KEY_COLUMN_USAGE "+
                    "WHERE " +
                    "   REFERENCED_TABLE_NAME = ? AND " +
                    "   TABLE_NAME IN (%1s) AND " +
                    "   TABLE_SCHEMA IN ('host_03', 'host_04', 'host_05', 'host_06', "+
                    "                    'host_07', 'host_08', 'host_09', 'host_10')";

    private static final String REFERENCED_TABLES_QUERY_STRING =
            "SELECT " +
                    "   TABLE_NAME AS " + FIELD_TABLE_NAME + ", " +
                    "   COLUMN_NAME AS "+ FIELD_COLUMN_NAME + ", " +
                    "   REFERENCED_TABLE_NAME AS " + FIELD_REFERENCED_TABLE_NAME + ", " +
                    "   REFERENCED_COLUMN_NAME AS "+ FIELD_REFERENCED_COLUMN_NAME + " " +
                    "FROM " +
                    "   INFORMATION_SCHEMA.KEY_COLUMN_USAGE "+
                    "WHERE " +
                    "   TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL ";
    
    private static final String PRIMARY_KEYS_QUERY_STRING =
            "SELECT " +
                    "   COLUMN_NAME AS " + FIELD_COLUMN_NAME + ", " +
                    "   EXTRA AS " + FIELD_EXTRA + " " +
                    "FROM INFORMATION_SCHEMA.COLUMNS " +
                    "WHERE " +
                    "   TABLE_NAME = ? AND " +
                    "   COLUMN_KEY = 'PRI'";
    
    private static final String INSERT_HOST_QUERY_STRING =
            "INSERT INTO tbl_hosts (name) VALUES (?)";
    
    private static final String SELECT_QUERY_STRING =
            "SELECT * FROM %1$s WHERE %2$s";
    
    private static final String REPLACE_QUERY_STRING =
            "REPLACE INTO %1$s (%2$s) VALUES (%3$s)";

    private static final Logger log = Logger.getLogger(DbCopyService.class);
    
    private List<String> commonTables;
    private List<String> excludedPagesTables;
    private List<String> topTables;
    private List<String> hostMonTables;
    private List<String> indexHistoryTables;

    /**
     * Копирует данные для хоста
     *
     * @return  hostDbHostInfo в разработческой базе
     */
    public HostDbHostInfo copyFrom(String hostName, Boolean copyExcludedPages, Boolean copyTopData, Boolean copyHostMonData, Boolean copyIndexHistory) throws UserException, InternalException {
        log.debug("start copying for " + hostName +
                " top = " + copyTopData +
                " host-mon = " + copyHostMonData +
                " copyIndexHistory = " + copyIndexHistory);
        WMCPartition prodPartition = new WMCCopyPartition(null, hostName, null, true);
        log.debug("getting host_id by name");
        Long hostId = getJdbcTemplate(prodPartition).safeQueryForLong(
                HOST_ID_QUERY_STRING, hostName);
        if (hostId == null) {
            throw new UserException(UserProblem.HOST_NOT_FOUND_IN_DATABASE,
                    "Host " + hostName + " not found in production database");
        }
        prodPartition = new WMCCopyPartition(hostId, hostName, null, true);

        log.debug("get references to tbl_hosts " + hostId);
        
        List<String> tables = new LinkedList<String>();
        tables.addAll(commonTables);
        if (copyExcludedPages != null && copyExcludedPages) {
            tables.addAll(excludedPagesTables);
        }
        if (copyTopData != null && copyTopData) {
            tables.addAll(topTables);
        }
        if (copyHostMonData != null && copyHostMonData) {
            tables.addAll(hostMonTables);
        }
        if (copyIndexHistory != null && copyIndexHistory) {
            tables.addAll(indexHistoryTables);
        }

        List<String> partitionedTables = new LinkedList<String>();
        for (String table : tables) {
            partitionedTables.add(prodPartition.getReadySqlString(table));
        }
        // получаем ограничения внешнего ключа для host_id
        List<Reference> hostInfoReferences = getReferencingTableColumns(prodPartition, "tbl_hosts", partitionedTables);

        log.debug("filter references");
        hostInfoReferences = filterPartitions(hostInfoReferences, prodPartition);

        // отношение зависимостей таблиц
        Map<TableColumn, TableColumn> dependsOn = new LinkedHashMap<TableColumn, TableColumn>();
        Map<String, MetaInfo> meta = new LinkedHashMap<String, MetaInfo>();
        // множество таблиц, для которых зависимости по внешним ключам уже искали
        Set<String> processed = new LinkedHashSet<String>();

        log.debug("add references");
        for (Reference r : hostInfoReferences) {
            String fromTableName = r.getFrom().getTableName();
            if (!processed.contains(fromTableName)) {
                List<Reference> refs = getReferencedTableColumns(prodPartition, fromTableName);
                for (Reference ref : refs) {
                    dependsOn.put(ref.getFrom(), ref.getTo());
                }
                processed.add(fromTableName);
            }
        }
        log.debug("inferring references");
        Set<TableColumn> prevKeys;
        do {
            prevKeys = new HashSet<TableColumn>(dependsOn.keySet());

            // множество таблиц, для которых еще не искали ссылки по внешним ключам
            Set<String> discoveredTables = new HashSet<String>();
            for (TableColumn tableColumn : dependsOn.values()) {
                discoveredTables.add(tableColumn.getTableName());
            }
            discoveredTables.removeAll(processed);

            for (String table : discoveredTables) {
                log.debug("inferring references for " + table);
                List<Reference> refs = getReferencedTableColumns(prodPartition, table);
                for (Reference ref : refs) {
                    assert ref.getTo().getColumnName() != null : "referenced column is null";
                    assert ref.getFrom().getColumnName() != null : "referencing column is null";
                    dependsOn.put(ref.getFrom(), ref.getTo());
                }
                processed.add(table);
            }
        } while (!prevKeys.equals(dependsOn.keySet()));

        log.debug("processing primary keys");
        // Устанавливаем первичные ключи для всех таблиц
        for (String tableName : processed) {
            Map<String, Boolean> pk = getPrimaryKeys(prodPartition, tableName);
            MetaInfo metaInfo = meta.get(tableName);
            if (metaInfo == null) {
                metaInfo = new MetaInfo();
                meta.put(tableName, metaInfo);
            }
            metaInfo.setPrimaryKeys(pk);
        }

        log.debug("setting foreign keys for meta");
        for (Map.Entry<TableColumn, TableColumn> entry : dependsOn.entrySet()) {
            meta.get(entry.getKey().getTableName()).putForeignKey(entry.getKey(), entry.getValue());
        }

        WMCPartition devPartition = new WMCCopyPartition(null, hostName, null, false);
        Long devHostId = getJdbcTemplate(devPartition).safeQueryForLong(
                HOST_ID_QUERY_STRING, hostName);
        // Пересоздаем devPartition, чтобы работали разбитые таблицы
        if (devHostId != null) {
            devPartition = new WMCCopyPartition(devHostId, hostName, null, false);
        } else {
            getJdbcTemplate(devPartition).update(INSERT_HOST_QUERY_STRING, hostName);
            devHostId = getJdbcTemplate(devPartition).safeQueryForLong(HOST_ID_QUERY_STRING, hostName);
            devPartition = new WMCCopyPartition(devHostId, hostName, null, false);
        }

        // извлекаем данные по зависимостям
        for (Reference r: hostInfoReferences) {
            String tableName = r.getFrom().getTableName();
            String columnName = r.getFrom().getColumnName();
            List<DataRow> rows = select(prodPartition, tableName, Collections.singletonMap(columnName, Long.toString(hostId)));
            for (DataRow row : rows) {                
                insert(prodPartition, devPartition, row, meta);
            }
        }

        // sitemap errors
        updateSitemapErrors(prodPartition, hostId, devPartition, devHostId);
        updateSitemapCurErrors(prodPartition, hostId, devPartition, devHostId);

        // infected urls
        updateInfectedUrls(prodPartition, hostId, devPartition, devHostId);

        return null;
    }

    private void updateSitemapErrors(IPartition prodPartition, long prodHostId, IPartition devPartition, long devHostId
    ) throws InternalException {
        updateSitemapErrorsImpl(prodPartition, prodHostId, devPartition, devHostId,
                "tbl_sitemaps", "tbl_sitemap_errors", "tbl_sitemap_parse_errors");
    }

    private void updateSitemapCurErrors(IPartition prodPartition, long prodHostId, IPartition devPartition, long devHostId
    ) throws InternalException {
        updateSitemapErrorsImpl(prodPartition, prodHostId, devPartition, devHostId,
                "tbl_sitemaps_cur", "tbl_sitemap_cur_errors", "tbl_sitemap_cur_parse_errors");
    }

    private void updateSitemapErrorsImpl(IPartition prodPartition, long prodHostId, IPartition devPartition, long devHostId,
                                    String sitemapsTableName, String sitemapErrorsTableName, String sitemapParseErrorsTableName
                                    ) throws InternalException {
        List<DataRow> sitemaps = getJdbcTemplate(prodPartition).query(
                "SELECT sitemap_id, host_name, url_name FROM " + sitemapsTableName + " WHERE host_id = ?",
                dataRowMapper, prodHostId);

        for (DataRow sitemap : sitemaps) {
            long prodSitemapId = Long.valueOf(sitemap.getFieldsValues().get("sitemap_id"));
            String hostName = sitemap.getFieldsValues().get("host_name");
            String urlName = sitemap.getFieldsValues().get("url_name");
            List<DataRow> devSitemaps = getJdbcTemplate(devPartition).query(
                    "SELECT sitemap_id FROM " + sitemapsTableName + " WHERE host_name = BINARY ? and url_name = BINARY ?",
                    dataRowMapper, hostName, urlName);
            if (devSitemaps.size() == 0) {
                continue;
            }

            long devSitemapId = Long.valueOf(devSitemaps.iterator().next().getFieldsValues().get("sitemap_id"));

            getJdbcTemplate(devPartition).update("DELETE FROM " + sitemapErrorsTableName + " WHERE sitemap_id = ?", devSitemapId);
            
            List<DataRow> sitemapErrors = getJdbcTemplate(prodPartition).query(
                    "SELECT * FROM " + sitemapErrorsTableName + " WHERE sitemap_id = ?",
                    dataRowMapper, prodSitemapId);
            for (DataRow sitemapError : sitemapErrors) {
                Integer type = Integer.valueOf(sitemapError.getFieldsValues().get("type"));
                getJdbcTemplate(devPartition).update(
                        "INSERT INTO " + sitemapErrorsTableName + " (type, sitemap_id) VALUES (?, ?)",
                        type, devSitemapId);
                long devErrorId = getJdbcTemplate(devPartition).queryForLong("SELECT LAST_INSERT_ID()");
                long prodErrorId = Long.valueOf(sitemapError.getFieldsValues().get("error_id"));
                List<DataRow> parseErrors = getJdbcTemplate(prodPartition).query(
                        "SELECT * FROM " + sitemapParseErrorsTableName + " WHERE error_id = ?",
                        dataRowMapper, prodErrorId);
                for (DataRow parseError : parseErrors) {
                    Integer line = Integer.valueOf(parseError.getFieldsValues().get("line"));
                    Integer position = Integer.valueOf(parseError.getFieldsValues().get("position"));
                    getJdbcTemplate(devPartition).update(
                            "INSERT INTO " + sitemapParseErrorsTableName + " (error_id, line, position) VALUES (?, ?, ?)",
                            devErrorId, line, position);
                }
            }
        }
    }

    public void updateInfectedUrls(IPartition prodPartition, Long hostId, IPartition devPartition, Long devHostId) throws InternalException {
        List<DataRow> infectedUrls =  getJdbcTemplate(prodPartition).query(
                "SELECT * FROM tbl_infected_urls WHERE host_id = ?", dataRowMapper, hostId);
        for (DataRow infectedUrl : infectedUrls) {
            String url = infectedUrl.getFieldsValues().get("url");
            String name = infectedUrl.getFieldsValues().get("name");
            String lastCheck = infectedUrl.getFieldsValues().get("last_check");
            String dirty = infectedUrl.getFieldsValues().get("dirty");
            String urlsChain = infectedUrl.getFieldsValues().get("urls_chain");
            getJdbcTemplate(devPartition).update(
                    "REPLACE INTO tbl_infected_urls (host_id, url, name, last_check, dirty, urls_chain) " +
                    "VALUES (?, ?, ?, ?, ?, ?)", 
                    devHostId, url, name, lastCheck, dirty, urlsChain);
        }
    }

    /**
     * Вставляет строчку с зависимостями
     *
     * @param prodPartition     partition в боевой базе
     * @param devPartition      partition в разработческой
     * @param row               вставляемая строка данных
     * @param meta              метаинформация
     * @return вставленная строка данных
     * @throws InternalException
     */
    private DataRow insert(IPartition prodPartition, IPartition devPartition, DataRow row, Map<String, MetaInfo> meta) throws InternalException {
        String tableName = row.getTableName();
        MetaInfo metaInfo = meta.get(tableName);
        if (metaInfo.getForeignKeys().isEmpty()) {
            Map<String, Boolean> pkMap = metaInfo.getPrimaryKeys();
            Map<String, String> fieldValues = new HashMap<String, String>(row.getFieldsValues());
            for (Map.Entry<String, Boolean> entry : pkMap.entrySet()) {
                if (entry.getValue()) {
                    // автоинкрементные первичные ключи выбрасываем из рассмотрения
                    fieldValues.remove(entry.getKey());
                }
            }
            List<DataRow> existingRows = select(devPartition, tableName, fieldValues);
            if (existingRows.isEmpty()) {
                return replace(devPartition, tableName, metaInfo, row, Collections.<TableColumn, String>emptyMap());
            } else {
                return existingRows.iterator().next();
            }
        } else {
            Map<String, List<TableColumn>> fkToTheSameTable = metaInfo.getReferencingColumnsForTable();

            // T1.FA -> T2.A
            // T1.FB -> T2.A

            // 1) T1.FA == T1.FB || T1.FA != T1.FB && (T1.FA == NULL || T1.FB == NULL)
            // 2) T1.FA != T1.FB && T1.FA != NULL && T1.FB != NULL

            // вставляем записи в зависимые таблицы
            Map<TableColumn, String> foreignKeyValues = new LinkedHashMap<TableColumn, String>();
            for (Map.Entry<String, List<TableColumn>> entry : fkToTheSameTable.entrySet()) {
                String table = entry.getKey();
                List<TableColumn> foreignKeys = entry.getValue();

                // NB! рассчитываем, что одна строка не может ссылаться на две разных строки одной таблицы
                // иначе получится условие where, по которому ничего не выберется
                Map<String, String> fieldValues = new LinkedHashMap<String, String>();
                boolean areAllForeignKeysNull = true;
                boolean areAllForeignKeysToTheSameColumn = false;
                Set<TableColumn> referredColumns = new HashSet<TableColumn>();
                for (TableColumn foreignKey : foreignKeys) {
                    TableColumn referredColumn = metaInfo.getForeignKeys().get(foreignKey);
                    if (referredColumns.contains(referredColumn)) {
                        assert referredColumns.size() == 1:
                                "multiple references to one table column from several foreign keys of a table";
                        areAllForeignKeysToTheSameColumn = true;
                    }
                    referredColumns.add(referredColumn);
                    String value = row.getFieldsValues().get(foreignKey.getColumnName());
                    fieldValues.put(referredColumn.getColumnName(), value);
                    if (value != null && !"null".equalsIgnoreCase(value)) {
                        areAllForeignKeysNull = false;
                    }
                }

                if (areAllForeignKeysNull) {
                    continue;
                }

                if (areAllForeignKeysToTheSameColumn) {
                    // Делаем отдельные select на каждый FK
                    for (TableColumn foreignKey : foreignKeys) {
                        TableColumn referredColumn = metaInfo.getForeignKeys().get(foreignKey);
                        String value = row.getFieldsValues().get(foreignKey.getColumnName());
                        if (value == null || "null".equalsIgnoreCase(value)) {
                            continue;
                        }
                        fieldValues.clear();
                        fieldValues.put(referredColumn.getColumnName(), value);

                        List<DataRow> rs = select(prodPartition, table, fieldValues);
                        assert rs.size() == 1;
                        DataRow r = rs.iterator().next();
                        DataRow inserted = insert(prodPartition, devPartition, r, meta);
                        foreignKeyValues.put(dev(devPartition, foreignKey), inserted.getFieldsValues().get(referredColumn.getColumnName()));
                    }
                } else {
                    // Делаем один запрос, в котором все значения FK объединяем по AND
                    List<DataRow> rs = select(prodPartition, table, fieldValues);
                    assert rs.size() == 1;
                    DataRow r = rs.iterator().next();
                    DataRow inserted = insert(prodPartition, devPartition, r, meta);
                    for (TableColumn foreignKey : foreignKeys) {
                        TableColumn referredColumn = metaInfo.getForeignKeys().get(foreignKey);
                        foreignKeyValues.put(dev(devPartition, foreignKey), inserted.getFieldsValues().get(referredColumn.getColumnName()));
                    }
                }

            }
            List<DataRow> existingRows = selectWithDependencies(
                    devPartition, tableName, metaInfo, row, foreignKeyValues);
            if (existingRows.isEmpty()) {
                return replace(devPartition, tableName, metaInfo, row, foreignKeyValues);
            } else {
                return existingRows.iterator().next();
            }
        }
    }

    private TableColumn dev(IPartition partition, TableColumn tc) {
        return new TableColumn(replaceWithRightPartition(partition, tc.getTableName()), tc.getColumnName());
    }
    
    private List<DataRow> select(IPartition partition, String tableName, Map<String, String> whereClauseMap) throws InternalException {
        StringBuilder whereBuilder = new StringBuilder();
        ArrayList<Object> pars = new ArrayList<Object>();
        String and = " ";
        for (Map.Entry<String, String> entry: whereClauseMap.entrySet()) {
            whereBuilder.append(and).append(" ").append(entry.getKey());
            if (entry.getValue() == null || "NULL".equalsIgnoreCase(entry.getValue())) {
                whereBuilder.append(" IS NULL ");
            } else {
                whereBuilder.append(" = ? ");
                pars.add(entry.getValue());
            }
            and = " AND ";
        }
        String query = String.format(
                SELECT_QUERY_STRING, replaceWithRightPartition(partition, tableName), whereBuilder.toString());
        log.debug(query + "/" + pars.toString());
        return getJdbcTemplate(partition).query(query, dataRowMapper, pars.toArray());
    }

    /**
     * Нужно ли вставлять значение для колонки
     *
     * @param metaInfo      метаинформация для таблицы <code>tableName</code>
     * @param tableName     имя таблицы
     * @param column        имя колонки таблицы tableName
     * @return
     */
    private boolean needInsertColumn(MetaInfo metaInfo, String tableName, String column) {
        boolean isFK = metaInfo.getForeignKeys().containsKey(new TableColumn(tableName, column));
        boolean isPK = metaInfo.getPrimaryKeys().containsKey(column);
        boolean isAI = isPK ? metaInfo.getPrimaryKeys().get(column) : false;
        return !isAI || isPK && isFK;
    }

    private DataRow replace(IPartition partition, String tableName, MetaInfo metaInfo, DataRow row, Map<TableColumn, String> dependencies) throws InternalException {
        ArrayList<Object> params = new ArrayList<Object>();

        // добавляем названия колонок для вставки
        LinkedList<String> columns = new LinkedList<String>();
        for (String column : row.getFieldsValues().keySet()) {
            if (needInsertColumn(metaInfo, tableName, column)) {
                columns.add(column);
            }
        }
        String columnsList = SqlUtil.getCommaSeparatedList(columns, COLUMNS_PARAMETERIZER);

        Map<String, String> constraints = new LinkedHashMap<String, String>();
        // Добавляем значения колонок для вставки
        for (String column : columns) {
            String value = row.getFieldsValues().get(column);
            TableColumn tableColumn = new TableColumn(tableName, column);
            if (metaInfo.getForeignKeys().containsKey(tableColumn)) {
                value = dependencies.get(dev(partition, tableColumn));
            }
            params.add(value);
            
            constraints.put(column, value);
        }
        String valuesList = SqlUtil.createQuestionMarks(params.size());

        String sql = String.format(
                REPLACE_QUERY_STRING, replaceWithRightPartition(partition, tableName), columnsList, valuesList);
        log.debug(sql + " / " + params.toString());
        getJdbcTemplate(partition).update(sql, params.toArray());
        
        return select(partition, tableName, constraints).iterator().next();
    }

    /**
     * Делает запрос к разработческой базе с данными из строки <code>row</code>,
     * замененными значениями внешних ключей из <code>dependencies</code>
     *
     * @param devPartition  партиция для разработческой базы
     * @param tableName     таблица
     * @param metaInfo      метаинформация
     * @param row           значения строки из продуктовой базы
     * @param dependencies  значения внешних ключей
     * @return
     * @throws InternalException
     */
    public List<DataRow> selectWithDependencies(
            IPartition devPartition,
            String tableName, 
            MetaInfo metaInfo,
            DataRow row,
            Map<TableColumn, String> dependencies) throws InternalException {
        // замена внешних ключей
        Map<String,String> whereColumns = new HashMap<String, String>(row.getFieldsValues());
        // добавляем названия колонок для вставки
        LinkedList<String> columns = new LinkedList<String>();
        for (String column : row.getFieldsValues().keySet()) {
            if (needInsertColumn(metaInfo, tableName, column)) {
                columns.add(column);
            }
            TableColumn referringColumn = new TableColumn(tableName, column);
            TableColumn referredColumn = metaInfo.getForeignKeys().get(referringColumn);
            if (referredColumn != null) {
                String value = dependencies.get(dev(devPartition, referringColumn));
                whereColumns.put(column, value);
            }
        }
        whereColumns.keySet().retainAll(columns);
        return select(devPartition, tableName, whereColumns);
    }

    public List<Reference> getReferencingTableColumns(IPartition partition, String tableName) throws InternalException {
        return getJdbcTemplate(partition).query(REFERENCING_TABLES_QUERY_STRING, tableColumnMapper, tableName);
    }

    public List<Reference> getReferencingTableColumns(IPartition partition, String tableName, List<String> desiredTables) throws InternalException {
        if (desiredTables.isEmpty()) {
            return getReferencingTableColumns(partition, tableName);
        } else {
            return getJdbcTemplate(partition).query(
                    String.format(DESIRED_REFERENCING_TABLES_QUERY_STRING,
                            SqlUtil.getCommaSeparatedList(desiredTables, VALUES_PARAMETERIZER)),
                    tableColumnMapper, tableName);
        }
    }
    
    public List<Reference> getReferencedTableColumns(IPartition partition, String tableName) throws InternalException {
        return getJdbcTemplate(partition).query(REFERENCED_TABLES_QUERY_STRING, tableColumnMapper, tableName);
    }
    
    public Map<String, Boolean> getPrimaryKeys(IPartition partition, String tableName) throws InternalException {
        List<PrimaryKeyInfo> info = getJdbcTemplate(partition).query(
                PRIMARY_KEYS_QUERY_STRING, primaryKeyMapper, tableName);
        Map<String, Boolean> pk = new LinkedHashMap<String, Boolean>();
        for (PrimaryKeyInfo pkInfo : info) {
            pk.put(pkInfo.getColumnName(), pkInfo.isAutoIncrement());
        }
        return pk;
    }
    
    public List<Reference> filterPartitions(List<Reference> tableColumns, IPartition partition) {
        Set<String> distinctPartiotionedTables = new LinkedHashSet<String>();
        List<Reference> result = new LinkedList<Reference>();
        for (Reference reference : tableColumns) {
            String tableName = reference.getFrom().getTableName();
            if (tableName.matches(".*[0-9][0-9][0-9]")) {
                String shortTableName = tableName.substring(0, tableName.length() - 3);
                distinctPartiotionedTables.add(shortTableName);
                String rightTableName = partition.getReadySqlString(shortTableName);
                if (tableName.equals(rightTableName)) {
                    result.add(reference);
                }
            } else {
                result.add(reference);
            }
        }
        return result;
    }
    
    private String replaceWithRightPartition(IPartition partition, String tableName) {
        if (tableName.matches(".*[0-9][0-9][0-9]")) {
            String shortTableName = tableName.substring(0, tableName.length() - 3);
            return partition.getReadySqlString(shortTableName);
        } else {
            return tableName;
        }
    }
    
    public void updateDicRegions(int dbNumber) throws InternalException {
        WMCPartition prodPartition = new WMCCopyPartition(dbNumber, true);
        WMCPartition devPartition = new WMCCopyPartition(dbNumber, false);
        for (int i = 0; i < 3; i++) {
            List<DataRow> list = getJdbcTemplate(prodPartition).query(
                    "SELECT region_id, name, parent_id, key_region_id, geotype FROM tbl_dic_region LIMIT 10000 OFFSET " + i * 10000,
                    dataRowMapper);
            for (DataRow dr : list) {
                String name = dr.getFieldsValues().get("name");
                String krid = dr.getFieldsValues().get("key_region_id");
                String parentId = dr.getFieldsValues().get("parent_id");
                String geoType = dr.getFieldsValues().get("geotype");
                String regionId = dr.getFieldsValues().get("region_id");

                Long count = getJdbcTemplate(devPartition).queryForLong(
                        "SELECT COUNT(region_id) FROM tbl_dic_region WHERE region_id = ?", regionId);
                if (count == 0) {
                     log.debug(regionId + ", " + name + " is absent");
                }
                getJdbcTemplate(devPartition).update(
                        "UPDATE tbl_dic_region SET name = ?, key_region_id = ?, parent_id = ?, geotype = ? " +
                                "WHERE region_id = ?", name, krid, parentId, geoType, regionId);
            }
        }
    }

    private static final ParameterizedRowMapper<Reference> tableColumnMapper = new ParameterizedRowMapper<Reference>() {
        @Override
        public Reference mapRow(ResultSet resultSet, int i) throws SQLException {
            final TableColumn from = new TableColumn(
                    resultSet.getString(FIELD_TABLE_NAME), resultSet.getString(FIELD_COLUMN_NAME));
            final TableColumn to = new TableColumn(
                    resultSet.getString(FIELD_REFERENCED_TABLE_NAME),
                    resultSet.getString(FIELD_REFERENCED_COLUMN_NAME));
            return new Reference(from, to);
        }
    };

    private static final ParameterizedRowMapper<DataRow> dataRowMapper = new ParameterizedRowMapper<DataRow>() {
        @Override
        public DataRow mapRow(ResultSet resultSet, int j) throws SQLException {
            String tableName = resultSet.getMetaData().getTableName(1);
            Map<String, String> fieldValues = new LinkedHashMap<String, String>();
            for (int i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) {
                fieldValues.put(resultSet.getMetaData().getColumnName(i), resultSet.getString(i));
                if (resultSet.wasNull()) {
                    fieldValues.put(resultSet.getMetaData().getColumnName(i), null);
                }
            }
            return new DataRow(tableName, fieldValues);
        }
    };
    
    private static ParameterizedRowMapper<PrimaryKeyInfo> primaryKeyMapper = new ParameterizedRowMapper<PrimaryKeyInfo>() {
        @Override
        public PrimaryKeyInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
            String extra = SqlUtil.getStringNullable(rs, FIELD_EXTRA);
            if (extra == null) {
                extra = "";
            }
            return new PrimaryKeyInfo(rs.getString(FIELD_COLUMN_NAME), extra.contains("auto_increment"));
        }
    };

    private static final SqlUtil.ListParameterizer<String> VALUES_PARAMETERIZER = new SqlUtil.ListParameterizer<String>() {
        @Override
        public String getParameter(int i, String obj) {
            return obj;
        }

        @Override
        public int getParamNumber() {
            // скобочки будут в запросе
            return 1;
        }

        @Override
        public boolean isQuotesNeeded(int i) {
            return true;
        }
    };

    private static final SqlUtil.ListParameterizer<String> COLUMNS_PARAMETERIZER = new SqlUtil.ListParameterizer<String>() {
        @Override
        public String getParameter(int i, String obj) {
            return obj;
        }

        @Override
        public int getParamNumber() {
            // скобочки будут в запросе
            return 1;
        }

        @Override
        public boolean isQuotesNeeded(int i) {
            return false;
        }
    };

    @Required
    public void setCommonTables(List<String> commonTables) {
        this.commonTables = commonTables;
    }

    @Required
    public void setExcludedPagesTables(List<String> excludedPagesTables) {
        this.excludedPagesTables = excludedPagesTables;
    }

    @Required
    public void setTopTables(List<String> topTables) {
        this.topTables = topTables;
    }

    @Required
    public void setHostMonTables(List<String> hostMonTables) {
        this.hostMonTables = hostMonTables;
    }

    @Required
    public void setIndexHistoryTables(List<String> indexHistoryTables) {
        this.indexHistoryTables = indexHistoryTables;
    }
}
