package ru.yandex.wmconsole.service;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.transaction.TransactionStatus;

import ru.yandex.common.framework.pager.Pager;
import ru.yandex.wmconsole.data.LanguageEnum;
import ru.yandex.wmconsole.data.NotificationTypeEnum;
import ru.yandex.wmconsole.data.WMCHistoryActionEnum;
import ru.yandex.wmconsole.data.WMCHistoryObjectTypeEnum;
import ru.yandex.wmconsole.data.info.DraftMessageInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmtools.common.data.HistoryActionEnum;
import ru.yandex.wmtools.common.data.IHistoryAction;
import ru.yandex.wmtools.common.data.info.WMUserInfo;
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.service.AbstractDbService;
import ru.yandex.wmtools.common.service.HistoryService;
import ru.yandex.wmtools.common.util.ServiceTransactionCallback;
import ru.yandex.wmtools.common.util.ServiceTransactionCallbackWithoutResult;
import ru.yandex.wmtools.common.util.SqlUtil;

/**
 * Created by IntelliJ IDEA.
 * User: senin
 * Date: 20.03.2007
 * Time: 10:09:24
 */
public abstract class AbstractDraftMessagesService<MessageInfo extends DraftMessageInfo> extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(AbstractDraftMessagesService.class);

    private static final String FIELD_MESSAGE_ID = "message_id";
    private static final String FIELD_HEADER = "header";
    private static final String FIELD_CONTENT = "content";
    private static final String FIELD_LAST_EDITOR = "last_editor";
    private static final String FIELD_STATE = "state";
    private static final String FIELD_TYPE = "type";
    private static final String FIELD_LAST_ACCESS = "last_access";
    private static final String FIELD_LANGUAGE = "lang";

    private static final String UPDATE_DRAFT_MESSAGE_QUERY =
            "UPDATE " +
                    "    tbl_draft_messages " +
                    "SET " +
                    "    last_editor = ?, " +
                    "    last_access = NOW(), " +
                    "    state = ?, " +
                    "    header = ?, " +
                    "    content = ?, " +
                    "    lang = ? " +
                    "WHERE " +
                    "    message_id = ? ";

    private static final String UPDATE_DRAFT_MESSAGE_REMOVE_STATE_QUERY =
            "UPDATE " +
                    "    tbl_draft_messages " +
                    "SET " +
                    "    last_editor = ?, " +
                    "    last_access = NOW(), " +
                    "    state = " + HistoryActionEnum.REMOVE.getValue() + " " +
                    "WHERE " +
                    "    message_id = ? ";

    private static final String INSERT_DRAFT_MESSAGE_QUERY =
            "INSERT INTO " +
                    "   tbl_draft_messages (last_editor, last_access, state, type, header, content, lang) " +
                    "VALUES (?, NOW(), ?, ?, '', '', NULL)";


    private static final String SELECT_DRAFT_MESSAGES_LIST_QUERY =
            "SELECT " +
                    "    dm.message_id AS " + FIELD_MESSAGE_ID + ", " +
                    "    dm.header AS " + FIELD_HEADER + ", " +
                    "    dm.content AS " + FIELD_CONTENT + ", " +
                    "    dm.last_editor AS " + FIELD_LAST_EDITOR + ", " +
                    "    dm.state AS " + FIELD_STATE + ", " +
                    "    dm.type AS " + FIELD_TYPE + ", " +
                    "    dm.last_access AS " + FIELD_LAST_ACCESS + ", " +
                    "    dm.lang AS " + FIELD_LANGUAGE + " " +
                    "   %4$s " +
                    "FROM " +
                    "    tbl_draft_messages dm " +
                    "WHERE " +
                    "    dm.state IN (%1$s) " +
                    "AND " +
                    "    dm.type = %2$s " +
                    "ORDER BY " +
                    "    dm.last_access DESC " +
                    " %3$s ";

    private static final String SELECT_DRAFT_MESSAGES_LIST_COUNT_QUERY =
            "SELECT " +
                    "    count(*) " +
                    "FROM " +
                    "    tbl_draft_messages dm " +
                    "WHERE " +
                    "    dm.state IN (%1$s) " +
                    "AND " +
                    "    dm.type = %2$s ";

    private static final String SELECT_DRAFT_MESSAGE_QUERY =
            "SELECT " +
                    "    dm.message_id AS " + FIELD_MESSAGE_ID + ", " +
                    "    dm.header AS " + FIELD_HEADER + ", " +
                    "    dm.content AS " + FIELD_CONTENT + ", " +
                    "    dm.last_editor AS " + FIELD_LAST_EDITOR + ", " +
                    "    dm.state AS " + FIELD_STATE + ", " +
                    "    dm.type AS " + FIELD_TYPE + ", " +
                    "    dm.last_access AS " + FIELD_LAST_ACCESS + ", " +
                    "    dm.lang AS " + FIELD_LANGUAGE + " " +
                    "    %1$s " +
                    "FROM " +
                    "    tbl_draft_messages dm " +
                    "WHERE " +
                    "    dm.message_id = ? ";

    protected IWMCUserInfoService userInfoService;
    protected SendInternalNotificationService sendInternalNotificationService;
    protected HistoryService historyService;
    private IWMCSupportService supportService;


    protected final ParameterizedRowMapper<DraftMessageInfo> draftMessageInfoMapper = new ParameterizedRowMapper<DraftMessageInfo>() {
        @Override
        public DraftMessageInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
            long messageId = rs.getLong(FIELD_MESSAGE_ID);
            String header = rs.getString(FIELD_HEADER);
            String content = rs.getString(FIELD_CONTENT);
            Integer lang = SqlUtil.getIntNullable(rs, FIELD_LANGUAGE);
            LanguageEnum language = lang != null ? LanguageEnum.R.fromValueOrNull(lang) : null;

            WMUserInfo lastEditor = null;
            try {
                long lastEditorId = rs.getLong(FIELD_LAST_EDITOR);
                if (!rs.wasNull()) {
                    lastEditor = userInfoService.getUserInfo(lastEditorId);
                }
            } catch (UserException e) {
                // If an attempt to find user info caused an exception - we leave null value.
                // This is not an outstanding situation.
            } catch (InternalException e) {
                // If an attempt to find user info caused an exception - we leave null value.
                // This is not an outstanding situation.
            }

            Byte stateValue = rs.getByte(FIELD_STATE);
            IHistoryAction state = HistoryActionEnum.getByValue(stateValue);
            if (state == null) {
                state = WMCHistoryActionEnum.getByValue(stateValue);
            }

            WMCHistoryObjectTypeEnum type = WMCHistoryObjectTypeEnum.R.fromValueOrNull(rs.getByte(FIELD_TYPE));

            Date lastAccess = SqlUtil.safeGetTimestamp(rs, FIELD_LAST_ACCESS);

            return new DraftMessageInfo(messageId, lastEditor, header, content, state, lastAccess, type, language);
        }
    };

    public List<MessageInfo> getDraftMessagesList(Pager pager, boolean sent) throws InternalException {
        String countSql;
        String selectSql;
        if (sent) {
            countSql = String.format(SELECT_DRAFT_MESSAGES_LIST_COUNT_QUERY,
                    WMCHistoryActionEnum.SEND.getValue(), "%1$s", "%2$s");
            selectSql = String.format(SELECT_DRAFT_MESSAGES_LIST_QUERY,
                    WMCHistoryActionEnum.SEND.getValue(), "%1$s", "%2$s", getAdditionalFieldsString());
        } else {
            countSql = String.format(SELECT_DRAFT_MESSAGES_LIST_COUNT_QUERY,
                    HistoryActionEnum.ADD.getValue() + ", " + HistoryActionEnum.EDIT.getValue(), "%1$s", "%2$s");
            selectSql = String.format(SELECT_DRAFT_MESSAGES_LIST_QUERY,
                    HistoryActionEnum.ADD.getValue() + ", " + HistoryActionEnum.EDIT.getValue(), "%1$s", "%2$s", getAdditionalFieldsString());
        }

        countSql = String.format(countSql, getWmcHistoryObjectType().getValue(), "%1$s");
        selectSql = String.format(selectSql, getWmcHistoryObjectType().getValue(), "%1$s");

        return getJdbcTemplate(WMCPartition.nullPartition()).pageableSelect(countSql, selectSql, getMapper(), pager);
    }

    public DraftMessageInfo getAbstractDraftMessage(long messageId) throws InternalException, UserException {
        return getDraftMessage(messageId, draftMessageInfoMapper, "");
    }

    private <T extends DraftMessageInfo> T getDraftMessage(long messageId,
                                                           ParameterizedRowMapper<T> mapper, String additionalFields) throws InternalException, UserException {
        List<T> resultList = getJdbcTemplate(WMCPartition.nullPartition())
                .query(String.format(SELECT_DRAFT_MESSAGE_QUERY, additionalFields), mapper, messageId);

        if (resultList.size() != 1) {
            throw new UserException(WMCUserProblem.NO_SUCH_MESSAGE, WMCUserProblem.NO_SUCH_MESSAGE.toString() + ". MessageId: " + messageId);
        }
        return resultList.get(0);
    }

    public MessageInfo getDraftMessage(long messageId) throws UserException, InternalException {
        return getDraftMessage(messageId, getMapper(), getAdditionalFieldsString());
    }

    protected void checkPermissions(long userId) throws UserException, InternalException {
        if (!supportService.isSupportAndNotBlockedUser(userId)) {
            throw new UserException(UserProblem.USER_NOT_PERMITTED, "User " + userId + " is not in list of supports or blocked!");
        }
    }

    protected long addNewMessage(final long authorId) throws UserException, InternalException {
        final WMCHistoryObjectTypeEnum type = getWmcHistoryObjectType();
        // check permissions
        checkPermissions(authorId);

        // in transaction

        return (Long) getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallback() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) throws InternalException {
                // write to tbl_draft_message
                long messageId = addMessageToDraftMessages(authorId, type);

                // write to history
                historyService.addEvents(authorId, authorId, HistoryActionEnum.ADD, type, messageId);

                return messageId;
            }
        });
    }

    private void internalEditMessage(
            final long messageId,
            final long authorId,
            final String header,
            final String content,
            final IHistoryAction state,
            final WMCHistoryObjectTypeEnum type,
            final LanguageEnum language) throws InternalException, UserException {

        // check permissions
        if (!supportService.isSupportAndNotBlockedUser(authorId)) {
            throw new UserException(UserProblem.USER_NOT_PERMITTED, "User " + authorId + " is not in list of supports or blocked!");
        }

        // check message is not sent or deleted
        DraftMessageInfo message = getAbstractDraftMessage(messageId);
        if ((message.getState() != HistoryActionEnum.ADD) && (message.getState() != HistoryActionEnum.EDIT)) {
            throw new UserException(WMCUserProblem.MESSAGE_IS_READ_ONLY, "Now you can't edit any options of this message. It's because of a state: " + message.getState());
        }

        final Integer lang = language != null ? language.getId() : null;

        // in transaction
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                // write to tbl_draft_message
                editMessageToDraftMessages(messageId, authorId, state, header, content, lang);

                // write to history
                historyService.addEvents(authorId, authorId, state, type, messageId);
            }
        });
    }

    public void removeMessage(final long messageId, final long authorId) throws UserException, InternalException {
        final DraftMessageInfo message = getAbstractDraftMessage(messageId);

        // check permissions
        if (!supportService.isSupportAndNotBlockedUser(authorId)) {
            throw new UserException(UserProblem.USER_NOT_PERMITTED, "User " + authorId + " is not in list of supports or blocked!");
        }

        // check message is not sent or deleted
        if ((message.getState() != HistoryActionEnum.ADD) && (message.getState() != HistoryActionEnum.EDIT)) {
            throw new UserException(WMCUserProblem.MESSAGE_IS_READ_ONLY, "Now you can't edit any options of this message. It's because of a state: " + message.getState());
        }

        // in transaction
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {
                // write to tbl_draft_message
                removeMessageToDraftMessages(messageId, authorId);

                // write to history
                historyService.addEvents(authorId, authorId, HistoryActionEnum.REMOVE, message.getType(), messageId);
            }
        });
    }

    public void saveMessage(final long messageId, final long authorId, final String header, final String content, final LanguageEnum language) throws UserException, InternalException {
        final DraftMessageInfo message = getAbstractDraftMessage(messageId);

        internalEditMessage(messageId, authorId, header, content, HistoryActionEnum.EDIT, message.getType(), language);
    }

    // ONLY MASTER BASE IS USED TO READ IMPORTANT THINGS!!
    public void sendMessage(final long messageId, final long authorId, final String mode) throws UserException, InternalException {
        final MessageInfo message = getDraftMessage(messageId);

        // check permissions
        if (!supportService.isSupportAndNotBlockedUser(authorId)) {
            throw new UserException(UserProblem.USER_NOT_PERMITTED, "User " + authorId + " is not in list of supports or blocked!");
        }

        if (!supportService.isUserHasPermissionsOnAction(authorId, message.getType(), WMCHistoryActionEnum.SEND)) {
            throw new UserException(UserProblem.USER_NOT_PERMITTED, "User " + authorId + " does not have permissions to send " + message.getType() + " message!");
        }

        // check message is not sent or deleted
        if ((message.getState() != HistoryActionEnum.ADD) && (message.getState() != HistoryActionEnum.EDIT)) {
            throw new UserException(WMCUserProblem.MESSAGE_IS_READ_ONLY, "Now you can't edit any options of this message. It's because of a state: " + message.getState());
        }

        // in transaction
        getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(new ServiceTransactionCallbackWithoutResult() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException, UserException {
                // mark message as sent
                internalEditMessage(messageId, authorId, message.getHeader(), message.getContent(), WMCHistoryActionEnum.SEND, message.getType(), message.getLanguage());

                // send message
                Map<String, Object> params = new HashMap<String, Object>();

                params.put("header", message.getHeader());
                params.put("message", message.getContent());
                params.put("mode", mode);
                if (message.getLanguage() != null) {
                    params.put("message-lang", message.getLanguage());
                }
                params.putAll(getNotifierRequestParams(message));

                sendInternalNotificationService.sendInternalNotification(getNotificationType(), params);
                // write to history
                historyService.addEvents(authorId, authorId, WMCHistoryActionEnum.SEND, message.getType(), messageId);
            }
        });
    }

    private long addMessageToDraftMessages(final long authorId, final WMCHistoryObjectTypeEnum type) throws InternalException {
        GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder();
        PreparedStatementCreator psc = new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
                PreparedStatement ps = connection.prepareStatement(INSERT_DRAFT_MESSAGE_QUERY,
                        Statement.RETURN_GENERATED_KEYS);
                ps.setLong(1, authorId);
                ps.setByte(2, HistoryActionEnum.ADD.getValue());
                ps.setByte(3, type.getValue());
                return ps;
            }
        };
        getJdbcTemplate(WMCPartition.nullPartition()).getJdbcOperations().update(psc, generatedKeyHolder);
        long messageId = generatedKeyHolder.getKey().longValue();
        log.debug("Adding draft message OK. messageId = " + messageId);
        return messageId;
    }

    private void editMessageToDraftMessages(
            final long messageId,
            final long authorId,
            final IHistoryAction state,
            final String header,
            final String content,
            final Integer language) throws InternalException {

        getJdbcTemplate(WMCPartition.nullPartition()).update(
                UPDATE_DRAFT_MESSAGE_QUERY, authorId, state.getValue(), header, content, language, messageId);
    }

    private void removeMessageToDraftMessages(final long messageId, final long authorId) throws InternalException {
        getJdbcTemplate(WMCPartition.nullPartition()).update(UPDATE_DRAFT_MESSAGE_REMOVE_STATE_QUERY, authorId, messageId);
    }

    protected abstract NotificationTypeEnum getNotificationType();

    protected abstract WMCHistoryObjectTypeEnum getWmcHistoryObjectType();

    protected abstract ParameterizedRowMapper<MessageInfo> getMapper();

    protected abstract String getAdditionalFieldsString();

    protected abstract Map<String, Object> getNotifierRequestParams(DraftMessageInfo message) throws InternalException;

    @Required
    public void setUserInfoService(IWMCUserInfoService userInfoService) {
        this.userInfoService = userInfoService;
    }

    @Required
    public void setSendInternalNotificationService(SendInternalNotificationService sendInternalNotificationService) {
        this.sendInternalNotificationService = sendInternalNotificationService;
    }

    @Required
    public void setSupportService(IWMCSupportService supportService) {
        this.supportService = supportService;
    }

    @Required
    public void setHistoryService(HistoryService historyService) {
        this.historyService = historyService;
    }
}
