package ru.yandex.wmconsole.service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import ru.yandex.common.framework.pager.Pager;
import ru.yandex.common.util.db.LongRowMapper;
import ru.yandex.wmconsole.data.NotificationTypeEnum;
import ru.yandex.wmconsole.data.info.NotificationMessageInfo;
import ru.yandex.wmconsole.data.info.NotificationTypeInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.service.AbstractDbService;
import ru.yandex.wmtools.common.util.SqlUtil;

/**
 * Service for managing internal notification messages
 *
 * @author Andrey Mima (amima@yandex-team.ru)
 */
public class MessageService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(MessageService.class);

    private static final String FIELD_USER_ID = "user_id";
    private static final String FIELD_MESSAGE_ID = "message_id";
    private static final String FIELD_VIEWED = "viewed";
    private static final String FIELD_HIDDEN = "hidden";
    private static final String FIELD_MESSAGE = "message";
    private static final String FIELD_NOTIFICATION_TYPE = "notification_type";
    private static final String FIELD_RECEIVE_TIME = "receive_time";

    private static final String FIELD_SERVICE_NAME = "name";

    private static final String SELECT_USER_MESSAGES_COUNT_QUERY =
            "SELECT " +
                    "count(*) " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "%1$s ";

    private static final String SELECT_USER_UNREAD_MESSAGES_COUNT_QUERY =
            "SELECT " +
                    "count(*) " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "AND " +
                    "viewed = 0";

    private static final String SELECT_USER_READ_MESSAGES_COUNT_QUERY =
            "SELECT " +
                    "count(*) " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "AND " +
                    "viewed = 1";

    private static final String SELECT_USER_MESSAGES_QUERY =
            "SELECT " +
                    "message_id, user_id, message, notification_type, receive_time, viewed, hidden " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "%1$s " +
                    "ORDER BY " +
                    "receive_time DESC, " +
                    "message_id DESC " +
                    "%2$s ";

    private static final String SELECT_LEFT_NEIGHBOUR_QUERY =
            "SELECT " +
                    "message_id, user_id, message, notification_type, receive_time, viewed, hidden " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "AND " +
                    "message_id < ? " +
                    "ORDER BY " +
                    "receive_time DESC, " +
                    "message_id DESC " +
                    "LIMIT 0, 1";

    private static final String SELECT_RIGHT_NEIGHBOUR_QUERY =
            "SELECT " +
                    "message_id, user_id, message, notification_type, receive_time, viewed, hidden " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "AND " +
                    "message_id > ? " +
                    "ORDER BY " +
                    "receive_time ASC, " +
                    "message_id ASC " +
                    "LIMIT 0, 1";

    private static final String SELECT_MESSAGE_QUERY =
            "SELECT " +
                    "message_id, user_id, message, notification_type, receive_time, viewed, hidden " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "message_id = ?";

    private static final String SELECT_MESSAGE_USER_QUERY =
            "SELECT " +
                    "user_id " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "message_id = ? " +
                    "AND " +
                    "hidden = 0";

    private static final String SELECT_MESSAGES_USER_QUERY =
            "SELECT " +
                    "user_id " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "message_id IN (%s) " +
                    "AND " +
                    "hidden = 0";

    private static final String SELECT_USER_LAST_MESSAGES_QUERY =
            "SELECT " +
                    "message_id, user_id, message, notification_type, receive_time, viewed, hidden " +
                    "FROM " +
                    "tbl_messages " +
                    "WHERE " +
                    "user_id = ? " +
                    "AND " +
                    "hidden = 0 " +
                    "AND " +
                    "viewed = 0 " +
                    "ORDER BY " +
                    "receive_time DESC, " +
                    "message_id DESC " +
                    "LIMIT 0, ?";

    private static final String INSERT_MESSAGE_FOR_USER_QUERY =
            "INSERT INTO " +
                    "tbl_messages (user_id, message, notification_type, receive_time) " +
                    "VALUES " +
                    "(?, ?, ?, NOW())";

    private static final String UPDATE_MESSAGES_MARK_HIDDEN_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "hidden = 1 " +
                    "WHERE " +
                    "message_id IN (%s)";

    private static final String UPDATE_MESSAGES_MARK_READ_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "viewed = 1 " +
                    "WHERE " +
                    "message_id IN (%s)";

    private static final String UPDATE_MESSAGES_MARK_UNREAD_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "viewed = 0 " +
                    "WHERE " +
                    "message_id IN (%s)";

    private static final String UPDATE_MARK_ONE_READ_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "viewed = 1 " +
                    "WHERE " +
                    "message_id" + " = ? ";

    private static final String UPDATE_MARK_ALL_READ_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "viewed = 1 " +
                    "WHERE " +
                    "user_id" + " = ? ";

    private static final String UPDATE_MARK_ONE_HIDDEN_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "hidden = 1 " +
                    "WHERE " +
                    "message_id" + " = ? ";

    private static final String UPDATE_MARK_ALL_HIDDEN_QUERY =
            "UPDATE " +
                    "tbl_messages " +
                    "SET " +
                    "hidden = 1 " +
                    "WHERE " +
                    "user_id" + " = ? ";

    private static final String SELECT_USER_MESSAGES_TYPES_QUERY =
            "SELECT " +
                    "DISTINCT " +
                    "   m.notification_type AS " + FIELD_NOTIFICATION_TYPE + ", " +
                    "   s.name AS " + FIELD_SERVICE_NAME + " " +
                    "FROM " +
                    "   tbl_messages m " +
                    "LEFT JOIN " +
                    "   tbl_foreign_services fs " +
                    "ON " +
                    "   m.notification_type = fs.service_id " +
                    "LEFT JOIN " +
                    "   tbl_dic_service s " +
                    "ON " +
                    "   fs.plugin_id = s.id " +
                    "WHERE " +
                    "  m.user_id = ? " +
                    "AND " +
                    "   m.hidden = 0 ";

    private static final ParameterizedRowMapper<NotificationMessageInfo> messageRowMapper =
            new ParameterizedRowMapper<NotificationMessageInfo>() {
                @Override
                public NotificationMessageInfo mapRow(ResultSet resultSet, int rowNum) throws SQLException {
                    Long messageId = resultSet.getLong(FIELD_MESSAGE_ID);
                    long userId = resultSet.getLong(FIELD_USER_ID);
                    String message = resultSet.getString(FIELD_MESSAGE);

                    Integer notificationTypeValue = resultSet.getInt(FIELD_NOTIFICATION_TYPE);
                    NotificationTypeEnum notificationType = NotificationTypeEnum.R.fromValueOrNull(notificationTypeValue);
                    if (notificationType == null) {
                        //Service message
                        notificationType = NotificationTypeEnum.SERVICE_MESSAGE;
                    } else {
                        //Some common message, so we don't need to save it's number
                        notificationTypeValue = null;
                    }

                    Date receiveTime = resultSet.getTimestamp(FIELD_RECEIVE_TIME);
                    Boolean viewed = resultSet.getBoolean(FIELD_VIEWED);
                    Boolean hidden = resultSet.getBoolean(FIELD_HIDDEN);

                    return new NotificationMessageInfo(
                            messageId, userId, message, notificationType, notificationTypeValue, receiveTime, viewed, hidden
                    );
                }
            };

    private static final ParameterizedRowMapper<NotificationTypeInfo> notificationTypeInfoMapper = new ParameterizedRowMapper<NotificationTypeInfo>() {
        @Override
        public NotificationTypeInfo mapRow(ResultSet resultSet, int i) throws SQLException {
            return new NotificationTypeInfo(
                    resultSet.getInt(FIELD_NOTIFICATION_TYPE),
                    resultSet.getString(FIELD_SERVICE_NAME));
        }
    };

    public Integer getUserUnreadMessagesCount(Long userId) throws InternalException {
        return getJdbcTemplate(new WMCPartition(null, userId)).safeQueryForInt(SELECT_USER_UNREAD_MESSAGES_COUNT_QUERY, userId);
    }

    public Integer getUserReadMessagesCount(Long userId) throws InternalException {
        return getJdbcTemplate(new WMCPartition(null, userId)).safeQueryForInt(SELECT_USER_READ_MESSAGES_COUNT_QUERY, userId);
    }

    public List<NotificationMessageInfo> getLastMessages(Long userId, int count) throws InternalException {
        if (count < 1) {
            return null;
        }
        List<NotificationMessageInfo> lastMessages =
                getJdbcTemplate(new WMCPartition(null, userId)).query(SELECT_USER_LAST_MESSAGES_QUERY, messageRowMapper, userId, count + 20);
        if (lastMessages.size() == 0) {
            return null;
        }

        Collections.sort(lastMessages);
        return lastMessages.subList(0, Math.min(lastMessages.size(), count));
    }

    /**
     * Returns all user's internal notification messages that are not hidden, with paging
     *
     * @param pager  paging limit utility
     * @param userId id of user who requested the message
     * @return list of internal notification messages
     * @throws InternalException something wrong with DB access
     */
    public List<NotificationMessageInfo> getUserMessages(Pager pager, Long userId) throws InternalException {
        return getUserMessages(pager, userId, null);
    }

    public Long getPrevNeighbourId(NotificationMessageInfo center) throws InternalException {
        NotificationMessageInfo message = getJdbcTemplate(new WMCPartition(null, center.getUserId())).safeQueryForObject(
                SELECT_LEFT_NEIGHBOUR_QUERY,
                messageRowMapper,
                center.getUserId(),
                center.getMessageId()
        );

        return (message != null) ? message.getMessageId() : null;
    }

    public Long getNextNeighbourId(NotificationMessageInfo center) throws InternalException {
        NotificationMessageInfo message = getJdbcTemplate(new WMCPartition(null, center.getUserId())).safeQueryForObject(
                SELECT_RIGHT_NEIGHBOUR_QUERY,
                messageRowMapper,
                center.getUserId(),
                center.getMessageId()
        );

        return (message != null) ? message.getMessageId() : null;
    }

    /**
     * Returns single user's internal notification message
     *
     * @param messageId requested message id
     * @param userId    for segmentation
     * @return internal notification message
     * @throws InternalException something wrong with DB access, or user does not own message, or message does not exist
     */
    public NotificationMessageInfo getUserMessage(Long messageId, Long userId) throws InternalException {
        return getJdbcTemplate(new WMCPartition(null, userId)).queryForObject(
                SELECT_MESSAGE_QUERY,
                messageRowMapper,
                messageId
        );
    }

    /**
     * Throws exception in case user does not own message or message does not exist
     *
     * @param messageId requested message id
     * @param userId    id of user requested the message
     * @throws InternalException something wrong with DB access
     * @throws UserException     thrown in case user does not own message or message does not exist
     */
    public void checkUserOwnsMessage(Long messageId, Long userId) throws UserException, InternalException {
        Long messageUserId = getJdbcTemplate(new WMCPartition(null, userId)).safeQueryForLong(
                SELECT_MESSAGE_USER_QUERY,
                messageId
        );

        if (messageUserId == null) {
            throw new UserException(WMCUserProblem.NO_SUCH_MESSAGE, "Message does not exist");
        }

        if (!messageUserId.equals(userId)) {
            throw new UserException(WMCUserProblem.MESSAGE_NOT_OWNED_BY_USER, "User does not own the message");
        }
    }

    /**
     * Throws exception in case user does not own message or message does not exist
     *
     * @param messageIds requested message ids
     * @param userId     id of user requested the message
     * @throws InternalException something wrong with DB access
     * @throws UserException     thrown in case user does not own message or message does not exist
     */
    public void checkUserOwnMessages(List<Long> messageIds, Long userId) throws InternalException, UserException {
        if (messageIds == null || messageIds.size() == 0) {
            log.warn("message id list for security check is null or empty");
            throw new IllegalArgumentException("message id list for security check is null or empty");
        }

        String queryString = String.format(SELECT_MESSAGES_USER_QUERY, SqlUtil.getCommaSeparatedList(messageIds));

        List<Long> messageUserIds = getJdbcTemplate(new WMCPartition(null, userId)).query(queryString, new LongRowMapper());

        if (messageUserIds == null || messageUserIds.size() < messageIds.size()) {
            throw new UserException(WMCUserProblem.NO_SUCH_MESSAGE, "Message does not exist");
        }

        for (Long messageUserId : messageUserIds) {
            if (!messageUserId.equals(userId)) {
                throw new UserException(WMCUserProblem.MESSAGE_NOT_OWNED_BY_USER, "User does not own the message");
            }
        }
    }

    /**
     * Marks single internal notification message as read (it will no longer be represented as unread)
     *
     * @param messageId message to mark
     * @param userId    for segmentation
     * @throws InternalException something wrong with DB access, or user does not own message, or message does not exist
     */
    public void markOneRead(Long messageId, Long userId) throws InternalException {
        getJdbcTemplate(new WMCPartition(null, userId)).update(UPDATE_MARK_ONE_READ_QUERY, messageId);
    }

    /**
     * Marks single internal notification message as hidden (user will never see it, it is "deleted")
     *
     * @param messageId message to mark
     * @param userId    for segmentation
     * @throws InternalException something wrong with DB access, or user does not own message, or message does not exist
     */
    public void markOneHidden(Long messageId, Long userId) throws InternalException {
        getJdbcTemplate(new WMCPartition(null, userId)).update(UPDATE_MARK_ONE_HIDDEN_QUERY, messageId);
    }

    /**
     * Marks all internal notification messages as read (they will no longer be represented as unread)
     *
     * @param userId user requested to mark messages
     * @throws InternalException something wrong with DB access
     */
    public void markAllRead(Long userId) throws InternalException {
        getJdbcTemplate(new WMCPartition(null, userId)).update(UPDATE_MARK_ALL_READ_QUERY, userId);
    }

    /**
     * Marks all internal notification messages as hidden (user will never see them, they are "deleted")
     *
     * @param userId user requested to mark messages
     * @throws InternalException something wrong with DB access
     */
    public void markAllHidden(Long userId) throws InternalException {
        getJdbcTemplate(new WMCPartition(null, userId)).update(UPDATE_MARK_ALL_HIDDEN_QUERY, userId);
    }

    /**
     * Marks the list of messages as unread
     *
     * @param messageIds messages to unread
     * @param userId     for segmentation
     * @throws InternalException database problems
     */
    public void markUnread(List<Long> messageIds, Long userId) throws InternalException {
        if (messageIds == null || messageIds.size() == 0) {
            log.warn("messages to hide are null or empty");
            return;
        }

        String queryString = String.format(UPDATE_MESSAGES_MARK_UNREAD_QUERY, SqlUtil.getCommaSeparatedList(messageIds));
        getJdbcTemplate(new WMCPartition(null, userId)).update(queryString);
    }

    /**
     * Marks the list of messages as read
     *
     * @param messageIds messages to read
     * @param userId     for segmentation
     * @throws InternalException database problems
     */
    public void markRead(List<Long> messageIds, Long userId) throws InternalException {
        if (messageIds == null || messageIds.size() == 0) {
            log.warn("messages to hide are null or empty");
            return;
        }

        String queryString = String.format(UPDATE_MESSAGES_MARK_READ_QUERY, SqlUtil.getCommaSeparatedList(messageIds));
        getJdbcTemplate(new WMCPartition(null, userId)).update(queryString);
    }

    /**
     * Marks the list of messages as hidden (user will never see them, they are "deleted")
     *
     * @param messageIds messages to hide
     * @param userId     for segmentation
     * @throws InternalException database problems
     */
    public void markHidden(List<Long> messageIds, Long userId) throws InternalException {
        if (messageIds == null || messageIds.size() == 0) {
            log.warn("messages to hide are null or empty");
            return;
        }

        String queryString = String.format(UPDATE_MESSAGES_MARK_HIDDEN_QUERY, SqlUtil.getCommaSeparatedList(messageIds));
        getJdbcTemplate(new WMCPartition(null, userId)).update(queryString);
    }

    /**
     * Adds new internal notification message for user to the database
     *
     * @param userId           user to add new internal notification message for
     * @param message          internal notification message text string
     * @param notificationType message notification type
     * @throws InternalException something wrong with database access
     */
    public void sendMessageToUser(Long userId, String message, int notificationType)
            throws InternalException {
        getJdbcTemplate(new WMCPartition(null, userId)).update(
                INSERT_MESSAGE_FOR_USER_QUERY,
                userId,
                message,
                notificationType
        );
    }

    private String getWhereClauseForTypes(List<Integer> types) {
        if (types == null || types.isEmpty()) {
            return " ";
        }

        return " AND notification_type IN (" + SqlUtil.getCommaSeparatedList(types) + ") ";
    }

    public List<NotificationTypeInfo> getMessageTypesForUser(long userId) throws InternalException {
        return getJdbcTemplate(new WMCPartition(null, userId))
                .query(SELECT_USER_MESSAGES_TYPES_QUERY, notificationTypeInfoMapper, userId);
    }

    public List<NotificationMessageInfo> getUserMessages(Pager pager, Long userId, List<Integer> types)
            throws InternalException {
        return getJdbcTemplate(new WMCPartition(null, userId)).pageableSelect(
                String.format(SELECT_USER_MESSAGES_COUNT_QUERY, getWhereClauseForTypes(types)),
                String.format(SELECT_USER_MESSAGES_QUERY, getWhereClauseForTypes(types), "%1$s"), messageRowMapper, pager, userId);
    }
}
