package ru.yandex.wmconsole.service;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
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.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.transaction.TransactionStatus;

import ru.yandex.wmconsole.data.NotificationChannelEnum;
import ru.yandex.wmconsole.data.NotificationTypeEnum;
import ru.yandex.wmconsole.data.UserNotificationOptions;
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.IServiceJdbcTemplate;
import ru.yandex.wmtools.common.util.ServiceTransactionCallbackWithoutResult;

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

    private static final Set<NotificationOptionInfo> DEFAULT_OPTIONS =
    Collections.unmodifiableSet(new HashSet<NotificationOptionInfo>() {
        {
            add(new NotificationOptionInfo(NotificationTypeEnum.GLOBAL_MESSAGE, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.PERSONAL_MESSAGE, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.SPAM, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.ERRORS_GROW, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.XSS, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.VIRUS, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.VIRUS, NotificationChannelEnum.EMAIL));
            add(new NotificationOptionInfo(NotificationTypeEnum.VIRUS_FOR_MIRROR, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.VIRUS_FOR_MIRROR, NotificationChannelEnum.EMAIL));
            add(new NotificationOptionInfo(NotificationTypeEnum.HEALED, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.CHECK_INFECTED, NotificationChannelEnum.MESSAGE));
            //add(new NotificationOptionInfo(NotificationTypeEnum.CHECK_CURED, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.CLOAKING, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.MIRROR_CHANGED, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.HOST_VERIFIED, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.HOST_VERIFIED_FOR_ME, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.DELEGATION, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_CHANGED, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.DOORWAY, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.NEW_BAD_SOFTWARE, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.NEW_BAD_SOFTWARE, NotificationChannelEnum.EMAIL));
            add(new NotificationOptionInfo(NotificationTypeEnum.CLEAR_BAD_SOFTWARE, NotificationChannelEnum.MESSAGE));
            add(new NotificationOptionInfo(NotificationTypeEnum.CLEAR_BAD_SOFTWARE, NotificationChannelEnum.EMAIL));
        }
    });

    private static final String SELECT_NOTIFICATION_OPTIONS_FOR_USER_QUERY =
            "SELECT " +
                        "notification_type, notification_channel " +
                    "FROM " +
                        "tbl_user_notification_options " +
                    "WHERE " +
                        "user_id = ?";

    private static final String DELETE_ALL_NOTIFICATION_OPTIONS_FOR_USER_QUERY =
            "DELETE FROM " +
                        "tbl_user_notification_options " +
                    "WHERE " +
                        "user_id = ?";

    private static final String INSERT_NOTIFICATION_OPTIONS_FOR_USER_QUERY =
            "INSERT INTO " +
                        "tbl_user_notification_options (user_id, notification_type, notification_channel) " +
                    "VALUES " +
                        "(?, ?, ?)";

    private final static String UPDATE_OPTIONS_UPDATED_ON_QUERY =
            "UPDATE " +
                        "tbl_users " +
                    "SET " +
                        "options_updated_on = NOW() " +
                    "WHERE " +
                        "user_id = ?";

    private static final ParameterizedRowMapper<NotificationOptionInfo> NOTIFICATION_OPTION_MAPPER =
            new ParameterizedRowMapper<NotificationOptionInfo>() {
                @Override
                public NotificationOptionInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
                    NotificationChannelEnum channel =
                            NotificationChannelEnum.R.fromValueOrNull(rs.getInt("notification_channel"));
                    int intType = rs.getInt("notification_type");
                    if (intType <= NotificationTypeEnum.MAX_VALUE) {
                        NotificationTypeEnum type = NotificationTypeEnum.R.fromValueOrNull(rs.getInt("notification_type"));
                        return new NotificationOptionInfo(type, channel);
                    } else {
                        return new NotificationOptionInfo(intType, channel);
                    }
                }
            };

    private static class OptionsBatchPreparedStatementSetter implements BatchPreparedStatementSetter {
        private Long userId;
        private Iterator<NotificationOptionInfo> it;
        private int size;

        public OptionsBatchPreparedStatementSetter(Long userId, Set<NotificationOptionInfo> options) {
            this.userId = userId;
            this.size = options.size();
            this.it = options.iterator();
        }

        @Override
        public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
            NotificationOptionInfo option = it.next();
            preparedStatement.setLong(1, userId);
            preparedStatement.setInt(2, option.getNotificationType());
            preparedStatement.setInt(3, option.getNotificationChannel().getValue());
        }

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

    public static Set<NotificationOptionInfo> getDefaultOptions() {
        return DEFAULT_OPTIONS;
    }

    private ForeignServicesService foreignServicesService;


    public UserNotificationOptions getUserNotificationOptions(Long userId)
            throws InternalException {
        IServiceJdbcTemplate jdbcTemplate = getJdbcTemplate(WMCPartition.nullPartition());
        List<NotificationOptionInfo> notificationOptions = jdbcTemplate.query(
                SELECT_NOTIFICATION_OPTIONS_FOR_USER_QUERY,
                NOTIFICATION_OPTION_MAPPER,
                userId
        );

        Set<NotificationOptionInfo> switchedOptions = getSwitchedRealAndDenial(notificationOptions);

        UserNotificationOptions result = new UserNotificationOptions(userId);
        for (NotificationOptionInfo option : switchedOptions) {
            result.addNotificationOption(option);
            if (option.getNotificationType().equals(NotificationTypeEnum.VIRUS.getValue())) {
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.VIRUS_DOMAIN, option.getNotificationChannel()));
            }
            if (option.getNotificationType().equals(NotificationTypeEnum.HOST_STATUS_CHANGED.getValue())) {
                final NotificationChannelEnum channel = option.getNotificationChannel();
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_CONNECTION_FAILED, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_DNS_ERROR, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_DISALLOW, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_WILL_CONNECTION_FAILED, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_WILL_DNS_ERROR, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_WILL_DISALLOW, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_WARN_CONNECTION_FAILED, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_WARN_DNS_ERROR, channel));
                result.addNotificationOption(new NotificationOptionInfo(NotificationTypeEnum.HOST_STATUS_WARN_DISALLOW, channel));
            }
        }

        return result;
    }

    public void updateUserNotificationOptions(final UserNotificationOptions options)
            throws UserException, InternalException {
        if (options == null) {
            return;
        }

        Set<NotificationOptionInfo> notificationOptions = options.getNotificationOptions();
        if ((notificationOptions == null) || (notificationOptions.size() == 0)) {
            // nothing to update
            return;
        }

        Set<NotificationOptionInfo> switchedOptions = getSwitchedRealAndDenial(notificationOptions);

        final BatchPreparedStatementSetter batchPreparedStatementSetter =
                new OptionsBatchPreparedStatementSetter(options.getUserId(), switchedOptions);

        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                getJdbcTemplate(WMCPartition.nullPartition()).update(
                        DELETE_ALL_NOTIFICATION_OPTIONS_FOR_USER_QUERY, options.getUserId()
                );

                getJdbcTemplate(WMCPartition.nullPartition()).getJdbcOperations().batchUpdate(
                        INSERT_NOTIFICATION_OPTIONS_FOR_USER_QUERY,
                        batchPreparedStatementSetter
                );
            }
        });

        log.debug("Options updated for user " + options.getUserId());
        updateOptionsUpdateTime(options.getUserId());
    }

    private void updateOptionsUpdateTime(Long userId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(
                UPDATE_OPTIONS_UPDATED_ON_QUERY,
                userId
        );
    }

    /**
     * For all default on options denies real options to store them in database.
     * For all default on options denies database options to return them for view as real.
     * Made for not storing default on data.
     * Example: SPAM on - not stored. SPAM off - stored. SEARCH_UPDATE on - stored. SEARCH_UPDATE off - not stored
     * Example: SPAM on - viewed off. SPAM off - viewed on. SEARCH_UPDATE on - viewed on. SEARCH_UPDATE off - viewed off
     * @param options options to switch
     * @return switched options
     */
    private Set<NotificationOptionInfo> getSwitchedRealAndDenial(Collection<NotificationOptionInfo> options) throws InternalException {
        Set<NotificationOptionInfo> result = new HashSet<NotificationOptionInfo>(options);
        Set<NotificationOptionInfo> toSwitch = new HashSet<NotificationOptionInfo>();

        for (NotificationOptionInfo defaultOption : DEFAULT_OPTIONS) {
            if (result.contains(defaultOption)) {
                toSwitch.add(defaultOption);
                result.remove(defaultOption);
            }
        }

        for (NotificationOptionInfo defaultOption : DEFAULT_OPTIONS) {
            if (!toSwitch.contains(defaultOption)) {
                result.add(defaultOption);
            }
        }

        Set<NotificationOptionInfo> foreignDefaults = new HashSet<NotificationOptionInfo>();
        List<Integer> foreignServices = foreignServicesService.getAllForeignServices();
        for (Integer serviceId : foreignServices) {
            foreignDefaults.add(new NotificationOptionInfo(serviceId, NotificationChannelEnum.MESSAGE));
        }

        for (NotificationOptionInfo defaultOption : foreignDefaults) {
            if (result.contains(defaultOption)) {
                toSwitch.add(defaultOption);
                result.remove(defaultOption);
            }
        }

        for (NotificationOptionInfo defaultOption : foreignDefaults) {
            if (!toSwitch.contains(defaultOption)) {
                result.add(defaultOption);
            }
        }

        return result;
    }

    @Required
    public void setForeignServicesService(ForeignServicesService foreignServicesService) {
        this.foreignServicesService = foreignServicesService;
    }
}
