package ru.yandex.wmconsole.service;

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

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.misc.ip.Ipv4Address;
import ru.yandex.webmaster.common.urltree.YandexSearchShard;
import ru.yandex.wmconsole.data.HostInfoStatusEnum;
import ru.yandex.wmconsole.data.UpdateStateEnum;
import ru.yandex.wmconsole.data.VerificationStateEnum;
import ru.yandex.wmconsole.data.info.FastHostInfo;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.dao.TblUrlTreesDao;
import ru.yandex.wmconsole.verification.VerificationTypeEnum;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.service.AbstractDbService;
import ru.yandex.wmtools.common.util.SqlUtil;

/**
 * Created by IntelliJ IDEA.
 * User: senin
 * Date: 20.03.2007
 * Time: 10:09:24
 */
public class FastHostListService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(FastHostListService.class);

    private static final String FIELD_HOSTNAME = "host_name";
    private static final String FIELD_URL_COUNT = "url_count";
    private static final String FIELD_VERIFICATION_STATE = "verified";
    private static final String FIELD_VERIFICATION_TYPE = "verification_type";
    private static final String FIELD_HOST_ID = "host_id";
    private static final String FIELD_UPDATE_STATE = "update_state";
    private static final String FIELD_PRECISE_INDEX_COUNT = "precise_index_count";
    private static final String FIELD_INDEX_COUNT = "index_count";
    private static final String FIELD_INDEX_COUNT_TIME = "index_count_time";
    private static final String FIELD_VIRUSED_STATE = "virused_state";
    private static final String FIELD_TCY = "tcy";
    private static final String FIELD_HOST_INFO_STATUS = "status";
    private static final String FIELD_HOST_INFO_STATUS_DATE = "status_date";
    private static final String FIELD_HOST_INFO_STATUS_HTARC = "status_htarc";
    private static final String FIELD_HOST_INFO_PENALTY_HTARC = "penalty_htarc";
    private static final String FIELD_HOST_INFO_STATUS_YA = "status_ya";
    private static final String FIELD_HOST_INFO_PENALTY_YA = "penalty_ya";
    private static final String FIELD_SPIDER_IP = "spider_ip";
    private static final String FIELD_LAST_ACCESS = "last_access";
    private static final String FIELD_HTARC_IN_INDEX = "htarc_in_index";
    private static final String FIELD_PREV_IN_INDEX = "prev_in_index";
    private static final String FIELD_BANNED_COUNT = "banned_count";

    private HostDbHostInfoService hostDbHostInfoService;
    private LinksCacheService linksCacheService;
    private TblUrlTreesDao tblUrlTreesDao;

    private static final String SELECT_USER_DB_HOST_LIST_QUERY =
            "SELECT " +
                    "    h.host_id AS " + FIELD_HOST_ID + ", " +
                    "    h.name AS " + FIELD_HOSTNAME + ", " +
                    "    uh.state AS " + FIELD_VERIFICATION_STATE + ", " +
                    "    uh.verification_type AS " + FIELD_VERIFICATION_TYPE + " " +
                    "FROM (" +
                    "    tbl_users_hosts uh " +
                    "LEFT JOIN " +
                    "    tbl_hosts h ON (uh.host_id = h.host_id)) " +
                    "WHERE " +
                    "    uh.user_id = ? AND h.mirror_id IS NULL";

    private static final String SELECT_HOST_DB_HOST_LIST_QUERY =
            "SELECT " +
                    "    h.name AS " + FIELD_HOSTNAME + ", " +
                    "    tc.tcy AS " + FIELD_TCY + ", " +
                    "    rdbi.state AS " + FIELD_UPDATE_STATE + ", " +
                    "    rdbi.index_count AS " + FIELD_PRECISE_INDEX_COUNT + ", " +
                    "    rdbi.banned_count AS " + FIELD_BANNED_COUNT + ", " +
                    "    rdbi.urls AS " + FIELD_URL_COUNT + ", " +
                    "    rdbi.htarc_in_index AS " + FIELD_HTARC_IN_INDEX + ", " +
                    "    rdbi.prev_in_index AS " + FIELD_PREV_IN_INDEX + ", " +
                    "    COALESCE(pi.last_access_ya, rdbi.last_access) AS " + FIELD_LAST_ACCESS + ", " +
                    "    si.spider_ip AS " + FIELD_SPIDER_IP + ", " +
                    "    ih.state AS " + FIELD_VIRUSED_STATE + ", " +
                    "    hi.status AS " + FIELD_HOST_INFO_STATUS + ", " +
                    "    hi.status_date AS " + FIELD_HOST_INFO_STATUS_DATE + "," +
                    "    pi.status_htarc AS " + FIELD_HOST_INFO_STATUS_HTARC + ", " +
                    "    pi.penalty_htarc AS " + FIELD_HOST_INFO_PENALTY_HTARC + ", " +
                    "    pi.status_ya AS " + FIELD_HOST_INFO_STATUS_YA + ", " +
                    "    pi.penalty_ya AS " + FIELD_HOST_INFO_PENALTY_YA + " " +
                    "FROM (((((" +
                    "    tbl_hosts h " +
                    "LEFT JOIN " +
                    "    tbl_robotdb_info rdbi ON (h.host_id = rdbi.host_id)) " +
                    "LEFT JOIN " +
                    "    tbl_tcy_cache tc ON (h.host_id = tc.host_id)) " +
                    "LEFT JOIN " +
                    "    tbl_infected_hosts ih ON (h.host_id = ih.host_id)) " +
                    "LEFT JOIN " +
                    "    tbl_host_info hi ON (h.host_id = hi.host_id)) " +
                    "LEFT JOIN " +
                    "    tbl_penalty_info pi ON (h.host_id = pi.host_id)) " +
                    "LEFT JOIN " +
                    "    ( " +
                    "       SELECT " +
                    "           host_id, " +
                    "           MAX(spider_ip) AS spider_ip " +
                    "       FROM " +
                    "           ( " +
                    "               SELECT " +
                    "                   host_id, " +
                    "                   MAX(date) AS date " +
                    "               FROM " +
                    "                   tbl_spider_ips tmp1 " +
                    "               NATURAL JOIN " +
                    "                   tbl_hosts h2 " +
                    "               WHERE " +
                    "                   h2.name IN (%1$s) " +
                    "               GROUP BY host_id " +
                    "           ) tmp2 " +
                    "       NATURAL JOIN " +
                    "           tbl_spider_ips tmp3 " +
                    "       GROUP BY host_id" +
                    "    ) si " +
                    "ON (h.host_id = si.host_id ) " +
                    "WHERE " +
                    "    h.name IN (%1$s) ";

    private class HostDbHostInfoFetcher implements Runnable, ParameterizedRowMapper<FastHostInfo> {
        private final int dbIndex;
        private final List<String> hosts;
        private final Map<String, FastHostInfo> hostInfoMap;
        private final boolean fetchUncachedXml;
        private final boolean fetchUncachedTcy;

        private volatile InternalException error = null;

        public HostDbHostInfoFetcher(final int dbIndex, final List<String> hosts, final Map<String, FastHostInfo> hostInfoMap,
                                     final boolean fetchUncachedXml, final boolean fetchUncachedTcy) {
            this.dbIndex = dbIndex;
            this.hosts = hosts;
            this.hostInfoMap = hostInfoMap;
            this.fetchUncachedXml = fetchUncachedXml;
            this.fetchUncachedTcy = fetchUncachedTcy;
        }

        @Override
        public void run() {
            try {
                StringBuilder commaSeparatedHosts = new StringBuilder();
                String separator = "";
                for (String host: hosts) {
                    commaSeparatedHosts.append(separator);
                    commaSeparatedHosts.append("'");
                    commaSeparatedHosts.append(host);
                    commaSeparatedHosts.append("'");
                    separator = ", ";
                }

                // достаём всё, что можно из базы
                getJdbcTemplate(new WMCPartition(dbIndex)).query(String.format(SELECT_HOST_DB_HOST_LIST_QUERY, commaSeparatedHosts), this);

                // обновляем кеши, если нужно
                updateCacheValues();
            } catch (InternalException e) {
                this.error = e;
            }
        }

        private void updateCacheValues() throws InternalException {
            for (String host: hosts) {
                String hostName = host.toLowerCase();
                FastHostInfo fastHostInfo = hostInfoMap.get(hostName);
                if (fastHostInfo.getTcy() == null && fetchUncachedTcy) {
                    try {
                        log.debug("tcy is null for " + hostName);
                        Integer tcy = linksCacheService.getAndCacheTcyByHostname(hostName);
                        fastHostInfo.setTcy(tcy);
                    } catch (Throwable e) {
                        log.error("Ignoring the following exception", e);
                    }
                }

                final HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(hostName, true);
                if (hostDbHostInfo == null) {
                    continue;
                }
                YandexSearchShard shard = tblUrlTreesDao.getOptimumShardId(hostDbHostInfo);
                Long indexCount = tblUrlTreesDao.getIndexCount(hostDbHostInfo, shard);
                fastHostInfo.setIndexCount(indexCount);

                if (fastHostInfo.getIndexCount() == null) {
                    // Делаем запрос к xml-поиску
                    try {
                        log.debug("checking index_count cache for " + hostName);
                        indexCount = linksCacheService.checkCacheAndGetIndexCount(hostDbHostInfo, shard);
                        fastHostInfo.setIndexCount(indexCount);
                    } catch (Throwable e) {
                        log.error("Ignoring the following exception", e);
                    }
                }
            }
        }

        @Override
        public FastHostInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
            String hostName = rs.getString(FIELD_HOSTNAME);

            Integer updateStateInt = rs.getInt(FIELD_UPDATE_STATE);
            UpdateStateEnum updateState = UpdateStateEnum.R.fromValueOrNull(updateStateInt);

            Long urlCount = SqlUtil.getLongNullable(rs, FIELD_URL_COUNT);

            Integer tcy = SqlUtil.getIntNullable(rs, FIELD_TCY);

            boolean virusedState = rs.getObject(FIELD_VIRUSED_STATE) != null;

            HostInfoStatusEnum hostInfoStatus = HostInfoStatusEnum.R.fromValueOrNull(rs.getInt(FIELD_HOST_INFO_STATUS));
            if (rs.wasNull()) {
                hostInfoStatus = null;
            }
            final Date hostInfoStatusDate = SqlUtil.safeGetTimestamp(rs, FIELD_HOST_INFO_STATUS_DATE);

            HostInfoStatusEnum hostInfoStatusHtarc = HostInfoStatusEnum.R.fromValueOrNull(rs.getInt(FIELD_HOST_INFO_STATUS_HTARC));
            Integer penaltyHtarc = null;
            if (rs.wasNull()) {
                hostInfoStatusHtarc = null;
            } else {
                penaltyHtarc = SqlUtil.getIntNullable(rs, FIELD_HOST_INFO_PENALTY_HTARC);
            }

            HostInfoStatusEnum hostInfoStatusYa = HostInfoStatusEnum.R.fromValueOrNull(rs.getInt(FIELD_HOST_INFO_STATUS_YA));
            Integer penaltyYa = null;
            if (rs.wasNull()) {
                hostInfoStatusYa = null;
            } else {
                penaltyYa = SqlUtil.getIntNullable(rs, FIELD_HOST_INFO_PENALTY_YA);
            }

            Boolean htarcInIndex = SqlUtil.getBooleanNullable(rs, FIELD_HTARC_IN_INDEX);
            Boolean prevInIndex = SqlUtil.getBooleanNullable(rs, FIELD_PREV_IN_INDEX);

            Long ip = SqlUtil.getLongNullable(rs, FIELD_SPIDER_IP);
            String stringIp = ip != null ? Ipv4Address.valueOf(ip.intValue()).toString() : null;

            Date lastAccess = SqlUtil.safeGetTimestamp(rs, FIELD_LAST_ACCESS);

            FastHostInfo res = hostInfoMap.get(hostName.toLowerCase());
            res.setHostDbFields(updateState,
                    urlCount,
                    virusedState,
                    tcy,
                    null,
                    null,
                    hostInfoStatus,
                    hostInfoStatusDate,
                    stringIp,
                    hostInfoStatusHtarc,
                    penaltyHtarc,
                    hostInfoStatusYa,
                    penaltyYa,
                    lastAccess,
                    htarcInIndex,
                    prevInIndex);

            return res;
        }

        public InternalException getError() {
            return error;
        }
    }

    private static final ParameterizedRowMapper<FastHostInfo> fastUserDbHostInfoMapper = new ParameterizedRowMapper<FastHostInfo>() {
        @Override
        public FastHostInfo mapRow(ResultSet resultSet, int rowNumber) throws SQLException {
            return new FastHostInfo(
                    resultSet.getLong(FIELD_HOST_ID),
                    resultSet.getString(FIELD_HOSTNAME),
                    VerificationStateEnum.R.fromValueOrNull(resultSet.getInt(FIELD_VERIFICATION_STATE)),
                    VerificationTypeEnum.R.fromValueOrNull(resultSet.getInt(FIELD_VERIFICATION_TYPE))
            );
        }
    };

    public Collection<FastHostInfo> getFastHostList(Pager pager, Long userId,
                                                    final boolean fetchUncachedXml, final boolean fetchUncachedTcy) throws InternalException {
        final List<FastHostInfo> tempHostInfos = getJdbcTemplate(new WMCPartition(null, userId)).query(
                SELECT_USER_DB_HOST_LIST_QUERY, fastUserDbHostInfoMapper, userId);

        List<FastHostInfo> hostInfos;
        Collections.sort(tempHostInfos);
        if (pager == null) {
            hostInfos = tempHostInfos;
        } else {
            pager.setCount(tempHostInfos.size());
            hostInfos = tempHostInfos.subList(pager.getFrom() - 1, Math.min(pager.getTo(), tempHostInfos.size()));
        }

        Map<String, FastHostInfo> userDbHostInfoMap = new HashMap<String, FastHostInfo>();
        List<String>[] hosts = new List[WMCPartition.getHostDbCount(getDatabaseCount())];

        for (FastHostInfo fastHostInfo : hostInfos) {
            String lowerHostName = fastHostInfo.getName().toLowerCase();
            userDbHostInfoMap.put(lowerHostName, fastHostInfo);

            int dbIndex = WMCPartition.getDatabaseIndex(getDatabaseCount(), lowerHostName);
            if (hosts[dbIndex] == null) {
                hosts[dbIndex] = new LinkedList<String>();
            }
            hosts[dbIndex].add(lowerHostName);
        }

        HostDbHostInfoFetcher[] run = new HostDbHostInfoFetcher[hosts.length];
        Thread[] thr = new Thread[hosts.length];
        for (int i = 0; i < hosts.length; i++) {
            if (hosts[i] == null) {
                continue;
            }

            run[i] = new HostDbHostInfoFetcher(i, hosts[i], userDbHostInfoMap, fetchUncachedXml, fetchUncachedTcy);
            thr[i] = new Thread(run[i]);
            thr[i].start();
        }

        for (int i = 0; i < hosts.length; i++) {
            if (hosts[i] == null) {
                continue;
            }

            try {
                thr[i].join();

                //noinspection ThrowableResultOfMethodCallIgnored
                if (run[i].getError() != null) {
                    throw run[i].getError();
                }
            } catch (InterruptedException e) {
                throw new InternalException(InternalProblem.PROCESSING_ERROR, "HostDbHostInfoFetcher #" + i + " caused InterruptedException", e);
            }
        }
        return hostInfos;
    }

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

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

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