package ru.yandex.wmconsole.servantlet.errors;

import java.util.ArrayList;
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 org.springframework.transaction.TransactionStatus;

import ru.yandex.common.framework.core.ServRequest;
import ru.yandex.common.framework.core.ServResponse;
import ru.yandex.wmconsole.data.SeverityEnum;
import ru.yandex.wmconsole.data.UserErrorOptions;
import ru.yandex.wmconsole.data.info.UserOptionsInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.error.ClientException;
import ru.yandex.wmconsole.error.ClientProblem;
import ru.yandex.wmconsole.servantlet.WMCAuthenticationServantlet;
import ru.yandex.wmconsole.service.ErrorInfoService;
import ru.yandex.wmconsole.service.UserOptionsService;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.error.UserProblem;
import ru.yandex.wmtools.common.util.ServiceTransactionCallbackWithoutResult;

public class ErrorOptionsChangeServantlet extends WMCAuthenticationServantlet {
    private static final Logger log = LoggerFactory.getLogger(ErrorOptionsChangeServantlet.class);

    private static final String PARAM_RESET = "reset";
    private static final String PARAM_HOST = "host";
    private static final String PARAM_GLOBAL_OPTIONS = "global-options";

    private ErrorInfoService errorInfoService;
    private UserOptionsService userOptionsService;

    @Override
    public void doProcess(final ServRequest req, final ServResponse res, final long userId) throws InternalException, UserException {

        errorInfoService.getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(
                new ServiceTransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws UserException, InternalException {
                        // Включили по умолчанию сохранение для глобальных настроек для обратной совместимости
                        boolean useGlobalErrorOptions = req.getParamAsBoolean(PARAM_GLOBAL_OPTIONS, true);
                        UserOptionsInfo userOptions = userOptionsService.getUserOptions(userId);
                        if (userOptions.isUseGlobalErrorOptions() != useGlobalErrorOptions) {
                            UserOptionsInfo newUserOptions = userOptions.replaceUseGlobalErrorOptions(useGlobalErrorOptions);
                            userOptionsService.updateUserOptions(newUserOptions);
                            if (useGlobalErrorOptions) {
                                // переключили с индивидуальных настроек на глобальные
                                errorInfoService.removeUserHostErrorOptions(userId);
                            } else {
                                // переключили с глобальных настроек на индивидуальные
                                // копировать глобальные настройки в настройки для подтвержденных хостов
                                errorInfoService.copyUserErrorOptions(userId);
                                errorInfoService.removeUserErrorOptions(userId);
                            }
                        }

                        Long hostId = getLongParam(req, PARAM_HOST);
                        if (!useGlobalErrorOptions && hostId == null) {
                            // нечего сохранять, так как хост не указан
                            return;
                        }

                        if (!useGlobalErrorOptions) {
                            // проверяем, что пользователь владеет хостом и что хост подтвержден
                            if (!getVerifyService().isHostVerifiedByUser(hostId, userId)) {
                                throw new ClientException(ClientProblem.HOST_NOT_VERIFIED_BY_USER,
                                        "Host is not verified by specified user");
                            }
                        }

                        String reset = req.getParam(PARAM_RESET, true);
                        if (reset != null) {
                            restoreDefaults(req, userId, hostId);
                        } else {
                            changeOptions(req, userId, hostId, useGlobalErrorOptions);
                        }
                    }
                }
        );
    }

    private void changeOptions(
            ServRequest req,
            long userId,
            Long host,
            boolean useGlobalOptions) throws InternalException, UserException {
        UserErrorOptions userErrorOptions = errorInfoService.getUserErrorOptions(userId, host, useGlobalOptions);

        UserErrorOptions oldUserErrorOptions;
        try {
            oldUserErrorOptions = userErrorOptions.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError("Clone not supported, while must be supported.");
        }

        parseParameters(req, userErrorOptions);
        UserErrorOptions diff = oldUserErrorOptions.whatsNew(userErrorOptions);
        errorInfoService.updateUserErrorOptions(diff, host, useGlobalOptions);
    }

    private void restoreDefaults(ServRequest req, long userId, Long hostId) throws InternalException {
        List<Integer> codes = parseResetParameters(req);
        errorInfoService.restoreDefaults(userId, hostId, codes);
    }

    /**
     * parsing params of such a kind: 404=err&401=ward&200=ok ....
     *
     * @param req              ...
     * @param userErrorOptions ...
     * @throws UserException
     *          ...
     */
    private void parseParameters(ServRequest req, UserErrorOptions userErrorOptions) throws UserException {
        //noinspection unchecked
        Map<String, String> params = req.getParams();

        for (Map.Entry<String, String> param : params.entrySet()) {
            Integer code;
            try {
                code = Integer.parseInt(param.getKey());
            } catch (NumberFormatException e) {
                continue;
            }

            SeverityEnum sev = SeverityEnum.R.valueOfOrNull(param.getValue());
            if (sev != null) {
                if (SeverityEnum.ERROR.equals(sev) ||
                        SeverityEnum.WARNING.equals(sev) ||
                        SeverityEnum.INFO.equals(sev)) {
                    throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE,
                            "Invalid value of param '" + code + "'.", code.toString(), param.getValue());
                }

                try {
                    userErrorOptions.addError(sev, code);
                } catch (IllegalArgumentException e) {
                    throw new UserException(UserProblem.SUCH_VALUE_NOT_ALLOWED_HERE, e.getMessage(), e, param.getKey(), param.getValue());
                }
                log.debug("Parameter: code = " + code + "; severity = " + sev);
            } else {
                throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "Invalid value of param '" + code + "'.", code.toString(), param.getValue());
            }
        }
    }

    private List<Integer> parseResetParameters(ServRequest req) {
        //noinspection unchecked
        Set<String> strCodes = req.getParams().keySet();

        List<Integer> codes = new ArrayList<Integer>();
        for (String strCode : strCodes) {
            try {
                codes.add(Integer.parseInt(strCode));
            } catch (NumberFormatException e) {
                //noinspection UnnecessaryContinue
                continue;
            }
        }
        return codes;
    }

    @Required
    public void setErrorInfoService(ErrorInfoService errorInfoService) {
        this.errorInfoService = errorInfoService;
    }

    @Required
    public void setUserOptionsService(UserOptionsService userOptionsService) {
        this.userOptionsService = userOptionsService;
    }
}
