package com.sonic.common.event;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.hash.Hashing;
import com.sonic.common.utils.FastjsonUtils;
import lombok.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.cglib.beans.BeanMap;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 统一系统事件对象.
 * 消息格式如下
 * {
 * "eventId": "0101_xxx" //消息id
 * "eventScene":"teaching",
 * "eventModule":"im",
 * "eventName":"message_sent",
 * "eventTime":"2019-07-22 16:10:10.666",
 * "data":{"message_id":"xxx","tags":"xxx"} //消息ID,消息标签
 * "schemaHash": "xxxx" //数据的schema hash
 * }
 * @author code
 */
@Data
@AllArgsConstructor
public class Event {

    /**
     * 唯一标识.唯一区分一条事件.可以做幂等处理. 默认是app_id + uuid
     */
    private String eventId;

    /**
     * 使用字符串，方便扩展。<br>
     * 建议优先参考{@link BuildInScene}这个枚举值中是否有合适的code
     */
    private String eventScene;

    private String eventModule;

    private String eventName;

    @JSONField(format = "yyyy-MM-dd'T'HH:mm:ss.SSS")
    private LocalDateTime eventTime;

    private MessagePostProcessor messagePostProcessor;

    private Object data;

    /**
     * event的schema hash. 在发送的时候填充.
     * 接受方可以通过简单的检查这个标示来判断event的scheme是否发生变化.
     */
    @Setter(AccessLevel.PACKAGE)
    private String schemaHash;

    /**
     * 提供默认构造函数，仅用于json反序列化时使用<br>
     * <b>不建议直接使用默认构造函数进行event的构建。构建event，请使用Event.builder().xx(xx).build();</b>
     * XXX: 此处不使用@NoArgsConstrutor是因为其于@Builder.Default会产生冲突。;
     * @Deprecated
     */
    public Event(){
    }

    @Builder
    public Event(String eventScene, String eventModule, String eventName, Object data) {
        this.eventScene = eventScene;
        this.eventModule = eventModule;
        this.eventName = eventName;
        this.data = data;

        this.eventTime = LocalDateTime.now();
    }

    public Event(String eventScene, String eventModule, String eventName, MessagePostProcessor messagePostProcessor, Object data) {
        this.eventScene = eventScene;
        this.eventModule = eventModule;
        this.eventName = eventName;
        this.messagePostProcessor = messagePostProcessor;
        this.data = data;

        this.eventTime = LocalDateTime.now();
    }

    public String toJsonString() {
        return JSONObject.toJSONString(this, FastjsonUtils.SERIALIZER_FEATURES);
    }

    @Override
    public String toString() {
        return toJsonString();
    }

    /**
     * 该方法返回一个字符串，该字符串可用于唯一标注一种类型的消息。<br>
     * 对于kafka，可以将该值用于消息的key，<br>
     * 对于rabbit，可以将该值用于消息的routingKey。<br>
     * 以方便消费方快速获取消息类型
     *
     * @return
     */
    @JSONField(serialize = false)
    public EventCode getEventCode() {
        return new EventCode(eventScene, eventModule, eventName);
    }

    @JSONField(serialize = false)
    public String getAppId() {
        return StringUtils.left(eventId, 4);
    }

    /**
     * 发送前，对必填参数做校验
     */
    public void check() {
        Preconditions.checkArgument(!Strings.isNullOrEmpty(eventScene), "发送消息 -> 事件场景不能为空");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(eventModule), "发送消息 -> 事件模块不能为空");
        Preconditions.checkArgument(!Strings.isNullOrEmpty(eventName), "发送消息 -> 事件名字不能为空");
    }

    public <T> T normalizedData(Class<T> tClass) {
        return JSON.parseObject(JSON.toJSONString(data), tClass);
    }

    /**
     * 构建消息的schema hash
     * @param routingKey rabbitmq 的 routingKey
     * @return schema hash
     */
    public String buildSchemaHash(String routingKey) {
        String schema;
        // FIXME: beanmap不能处理data是集合类型对象. 这些需要特殊处理
        // 建议data总是一个bean对象.
        Object data = getData();
        if (data == null) {
            schema = "null";
        } else if (data instanceof Collection
                || data instanceof Map
                || data instanceof Number
                || data.getClass().isArray()
                || data.getClass().isPrimitive()
                || data instanceof String) {
            schema = data.getClass().getCanonicalName();
        } else {
            try {
                BeanMap beanMap = BeanMap.create(data);
                // 取payload中每个key作为schema
                schema = beanMap.keySet().stream().map(e -> e + "=" + beanMap.getPropertyType((String) e).getSimpleName())
                        .sorted().collect(Collectors.joining(",")).toString();
            } catch (Exception e) {
                // ignore
                schema = "error";
            }
        }
        return Hashing.murmur3_128().hashString(routingKey + getEventScene() + getEventModule()
                + getEventName() + schema, Charsets.UTF_8).toString();
    }

    /**
     * 消息发生场景
     */
    @AllArgsConstructor
    @Getter
    public enum BuildInScene {
        /** basic services 基础服务 */
        BS("bs"),
        SONIC("sonic"),
        ;
        private final String code;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class EventCode {
        private String scene;
        private String module;
        private String name;
    }
}
