package ru.yandex.wmconsole.service;

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

import org.jetbrains.annotations.Nullable;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.transaction.TransactionStatus;

import ru.yandex.common.framework.pager.Pager;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.data.poll.Answer;
import ru.yandex.wmconsole.data.poll.AnswerOptionInfo;
import ru.yandex.wmconsole.data.poll.AnswerTypeEnum;
import ru.yandex.wmconsole.data.poll.PollInfo;
import ru.yandex.wmconsole.data.poll.PollStatusEnum;
import ru.yandex.wmconsole.data.poll.QuestionInfo;
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.ServiceTransactionCallback;
import ru.yandex.wmtools.common.util.ServiceTransactionCallbackWithoutResult;

/**
 * Сервис опросов
 *
 * User: azakharov
 * Date: 14.03.12
 * Time: 14:28
 */
public class PollService extends AbstractDbService {
    private static final String FIELD_POLL_ID = "poll_id";
    private static final String FIELD_TITLE = "title";
    private static final String FIELD_DESCRIPTION = "description";
    private static final String FIELD_STATUS_ID = "status_id";

    private static final String FIELD_QUESTION_ID = "question_id";
    private static final String FIELD_TEXT = "text";
    private static final String FIELD_TYPE_ID = "type_id";
    private static final String FIELD_REQUIRED = "required";
    private static final String FIELD_NUMBER = "num";

    private static final String FIELD_OPTION_ID = "option_id";
    private static final String FIELD_TEXT_EXPECTED = "text_expected";
    private static final String FIELD_SCORE = "score";

    private static final String SELECT_POLL_BY_ID_QUERY =
            "SELECT " +
                    "   poll_id AS " + FIELD_POLL_ID + ", " +
                    "   title AS " + FIELD_TITLE + ", " +
                    "   description AS " + FIELD_DESCRIPTION + ", " +
                    "   status_id AS " + FIELD_STATUS_ID + " " +
                    "FROM tbl_poll " +
                    "WHERE poll_id = ?";

    private static final String INSERT_POLL_QUERY =
            "INSERT INTO tbl_poll (title, description, status_id, created_by_user_id, created_on) values (?, ?, ?, ?, NOW())";

    private static final String SELECT_POLL_COUNT_QUERY =
            "SELECT COUNT(*) FROM tbl_poll ";

    private static final String SELECT_POLL_LIST_QUERY =
            "SELECT " +
                    "   poll_id AS " + FIELD_POLL_ID + ", " +
                    "   title AS " + FIELD_TITLE + ", " +
                    "   description AS " + FIELD_DESCRIPTION + ", " +
                    "   status_id AS " + FIELD_STATUS_ID + " " +
                    "FROM tbl_poll " +
                    "ORDER BY status_id " +
                    "%1$s";

    private static final String SELECT_QUESTIONS_BY_POLL_ID_QUERY =
            "SELECT " +
                    "   question_id AS " + FIELD_QUESTION_ID + ", " +
                    "   poll_id AS " + FIELD_POLL_ID + ", " +
                    "   type_id AS " + FIELD_TYPE_ID + ", " +
                    "   text AS " + FIELD_TEXT + ", " +
                    "   description AS " + FIELD_DESCRIPTION + ", " +
                    "   required AS " + FIELD_REQUIRED + ", " +
                    "   num AS " + FIELD_NUMBER + " " +
                    "FROM tbl_question " +
                    "WHERE poll_id = ? " +
                    "ORDER BY num ASC";

    private static final String SELECT_ANSWER_OPTIONS_BY_QUESTION_ID_QUERY =
            "SELECT " +
                    "   option_id, " +
                    "   question_id, " +
                    "   text, " +
                    "   description AS " + FIELD_DESCRIPTION + ", " +
                    "   text_expected, " +
                    "   num, " +
                    "   score " +
                    "FROM tbl_answer_option " +
                    "WHERE question_id = ? " +
                    "ORDER BY num";

    private static final String SELECT_CHECK_ALREADY_POLLED_QUERY =
            "SELECT COUNT(*) " +
                    "FROM tbl_poll p " +
                    "JOIN tbl_question q ON q.poll_id = p.poll_id " +
                    "JOIN tbl_answer a ON a.question_id = q.question_id " +
                    "WHERE p.poll_id = ? AND a.user_id = ?";

    private static final String INSERT_ANSWERS_QUERY =
            "INSERT INTO tbl_answer ( " +
                    "   option_id, " +
                    "   question_id, " +
                    "   user_id," +
                    "   text ) " +
                    "VALUES ( ?, ?, ?, ?);";

    private static final String DELETE_POLL_QUERY =
            "DELETE FROM tbl_poll WHERE poll_id = ?";

    private static final String UPDATE_POLL_QUERY =
            "UPDATE tbl_poll " +
                    "SET title = ?, description = ?, status_id = ?, created_by_user_id = ?, created_on = NOW() " +
                    "WHERE poll_id=?";

    private static final String INSERT_QUESTION_QUERY =
            "INSERT INTO tbl_question (" +
                    "   poll_id, " +
                    "   type_id, " +
                    "   text, " +
                    "   description, " +
                    "   required, " +
                    "   num ) "+
                    "VALUES (?, ?, ?, ?, ?, ?)";

    private static final String SELECT_QUESTIONS_COUNT_BY_POLL_ID_AND_POSITION_QUERY =
            "SELECT COUNT(*) FROM tbl_question WHERE poll_id = ? AND num = ?";

    private static final String UPDATE_SHIFT_QUESTION_POSITION_QUERY =
            "UPDATE tbl_question " +
                    "SET num = num + 1 " +
                    "WHERE poll_id = ? AND num >= ? ";

    private static final String UPDATE_QUESTION_QUERY =
            "UPDATE tbl_question " +
                    "SET type_id = ?, text = ?, description = ?, required = ?, num = ? " +
                    "WHERE poll_id = ? AND question_id = ?";

    private static final String UPDATE_MODIFIED_USER_AND_TIME_QUERY =
            "UPDATE tbl_poll "+
                    "SET modified_on = NOW(), modified_by_user_id = ? " +
                    "WHERE poll_id = ?";

    private static final String SELECT_LAST_INSERTED_ID_QUERY =
            "SELECT last_insert_id()";

    private static final String DELETE_QUESTION_QUERY =
            "DELETE FROM tbl_question "+
                    "WHERE poll_id = ? AND question_id = ?";

    private static final String DELETE_OPTIONS_QUERY =
            "DELETE FROM tbl_answer_option WHERE question_id = ?";

    private static final String INSERT_ANSWER_OPTION_QUERY =
            "INSERT INTO tbl_answer_option "+
                    "(question_id, text, description, num, score, text_expected) " +
                    "VALUES (?, ?, ?, ?, ?, ?)";

    private static final String SELECT_ANSWER_OPTIONS_COUNT_BY_QUESTION_ID_AND_NUMBER_QUERY =
            "SELECT COUNT(*) FROM tbl_answer_option WHERE question_id = ? AND num = ?";

    private static final String UPDATE_SHIFT_ANSWER_OPTION_POSITION_QUERY =
            "UPDATE tbl_answer_option " +
                    "SET num = num + 1 " +
                    "WHERE question_id = ? AND num >= ?";

    private static final String UPDATE_MOVE_ANSWER_OPTION_POSITION_QUERY =
            "UPDATE tbl_answer_option " +
                    "SET num = num %1$s 1 " +
                    "WHERE question_id = ? AND num %2$s ? AND num %3$s ?";

    private static final String DELETE_ANSWER_OPTION_QUERY =
            "DELETE FROM tbl_answer_option WHERE option_id = ?";

    private static final String SELECT_ANSWER_OPTION_BY_ID_QUERY =
            "SELECT " +
                    "   option_id, " +
                    "   question_id, " +
                    "   text, " +
                    "   description AS " + FIELD_DESCRIPTION + ", " +
                    "   text_expected, " +
                    "   num, " +
                    "   score " +
                    "FROM " +
                    "   tbl_answer_option " +
                    "WHERE " +
                    "   option_id = ?";

    private static final String UPDATE_ANSWER_OPTION_QUERY =
            "UPDATE tbl_answer_option " +
                    "SET text = ?, description = ?, score = ?, num = ?, text_expected = ? " +
                    "WHERE option_id = ?";


    public PollInfo getPoll(final long pollId) throws InternalException {
        final List<PollInfo> res = getJdbcTemplate(WMCPartition.nullPartition()).query(
                SELECT_POLL_BY_ID_QUERY,
                pollRowMapper, pollId);
        if (res.size() >= 1) {
            return res.get(0);
        } else {
            return null;
        }
    }

    public List<QuestionInfo> getQuestions(final PollInfo pollInfo) throws InternalException {
        final List<QuestionInfo> questions =  getJdbcTemplate(WMCPartition.nullPartition()).query(
                SELECT_QUESTIONS_BY_POLL_ID_QUERY,
                 questionRowMapper, pollInfo.getId());
        for (QuestionInfo qi : questions) {
            // Достаем из базы варианты ответа и добавляем их в вопросы
            qi.setAnswerOptions(getAnswerOptions(qi));
        }
        return questions;
    }

    public List<AnswerOptionInfo> getAnswerOptions(final QuestionInfo questionInfo) throws InternalException {
        return getJdbcTemplate(WMCPartition.nullPartition()).query(
                SELECT_ANSWER_OPTIONS_BY_QUESTION_ID_QUERY,
                optionRowMapper, questionInfo.getId());
    }


    public boolean isAnswered(long userId, long pollId) throws InternalException {
        Long count = getJdbcTemplate(WMCPartition.nullPartition()).queryForLong(
                SELECT_CHECK_ALREADY_POLLED_QUERY,
               pollId,
               userId
        );
        return  (count != null) ? (count > 0) : false;
    }

    /**
     * Сохраняет все ответы пользователя в рамках одной транзакции
     * @param answers ответы пользователя
     * @param userId  идентификатор пользователя
     * @param pollId  идентификатор опроса
     */
    public void persistAnswers(final List<Answer> answers, final long userId, final long pollId) throws UserException, InternalException {
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws UserException, InternalException {
                // Проверяем, что пользователь еще не отвечал на данный опрос
                if (isAnswered(userId, pollId)) {
                    throw new UserException(WMCUserProblem.POLL_ALREADY_ANSWERED, "User has already been polled");
                }

                // Сохраняем опрос
                for (Answer a : answers) {
                    int ret = getJdbcTemplate(WMCPartition.nullPartition()).update(
                            INSERT_ANSWERS_QUERY,
                            a.getOptionId(),
                            a.getQuestionId(),
                            a.getUserId(),
                            a.getText()
                    );

                    if (ret == 0) {
                        throw new RuntimeException(
                                "No rows updated while adding poll answers to db. question_id = " + a.getQuestionId() +
                                        " option_id = " + a.getOptionId() + " user_id = " + a.getUserId());
                    }
                }
            }
        });
    }

    /**
     * Получить список опросов
     * @param pager     разделитель по страницам
     * @return          список опросов
     * @throws InternalException при ошибках работы с БД
     */
    public List<PollInfo> getPollList(final Pager pager) throws InternalException {
        final List<PollInfo> res = getJdbcTemplate(WMCPartition.nullPartition()).pageableSelect(
                SELECT_POLL_COUNT_QUERY,
                SELECT_POLL_LIST_QUERY,
                pollRowMapper,
                pager);
        return res;
    }

    /**
     * Добавление опроса
     * @param title         заголовок опроса
     * @param description   описание опроса
     * @param userId        идентификатор пользователя, добавляющего опрос
     * @return              информация о вновь добавленном опросе
     * @throws UserException        при ошибках БД
     * @throws InternalException    при ошибках БД
     */
    public PollInfo addPoll(final String title, final String description, final long userId) throws UserException, InternalException {
        return (PollInfo)getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallback() {
            @Override
            public PollInfo doInTransaction(TransactionStatus transactionStatus) throws UserException, InternalException {
                getJdbcTemplate(WMCPartition.nullPartition()).update(
                        INSERT_POLL_QUERY,
                        title,
                        description,
                        PollStatusEnum.NEW.getId(),
                        userId);
                final Long pollId = getJdbcTemplate(WMCPartition.nullPartition()).queryForLong(
                        SELECT_LAST_INSERTED_ID_QUERY);
                return getPoll(pollId);
            }
        });
    }

    /**
     * Удалить опрос
     * @param pollId    идентификатор опроса
     * @throws InternalException при ошибках работы с БД
     */
    public void deletePoll(final long pollId) throws InternalException, UserException {
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws UserException, InternalException {
                final PollInfo poll = getPoll(pollId);
                if (poll == null) {
                    throw new UserException(WMCUserProblem.POLL_NOT_FOUND, "No such poll");
                }

                if (poll.getStatus() == PollStatusEnum.RUNNING) {
                    throw new UserException(WMCUserProblem.POLL_STATUS_ERROR,
                            "Can't delete poll because poll status is " + PollStatusEnum.RUNNING);
                }

                // Из других таблиц всё удаляется благодаря триггеру ON DELETE CASCADE
                getJdbcTemplate(WMCPartition.nullPartition()).update(DELETE_POLL_QUERY, pollId);
            }
        });
    }

    public void persistPoll(final PollInfo poll, final long userId) throws InternalException, UserException {
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws UserException, InternalException {
                // Проверяем, что опрос еще не выполняется
                PollInfo oldPoll = getPoll(poll.getId());
                if (oldPoll.getStatus() == PollStatusEnum.RUNNING) {
                    throw new UserException(WMCUserProblem.POLL_STATUS_ERROR, "Running poll can't be changed");
                }
                // Сохраняем изменения в опросе
                getJdbcTemplate(WMCPartition.nullPartition()).update(
                        UPDATE_POLL_QUERY,
                        poll.getTitle(),
                        poll.getDescription(),
                        poll.getStatus().getId(),
                        userId,
                        poll.getId());
            }
        });
    }

    /**
     * Добавление вопроса к опросу
     *
     * @param pollId       идентификатор опроса
     * @param typeId       идентификатор типа вопроса
     * @param text         текст вопроса
     * @param description  пояснение
     * @param required     признак обязательности вопроса
     * @param position     порядковый номер вопроса
     * @param userId       идентификатор пользователя
     *
     * при совпадении приводит к смещению существующих вопросов вниз (увеличению их номеров на 1)
     *
     * @return идентификатор вопроса
     * @throws UserException        когда не найден опрос (POLL_NOT_FOUND) или опрос запущен (POLL_STATUS_ERROR)
     * @throws InternalException    при ошибках записи в БД
     */
    public Long addQuestion(final long pollId,
                             final int typeId,
                             final String text,
                             @Nullable final String description,
                             final boolean required,
                             @Nullable final Integer position,
                             final long userId) throws UserException, InternalException {

        return (Long)getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallback() {
            @Override
            public Long doInTransaction(TransactionStatus transactionStatus) throws UserException, InternalException {
                // Проверяем, что опрос еще не выполняется
                PollInfo poll = getPoll(pollId);
                if (poll == null) {
                    throw new UserException(WMCUserProblem.POLL_NOT_FOUND, "No such poll");
                }
                if (poll.getStatus() == PollStatusEnum.RUNNING) {
                    throw new UserException(WMCUserProblem.POLL_STATUS_ERROR, "Running poll can't be changed");
                }

                int num = position != null ? position : 1;

                // Проверяем наличи вопросов с таким же номером и смещаем номера на 1, если есть совпадение
                int count = getJdbcTemplate(WMCPartition.nullPartition()).safeQueryForInt(
                        SELECT_QUESTIONS_COUNT_BY_POLL_ID_AND_POSITION_QUERY, pollId, num);
                if (count > 0) {
                    getJdbcTemplate(WMCPartition.nullPartition()).update(
                            UPDATE_SHIFT_QUESTION_POSITION_QUERY, pollId, num);
                }

                // Добавляем вопрос
                getJdbcTemplate(WMCPartition.nullPartition()).update(
                        INSERT_QUESTION_QUERY,
                        pollId, typeId, text, description, required, num);
                final Long questionId = getJdbcTemplate(WMCPartition.nullPartition()).queryForLong(
                        SELECT_LAST_INSERTED_ID_QUERY);
                addDefaultAnswerOption(questionId, AnswerTypeEnum.R.fromValueOrNull(typeId));

                // Обновляем время последнего изменения опроса и записываем id изменившего пользователя
                updateModifiedUserAndTime(userId, pollId);

                return questionId;
            }
        });
    }

    /**
     * Удалить вопрос из опроса
     *
     * @param userId        идентификатор пользователя
     * @param pollId        идентификатор опроса
     * @param questionId    идентификатор вопроса
     * @throws InternalException
     * @throws UserException в случае опрос не найден (POLL_NOT_FOUND), опрос запущен (POLL_STATUS_ERROR),
     *         вопрос не найден (POLL_QUESTION_NOT_FOUND)
     */
    public void deleteQuestion(final long userId, final long pollId, final long questionId) throws InternalException, UserException {
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws UserException, InternalException {
                // Проверяем, что опрос еще не выполняется
                PollInfo poll = getPoll(pollId);
                if (poll == null) {
                    throw new UserException(WMCUserProblem.POLL_NOT_FOUND, "No such poll");
                }
                if (poll.getStatus() == PollStatusEnum.RUNNING) {
                    throw new UserException(WMCUserProblem.POLL_STATUS_ERROR, "Running poll can't be changed");
                }

                // Проверяем, что в опросе есть такой вопрос
                List<QuestionInfo> questions = getQuestions(poll);
                boolean found = false;
                for (QuestionInfo q : questions) {
                    if (questionId == q.getId()) {
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    throw new UserException(WMCUserProblem.POLL_QUESTION_NOT_FOUND, "Question not found");
                }

                // Удаляем вопрос
                getJdbcTemplate(WMCPartition.nullPartition()).update(DELETE_QUESTION_QUERY, pollId, questionId);
                // Обновляем дату модификации опроса
                getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_MODIFIED_USER_AND_TIME_QUERY, userId, pollId);
            }
        });
    }

    public void persistQuestion(final QuestionInfo questionInfo, final long userId) throws UserException, InternalException {
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws UserException, InternalException {
                long questionId = questionInfo.getId();
                long pollId = questionInfo.getPollId();

                // Проверяем, что опрос еще не выполняется
                PollInfo poll = getPoll(pollId);
                if (poll == null) {
                    throw new UserException(WMCUserProblem.POLL_NOT_FOUND, "No such poll");
                }
                if (poll.getStatus() == PollStatusEnum.RUNNING) {
                    throw new UserException(WMCUserProblem.POLL_STATUS_ERROR, "Running poll can't be changed");
                }

                // Проверяем, что в опросе есть такой вопрос
                List<QuestionInfo> questions = getQuestions(poll);
                boolean found = false;
                QuestionInfo oldQuestionInfo = questionInfo;
                for (QuestionInfo q : questions) {
                    if (questionId == q.getId()) {
                        found = true;
                        oldQuestionInfo = q;
                        break;
                    }
                }
                if (!found) {
                    throw new UserException(WMCUserProblem.POLL_QUESTION_NOT_FOUND, "Question not found");
                }

                if (!oldQuestionInfo.getType().equals(questionInfo.getType())) {
                    // у старого и нового вопроса отличаются типы

                    if (!AnswerTypeEnum.TEXT.equals(oldQuestionInfo.getType()) &&
                            AnswerTypeEnum.TEXT.equals(questionInfo.getType())) {
                        // удалить все варианты
                        getJdbcTemplate(WMCPartition.nullPartition()).update(
                                DELETE_OPTIONS_QUERY, questionInfo.getId());
                        // создать один вариант типа текст
                        addDefaultAnswerOption(questionInfo.getId(), questionInfo.getType());
                    }
                }

                // Сохраняем изменения в опросе
                getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_QUESTION_QUERY,
                        questionInfo.getType().getId(),
                        questionInfo.getText(),
                        questionInfo.getDescription(),
                        questionInfo.isRequired(),
                        questionInfo.getNumber(),
                        questionInfo.getPollId(),
                        questionInfo.getId());

                // Обновляем время модификации и пользователя
                updateModifiedUserAndTime(userId, questionInfo.getPollId());
            }
        });
    }

    /**
     * Добавляет вариант ответа по умолчанию для вопроса и типа
     *
     * @param questionId    идентификатор вопроса
     * @param type          тип вопроса
     * @throws InternalException
     */
    private void addDefaultAnswerOption(long questionId, AnswerTypeEnum type) throws InternalException {
        AnswerOptionInfo answerOptionInfo = new AnswerOptionInfo(null,
                questionId,"<текст варианта ответа>","<описание варианта ответа>",
                1, null, AnswerTypeEnum.TEXT.equals(type));

        addAnswerOption(answerOptionInfo);
    }

    /**
     * Добавляет вариант ответа. Если номер совпадает с другим вариантом ответа, то номер сдвигаются
     *
     * @param answerOptionInfo
     * @throws InternalException
     */
    public void addAnswerOption(AnswerOptionInfo answerOptionInfo) throws InternalException {
        // проверить, нет ли вариантов ответа с таким номером. если есть, то сдвинуть вниз
        Integer count = getJdbcTemplate(WMCPartition.nullPartition()).queryForInt(
                SELECT_ANSWER_OPTIONS_COUNT_BY_QUESTION_ID_AND_NUMBER_QUERY,
                answerOptionInfo.getQuestionId(), answerOptionInfo.getNumber());
        if (count > 0) {
             getJdbcTemplate(WMCPartition.nullPartition()).update(
                     UPDATE_SHIFT_ANSWER_OPTION_POSITION_QUERY,
                     answerOptionInfo.getQuestionId(), answerOptionInfo.getNumber());
        }

        getJdbcTemplate(WMCPartition.nullPartition()).update(
                INSERT_ANSWER_OPTION_QUERY,
                answerOptionInfo.getQuestionId(),
                answerOptionInfo.getText(),
                answerOptionInfo.getDescription(),
                answerOptionInfo.getNumber(),
                answerOptionInfo.getScore(),
                answerOptionInfo.isTextExpected());
    }

    /**
     * Удаляет вариант ответа
     *
     * @param optionId
     * @throws InternalException
     */
    public void deleteAnswerOption(long optionId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(DELETE_ANSWER_OPTION_QUERY, optionId);
    }

    public void editAnswerOption(AnswerOptionInfo oldOptionInfo, AnswerOptionInfo newOptionInfo) throws InternalException {
        if (oldOptionInfo.getNumber() != newOptionInfo.getNumber()) {
            Integer count = getJdbcTemplate(WMCPartition.nullPartition()).queryForInt(
                    SELECT_ANSWER_OPTIONS_COUNT_BY_QUESTION_ID_AND_NUMBER_QUERY,
                    newOptionInfo.getQuestionId(),
                    newOptionInfo.getNumber());
            if (count > 0) {
                final String query;
                if (newOptionInfo.getNumber() > oldOptionInfo.getNumber()) {
                    query = String.format(UPDATE_MOVE_ANSWER_OPTION_POSITION_QUERY, "+", ">=", "<");
                } else {
                    query = String.format(UPDATE_MOVE_ANSWER_OPTION_POSITION_QUERY, "-", ">", "<=");
                }
                int lowNumber = Math.min(newOptionInfo.getNumber(), oldOptionInfo.getNumber());
                int highNumber = Math.max(newOptionInfo.getNumber(), oldOptionInfo.getNumber());
                getJdbcTemplate(WMCPartition.nullPartition()).update(
                        query, newOptionInfo.getQuestionId(),
                        lowNumber,
                        highNumber);
            }
        }
        getJdbcTemplate(WMCPartition.nullPartition()).update(
                UPDATE_ANSWER_OPTION_QUERY,
                newOptionInfo.getText(),
                newOptionInfo.getDescription(),
                newOptionInfo.getScore(),
                newOptionInfo.getNumber(),
                newOptionInfo.isTextExpected(),
                newOptionInfo.getId());
    }

    /**
     * Получить вариант ответа по идентификатору
     *
     * @param optionId      идентификатор варианта ответа
     * @return              вариант ответа или null
     * @throws InternalException
     */
    public @Nullable AnswerOptionInfo getAnswerOptionById(long optionId) throws  InternalException {
        List<AnswerOptionInfo> options = getJdbcTemplate(WMCPartition.nullPartition()).query(
                SELECT_ANSWER_OPTION_BY_ID_QUERY, optionRowMapper, optionId);
        if (options.isEmpty()) {
            return null;
        } else {
            return options.get(0);
        }
    }

    /**
     * Обновляем время последнего изменения опроса и записываем id изменившего пользователя
     */
    private void updateModifiedUserAndTime(long userId, long pollId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_MODIFIED_USER_AND_TIME_QUERY, userId, pollId);
    }

    private static final ParameterizedRowMapper<PollInfo> pollRowMapper = new ParameterizedRowMapper<PollInfo>() {
        @Override
        public PollInfo mapRow(ResultSet resultSet, int i) throws SQLException {
            final int statusId = resultSet.getInt(FIELD_STATUS_ID);
            final PollStatusEnum status = resultSet.wasNull() ? null : PollStatusEnum.R.fromValueOrNull(statusId);
            return new PollInfo(
                    resultSet.getLong(FIELD_POLL_ID),
                    resultSet.getString(FIELD_TITLE),
                    resultSet.getString(FIELD_DESCRIPTION),
                    status
            );
        }
    };

    private static final ParameterizedRowMapper<QuestionInfo> questionRowMapper = new ParameterizedRowMapper<QuestionInfo>() {
        @Override
        public QuestionInfo mapRow(ResultSet resultSet, int i) throws SQLException {
            final int answerTypeId = resultSet.getInt(FIELD_TYPE_ID);
            final AnswerTypeEnum type = resultSet.wasNull() ? null : AnswerTypeEnum.R.fromValueOrNull(answerTypeId);
            return new QuestionInfo(
                    resultSet.getLong(FIELD_QUESTION_ID),
                    resultSet.getLong(FIELD_POLL_ID),
                    type,
                    resultSet.getString(FIELD_TEXT),
                    resultSet.getString(FIELD_DESCRIPTION),
                    resultSet.getBoolean(FIELD_REQUIRED),
                    resultSet.getInt(FIELD_NUMBER)
            );
        }
    };

    private static final ParameterizedRowMapper<AnswerOptionInfo> optionRowMapper = new ParameterizedRowMapper<AnswerOptionInfo>() {
        @Override
        public AnswerOptionInfo mapRow(ResultSet resultSet, int i) throws SQLException {
            return new AnswerOptionInfo(
                    resultSet.getLong(FIELD_OPTION_ID),
                    resultSet.getLong(FIELD_QUESTION_ID),
                    resultSet.getString(FIELD_TEXT),
                    resultSet.getString(FIELD_DESCRIPTION),
                    resultSet.getInt(FIELD_NUMBER),
                    resultSet.getInt(FIELD_SCORE),
                    resultSet.getBoolean(FIELD_TEXT_EXPECTED)
            );
        }
    };
}
