package ru.yandex.webmaster.common.host.dao;

import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.joda.time.DateTime;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import ru.yandex.common.util.db.LongRowMapper;
import ru.yandex.misc.db.q.SqlCondition;
import ru.yandex.misc.db.q.SqlLimits;
import ru.yandex.wmconsole.data.VerificationStateEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.ShortUsersHostsInfo;
import ru.yandex.wmconsole.data.info.UsersHostsInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.UsersHostsService;
import ru.yandex.wmconsole.verification.VerificationTypeEnum;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.service.AbstractDbService;
import ru.yandex.wmtools.common.util.CollectionUtil;
import ru.yandex.wmtools.common.util.SqlUtil;

/**
 * <pre>
 *     CREATE TABLE `tbl_users_hosts` (
 *       `user_id` bigint(20) NOT NULL,
 *       `host_id` bigint(20) NOT NULL,
 *       `state` tinyint(5) NOT NULL DEFAULT '-128',
 *       `verification_uin` bigint(20) DEFAULT NULL,
 *       `verification_type` tinyint(5) NOT NULL DEFAULT '0',
 *       `verified_on` datetime DEFAULT NULL,
 *       `verify_fault_log` varchar(255) DEFAULT NULL,
 *       PRIMARY KEY (`host_id`,`user_id`),
 *       UNIQUE KEY `verification_uin_unq` (`verification_uin`),
 *       KEY `users_hosts_pk_index` (`user_id`,`host_id`),
 *       KEY `users_users_hosts_fk_index` (`user_id`),
 *       KEY `hosts_users_hosts_fk_index` (`host_id`),
 *       KEY `verification_type_dic_verification_type_fk` (`verification_type`),
 *       KEY `state_verification_state_fk` (`state`),
 *       KEY `idx_host_state_verifiedon` (`host_id`,`state`,`verified_on`),
 *       KEY `idx_state_verifiedon` (`state`,`verified_on`),
 *       KEY `idx_host_state` (`host_id`,`state`),
 *       CONSTRAINT `state_verification_state_fk` FOREIGN KEY (`state`) REFERENCES `tbl_dic_verification_state` (`id`) ON DELETE NO ACTION,
 *       CONSTRAINT `users_hosts_hosts_fk` FOREIGN KEY (`host_id`) REFERENCES `tbl_hosts` (`host_id`) ON DELETE NO ACTION,
 *       CONSTRAINT `users_users_hosts_fk` FOREIGN KEY (`user_id`) REFERENCES `tbl_users` (`user_id`) ON DELETE CASCADE,
 *       CONSTRAINT `verification_type_dic_verification_type_fk` FOREIGN KEY (`verification_type`) REFERENCES `tbl_dic_verification_type` (`id`) ON DELETE NO ACTION
 *   ) ENGINE=InnoDB DEFAULT CHARSET=utf8
 * </pre>
 *
 * @author aherman
 */
public class TblUsersHostsDao extends AbstractDbService {
    private static final int MAX_FAULT_LOG_LENGTH = 255;

    public boolean isAdded(long userId, BriefHostInfo hostInfo) throws InternalException {
        String q = "SELECT count(*) FROM tbl_users_hosts WHERE user_id = ? AND host_id = ?";
        return getJdbcTemplate(WMCPartition.nullPartition())
                .queryForInt(q, userId, hostInfo.getId()) > 0;
    }

    public long addHostToUser(long userId, BriefHostInfo briefHostInfo) throws InternalException {
        String q = "INSERT INTO tbl_users_hosts (user_id, host_id, verification_uin) VALUES (?, ?, ?)";

        long uin = getRandomUIN();
        getJdbcTemplate(new WMCPartition(null, null, userId)).update(q, userId, briefHostInfo.getId(), uin);
        return uin;
    }

    public long getUin(long userId, long hostId) throws InternalException {
        return getJdbcTemplate(WMCPartition.nullPartition())
                .queryForLong("SELECT verification_uin FROM tbl_users_hosts WHERE user_id = ? AND host_Id = ?",
                        userId, hostId);
    }

    public void addUserHostFromNewWmc(long userId, long hostId, VerificationStateEnum state, VerificationTypeEnum type,
                                      long uin, Date verifiedOn) throws InternalException {
        int stateV = state == null ? VerificationStateEnum.NEVER_VERIFIED.getValue() : state.value();
        int typeV = type == null ? VerificationTypeEnum.META_TAG.value() : type.value();
        String q = "INSERT INTO tbl_users_hosts (user_id, host_id, verification_uin, verified_on, verification_type, state) " +
                "VALUES (?, ?, ?, ?, ?, ?) " +
                "ON DUPLICATE KEY UPDATE " +
                "verification_uin=VALUES(verification_uin)," +
                "verified_on=VALUES(verified_on)," +
                "verification_type=VALUES(verification_type)," +
                "state=VALUES(state)";
        getJdbcTemplate(WMCPartition.nullPartition())
                .update(q, userId, hostId, uin, verifiedOn, typeV, stateV);
    }

    private long getRandomUIN() {
        long uin = UUID.randomUUID().getLeastSignificantBits();
        return (uin >= 0) ? uin : -uin - 1;
    }

    public long countUserVerifiedHosts(long userId, BriefHostInfo briefHostInfo) throws InternalException {
        String q = "SELECT count(*) FROM tbl_users_hosts WHERE host_id = ? AND state IN (%1$s)";
        String query = String.format(q, VerificationStateEnum.getCommaSeparatedListForVerifiedStates());
        return getJdbcTemplate(new WMCPartition(null, null, userId)).queryForLong(query, briefHostInfo.getId());
    }

    public int countHostsAddedToUser(long userId) throws InternalException {
        String q = "SELECT count(*) FROM  tbl_users_hosts WHERE user_id = ?";
        return getJdbcTemplate(WMCPartition.nullPartition()).queryForInt(q, userId);
    }

    public int countUnverifiedHostsAddedToUser(long userId) throws InternalException {
        String q = "SELECT COUNT(*) FROM tbl_users_hosts WHERE user_id = ? AND state != ?";
        return getJdbcTemplate(WMCPartition.nullPartition()).queryForInt(
                q, userId, VerificationStateEnum.VERIFIED.value());
    }

    public long countDistinctVerifiedHosts(long minHostId, long maxHostId) throws InternalException {
        final String query = "SELECT COUNT(DISTINCT uh.host_id) FROM tbl_users_hosts uh " +
                "WHERE uh.state = ? AND host_id >= ? and host_id <= ?";
        long hostCount = getJdbcTemplate(WMCPartition.nullPartition()).queryForLong(
                query, VerificationStateEnum.VERIFIED.getValue(), minHostId, maxHostId);
        return hostCount;
    }

    public long getMaxVerifiedHostId(DateTime now) throws InternalException {
        String verifiedHostsQuery = "SELECT MAX(host_id) FROM tbl_users_hosts WHERE state in (%s) AND verified_on < ?";
        return getJdbcTemplate(WMCPartition.nullPartition()).queryForLong(
                String.format(verifiedHostsQuery, VerificationStateEnum.getCommaSeparatedListForVerifiedStates()), now.toDate());
    }

    public List<ShortUsersHostsInfo> listUsersForHostShortInfo(long hostId) throws InternalException {
        String q = "SELECT user_id, host_id FROM tbl_users_hosts WHERE host_id = ?";
        return CollectionUtil.removeNulls(getJdbcTemplate(WMCPartition.nullPartition()).query(
                q, shortUsersHostsInfoMapper, hostId));
    }

    public void updateVerificationInfo(long userId, long hostId, VerificationTypeEnum verType) throws
            InternalException {
        String q = "UPDATE tbl_users_hosts SET state = ?, verification_type = ? WHERE user_id = ? AND host_id = ? ";
        getJdbcTemplate(WMCPartition.nullPartition()).update(q,
                VerificationStateEnum.IN_PROGRESS.value(), verType.value(), userId, hostId);
    }

    public void updateVerificationState(long userId, long hostId, VerificationStateEnum verificationStateEnum) throws
            InternalException {
        String q = "UPDATE tbl_users_hosts SET state = ?, verified_on = NOW() WHERE host_id = ? AND user_id = ?";
        getJdbcTemplate(WMCPartition.nullPartition()).update(q, verificationStateEnum.value(), hostId, userId);
    }

    public List<Long> getUserHosts(long userId) throws InternalException {
        String q = "SELECT host_id FROM tbl_users_hosts WHERE user_id = ?";
        return getJdbcTemplate(WMCPartition.nullPartition()).query(q, new LongRowMapper(), userId);
    }

    public List<Long> getVerifiedUserByHosts(long hostId) throws InternalException {
        String q = "SELECT user_id FROM tbl_users_hosts WHERE state IN (%1$s) AND host_id = ?";
        String query = String.format(q, VerificationStateEnum.getCommaSeparatedListForVerifiedStates());
        return getJdbcTemplate(WMCPartition.nullPartition()).query(query, new LongRowMapper(), hostId);
    }

    public List<Long> getUsersForHost(long hostId, SqlCondition condition) throws InternalException {
        SqlCondition c = hostIdCondition(hostId).and(condition);
        String q = "SELECT user_id FROM tbl_users_hosts WHERE " + c.sql();
        return getJdbcTemplate(WMCPartition.nullPartition()).query(q, new LongRowMapper(), c.args().toArray());
    }

    public static SqlCondition verificationStateCondition(EnumSet<VerificationStateEnum> states) {
        Set<Integer> stateSet = new HashSet<>(states.size());
        for (VerificationStateEnum state : states) {
            stateSet.add(state.value());
        }
        return SqlCondition.column("state").inSet(stateSet);
    }

    public static SqlCondition verificationTypeCondition(EnumSet<VerificationTypeEnum> types) {
        Set<Integer> typeSet = new HashSet<>(types.size());
        for (VerificationTypeEnum type : types) {
            typeSet.add(type.value());
        }
        return SqlCondition.column("verification_type").inSet(typeSet);
    }

    public static SqlCondition hostIdCondition(long hostId) {
        return SqlCondition.column("host_id").eq(hostId);
    }

    public void updateVerificationStatAndType(UsersHostsInfo info) throws InternalException {
        String q = "UPDATE tbl_users_hosts SET state = ?, verify_fault_log = ?, verified_on = ?, verification_type = ? " +
                " WHERE host_id = ? AND user_id = ? ";
        String faultLog = truncateFaultLog(info);
        getJdbcTemplate(WMCPartition.nullPartition()).update(q,
                info.getVerificationState().value(), faultLog, info.getVerificationDate(),
                info.getVerificationType().value(), info.getHostId(), info.getUserId());
    }

    public int updateVerificationStateAndTypeByState(UsersHostsInfo info, VerificationStateEnum state) throws
            InternalException {
        String q = "UPDATE tbl_users_hosts SET state = ?, verify_fault_log = ?, verified_on = ?, verification_type = ? " +
                " WHERE host_id = ? AND user_id = ? AND state = ?";
        String faultLog = truncateFaultLog(info);
        return getJdbcTemplate(WMCPartition.nullPartition()).update(q,
                info.getVerificationState().value(), faultLog, info.getVerificationDate(),
                info.getVerificationType().value(), info.getHostId(), info.getUserId(), state.value());
    }

    public void updateVerification(BriefHostInfo hostInfo, long userId, VerificationTypeEnum verificationType, String faultLogMessage) throws InternalException {
        String q = "UPDATE tbl_users_hosts SET state = ?, verification_type = ?, verified_on = NOW(), verify_fault_log = ? WHERE user_id = ? AND host_id = ?";
        getJdbcTemplate(new WMCPartition(null, userId))
                .update(q, VerificationStateEnum.VERIFIED.getValue(), verificationType.value(), faultLogMessage,
                        userId, hostInfo.getId());
    }

    public void deleteUsersHosts(List<ShortUsersHostsInfo> newHostOwners, long oldHostId) throws InternalException {
        String q = "DELETE FROM tbl_users_hosts WHERE host_id = ? AND user_id IN (%1$s)";
        String fullQuery = String.format(q,
                SqlUtil.getCommaSeparatedList(newHostOwners,
                        new SqlUtil.ListParameterizer<ShortUsersHostsInfo>() {
                            @Override
                            public String getParameter(int i, ShortUsersHostsInfo obj) {
                                return String.valueOf(obj.getUserId());
                            }

                            @Override
                            public int getParamNumber() {
                                return 1;
                            }
                        }));

        getJdbcTemplate(WMCPartition.nullPartition()).update(fullQuery, oldHostId);
    }

    public void changeHostIdAndResetVerification(long newHostId, long oldHostId) throws InternalException {
        String q = "UPDATE tbl_users_hosts SET host_id = ?, state = ? WHERE host_id = ?";
        getJdbcTemplate(WMCPartition.nullPartition()).update(q, newHostId, VerificationStateEnum.WAITING.getValue(), oldHostId);
    }

    public void changeHostIdAndResetVerification(long newHostId, long oldHostId, List<Long> userIds) throws InternalException {
        if (userIds.isEmpty()) {
            return;
        }
        String q = String.format("UPDATE tbl_users_hosts SET host_id = ?, state = ? WHERE host_id = ? AND user_id IN (%s)",
                SqlUtil.getCommaSeparatedList(userIds));
        getJdbcTemplate(WMCPartition.nullPartition()).update(q, newHostId, VerificationStateEnum.WAITING.getValue(), oldHostId);
    }

    public int deleteHostsForUser(long userId, List<Long> hostIds) throws
            InternalException {
        String q = "DELETE FROM tbl_users_hosts WHERE user_id = ? AND host_id in (%1$s)";
        final String queryString = String.format(q, SqlUtil.getCommaSeparatedList(hostIds));
        return getJdbcTemplate(WMCPartition.nullPartition()).update(queryString, userId);
    }

    public int reverifyAll(long hostId) throws InternalException {
        String q = "UPDATE tbl_users_hosts SET state = ?, verified_on = NOW() WHERE host_id = ? AND verification_type IN (" +
                VerificationTypeEnum.TXT_FILE.value() +
                ", " + VerificationTypeEnum.META_TAG.value() +
                ", " + VerificationTypeEnum.HTML_FILE.value() +
                ", " + VerificationTypeEnum.MANUAL.value() +
                ", " + VerificationTypeEnum.DELEGATION.value() +
                ")";

        return getJdbcTemplate(WMCPartition.nullPartition()).update(q, VerificationStateEnum.RECHECK_WAITING.value(), hostId);
    }

    public List<UsersHostsInfo> findUserHostInfo(SqlCondition c, SqlLimits limits) throws InternalException {
        String q = "SELECT * FROM tbl_users_hosts WHERE " + c.sql() + " " + limits.toMysqlLimits();
        return getJdbcTemplate(WMCPartition.nullPartition()).query(q, getUserHostMapper(), c.args().toArray());
    }

    private String truncateFaultLog(UsersHostsInfo info) {
        String faultLog = info.getVerifyFaultLog();
        if (faultLog != null && faultLog.length() > MAX_FAULT_LOG_LENGTH) {
            faultLog = faultLog.substring(0, MAX_FAULT_LOG_LENGTH);
        }
        return faultLog;
    }

    private static final ParameterizedRowMapper<ShortUsersHostsInfo> shortUsersHostsInfoMapper =
            new ParameterizedRowMapper<ShortUsersHostsInfo>() {
                @Override
                public ShortUsersHostsInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
                    return new ShortUsersHostsInfo(rs.getLong("user_id"), rs.getLong("host_id"));
                }
            };

    private static ParameterizedRowMapper<UsersHostsInfo> getUserHostMapper() {
        return new ParameterizedRowMapper<UsersHostsInfo>() {
            @Override
            public UsersHostsInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
                long userId = rs.getLong("user_id");
                long hostId = rs.getLong("host_id");
                VerificationStateEnum verificationState = VerificationStateEnum.R.fromValueOrNull(rs.getInt("state"));
                long verificationUin = rs.getLong("verification_uin");
                VerificationTypeEnum verificationType =
                        VerificationTypeEnum.R.fromValueOrNull(rs.getInt("verification_type"));
                Date verifiedOn = rs.getDate("verified_on");
                String verifyFaultLog = rs.getString("verify_fault_log");
                return new UsersHostsInfo(verificationState, verificationUin, verificationType, verifiedOn,
                        verifyFaultLog, userId, hostId, null, null, null);
            }
        };
    }

    public static SqlCondition hostId(BriefHostInfo briefHostInfo) {
        return hostId(briefHostInfo.getId());
    }

    public static SqlCondition hostId(long hostId) {
        return SqlCondition.column("host_id").eq(hostId);
    }

    public static SqlCondition userId(long userId) {
        return SqlCondition.column("user_id").eq(userId);
    }

    public static SqlCondition userId(Set<Long> userId) {
        return SqlCondition.column("user_id").inSet(userId);
    }
}
