package cn.axzo.msg.center.notices.service.impl;

import cn.axzo.msg.center.common.utils.PlaceholderResolver;
import cn.axzo.msg.center.dal.MNSMessageAppDao;
import cn.axzo.msg.center.domain.entity.MNSBatchMessageRequest;
import cn.axzo.msg.center.domain.entity.MNSChannelMessageTemplate;
import cn.axzo.msg.center.domain.entity.MNSMessage;
import cn.axzo.msg.center.domain.entity.MNSMessageApp;
import cn.axzo.msg.center.domain.entity.MNSMessageChannel;
import cn.axzo.msg.center.domain.entity.MNSMessageRedo;
import cn.axzo.msg.center.domain.entity.MNSMessageTemplate;
import cn.axzo.msg.center.domain.enums.YesNoEnum;
import cn.axzo.msg.center.notices.common.annotation.ApiRequestLog;
import cn.axzo.msg.center.notices.common.constans.CommonConstants;
import cn.axzo.msg.center.notices.common.domain.BatchMessageSendContext;
import cn.axzo.msg.center.notices.common.domain.MessageContext;
import cn.axzo.msg.center.notices.common.enums.AvailableStatusEnum;
import cn.axzo.msg.center.notices.common.enums.BatchMessageRequestStatusEnum;
import cn.axzo.msg.center.notices.common.enums.BatchSendTypeEnum;
import cn.axzo.msg.center.notices.common.enums.MessageStatusEnum;
import cn.axzo.msg.center.notices.common.enums.MessageTypeEnum;
import cn.axzo.msg.center.notices.common.enums.NotifyTypeEnum;
import cn.axzo.msg.center.notices.common.enums.RetryingFlagEnum;
import cn.axzo.msg.center.notices.common.enums.ReturnCodeEnum;
import cn.axzo.msg.center.notices.common.exception.BizException;
import cn.axzo.msg.center.notices.common.utils.DateUtils;
import cn.axzo.msg.center.notices.integration.client.DingDingClient;
import cn.axzo.msg.center.notices.integration.client.impl.RateLimitException;
import cn.axzo.msg.center.notices.manager.api.BatchMessageRequestManger;
import cn.axzo.msg.center.notices.manager.api.MessageChannelRouter;
import cn.axzo.msg.center.notices.manager.api.MessageManager;
import cn.axzo.msg.center.notices.manager.api.MessageTemplateManager;
import cn.axzo.msg.center.notices.manager.api.RetryStrategy;
import cn.axzo.msg.center.notices.manager.api.SmsSendManager;
import cn.axzo.msg.center.notices.manager.api.dto.request.MessageSendRequestDto;
import cn.axzo.msg.center.notices.manager.api.dto.request.MnsRequestDto;
import cn.axzo.msg.center.notices.manager.api.dto.request.SendBatchMessageRequestDto;
import cn.axzo.msg.center.notices.manager.api.dto.request.SmsContentDto;
import cn.axzo.msg.center.notices.manager.api.dto.response.SendSmsCommonResponseDto;
import cn.axzo.msg.center.notices.manager.api.dto.response.TemplateParamDto;
import cn.axzo.msg.center.notices.manager.support.UserCellphoneProperties;
import cn.axzo.msg.center.notices.service.api.MessageService;
import cn.azxo.framework.common.logger.MethodAroundLog;
import cn.azxo.framework.common.utils.LogUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.PhoneUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.stream.Collectors;

/**
 * 消息服务
 *
 * @Author: LiuYang
 * @Date: 2021/5/20 18:23
 */
@Slf4j
@Service
@RefreshScope
public class MessageServiceImpl implements MessageService, EnvironmentAware {

  @Resource
  private MNSMessageAppDao mnsMessageAppDao;

  @Resource(name = "messageTemplateManager")
  private MessageTemplateManager messageTemplateManager;

  @Resource(name = "messageManager")
  private MessageManager messageManager;

  @Resource(name = "messageChannelRoute")
  private MessageChannelRouter messageChannelRouter;

  @Resource(name = "retryStrategy")
  private RetryStrategy retryStrategy;

  @Resource(name = "smsSendManagerComposite")
  private SmsSendManager smsSendManagerComposite;

  @Resource(name = "batchMessageRequestManger")
  private BatchMessageRequestManger batchMessageRequestManger;

  @Resource(name = "dingDingClient")
  private DingDingClient dingDingClient;

  @Resource(name = "userCellphoneProperties")
  private UserCellphoneProperties userCellphoneProperties;

  @Resource
  private MnsLimiter mnsLimiter;

  @Value("${prod.profile.string:master}")
  private String prodProfileString;

  @Value("${enable.send.single.message:0}")
  private Integer enableSendSingleMessage;

  @Value("${enable.send.batch.message:1}")
  private Integer enableSendBatchMessage;

  private Environment environment;

  @Override
  @ApiRequestLog
  @MethodAroundLog(source = "Controller", target = "Service", value = "发送单条短信")
  public void sendMessage(MnsRequestDto request) {
    log.info("[MessageServiceImpl#sendMessage] request starting {}", JSON.toJSONString(request));

    try {
      // 检查上游是否可调用
      checkAppCode(request.getAppCode());

      //手机号格式校验
      boolean isLegalPhone = PhoneUtil.isPhone(request.getPhoneNo());
      BizException.error(isLegalPhone, ReturnCodeEnum.INVALID_PARAMETER, "非法的手机号:" + request.getPhoneNo());

      mnsLimiter.maybeLimitSend(request);

      // 查询并校验
      MNSMessageTemplate messageTemplate = messageTemplateManager.queryAndCheckInnerTemplate(request.getTemplateNo());

      // 通过路由获取可用消息渠道
      MNSMessageChannel messageChannel = messageChannelRouter.route(request.getExpansion(), request.getTemplateNo());
      BizException.error(messageChannel != null, ReturnCodeEnum.MESSAGE_CHANNEL_NOT_VALID);
      log.info("[MessageServiceImpl#sendMessage] 选择渠道为 {}", JSON.toJSONString(messageChannel));

      // 生成短信记录
      MNSMessage message = initMessage(request, messageChannel, messageTemplate);
      log.info("[MessageServiceImpl#sendMessage] 生成短信模板 {}", JSON.toJSONString(message));

      // 保存消息
      messageManager.saveMessage(message);

      // 如果模拟发短信成功，则直接设置状态并为成功
      if (shouldSkip(request)) {
        successMessageAndSendDingDing(request, message, messageTemplate);
        return;
      }

      // 获取真实短信模板
      MNSChannelMessageTemplate channelTemplate = messageTemplateManager
          .queryChannelTemplate(request.getTemplateNo(), messageChannel.getChannelCode());

      // 发送短信
      try {
        MessageSendRequestDto dto = new MessageSendRequestDto();
        dto.setAppCode(request.getAppCode());
        dto.setChannelCode(message.getChannelCode());
        dto.setPhoneNo(message.getTargetAddress());
        dto.setInnerTemplateNo(messageTemplate.getTemplateNo());
        dto.setTemplateNo(channelTemplate.getTemplateNo());
        dto.setTemplateMap(request.getParams());
        dto.setAppRequestNo(message.getRequestNo());
        dto.setRequestChannelNo(message.getMessageOrderNo());
        dto.setTemplate(messageTemplate);
        dto.setChannelTemplate(channelTemplate);
        dto.setRequest(request);
        SendSmsCommonResponseDto response = smsSendManagerComposite.sendMessage(dto);
        messageManager.updateToProcessing(response.getBizId(), response.getRequestId(), message.getId());
        mnsLimiter.setSendSuccess(request);
      } catch (RateLimitException e) {
        messageManager.updateToFail(message.getId());
        mnsLimiter.setSendFail(request, e);
        // don't rethrow rate limit exception to let client retry
      } catch (BizException e) {
        messageManager.updateToFail(message.getId());
        mnsLimiter.setSendFail(request, e);
        throw e;
      }
    } catch (Exception e) {
      dingDingClient.notifySingleMessage(request.getTemplateNo(), request.getRequestNo(), e.getMessage());
      mnsLimiter.setSendFail(request, e);
      throw e;
    }
  }

  @Override
  @MethodAroundLog(source = "Controller", target = "Service", value = "发送批量短信", requestExcludeName = {"smsContentDtos"})
  public void sendBatchMessage(SendBatchMessageRequestDto request) {
    // 检查上游是否可调用
    checkAppCode(request.getAppCode());

    // 过滤重复手机号
    List<SmsContentDto> filterContentDtos = filterMobileRepeated(request);

    // 获取重复手机号条数
    int repeatNum = getRepeatedNum(request, filterContentDtos);

    // 查询并校验
    MNSMessageTemplate messageTemplate = messageTemplateManager.queryAndCheckInnerTemplate(request.getTemplateNo());

    // 验证请求参数
    if(messageTemplate.hasParam()) {
      checkTemplateParam(request);
    }

    // 保存批量短信请求
    MNSBatchMessageRequest entity = saveBatchMessageRequest(request, filterContentDtos, repeatNum);

    // 是否允许发送批量消息
    if(YesNoEnum.NO.getCode().equals(enableSendBatchMessage)) {
      // 更新批量消息请求为成功
      batchMessageRequestManger.updateRequestSuccess(CommonConstants.MOCK_CHANNEL, entity.getId());
      return;
    }

    // 实时直接发送 MQ
    if(BatchSendTypeEnum.isRealtime(request.getSendType())) {
      sendToMQ(request);
    }
  }

  /**
   * 设置消息成功，如果是验证码消息发送钉钉消息
   * @param request
   * @param message
   * @param template
   */
  private void successMessageAndSendDingDing(MnsRequestDto request,
                                             MNSMessage message, MNSMessageTemplate template) {
    message.setStatus(MessageStatusEnum.SUCCESS.getCode());
    message.setChannelCode(CommonConstants.MOCK_CHANNEL);
    message.setChannelName(CommonConstants.MOCK_CHANNEL);
    message.updateById();

    String messageContent;
    try {
      messageContent = PlaceholderResolver.tryResolve(template.getTemplateContent(), request.getParams());
    } catch (Exception e) {
      log.warn("fail to resolve template content, fallback to old method. request={}, template={}, message={}",
              request, template, message, e);
      //fallback
      messageContent = parse(template.getTemplateContent(), request.getParams());
    }
    dingDingClient.notifyMockMessage(request.getPhoneNo(), template.getTitle(), messageContent);
  }

  /**
   * 过滤重复手机号数据
   * @param request
   */
  private List<SmsContentDto> filterMobileRepeated(SendBatchMessageRequestDto request) {
    List<SmsContentDto> contentDtos = request.getSmsContentDtos();
    if(CollectionUtils.isEmpty(contentDtos)) {
      throw new BizException(ReturnCodeEnum.INVALID_PARAMETER, "短信内容不能为空");
    }
    return contentDtos.stream()
        .filter(distinctByKey(dto -> dto.getPhoneNo()))
        .collect(Collectors.toList());
  }

  /**
   * 过滤函数
   * @param keyExtractor
   * @param <T>
   * @return
   */
  private <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
      Map<Object, Boolean> seen = new ConcurrentHashMap<>();
      return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
  }

  /**
   * 获取重复手机号条数
   * @param request
   * @param dtos
   * @return
   */
  private int getRepeatedNum(SendBatchMessageRequestDto request, List<SmsContentDto> dtos) {
    int sourceSize = request.getSmsContentDtos().size();
    int finalSize = dtos.size();
    return sourceSize - finalSize;
  }

  /**
   * 初始化重试表
   * @param message
   * @return
   */
  private MNSMessageRedo initRedo(MNSMessage message) {
    MNSMessageRedo redo = new MNSMessageRedo();
    redo.setAppNo(message.getAppNo());
    redo.setRequestNo(message.getRequestNo());
    redo.setMessageOrderNo(message.getMessageOrderNo());
    // 重试number由每次重试的时候生成
    redo.setRedoOrderNo("");
    redo.setMessageType(message.getMessageType());
    redo.setNotifyType(message.getNotifyType());
    redo.setStatus(MessageStatusEnum.EXCEPTION.getCode());
    redo.setChannelCode(message.getChannelCode());
    redo.setChannelName(message.getChannelName());
    redo.setTemplateNo(message.getTemplateNo());
    redo.setSubject(message.getSubject());
    redo.setTargetAddress(message.getTargetAddress());
    redo.setRetryingFlag(RetryingFlagEnum.Y.name());
    redo.setRetryCount(0);
    redo.setNotifyRule(message.getNotifyRule());
    MessageContext messageContext = MessageContext.builder().retryCount(0)
        .notifyRule(message.getNotifyRule()).build();
    Integer nextNotifySecond = retryStrategy.getNextNotifySecond(messageContext);
    Date nextNotifyTime = DateUtils.plusSeconds(nextNotifySecond);
    redo.setRetryAt(nextNotifyTime);
    return redo;
  }

  /**
   * 初始化短信模板
   *
   * @param request
   * @param messageChannel
   * @param messageTemplate
   * @return
   */
  private MNSMessage initMessage(MnsRequestDto request,
                                 MNSMessageChannel messageChannel, MNSMessageTemplate messageTemplate) {
    MNSMessage message = new MNSMessage();
    message.setAppNo(request.getAppCode());
    message.setRequestNo(request.getRequestNo());
    message.setBatchNo(request.getRequestNo());
    message.setMessageOrderNo(IdUtil.simpleUUID());
    message.setMessageType(MessageTypeEnum.S.name());
    message.setNotifyType(NotifyTypeEnum.REAL.name());
    message.setStatus(MessageStatusEnum.INIT.getCode());
    message.setChannelCode(messageChannel.getChannelCode());
    message.setChannelName(messageChannel.getChannelName());
    message.setTemplateNo(messageTemplate.getTemplateNo());
    // subject 不能为空
    message.setSubject(JSON.toJSONString(request.getParams()));
    message.setTargetAddress(request.getPhoneNo());
    String notifyRule = retryStrategy.getRetryStrategy()
        .stream().map(integer -> integer + "").collect(Collectors.joining(","));
    message.setNotifyRule(notifyRule);
    message.setRemark("");
    return message;
  }

  /**
   * 检查appNo
   *
   * @param appCode
   */
  private void checkAppCode(String appCode) {
    MNSMessageApp app = mnsMessageAppDao.queryByAppCode(appCode);
    if (app == null) {
      throw new BizException(ReturnCodeEnum.INVALID_PARAMETER,"调用失败, appNo校验失败");
    }
    if (AvailableStatusEnum.NOT_AVAILABLE.getStatus().equals(app.getStatus())) {
      throw new BizException(ReturnCodeEnum.INVALID_PARAMETER,"调用失败, appNo校验失败");
    }
  }

  /**
   * 是否跳过
   * @return
   */
  private boolean shouldSkip(MnsRequestDto request){
    String cellphone = request.getPhoneNo();
    // 白名单包含
    if(checkCandidate(userCellphoneProperties.getWhitelist(), cellphone)) {
      return false;
    }

    // 黑名单包含
    if(checkCandidate(userCellphoneProperties.getBlacklist(), cellphone)) {
      return true;
    }

    // 生产环境
    if(isMaster()) {
      return false;
    }

    return true;
  }

  /**
   * 配置是否包含候选者
   * @param configList
   * @param candidate
   * @return
   */
  private boolean checkCandidate(List<String> configList, String candidate) {
    if(CollectionUtils.isEmpty(configList)){
      return false;
    }
    Optional<String> containCandidateOptional = configList.stream().filter(v -> v.equals(candidate)).findFirst();
    return containCandidateOptional.isPresent();
  }

  /**
   * 检查短信模板参数
   * @param request
   */
  private void checkTemplateParam(SendBatchMessageRequestDto request) {
    List<TemplateParamDto> templateParams = messageTemplateManager.queryTemplateParam(request.getTemplateNo());
    BizException.error(CollectionUtils.isNotEmpty(templateParams),
        ReturnCodeEnum.SYSTEM_ERROR, "变量模板参数名称列表未配置");

    request.getSmsContentDtos().stream().forEach(content -> {
      try {
        JSONObject paramJo = JSON.parseObject(content.getTemplateParams());
        templateParams.stream()
            .filter(param -> paramJo.get(param.getParamName()) != null)
            .findAny()
            .orElseThrow(() -> new BizException(ReturnCodeEnum.INVALID_PARAMETER, "手机号:" + content.getPhoneNo() + ",参数不合法"));
      } catch (Exception e) {
        throw new BizException(ReturnCodeEnum.INVALID_PARAMETER, e.getMessage());
      }
    });
  }

  /**
   * 保存批量短信请求
   * @param request
   * @param filterContentDtos
   * @param repeatNum
   */
  private MNSBatchMessageRequest saveBatchMessageRequest(SendBatchMessageRequestDto request,
                                                         List<SmsContentDto> filterContentDtos, int repeatNum) {
    MNSBatchMessageRequest entity = new MNSBatchMessageRequest();
    entity.setAppNo(request.getAppCode());
    entity.setRequestNo(request.getRequestNo());
    entity.setTemplateNo(request.getTemplateNo());
    entity.setSmsContent(JSON.toJSONString(filterContentDtos));
    entity.setTotalSize(request.getSmsContentDtos().size());
    entity.setRepeatCount(repeatNum);
    entity.setSendType(request.getSendType());
    entity.setStatus(BatchMessageRequestStatusEnum.INITIALIZE.getCode());
    batchMessageRequestManger.saveBatchMessageRequest(entity);
    return entity;
  }

  /**
   * 实时消息 MQ 处理
   * @param request
   */
  private void sendToMQ(SendBatchMessageRequestDto request) {
    BatchMessageSendContext context = new BatchMessageSendContext();
    context.setAppCode(request.getAppCode());
    context.setAppRequestNo(request.getRequestNo());
    context.setInnerTemplateNo(request.getTemplateNo());
    String messageKey = context.getMessageKey();
    log.info("MessageServiceImpl.sendBatchMessage request no {}", messageKey);
    // remove rocketmq
  }

  /**
   * 是否生产环境
   * @return
   */
  private boolean isMaster(){
    String[] activeProfiles = environment.getActiveProfiles();
    if(ArrayUtils.isEmpty(activeProfiles)) {
      return false;
    }
    boolean isMaster = prodProfileString.equals(activeProfiles[0]);
    if(isMaster) {
      return true;
    } else {
      return YesNoEnum.YES.getCode().equals(enableSendSingleMessage);
    }
  }

  /**
   * 解析短信模板
   * @param content
   * @param params
   * @return
   */
  private static String parse(String content, Map<String, Object> params) {
    if(MapUtils.isEmpty(params)) {
      return content;
    }
    try {
      Map<String, String> kvs = new HashMap<>();
      for (Entry<String, Object> paramEntry : params.entrySet()) {
        String key = paramEntry.getKey();
        Object value = paramEntry.getValue();
        if(value != null) {
          kvs.put(key, paramEntry.getValue().toString());
        } else {
          kvs.put(key, "");
        }
      }
      Matcher m = CommonConstants.MESSAGE_TEMPLATE_PARAM_PATTERN.matcher(content);
      StringBuffer sb = new StringBuffer();
      while (m.find()) {
        String group = m.group().replace("${", "").replace("}", "");
        String replacement = kvs.get(group);
        if (replacement == null) {
          replacement = "";
        }
        m.appendReplacement(sb, replacement);
      }
      //把符合的数据追加到sb尾
      m.appendTail(sb);
      return sb.toString();
    } catch (Exception e) {
      LogUtil.error("MessageServiceImpl.parse is error", e);
      return content;
    }
  }

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = environment;
  }
}
