package ru.yandex.wmconsole.service.dao;

import java.net.IDN;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.collections.ComparatorUtils;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import ru.yandex.common.util.functional.Comparators;
import ru.yandex.misc.db.q.ConditionUtils;
import ru.yandex.misc.db.q.SqlCondition;
import ru.yandex.misc.db.q.SqlLimits;
import ru.yandex.misc.db.q.SqlOrder;
import ru.yandex.webmaster.common.host.dao.TblHostsMainDao;
import ru.yandex.webmaster.common.util.HostnamePart;
import ru.yandex.wmconsole.data.VerificationStateEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.limits.HostLimitsInfo;
import ru.yandex.wmconsole.data.info.limits.HostLimitsOrder;
import ru.yandex.wmconsole.data.info.limits.HostsAndLimitsInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.verification.VerificationTypeEnum;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.service.AbstractDbService;

/**
 * @author aherman
 */
public class TblXmllimitDelegationsMetaDao extends AbstractDbService {

    private static SqlCondition delegationSubqueryCondition(long userId) {
        return SqlCondition.column("to_user_id").eq(userId);
    }

    private static SqlCondition mainUserSubqueryCondition(long userId) {
        return SqlCondition.all(
                SqlCondition.column("uh.user_id").eq(userId),
                SqlCondition.column("uh.state").eq(VerificationStateEnum.VERIFIED.value()),
                SqlCondition.column("uh.verification_type").eq(VerificationTypeEnum.CHEAT.value()).not(),

                SqlCondition.any(
                        SqlCondition.column("xd.host_id").isNull(),
                        SqlCondition.column("xd.to_user_id").ne(userId)
                )
        );
    }

    private static SqlCondition hostnameLikeCondition(HostnamePart hostnamePart) {
        if (hostnamePart == null) {
            return SqlCondition.trueCondition();
        }
        String likeStr;
        if (hostnamePart.getProtocol() == HostnamePart.Protocol.HTTPS) {
            likeStr = "https://" + hostnamePart.getHostnamePart();
        } else {
            likeStr = hostnamePart.getHostnamePart();
        }

        ConditionUtils.Column column = SqlCondition.column("h.name");
        if (hostnamePart.isPrefix()) {
            return column.like(likeStr + "%");
        } else {
            return column.like("%" + likeStr + "%");
        }
    }

    private static SqlCondition hostnameIDNCondition(HostnamePart hostnamePart) {
        String likeStr = "xn--";
        if (hostnamePart.getProtocol() == HostnamePart.Protocol.HTTPS) {
            likeStr = "https://xn--";
        }
        return SqlCondition.column("h.name").like(likeStr + "%");
    }

    private static String listHostQuery(SqlCondition delegateCondition, SqlCondition mainUserCondition,
            SqlCondition hostnameLikeCondition, SqlOrder order, SqlLimits limits)
    {
        SqlCondition isMainMirrorCondition = SqlCondition.column("h.mirror_id").isNull();
        SqlCondition hostCondition = isMainMirrorCondition.and(hostnameLikeCondition);
        return
                "SELECT h.name AS hostname, h.host_id AS host_id, hl.`limit` AS host_limit, t1.limit_owner_id, t1.owner_is_current_user"
                + "  FROM ("
                + "         (SELECT host_id, to_user_id AS limit_owner_id, 1 AS owner_is_current_user"
                + "            FROM tbl_xmllimit_delegations"
                + "            WHERE " + delegateCondition.sql()
                + "         )"
                + "         UNION"
                + "         (SELECT uh.host_id AS host_id, COALESCE(xd.to_user_id, 0) AS limit_owner_id, COALESCE(xd.to_user_id, 0) = uh.user_id AS owner_is_current_user"
                + "            FROM tbl_users_hosts uh"
                + "            LEFT JOIN tbl_xmllimit_delegations xd USING (host_id)"
                + "            WHERE " + mainUserCondition.sql()
                + "         )"
                + "       ) t1"
                + "  JOIN tbl_hosts h USING (host_id)"
                + "  LEFT JOIN tbl_hosts_limits hl USING (host_id)"
                + "  WHERE " + hostCondition.sql()
                + "  " + order.toSql()
                + "  " + limits.toMysqlLimits();
    }

    public HostsAndLimitsInfo getTotalLimit(long userId, HostnamePart hostnameFilter) throws InternalException {
        if (hostnameFilter != null && hostnameFilter.isIDN()) {
            return getHostsAndLimitsIDN(userId, hostnameFilter);
        } else {
            return getHostsAndLimitsNormalHost(userId, hostnameFilter);
        }
    }

    private HostsAndLimitsInfo getHostsAndLimitsIDN(long userId, HostnamePart hostnameFilter)
            throws InternalException {
        List<HostLimitsInfo> allLimitsInfos = findAllLimitsInfoIDN(userId, hostnameFilter, HostLimitsOrder.UNORDERED);
        List<HostLimitsInfo> result = filterByName(hostnameFilter, allLimitsInfos);

        if (result.isEmpty()) {
            return new HostsAndLimitsInfo(0, 0);
        }
        int totalHosts = result.size();
        int totalLimit = 0;

        for (HostLimitsInfo hostLimitsInfo : result) {
            totalLimit += hostLimitsInfo.getLimits();
        }
        return new HostsAndLimitsInfo(totalHosts, totalLimit);
    }

    private HostsAndLimitsInfo getHostsAndLimitsNormalHost(long userId, HostnamePart hostnameFilter)
            throws InternalException
    {
        SqlCondition delegateCondition = delegationSubqueryCondition(userId);
        SqlCondition mainUserCondition = mainUserSubqueryCondition(userId);
        SqlCondition hostnameLikeCondition = hostnameLikeCondition(hostnameFilter);

        String q = "SELECT sum(host_limit * owner_is_current_user) AS total_limit, count(*) AS total_hosts FROM ("
                + listHostQuery(delegateCondition,
                        mainUserCondition,
                        hostnameLikeCondition,
                        SqlOrder.unordered(),
                        SqlLimits.all())
                + ") t2";
        Object[] requestParams = getLimitQueryParams(delegateCondition, mainUserCondition, hostnameLikeCondition);

        List<HostsAndLimitsInfo> result =
                getJdbcTemplate(WMCPartition.nullPartition()).query(q, LIMIT_MAPPER, requestParams);
        return result.isEmpty() ? new HostsAndLimitsInfo(0, 0) : result.get(0);
    }

    public List<HostLimitsInfo> getLimits(long userId, HostnamePart hostnamePart, HostLimitsOrder order, int limitFirst,
            int limitCount) throws InternalException
    {
        if (hostnamePart != null && hostnamePart.isIDN()) {
            return getHostLimitsInfoIDN(userId, hostnamePart, order, limitFirst, limitCount);
        } else {
            return getHostLimitsInfoNormalDomain(userId, hostnamePart, order, limitFirst, limitCount);
        }
    }

    private List<HostLimitsInfo> getHostLimitsInfoIDN(long userId, HostnamePart hostnamePart, HostLimitsOrder order,
            int limitFirst, int limitCount) throws InternalException
    {
        List<HostLimitsInfo> allLimitsInfos = findAllLimitsInfoIDN(userId, hostnamePart, order);
        List<HostLimitsInfo> filteredLimitInfos = filterByName(hostnamePart, allLimitsInfos);
        if (filteredLimitInfos.isEmpty()) {
            return Collections.emptyList();
        }
        Comparator<HostLimitsInfo> comparator;
        if (order == HostLimitsOrder.HOSTNAME_ASC) {
            Collections.sort(filteredLimitInfos, getHostnameAscCompartor());
        } else if (order == HostLimitsOrder.HOSTNAME_DESC) {
            Collections.sort(filteredLimitInfos, getHostnameDescCompartor());
        }
        return getFirst(filteredLimitInfos, limitFirst, limitCount);
    }

    private List<HostLimitsInfo> findAllLimitsInfoIDN(long userId, HostnamePart hostnamePart,
            HostLimitsOrder order)
            throws InternalException
    {
        SqlCondition delegateCondition = delegationSubqueryCondition(userId);
        SqlCondition mainUserCondition = mainUserSubqueryCondition(userId);
        SqlCondition hostnameLikeCondition = hostnameIDNCondition(hostnamePart);

        SqlLimits range = SqlLimits.all();
        SqlOrder sqlOrder;
        switch (order) {
            case LIMIT_ASC: sqlOrder = byLimit(true); break;
            case LIMIT_DESC: sqlOrder = byLimit(false); break;
            case OWNER_ASC: sqlOrder = byOwner(true); break;
            case OWNER_DESC: sqlOrder = byOwner(false); break;

            default: sqlOrder = SqlOrder.unordered();
        }
        String q = listHostQuery(delegateCondition, mainUserCondition, hostnameLikeCondition, sqlOrder, range);
        Object[] requestParams = getLimitQueryParams(delegateCondition, mainUserCondition, hostnameLikeCondition);

        return getJdbcTemplate(WMCPartition.nullPartition()).query(q, HOST_LIMIT_MAPPER, requestParams);

    }

    private List<HostLimitsInfo> getHostLimitsInfoNormalDomain(long userId, HostnamePart hostnamePart,
            HostLimitsOrder order, int limitFirst, int limitCount)
            throws InternalException
    {
        SqlCondition delegateCondition = delegationSubqueryCondition(userId);
        SqlCondition mainUserCondition = mainUserSubqueryCondition(userId);
        SqlCondition hostnameLikeCondition = hostnameLikeCondition(hostnamePart);

        SqlLimits range = SqlLimits.range(limitFirst, limitCount);
        SqlOrder sqlOrder;
        switch (order) {
            case HOSTNAME_ASC: sqlOrder = byHostname(true); break;
            case HOSTNAME_DESC: sqlOrder = byHostname(false); break;
            case LIMIT_ASC: sqlOrder = byLimit(true); break;
            case LIMIT_DESC: sqlOrder = byLimit(false); break;
            case OWNER_ASC: sqlOrder = byOwner(true); break;
            case OWNER_DESC: sqlOrder = byOwner(false); break;
            default: sqlOrder = byLimit(false);
        }
        String q = listHostQuery(delegateCondition, mainUserCondition, hostnameLikeCondition, sqlOrder, range);
        Object[] requestParams = getLimitQueryParams(delegateCondition, mainUserCondition, hostnameLikeCondition);

        return getJdbcTemplate(WMCPartition.nullPartition()).query(q, HOST_LIMIT_MAPPER, requestParams);
    }

    private Object[] getLimitQueryParams(SqlCondition delegateCondition, SqlCondition mainUserCondition,
            SqlCondition hostnameLikeCondition)
    {
        List<Object> args = new LinkedList<>();
        args.addAll(delegateCondition.args());
        args.addAll(mainUserCondition.args());
        args.addAll(hostnameLikeCondition.args());
        return args.toArray();
    }

    public List<BriefHostInfo> findMissingLimitDelegations(long fromHostId, int size) throws InternalException {
        String q =
                "SELECT h.host_id, h.name, h.mirror_id " +
                " FROM tbl_hosts h " +
                " LEFT JOIN tbl_xmllimit_delegations xd USING (host_id) " +
                " LEFT JOIN tbl_users_hosts uh ON (h.host_id = uh.host_id AND uh.state = ?) " +
                " WHERE uh.state IS NOT NULL AND xd.to_user_id IS NULL AND h.host_id > ? GROUP BY h.host_id " + SqlLimits.first(size).toMysqlLimits();
        return getJdbcTemplate(WMCPartition.nullPartition())
                .query(q, TblHostsMainDao.getBriefHostInfoMapper(), VerificationStateEnum.VERIFIED.getValue(), fromHostId);
    }

    private static final ParameterizedRowMapper<HostLimitsInfo> HOST_LIMIT_MAPPER = new ParameterizedRowMapper<HostLimitsInfo>() {
        @Override
        public HostLimitsInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
            long hostId = rs.getLong("host_id");
            String hostname = rs.getString("hostname");
            int limits = rs.getInt("host_limit");
            long limitOwnerUserId = rs.getLong("limit_owner_id");

            return new HostLimitsInfo(hostId, hostname, limits, limitOwnerUserId);
        }
    };

    private static final ParameterizedRowMapper<HostsAndLimitsInfo> LIMIT_MAPPER = new ParameterizedRowMapper<HostsAndLimitsInfo>() {
        @Override
        public HostsAndLimitsInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
            int totalHosts = rs.getInt("total_hosts");
            int totalLimit = rs.getInt("total_limit");
            return new HostsAndLimitsInfo(totalHosts, totalLimit);
        }
    };

    public static SqlOrder byLimit(boolean ascending) {
        return SqlOrder.orderByColumn("host_limit", ascending).andThen("hostname");
    }

    public static SqlOrder byOwner(boolean ascending) {
        return SqlOrder.orderByColumn("owner_is_current_user", ascending).andThen("hostname");
    }

    public static SqlOrder byHostname(boolean ascending) {
        return SqlOrder.orderByColumn("hostname", ascending);
    }

    private static Comparator<HostLimitsInfo> getHostnameAscCompartor() {
        return new Comparator<HostLimitsInfo>() {
            @Override
            public int compare(HostLimitsInfo o1, HostLimitsInfo o2) {
                String hostname1 = IDN.toUnicode(o1.getHostname());
                String hostname2 = IDN.toUnicode(o2.getHostname());
                return hostname1.compareTo(hostname2);
            }
        };
    }

    private static Comparator<HostLimitsInfo> getHostnameDescCompartor() {
        return ComparatorUtils.reversedComparator(getHostnameAscCompartor());
    }

    private static List<HostLimitsInfo> filterByName(HostnamePart hostnamePart, List<HostLimitsInfo> allHostLimits) {
        List<HostLimitsInfo> result = new ArrayList<>(allHostLimits.size());
        for (HostLimitsInfo hostLimit : allHostLimits) {
            if (IDN.toUnicode(hostLimit.getHostname()).contains(hostnamePart.getHostnamePart())) {
                result.add(hostLimit);
            }
        }
        return result;
    }

    private static List<HostLimitsInfo> getFirst(List<HostLimitsInfo> limits, int from, int size) {
        if (from > limits.size() - 1) {
            return Collections.emptyList();
        }

        List<HostLimitsInfo> result = new ArrayList<>(size);
        int to = Math.min(from + size, limits.size());
        for(int i = from; i < to; i++) {
            result.add(limits.get(i));
        }
        return result;
    }
}
