package cn.axzo.msg.center.message.service.todo.manage;

import cn.axzo.basics.common.BeanMapper;
import cn.axzo.basics.common.constant.enums.TableIsDeleteEnum;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.framework.rocketmq.utils.TraceUtils;
import cn.axzo.msg.center.api.mq.PresetButtonPressedMessage;
import cn.axzo.msg.center.common.utils.BizAssertions;
import cn.axzo.msg.center.dal.TodoBusinessDao;
import cn.axzo.msg.center.dal.TodoBusinesses;
import cn.axzo.msg.center.dal.TodoDao;
import cn.axzo.msg.center.domain.entity.Todo;
import cn.axzo.msg.center.domain.entity.TodoBusiness;
import cn.axzo.msg.center.domain.entity.TodoHandoverMapping;
import cn.axzo.msg.center.domain.persistence.BaseEntityExt;
import cn.axzo.msg.center.inside.notices.config.PendingMessageBizConfig;
import cn.axzo.msg.center.message.domain.dto.MessageTemplateDTO;
import cn.axzo.msg.center.message.domain.param.PendingMessagePushParam;
import cn.axzo.msg.center.message.service.MessageTemplateNewService;
import cn.axzo.msg.center.message.service.todo.manage.broadcast.TodoBroadcaster;
import cn.axzo.msg.center.message.service.todo.manage.broadcast.TodoMqBroadcaster;
import cn.axzo.msg.center.message.service.todo.manage.event.HandoverEvent;
import cn.axzo.msg.center.message.service.todo.manage.event.NewTodoEvent;
import cn.axzo.msg.center.mq.MqMessageRecord;
import cn.axzo.msg.center.mq.MqProducer;
import cn.axzo.msg.center.service.enums.BizCategoryEnum;
import cn.axzo.msg.center.service.enums.IdentityTypeEnum;
import cn.axzo.msg.center.service.enums.MessageCategoryEnum;
import cn.axzo.msg.center.service.enums.MqMessageType;
import cn.axzo.msg.center.service.enums.PendingMessageStateEnum;
import cn.axzo.msg.center.service.enums.TodoType;
import cn.axzo.msg.center.service.enums.YesOrNo;
import cn.axzo.msg.center.service.pending.request.CompletePendingBySubCodeRequest;
import cn.axzo.msg.center.service.pending.request.CompletePendingMessageByIdRequest;
import cn.axzo.msg.center.service.pending.request.CompletePendingMessageRequest;
import cn.axzo.msg.center.service.pending.request.PresetButtonPressedRequest;
import cn.axzo.msg.center.service.pending.request.RevokeByTemplateCodeRequest;
import cn.axzo.msg.center.service.pending.request.RevokePendingMessageByIdRequest;
import cn.axzo.msg.center.service.pending.request.SetHideRequest;
import cn.axzo.msg.center.service.pending.request.TodoHandoverRequest;
import cn.axzo.msg.center.service.pending.request.UpdateBusinessFinalBizStateRequest;
import cn.axzo.msg.center.service.pending.request.UpdatePendingMessageByIdRequest;
import cn.axzo.msg.center.service.pending.response.PushPendingMessageDTO;
import cn.axzo.msg.center.service.util.JSONUtils;
import cn.axzo.msg.center.utils.DateFormatUtil;
import cn.axzo.msg.center.utils.QueryFormatter;
import cn.axzo.msg.center.utils.UUIDUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Uninterruptibles;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

/**
 * @author yanglin
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TodoManager {

    private final TodoBusinessDao todoBusinessDao;
    private final TodoDao todoDao;
    private final TodoRecordBuilder todoRecordBuilder;
    private final TodoLogger todoLogger;
    private final TodoMqBroadcaster todoMqBroadcaster;
    private final MessageTemplateNewService messageTemplateNewService;
    private final MqProducer mqProducer;
    private final ErrorAssembler errorAssembler;
    private final PendingMessageBizConfig cfg;
    private final ApplicationContext applicationContext;
    private final TodoBroadcaster todoBroadcaster;
    private final TransactionTemplate transactionTemplate;

    public List<PushPendingMessageDTO> send(PendingMessagePushParam request) {
        TodoRequestContext ctx = TodoRequestContext.create("send", request.normalize());
        // 因为有外层事务, 所以这里可以直接调用
        return send(ctx, request);
    }

    public List<PushPendingMessageDTO> send(TodoRequestContext ctx, PendingMessagePushParam request) {
        // 10 seconds at most
        for (int i = 0; i < 100; i++) {
            try {
                return transactionTemplate.execute(unused -> sendImpl(ctx, request));
            } catch (DuplicateKeyException e) {
                log.info("Try to save todo but todo business already exists, request={}. " +
                        "retryCount={}", request, i, e);
                Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
            }
        }
        throw new ServiceException("服务器内部错误");
    }

    /**
     * 发送待办或抄送
     */
    private List<PushPendingMessageDTO> sendImpl(TodoRequestContext ctx, PendingMessagePushParam request) {
        // 可能会存在重新查询模版的情况, 成本不高
        MessageTemplateDTO template = messageTemplateNewService
                .queryEnableTemplateByCode(request.getTemplateCode())
                .orElseThrow(() -> new ServiceException(
                        String.format("Can't find template. templateCode=%s", request.getTemplateCode())));
        BizAssertions.assertNotEmpty(request.getExecutor(), "接受者不能为空");
        BizAssertions.assertTrue(request.getExecutor().size() <= cfg.getPendingMaxReceivers(),
                String.format("待办接收者数量不能超过 %d", cfg.getPendingMaxReceivers()));
        // 线上有2个模版一直没有传bizCode. 在查询我发起的待办时, 会有问题, 有一些解决办法:
        // 1. 让他们改造, 传bizCode
        // 2. 待办自动生成bizCode
        // 3. 如果是系统生成的待办, 那么就不会有问题. 目前生成上有2个模版(进场、出场)都是系统生成的
        boolean genBizCode = cfg.getGenBizCodeTemplates().contains(template.getCode());
        if (genBizCode)
            request.setBizCode(UUIDUtil.uuidString());
        else if (StringUtils.isBlank(request.getBizCode()))
            request.setBizCode("");
        // 如果已经存在对应的待办业务, 就把待办追加到对应的待办业务上
        // 流程会并发, 这里对业务进行加锁
        TodoBusiness business = todoBusinessDao
                .findByBiz(request.getTemplateCode(), request.getBizCode(), true)
                .orElse(null);
        boolean businessCreated = false;
        TodoExt ext = new TodoExt(request, template);
        if (business == null) {
            business = todoRecordBuilder.buildBusiness(request, ext);
            business.getRecordExt().setGenBizCode(genBizCode);
            todoBusinessDao.save(business);
            businessCreated = true;
        }
        //批量默认为false
        YesOrNo supportBatchProcess = request.getSupportBatchProcess() == null ? YesOrNo.NO : request.getSupportBatchProcess();
        //业务待办设置是否批量处理
        if (template.getMsgCategory() == MessageCategoryEnum.BIZ_PENDING_MESSAGE
                && template.hasPresetButtons())
            supportBatchProcess = YesOrNo.YES;
        request.setSupportBatchProcess(supportBatchProcess);
        List<Todo> todos = todoRecordBuilder.buildTodos(
                request, ctx.getRequestNo(), ext, business);
        todoDao.saveBatch(todos);
        Todo sample = todos.get(0);
        // 兼容老接口, 因为查询待办详情是通过待办的identityCode查询的, 存一个样本待办给待办业务
        if (StringUtils.isBlank(business.getSampleTodoCode())
                && request.determineTodoType() == TodoType.EXECUTABLE)
            todoBusinessDao.updateSampleTodCode(business.getId(), sample.getIdentityCode());
        todoBroadcaster.fireTodoUpdates("send", todos);
        // 记录日志
        // @formatter:off
        ctx.addLogContent("templateTitle", template.getTitle())
           .addLogContent("templateContent", template.getContent())
           .addLogContent("title", ext.parseTitle())
           .addLogContent("content", ext.parseContent())
           .addLogContent("bizExtParams", ext.parseBizExtParams())
           .addLogContent("routerParams", ext.parseRouterExtParams())
           .addLogContent("state", sample.getState())
           .addLogContent("genBizCode", genBizCode);
        // @formatter:on
        if (businessCreated)
            todoLogger.logBusinessUpdated(ctx, business);
        todoLogger.logTodosUpdated(ctx, todos);
        applicationContext.publishEvent(new NewTodoEvent(this, template, todos));
        return todos.stream()
                .map(todo -> new PushPendingMessageDTO(
                        todo.getId(), todo.getIdentityCode(),
                        todo.getRequestNo(), todo.getExecutorId(),
                        todo.getExecutorPersonId()))
                .collect(toList());
    }

    // !! handover

    /**
     * 交接
     */
    @Transactional(rollbackFor = Exception.class)
    public Integer handover(TodoHandoverRequest request) {
        BizAssertions.assertNotBlank(request.getFromPersonName(), "交接来源人自然人姓名不能为空");
        BizAssertions.assertNotEmpty(request.getTodoIdentityCodes(), "需要转交的待办编码不能为空");
        List<Todo> candidates = todoDao.getByCodes(request.getTodoIdentityCodes());
        // 只处理未完成的
        candidates = candidates.stream()
                .filter(i -> i.getState() == PendingMessageStateEnum.HAS_BEEN_SENT)
                .collect(toList());
        // 只交接业务待办, 审批待办的转交审批自己已经闭环
        TodoBusinesses businesses = todoBusinessDao.getBusinesses(candidates);
        List<Todo> srcTodos = businesses.getTodoByCategory(BizCategoryEnum.OTHER);
        TodoRequestContext parentCtx = TodoRequestContext.create("handover", request)
                .addLogContent("todoCount", srcTodos.size());
        // 完成待办
        List<Long> srcIds = srcTodos.stream()
                .map(BaseEntityExt::getId)
                .collect(toList());
        todoDao.lambdaUpdate()
                .in(Todo::getId, srcIds)
                .in(Todo::getState,
                        PendingMessageStateEnum.HAS_BEEN_SENT,
                        PendingMessageStateEnum.PROCESSING)
                .set(Todo::getState, PendingMessageStateEnum.RETRACT)
                .update();
        // 创建新的待办
        List<Todo> destTodos = new ArrayList<>(srcTodos.size());
        for (Todo srcTodo : srcTodos) {
            // 复制待办内容
            Todo destTodo = BeanMapper.copyBean(srcTodo, Todo.class);
            destTodos.add(destTodo);
            // 重置下面的属性
            destTodo.setId(null);
            destTodo.setCreateAt(null);
            destTodo.setUpdateAt(null);
            destTodo.setIdentityCode(UUIDUtil.uuidString());
            // 身份id设置为0, 去身份
            destTodo.setExecutorId(0L);
            destTodo.setExecutorType(IdentityTypeEnum.NOT_SUPPORT);
            destTodo.setOuId(request.getToOuId());
            destTodo.setExecutorPersonId(request.getToPersonId());
            destTodo.setExecutorName(request.determineToPersonName());
        }
        todoDao.saveBatch(destTodos);
        todoBroadcaster.fireTodoUpdates("handover", srcTodos);
        todoBroadcaster.fireTodoUpdates("handover", destTodos);
        // build handover mappings
        List<TodoHandoverMapping> mappings = new ArrayList<>();
        for (int i = 0; i < srcTodos.size(); i++) {
            mappings.add(new TodoHandoverMapping(
                    srcTodos.get(i).getId(),
                    destTodos.get(i).getId()));
        }
        // save logs
        parentCtx.addLogContent("handoverMappings", mappings);
        todoLogger.logTodosUpdated(parentCtx.copy().addLogContent(
                "todoAction", "setComplete -> COMPLETED"), srcTodos);
        todoLogger.logTodosUpdated(parentCtx.copy().addLogContent(
                "todoAction", "create -> HAS_BEEN_SENT"), destTodos);
        applicationContext.publishEvent(new HandoverEvent(request, srcTodos, destTodos));
        return srcTodos.size();
    }

    // !! complete

    /**
     * 完成待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean completeByRequestNo(String todoRequestNo) {
        if (StringUtils.isBlank(todoRequestNo))
            return false;
        TodoRequestContext ctx = TodoRequestContext.create("completeByRequestNo", todoRequestNo);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getRequestNo, todoRequestNo)
                .set(Todo::getState, PendingMessageStateEnum.COMPLETED));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoCompleted(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    /**
     * 完成待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean completeById(CompletePendingMessageByIdRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("completeById", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getId, request.getId())
                .set(Todo::getState, PendingMessageStateEnum.COMPLETED));
        boolean businessUpdated = false;
        if (advanceResult.getBusiness() != null && request.getBizExtParams() != null) {
            JSONObject bizExtParams = JSONUtils.parseObjectOrThrow(
                    "bizExtParams", request.getBizExtParams());
            businessUpdated = todoBusinessDao.updateBizExtParams(advanceResult.getBusinessId(), bizExtParams);
            ctx.addLogContent("businessUpdate",
                    "bizExtParam", bizExtParams,
                    "businessUpdated", businessUpdated);
            todoLogger.logBusinessUpdated(ctx, advanceResult.getBusiness());
        }
        if (advanceResult.isAdvanced())
            todoLogger.logTodoCompleted(ctx, advanceResult.getAdvancedTodos());
        return advanceResult.isAdvanced() || businessUpdated;
    }

    /**
     * 完成待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean completeByBizCode(CompletePendingMessageRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("completeByBizCode", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getTemplateCode, request.getTemplateCode())
                .eq(Todo::getBizCode, request.getBizCode())
                .set(Todo::getState, PendingMessageStateEnum.COMPLETED));
        boolean businessUpdated = false;
        if (advanceResult.getBusiness() != null && request.determineUpdateFinalBizState()) {
            businessUpdated = todoBusinessDao.updateBizFinalState(
                    advanceResult.getBusinessId(), request.getBizFinalStateEnum());
            ctx.addLogContent("businessUpdate",
                    "bizFinalState", request.getBizFinalStateEnum(),
                    "businessId", advanceResult.getBusiness().getId(),
                    "businessUpdated", businessUpdated);
            todoLogger.logBusinessUpdated(ctx, advanceResult.getBusiness());
        }
        if (advanceResult.isAdvanced())
            todoLogger.logTodoCompleted(ctx, advanceResult.getAdvancedTodos());
        return advanceResult.isAdvanced() || businessUpdated;
    }

    @Transactional
    public boolean updateBusinessFinalBizState(UpdateBusinessFinalBizStateRequest request) {
        TodoBusiness business = todoBusinessDao
                .findByBiz(request.getTemplateCode(), request.getBizCode(), false)
                .orElse(null);
        boolean updated = false;
        if (business != null) {
            updated = todoBusinessDao.updateBizFinalState(
                    request.getTemplateCode(), request.getBizCode(), request.getBizFinalStateEnum());
            TodoRequestContext ctx = TodoRequestContext.create("updateBusinessFinalBizState", request)
                    .addLogContent("updated", updated);
            todoLogger.logBusinessUpdated(ctx, business);
        }
        return updated;
    }

    /**
     * 完成待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean completeBySubBizCode(CompletePendingBySubCodeRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("completeBySubBizCode", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getTemplateCode, request.getTemplateCode())
                .eq(Todo::getBizCode, request.getBizCode())
                .eq(Todo::getSubBizCode, request.getSubBizCode())
                .set(Todo::getState, PendingMessageStateEnum.COMPLETED));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoCompleted(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    /**
     * 恢复待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean rollbackBySubBizCode(CompletePendingBySubCodeRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("rollbackBySubBizCode", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getTemplateCode, request.getTemplateCode())
                .eq(Todo::getBizCode, request.getBizCode())
                .eq(Todo::getSubBizCode, request.getSubBizCode())
                .set(Todo::getState, PendingMessageStateEnum.HAS_BEEN_SENT));
        if (!advanceResult.isAdvanced()) {
            return false;
        }
        todoLogger.logTodoRollback(ctx, advanceResult.getAdvancedTodos());
        todoBroadcaster.fireTodoUpdates("rollbackBySubBizCode", advanceResult.getAdvancedTodos());
        return true;
    }

    // !! revoke

    /**
     * 撤回待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean revokeByRequestNo(String requestNo) {
        if (StringUtils.isBlank(requestNo))
            return false;
        TodoRequestContext ctx = TodoRequestContext.create("revokeByRequestNo", requestNo);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getRequestNo, requestNo)
                .set(Todo::getState, PendingMessageStateEnum.RETRACT));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoRevoked(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    /**
     * 撤回待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean revokeByIdCode(String idCode) {
        TodoRequestContext ctx = TodoRequestContext.create("revokeByIdCode", idCode);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getIdentityCode, idCode)
                .set(Todo::getState, PendingMessageStateEnum.RETRACT));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoRevoked(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    /**
     * 撤回待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean revokeById(RevokePendingMessageByIdRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("revokeById", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getId, request.getId())
                .set(Todo::getState, PendingMessageStateEnum.RETRACT));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoRevoked(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    /**
     * 撤回待办
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean revokeByBizCode(CompletePendingMessageRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("revokeByBizCode", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getTemplateCode, request.getTemplateCode())
                .eq(Todo::getBizCode, request.getBizCode())
                .set(Todo::getState, PendingMessageStateEnum.RETRACT));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoRevoked(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean revokeByTemplateCode(RevokeByTemplateCodeRequest request) {
        TodoRequestContext ctx = TodoRequestContext.create("revokeByTemplateCode", request);
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getTemplateCode, request.getTemplateCode())
                .set(Todo::getState, PendingMessageStateEnum.RETRACT));
        if (!advanceResult.isAdvanced())
            return false;
        todoLogger.logTodoRevoked(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    // !! update

    /**
     * 将待办设置为隐藏, 隐藏有时间期限
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean setHide(SetHideRequest request) {
        BizAssertions.assertTrue(StringUtils.isNotBlank(request.getSubBizCode()), "subBizCode不能为空");
        List<Todo> todos = todoDao.getBySubBizCode(request.getSubBizCode());
        if (todos.isEmpty())
            return false;
        int seconds = request.getHideSeconds() == null
                ? cfg.getPendingSetHideSeconds() : request.getHideSeconds();
        Date expireTime = DateTime.now().plusSeconds(seconds).toDate();
        boolean updated = todoDao.setExecutableHide(request.getSubBizCode(), expireTime);
        if (updated) {
            List<Todo> updatedTodos = todos.stream()
                    .filter(todo -> todo.getState() == PendingMessageStateEnum.HAS_BEEN_SENT)
                    .collect(toList());
            TodoRequestContext ctx = TodoRequestContext.create("setHide", request)
                    .addLogContent("expiredTime", expireTime.getTime())
                    .addLogContent("readableExpiredTime", DateFormatUtil.toReadableString(expireTime));
            todoLogger.logTodosUpdated(ctx, updatedTodos);
        }
        return updated;
    }

    /**
     * 将待办设置为执行中
     */
    @Transactional(rollbackFor = Exception.class)
    public Boolean batchSetProcessing(List<String> identityCodes, List<String> subBizCodes) {
        BizAssertions.assertTrue(
                CollectionUtils.isNotEmpty(identityCodes) || CollectionUtils.isNotEmpty(subBizCodes) ,
                "identityCodes或subBizCodes列表不能为空");
        List<Todo> todos;
        if (CollectionUtils.isNotEmpty(identityCodes))
            todos = todoDao.getByIdentityCodes(identityCodes);
         else
            todos = todoDao.getBySubBizCodes(subBizCodes, true);
        Boolean updated = todoDao.batchSetExecutableProcessing(identityCodes, subBizCodes);
        if (updated) {
            HashMap<String, Object> request = new HashMap<>();
            request.put("identityCodes", identityCodes);
            request.put("subBizCodes", subBizCodes);
            TodoRequestContext ctx = TodoRequestContext.create("batchSetProcessing", request);
            todoLogger.logSetTodoProcessing(ctx, todos);
            todoBroadcaster.fireTodoUpdates("batchSetProcessing", todos);
        }
        return updated;
    }

    /**
     * 更新待办的内容
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean updateBusinessById(UpdatePendingMessageByIdRequest request) {
        TodoBusiness business = todoBusinessDao.getById(request.getId());
        if (business == null)
            return false;
        JSONObject bizParam = StringUtils.isBlank(request.getBizParam())
                ? null : JSONUtils.parseObjectOrThrow("bizParam", request.getBizParam());
        JSONObject routerParam = StringUtils.isBlank(request.getRouterParam())
                ? null : JSONUtils.parseObjectOrThrow("routerParam", request.getRouterParam());
        BizAssertions.assertFalse(bizParam == null
                        && routerParam == null
                        && StringUtils.isBlank(request.getTemplateCode()),
                "routerParam、bizParam、templateCode至少要传一个");
        TodoBusiness businessUpdate = new TodoBusiness();
        businessUpdate.setId(business.getId());
        if (bizParam != null)
            businessUpdate.setBizExtParam(bizParam);
        if (routerParam != null)
            businessUpdate.setRouterParams(routerParam);
        if (StringUtils.isNotBlank(request.getTemplateCode()))
            businessUpdate.setTemplateCode(request.getTemplateCode());
        // 不通过 lambdaUpdate 更新, 因为JSON序列化有问题
        boolean updated = todoBusinessDao.updateById(businessUpdate);
        if (updated) {
            TodoRequestContext ctx = TodoRequestContext.create("updateBusinessById", request)
                    .addLogContent("updatedBizParam", bizParam)
                    .addLogContent("updatedRouterParam", routerParam)
                    .addLogContent("updatedTemplateCode", request.getTemplateCode());
            todoLogger.logBusinessUpdated(ctx, business);
        }
        return updated;
    }

    // !! 预设按钮

    /**
     * 点击预设按钮
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean firePresetButtonPressed(PresetButtonPressedRequest request) {
        Todo todo = todoDao.findTodoByCode(request.getIdentityCode()).orElse(null);
        if (todo == null)
            return false;
        TodoRequestContext ctx = TodoRequestContext.create("firePresetButtonPressed", request)
                .addLogContent("presetButtonType", request.getPresetButtonType());
        StateAdvanceResult advanceResult = advanceState(ctx, execAdvanceBuilder()
                .eq(Todo::getIdentityCode, request.getIdentityCode())
                .set(Todo::getState, PendingMessageStateEnum.COMPLETED));
        // isExecCompleted 可以排除是抄送待办的情况
        boolean isAdvancedOrCompleted = advanceResult.isAdvanced() || todo.isExecCompleted();
        // 支持重复发mq消息
        if (isAdvancedOrCompleted) {
            sendMqMessageOnPresetButtonPressed(ctx, request, todo);
            todoBroadcaster.fireTodoUpdates("presetButtonPressed", todo);
            // 如果不是重复发送, 就只记一条日志. 如果是重复发送, 就单独记录一条日志
            if (!advanceResult.isAdvanced())
                todoLogger.logTodoUpdated(ctx, todo);
        }
        // 如果不是重复发送, 就只记一条日志. 所以这个记录日志不能提前
        if (advanceResult.isAdvanced())
            todoLogger.logTodoCompleted(ctx, advanceResult.getAdvancedTodos());
        return isAdvancedOrCompleted;
    }

    /**
     * 点击预设按钮, 发送mq消息
     */
    private void sendMqMessageOnPresetButtonPressed(
            TodoRequestContext ctx, PresetButtonPressedRequest request, Todo todo) {
        PresetButtonPressedMessage message = new PresetButtonPressedMessage();
        BeanUtils.copyProperties(todo, message);
        message.setBtnPressedRequestNo(ctx.getRequestNo());
        message.setPresetButtonType(request.getPresetButtonType());
        TodoBusinesses businesses = todoBusinessDao.getBusinesses(todo);
        message.setTodoInfo(todoMqBroadcaster.createTodoInfo(businesses, todo));
        ctx.addLogContent("shardingKey", todo.getTemplateCode());
        try {
            mqProducer.send(MqMessageRecord
                    .builder(MqMessageType.TODO_PRESET_BUTTON_PRESSED, message)
                    .messageKey(todo.getId())
                    .operatorId(request.getOperatorId())
                    .shardingKey(todo.getTemplateCode())
                    .build());
            ctx.addLogContent("sendMqMessage", ImmutableMap.of("isSuccess", "true"));
        } catch (Exception e) {
            // 打印requestNo, 生产快速定位错误日志
            log.warn("预设按钮点击发送事件失败. requestNo={}, request={}", ctx.getRequestNo(), request, e);
            // 表记录中的字段也会记录requestNo, 这里重复记录requestNo, 这样能更快的找到
            String errorMessage = Throwables.getStackTraceAsString(e);
            ctx.addLogContent("sendMqMessage", ImmutableMap.of(
                    "traceId", TraceUtils.getOrCreateTraceId(),
                    "isSuccess", "false",
                    "request", request,
                    "requestNo", ctx.getRequestNo(),
                    "stacktrace", StringUtils.abbreviate(errorMessage, 1536)));
        }
    }

    // !! read only

    /**
     * 将只读待办设置为已读
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean setCopiedToMeRead(Long personId, String identityCode) {
        if (personId == null || personId <= 0)
            return false;
        ImmutableMap<String, Object> request = ImmutableMap.of("personId", personId, "identityCode", identityCode);
        TodoRequestContext ctx = TodoRequestContext.create("setCopiedToMeRead", request)
                .addLogContent("state", PendingMessageStateEnum.READ)
                .addLogContent("isSetAllRead", StringUtils.isBlank(identityCode));
        StateAdvanceResult advanceResult = advanceState(ctx, copiedAdvanceBuilder()
                .eq(Todo::getExecutorPersonId, personId)
                .eq(StringUtils.isNotBlank(identityCode), Todo::getIdentityCode, identityCode)
                .set(Todo::getState, PendingMessageStateEnum.READ));
        if (!advanceResult.isAdvanced()) {
            // 把错误信息设置得精确一点
            errorAssembler.exposeSetCopiedToMeReadFailReason(identityCode);
            return false;
        }
        todoLogger.logTodosUpdated(ctx, advanceResult.getAdvancedTodos());
        return true;
    }

    // !! advance state

    private StateAdvanceResult advanceState(TodoRequestContext ctx, StateAdvanceBuilder builder) {
        Supplier<BusinessTodos> advanceFailLogger = () -> {
            // rerun no state query in case of race condition
            BusinessTodos noStateBusinessTodos = getBusinessTodos(builder.getNoStateQuery());
            String failReason = noStateBusinessTodos.sampleTodo() == null ? "没有找到对应的待办" : "起初状态不匹配";
            // @formatter:off
            ctx.addLogContent("stateAdvanced", false)
               .addLogContent("stateAdvanceFail",
                        "failReason", failReason,
                        "currentStateSample", noStateBusinessTodos.sampleTodo());
            // @formatter:on
            if (cfg.isLogAdvanceTodoStateFail())
                todoLogger.logTodosUpdated(ctx, noStateBusinessTodos.todos);
            // throw an error? dunno
            log.warn("尝试推进待办状态, 但是 {}. ctx={}, query={}, currentStateSample={}",
                    failReason, ctx, QueryFormatter.format(builder.getQuery()), noStateBusinessTodos.sampleTodo());
            return noStateBusinessTodos;
        };
        BusinessTodos advanceTodos = getBusinessTodos(builder.getQuery());
        if (advanceTodos.todos.isEmpty()) {
            BusinessTodos noStateTodos = advanceFailLogger.get();
            return new StateAdvanceResult(false, noStateTodos.business, Collections.emptyList());
        }
        boolean updated = builder.getUpdate().update();
        if (updated) {
            ctx.addLogContent("stateAdvanced", true);
            Set<Long> updatedTodoIds = advanceTodos.todos.stream()
                    .map(BaseEntityExt::getId)
                    .collect(toSet());
            if (!updatedTodoIds.isEmpty()) {
                List<Todo> updatedTodos = todoDao.listByIds(updatedTodoIds);
                todoBroadcaster.fireTodoUpdates(ctx.getName(), updatedTodos);
            }
        } else {
            advanceFailLogger.get();
        }
        return new StateAdvanceResult(updated, advanceTodos.business, advanceTodos.todos);
    }

    private StateAdvanceBuilder execAdvanceBuilder() {
        return new StateAdvanceBuilder(todoDao)
                .set(Todo::getHideUntil, null)
                .eq(Todo::getType, TodoType.EXECUTABLE)
                .in(Todo::getState,
                        PendingMessageStateEnum.HAS_BEEN_SENT,
                        PendingMessageStateEnum.PROCESSING)
                .eq(Todo::getIsDelete, TableIsDeleteEnum.NORMAL.value);
    }

    private StateAdvanceBuilder copiedAdvanceBuilder() {
        return new StateAdvanceBuilder(todoDao)
                .set(Todo::getHideUntil, null)
                .eq(Todo::getType, TodoType.COPIED_TO_ME)
                .eq(Todo::getState, PendingMessageStateEnum.CREATED)
                .eq(Todo::getIsDelete, TableIsDeleteEnum.NORMAL.value);
    }

    BusinessTodos getBusinessTodos(LambdaQueryChainWrapper<Todo> query) {
        List<Todo> todos = query
                .orderByDesc(Todo::getId)
                .list();
        if (todos == null)
            return new BusinessTodos(null, null);
        TodoBusiness business = null;
        if (CollectionUtils.isNotEmpty(todos)) {
            Todo sample = todos.get(0);
            business = todoBusinessDao.getById(sample.getTodoBusinessId());
        }
        return new BusinessTodos(business, todos);
    }

    @Getter
    @RequiredArgsConstructor
    static class BusinessTodos {
        private final TodoBusiness business;
        private final List<Todo> todos;

        Todo sampleTodo() {
            return CollectionUtils.isEmpty(todos) ? null : todos.get(0);
        }
    }
}