package ru.yandex.wmconsole.service;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.transaction.TransactionStatus;

import ru.yandex.wmconsole.data.LanguageEnum;
import ru.yandex.wmconsole.data.NotificationTypeEnum;
import ru.yandex.wmconsole.data.info.NotificationOptionInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
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.ServiceTransactionCallbackWithoutResult;

/**
 * @author Andrey Mima (amima@yandex-team.ru)
 */
public class NotificationService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(NotificationService.class);

    private static final int MODULE = 256;

    private static final String INSERT_NOTIFICATION_QUERY =
            "INSERT INTO " +
                    "tbl_notifications " +
                    "(tbl_notifications.notification_type, tbl_notifications.issue_id, " +
                    "tbl_notifications.user_id, tbl_notifications.happened_time) " +
                    "VALUES " +
                    "(?, ?, ?, ?)";

    private static final String INSERT_NOTIFICATIONS_FOR_ALL_QUERY =
            "INSERT INTO " +
                    "tbl_notifications " +
                    "(tbl_notifications.issue_id, tbl_notifications.happened_time, " +
                    "tbl_notifications.notification_type,  tbl_notifications.user_id)" +
                    "SELECT " +
                    "?, ?, ?, tbl_users.user_id " +
                    "FROM " +
                    "tbl_users " +
                    "WHERE tbl_users.user_id MOD " + MODULE + " = ?";

    private static final String SMART_INSERT_NOTIFICATIONS_FOR_ALL_QUERY =
            "INSERT INTO " +
                    "tbl_notifications " +
                    "(tbl_notifications.notification_type, tbl_notifications.issue_id, " +
                    "tbl_notifications.happened_time, tbl_notifications.user_id)" +
                    "SELECT DISTINCT " +
                    "uno.notification_type, ?, ?, uno.user_id " +
                    "FROM " +
                    "tbl_user_notification_options uno " +
                    "WHERE " +
                    "notification_type = ? " +
                    "AND uno.user_id MOD " + MODULE + " = ?";

    private static final String INSERT_NOTIFICATIONS_FOR_ALL_WITH_LANGUAGE_QUERY =
            "INSERT INTO " +
                    "tbl_notifications " +
                    "(tbl_notifications.issue_id, tbl_notifications.happened_time, " +
                    "tbl_notifications.notification_type,  tbl_notifications.user_id)" +
                    "SELECT " +
                    "   ?, ?, ?, u.user_id " +
                    "FROM " +
                    "   tbl_users u " +
                    "LEFT JOIN " +
                    "   tbl_user_options uo " +
                    "ON " +
                    "   u.user_id = uo.user_id " +
                    "WHERE " +
                    "(" +
                    "   uo.email_lang = ? " +
                    "OR " +
                    "   (" +
                    "       uo.email_lang IS NULL " +
                    "   AND " +
                    "       ? = " + LanguageEnum.DEFAULT_EMAIL_LANGUAGE.getId() + " " +
                    "   ) " +
                    ") " +
                    "AND " +
                    "   u.user_id MOD " + MODULE + " = ?";

    private static final String INSERT_NOTIFICATION_FOR_USERS_QUERY =
            "INSERT INTO " +
                    "tbl_notifications " +
                    "(tbl_notifications.notification_type, tbl_notifications.issue_id, " +
                    "tbl_notifications.user_id, tbl_notifications.happened_time) " +
                    "VALUES " +
                    "(?, ?, ?, ?)";

    private int notificationRepeatAttempts = 1;
    private int notificationLockWaitMillis = 5000;

    private class NotificationBatchPreparedStatementSetter implements BatchPreparedStatementSetter {
        private final NotificationTypeEnum type;
        private final Long issueId;
        private final Date happenedDate;
        private final Iterator<Long> it;
        private final int size;

        public NotificationBatchPreparedStatementSetter(List<Long> userIds, NotificationTypeEnum type, Long issueId,
                                                        Date happenedDate) {
            this.type = type;
            this.issueId = issueId;
            this.happenedDate = happenedDate;
            this.size = userIds.size();
            this.it = userIds.iterator();
        }

        @Override
        public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
            preparedStatement.setInt(1, type.getValue());
            preparedStatement.setLong(2, issueId);
            preparedStatement.setLong(3, it.next());
            preparedStatement.setTimestamp(4, new Timestamp(happenedDate.getTime()));
        }

        @Override
        public int getBatchSize() {
            return size;
        }
    }

    public void insertNotificationForUser(NotificationTypeEnum type, Long issueId, Long userId, Date happenedDate)
            throws InternalException {
        log.debug("Inserting notifications for user " + userId + " of type " + type.name());

        lockWaitRepeatingUpdate(INSERT_NOTIFICATION_QUERY, type.getValue(), issueId, userId, new Timestamp(happenedDate.getTime()));
    }

    public void insertNotificationForUser(Integer type, Long issueId, Long userId, Date happenedDate)
            throws InternalException {
        log.debug("Inserting notifications for user " + userId + " of type " + type);

        lockWaitRepeatingUpdate(INSERT_NOTIFICATION_QUERY, type, issueId, userId, new Timestamp(happenedDate.getTime()));
    }

    public void insertNotificationForUsers(final NotificationTypeEnum type, final Long issueId,
                                           final List<Long> userIds, final Date happenedDate) throws InternalException {
        BatchPreparedStatementSetter batchPreparedStatementSetter =
                new NotificationBatchPreparedStatementSetter(userIds, type, issueId, happenedDate);

        log.debug("Inserting notifications for " + userIds.size() + " users of type " + type.name());

        lockWaitRepeatingBatchUpdate(INSERT_NOTIFICATION_FOR_USERS_QUERY, batchPreparedStatementSetter);
    }

    public void insertNotificationForAll(NotificationTypeEnum type, Long issueId, Date happenedDate) throws InternalException {
        log.debug("Inserting notifications for all of type " + type.name());
        Set<NotificationOptionInfo> defaultOptions = NotificationOptionsService.getDefaultOptions();
        boolean smartInsertApplicable = true;
        for (NotificationOptionInfo info : defaultOptions) {
            if (info.getNotificationType().equals(type.getValue())) {
                smartInsertApplicable = false;
            }
        }
        for (int module = 0; module < MODULE; module++) {
            try {
                if (smartInsertApplicable) {
                    lockWaitRepeatingUpdate(SMART_INSERT_NOTIFICATIONS_FOR_ALL_QUERY, issueId, new Timestamp(happenedDate.getTime()), type.getValue(), module);
                } else {
                    lockWaitRepeatingUpdate(INSERT_NOTIFICATIONS_FOR_ALL_QUERY, issueId, new Timestamp(happenedDate.getTime()), type.getValue(), module);
                }
            } catch (InternalException e) {
                log.error("Can not insert notifications for all of type " + type + " with issueId " + issueId + " and module " + module);
                throw e;
            }
        }
    }

    public void insertNotificationForAllWithLanguage(NotificationTypeEnum type, Long issueId, Date happenedDate, LanguageEnum language) throws InternalException {
        log.debug("Inserting notifications for all with lang " + language + " of type " + type.name());
        for (int m = 0; m < MODULE; m++) {
            try {
                lockWaitRepeatingUpdate(
                        INSERT_NOTIFICATIONS_FOR_ALL_WITH_LANGUAGE_QUERY,
                        issueId, new Timestamp(happenedDate.getTime()), type.getValue(), language.getId(), language.getId(), m);
            } catch (InternalException e) {
                log.error("Can not insert notifications for all of type " + type + " with issueId " + issueId + " and module " + m);
            }
        }
    }

    /**
     * Tries to repeat update query several times if it catch CannotAcquireLockException while execution
     *
     * @param sqlString     SQL query
     * @param requestParams SQL request parameters
     * @throws InternalException problems with database
     */
    private void lockWaitRepeatingUpdate(final String sqlString, final Object... requestParams) throws InternalException {
        final int REPEAT_TIMES = 1;
        InternalException toThrow = null;

        for (int attempt = 0; attempt < REPEAT_TIMES; attempt++) {
            try {
                log.debug("Success update logging: lockWaitRepeatingUpdate");
                getJdbcTemplate(WMCPartition.nullPartition()).update(sqlString, requestParams);
                return;
            } catch (InternalException e) {
                toThrow = e;
            }

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                log.warn("Sleep interrupted");
            }
        }

        log.error("All query repeat attempts failed");
        throw toThrow;
    }

    /**
     * Tries to repeat batch update query several times if it catch CannotAcquireLockException while execution
     *
     * @param sqlString                    SQL query
     * @param batchPreparedStatementSetter SQL request parameters batch setter
     * @throws InternalException problems with database
     */
    private void lockWaitRepeatingBatchUpdate(final String sqlString,
            final BatchPreparedStatementSetter batchPreparedStatementSetter) throws InternalException
    {
        InternalException toThrow = null;

        for (int attempt = 0; attempt < notificationRepeatAttempts; attempt++) {
            try {
                getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(
                        new ServiceTransactionCallbackWithoutResult() {
                            @Override
                            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus)
                                    throws InternalException
                            {
                                log.debug("Success update logging: lockWaitRepeatingBatchUpdate");
                                getJdbcTemplate(WMCPartition.nullPartition()).getJdbcOperations()
                                        .batchUpdate(sqlString, batchPreparedStatementSetter);
                            }
                        }
                );
                return;
            } catch (UserException e) {
                throw new AssertionError("UserException isn't thrown here");
            } catch (InternalException e) {
                toThrow = e;
            }

            try {
                Thread.sleep(notificationLockWaitMillis);
            } catch (InterruptedException e) {
                log.warn("Sleep interrupted");
            }
        }

        log.error("All query repeat attempts failed");
        throw toThrow;
    }

    @Required
    public void setNotificationRepeatAttempts(int notificationRepeatAttempts) {
        this.notificationRepeatAttempts = notificationRepeatAttempts;
    }

    @Required
    public void setNotificationLockWaitMillis(int notificationLockWaitMillis) {
        this.notificationLockWaitMillis = notificationLockWaitMillis;
    }
}
