package ru.yandex.wmconsole.servantlet.poll;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.common.framework.core.ServRequest;
import ru.yandex.common.framework.core.ServResponse;
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.servantlet.WMCAuthenticationServantlet;
import ru.yandex.wmconsole.service.PollService;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmtools.common.data.wrappers.StringWrapper;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;

/**
 * Сохранение ответа пользователя
 * <p/>
 * User: azakharov
 * Date: 14.03.12
 * Time: 14:24
 */
public class PollSaveServantlet extends WMCAuthenticationServantlet {
    private static final Logger log = LoggerFactory.getLogger(PollSaveServantlet.class);

    public static final String QUESTION_PREFIX = "question_";
    public static final String TEXT_ANSWER_PREFIX = "text_answer_";
    public static final String ANSWER_PREFIX = "answer_";

    private static final String PARAM_POLL = "poll_id";
    private static final String PARAM_DONT_SAVE = "dont_save";

    private static final String EXCEPTION_QUESTION_ID_PARAM = "question_id";
    private static final long MAX_TEXT_LENGTH = 1000;

    private PollService pollService;

    @Override
    protected void doProcess(ServRequest req, ServResponse res, long userId) throws UserException, InternalException {
        // debug info
        res.addData(new StringWrapper("request-params", req.getParams().toString()));

        // Проверяем пользователя
        checkUser(userId);

        final long pollId = getRequiredLongParam(req, PARAM_POLL);
        final PollInfo pollInfo = pollService.getPoll(pollId);
        if (pollInfo == null) {
            throw new UserException(WMCUserProblem.POLL_NOT_FOUND, "No such poll");
        }

        // Проверяем опрос
        checkPoll(pollInfo, userId);

        final List<QuestionInfo> questions = pollService.getQuestions(pollInfo);
        pollInfo.setQuestions(questions);

        saveAnswers(req, res, userId, pollId, questions, prepareTextParameters(req));
    }

    private void checkUser(long userId) throws InternalException, UserException {
        // проверяем, что пользователь существует
        assertUserExists(userId);
        // проверяем, что у него есть подтвержденные хосты
        if (getUsersHostsService().getAllHostsVerifiedByUser(userId).isEmpty()) {
            throw new UserException(WMCUserProblem.NO_VERIFIED_HOSTS, "User doesn't have verified hosts");
        }
    }

    private void checkPoll(final PollInfo pollInfo, Long userId) throws UserException, InternalException {
        // Проверяем, что опрос выполняется
        if (!PollStatusEnum.RUNNING.equals(pollInfo.getStatus())) {
            throw new UserException(WMCUserProblem.POLL_STATUS_ERROR, "Poll can not be used in status " + pollInfo.getStatus());
        }

        // проверяем, что пользователь еще не отвечал на данный опрос
        if (pollService.isAnswered(userId, pollInfo.getId())) {
            throw new UserException(WMCUserProblem.POLL_ALREADY_ANSWERED, "User has already been polled");
        }
    }

    private void saveAnswers(
            final ServRequest req,
            final ServResponse res,
            final long userId,
            final long pollId,
            final List<QuestionInfo> questions,
            final Map<Long, String> textAnswers) throws UserException, InternalException {
        final List<Answer> checkedAnswers = new LinkedList<Answer>();
        for (QuestionInfo q : questions) {
            final Map<AnswerOptionInfo, Answer> foundAnswers = new LinkedHashMap<AnswerOptionInfo, Answer>();
            // Получаем все имеющиеся ответы на данный вопрос
            final Set<Long> answers = getChoiseAnswers(req, QUESTION_PREFIX + q.getId(), ANSWER_PREFIX);
            // Проверяем по всем имеющимся вариантам ответов
            for (AnswerOptionInfo option : q.getAnswerOptions()) {
                String textAnswer = textAnswers.get(option.getId());

                final boolean isText = AnswerTypeEnum.TEXT.equals(q.getType());

                // текст для вопросов типа TEXT является основным ответом, а для других - вспомогательной информацией
                if ( (isText && textAnswer != null) ||
                        answers.contains(option.getId())) {
                    final Answer answer = new Answer();
                    answer.setQuestionId(q.getId());
                    answer.setOptionId(option.getId());
                    answer.setUserId(userId);
                    answer.setText(textAnswer);
                    foundAnswers.put(option, answer);
                }
            }
            checkQuestion(q, foundAnswers);
            checkedAnswers.addAll(foundAnswers.values());
        }
        if (!checkedAnswers.isEmpty()) {
            final Boolean dontSave = getBooleanParam(req, PARAM_DONT_SAVE);
            boolean save = dontSave != null ? !dontSave : true;
            if (save) {
                // Сохраняем ответы в базе
                pollService.persistAnswers(checkedAnswers, userId, pollId);
            } else {
                // Выводим ответы в лог и в выдачу
                dumpAnswer(req, res, questions, checkedAnswers);
            }
        }
    }

    private void dumpAnswer(ServRequest req, ServResponse res, List<QuestionInfo> questions, List<Answer> answers) throws InternalException {
        for (Answer a : answers) {
            QuestionInfo questionInfo = null;
            for (QuestionInfo q : questions) {
                if (q.getId().equals(a.getQuestionId())) {
                    questionInfo = q;
                }
            }
            AnswerOptionInfo optionInfo = null;
            for (AnswerOptionInfo o : pollService.getAnswerOptions(questionInfo)) {
                if (o.getId().equals(a.getOptionId())) {
                    optionInfo = o;
                }
            }
            final StringBuilder sb = new StringBuilder();
            sb.append(" question_id=");
            sb.append(a.getQuestionId());
            sb.append(" option_id=");
            sb.append(a.getOptionId());
            if (questionInfo != null && optionInfo != null) {
                sb.append(" question=");
                sb.append(questionInfo.getText());
                sb.append(" option=");
                sb.append(optionInfo.getText());
            }
            if (a.getText() != null) {
                sb.append(" text=");
                sb.append(a.getText());
            }
            log.debug(sb.toString());
        }
    }

    /**
     * Достает из мультипараметров значения вариантов ответов для вопросов с выбором
     *
     * @param req          объект запроса
     * @param key          имя параметра запроса
     * @param answerPrefix префикс ответа
     * @return множество идентификаторов, разобранных по шаблону [answerPrefix][id]
     */
    private Set<Long> getChoiseAnswers(final ServRequest req, final String key, final String answerPrefix) {
        final List<String> rawAnswers = req.getMultiParams(key);
        final Set<Long> result = new LinkedHashSet<Long>();
        for (String a : rawAnswers) {
            Long id = getNumericSuffix(a, answerPrefix);
            if (id != null) {
                result.add(id);
            }
        }
        return result;
    }

    private void checkQuestion(QuestionInfo q, Map<AnswerOptionInfo, Answer> foundAnswers) throws UserException, InternalException {
        if (q.isRequired() && foundAnswers.size() == 0) {
            throw new UserException(WMCUserProblem.POLL_QUESTION_REQUIRED,
                    "question " + q.getText() + " is required",
                    EXCEPTION_QUESTION_ID_PARAM,
                    q.getId().toString());
            // TODO: описать в документации про параметр question
        }

        final boolean isText = AnswerTypeEnum.TEXT.equals(q.getType());

        if (isText || AnswerTypeEnum.SINGLE.equals(q.getType())) {
            if (foundAnswers.size() > 1) {
                throw new UserException(WMCUserProblem.POLL_TOO_MANY_ANSWERS,
                        "Too many answers for " + q.getType() + " question " + q.getText(),
                        EXCEPTION_QUESTION_ID_PARAM, q.getId().toString());
            }
        }

        for (Map.Entry<AnswerOptionInfo, Answer> entry : foundAnswers.entrySet()) {
            AnswerOptionInfo optionInfo = entry.getKey();
            Answer a = entry.getValue();
            // Не учитываем текстовые результаты для вариантов ответа, которые не подразумевают текст
            if (!optionInfo.isTextExpected()) {
                a.setText(null);
            }
            // если обязательный вопрос типа текст и текста нет, то ругаемся, что текста нет
            if (isText && q.isRequired() && a.getText() == null) {
                throw new UserException(
                        WMCUserProblem.POLL_QUESTION_REQUIRED,
                        "user text expected for question " + q.getText() + " but not found",
                        EXCEPTION_QUESTION_ID_PARAM, q.getId().toString());
            }

            if (optionInfo.isTextExpected() && a.getText() == null) {
                throw new UserException(
                        WMCUserProblem.POLL_TEXT_EXPECTED,
                        "user text expected for question " + q.getText() + " but not found",
                        EXCEPTION_QUESTION_ID_PARAM, q.getId().toString());
            }

            // Если ввод пользователя допустим и размер текста превышает предел, выдаем ошибку
            if (optionInfo.isTextExpected() && a.getText() != null && a.getText().length() > MAX_TEXT_LENGTH) {
                throw new UserException(
                        WMCUserProblem.POLL_TEXT_LIMIT_EXCEEDED, "Text answer length of question " +
                        q.getText() + "exceeds 1000 characters",
                        EXCEPTION_QUESTION_ID_PARAM, q.getId().toString());
            }
        }
    }

    /**
     * Достает из параметров текстовые ответы и формирует Map id варианта ответа -> текст ответа
     *
     * @param req объект запроса
     * @return Map id варианта ответа -> текст ответа
     */
    private Map<Long, String> prepareTextParameters(ServRequest req) {
        final Map<Long, String> m = new LinkedHashMap<Long, String>();
        @SuppressWarnings("unchecked")
        final Map<String, String> params = req.getParams();
        for (Map.Entry<String, String> e : params.entrySet()) {
            final String param = e.getKey();
            String value = e.getValue();
            // Пустая строка считается отсутствием ответа
            if (value != null && value.isEmpty()) {
                value = null;
            }
            final Long id = getNumericSuffix(param, TEXT_ANSWER_PREFIX);
            if (id != null) {
                m.put(id, value);
            }
        }
        return m;
    }

    /**
     * Получить числовой суффикс параметра типа [text][num]
     *
     * @param param  полная строка параметра из префикса и суффикса
     * @param prefix текстовый префикс
     * @return числовой суффикс параметра
     */
    private Long getNumericSuffix(String param, String prefix) {
        if (param.startsWith(prefix)) {
            String idVal = param.substring(
                    prefix.length());
            try {
                return Long.parseLong(idVal);
            } catch (NumberFormatException nfe) {
                return null;
            }
        }
        return null;
    }

    @Required
    public void setPollService(PollService pollService) {
        this.pollService = pollService;
    }
}
