%%%-------------------------------------------------------------------
%%% @author
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 22. 9月 2023 16:38
%%%-------------------------------------------------------------------
-module(iot_host).
-author("aresei").
-include("iot.hrl").

-behaviour(gen_statem).

%% 心跳包检测时间间隔, 15分钟检测一次
-define(HEARTBEAT_INTERVAL, 900 * 1000).

%% 状态
-define(STATE_DENIED, denied).
-define(STATE_ACTIVATED, activated).

%% API
-export([start_link/2, get_name/1, get_alias_name/1, get_pid/1, handle/2, activate/2]).
-export([get_metric/1, publish_message/4, get_aes/1, get_status/1]).
-export([publish_directive/6, send_directive/5]).
-export([create_session/2, attach_channel/2]).
-export([reload_device/2, delete_device/2, activate_device/3]).
-export([heartbeat/1]).

%% gen_statem callbacks
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).

-record(state, {
    host_id :: integer(),
    %% 从数据库里面读取到的数据
    uuid :: binary(),
    %% aes的key, 后续通讯需要基于这个加密
    aes = <<>> :: binary(),
    has_session = false :: boolean(),
    %% 心跳计数器
    heartbeat_counter = 0 :: integer(),
    %% websocket相关
    channel_pid :: undefined | pid(),
    %% 主机的相关信息
    metrics = #{} :: map()
}).

%%%===================================================================
%%% API
%%%===================================================================
-spec get_pid(UUID :: binary()) -> undefined | pid().
get_pid(UUID) when is_binary(UUID) ->
    Name = get_name(UUID),
    whereis(Name).

-spec get_name(UUID :: binary()) -> atom().
get_name(UUID) when is_binary(UUID) ->
    binary_to_atom(<<"iot_host:", UUID/binary>>).

-spec get_alias_name(HostId :: integer()) -> atom().
get_alias_name(HostId0) when is_integer(HostId0) ->
    HostId = integer_to_binary(HostId0),
    binary_to_atom(<<"iot_host_id:", HostId/binary>>).

%% 处理消息
-spec handle(Pid :: pid(), Packet :: {atom(), binary()} | {atom(), {binary(), binary()}}) -> no_return().
handle(Pid, Packet) when is_pid(Pid) ->
    gen_statem:cast(Pid, {handle, Packet}).

-spec get_aes(Pid :: pid()) -> {ok, Aes :: binary()}.
get_aes(Pid) when is_pid(Pid) ->
    gen_statem:call(Pid, get_aes).

-spec get_status(Pid :: pid()) -> {ok, Status :: map()}.
get_status(Pid) when is_pid(Pid) ->
    gen_statem:call(Pid, get_status).

%% 激活主机, true 表示激活； false表示关闭激活
-spec activate(Pid :: pid(), Auth :: boolean()) -> ok.
activate(Pid, Auth) when is_pid(Pid), is_boolean(Auth) ->
    gen_statem:call(Pid, {activate, Auth}).

-spec get_metric(Pid :: pid()) -> {ok, MetricInfo :: map()}.
get_metric(Pid) when is_pid(Pid) ->
    gen_statem:call(Pid, get_metric).

-spec attach_channel(pid(), pid()) -> ok | {error, Reason :: binary()}.
attach_channel(Pid, ChannelPid) when is_pid(Pid), is_pid(ChannelPid) ->
    gen_statem:call(Pid, {attach_channel, ChannelPid}).

-spec create_session(Pid :: pid(), PubKey :: binary()) -> {ok, Reply :: binary()}.
create_session(Pid, PubKey) when is_pid(Pid), is_binary(PubKey) ->
    gen_statem:call(Pid, {create_session, PubKey}).

%% 这里占用的的调用进程的时间
-spec publish_message(Pid :: pid(), CommandType :: integer(), Params :: binary() | {Encrypt :: atom(), Params :: binary()}, Timeout :: integer()) ->
    ok | {ok, Response :: binary()} | {error, Reason :: any()}.
publish_message(Pid, CommandType, Params, Timeout) when is_pid(Pid), is_integer(CommandType), is_integer(Timeout) ->
    case gen_statem:call(Pid, {publish_message, self(), CommandType, Params}) of
        {ok, Ref} ->
            receive
                {ws_response, Ref} ->
                    ok;
                {ws_response, Ref, Response} ->
                    {ok, Response}
            after Timeout ->
                {error, timeout}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

-spec publish_directive(Pid :: pid(), DeviceUUID :: binary(), DirectiveType :: integer(), Version :: binary(), DirectiveParams :: binary() | map(), Timeout :: integer()) ->
    ok | {ok, Response :: binary()} | {error, Reason :: any()}.
publish_directive(Pid, DeviceUUID, DirectiveType, Version, DirectiveParams, Timeout)
    when is_pid(Pid), is_binary(DeviceUUID), is_integer(DirectiveType), is_binary(Version), is_binary(DirectiveParams); is_map(DirectiveParams), is_integer(Timeout) ->

    Directive = #{
        <<"device_uuid">> => DeviceUUID,
        <<"version">> => Version,
        <<"directive_type">> => DirectiveType,
        <<"directive">> => DirectiveParams
    },
    JsonDirective = iolist_to_binary(jiffy:encode(Directive, [force_utf8])),

    case gen_statem:call(Pid, {publish_directive, self(), JsonDirective}) of
        {ok, Ref} ->
            receive
                {ws_response, Ref} ->
                    ok;
                {ws_response, Ref, Response} ->
                    {ok, Response}
            after Timeout ->
                {error, timeout}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

-spec send_directive(Pid :: pid(), DeviceUUID :: binary(), DirectiveType :: integer(), Version :: binary(), DirectiveParams :: binary() | map()) ->
    ok | {error, Reason :: any()}.
send_directive(Pid, DeviceUUID, DirectiveType, Version, DirectiveParams)
    when is_pid(Pid), is_binary(DeviceUUID), is_integer(DirectiveType), is_binary(Version), is_binary(DirectiveParams); is_map(DirectiveParams) ->

    Directive = #{
        <<"device_uuid">> => DeviceUUID,
        <<"version">> => Version,
        <<"directive_type">> => DirectiveType,
        <<"directive">> => DirectiveParams
    },
    JsonDirective = iolist_to_binary(jiffy:encode(Directive, [force_utf8])),

    gen_statem:call(Pid, {send_directive, JsonDirective}).

%% 设备管理相关

-spec reload_device(Pid :: pid(), DeviceUUID :: binary()) -> ok | {error, Reason :: any()}.
reload_device(Pid, DeviceUUID) when is_pid(Pid), is_binary(DeviceUUID) ->
    gen_statem:call(Pid, {reload_device, DeviceUUID}).

-spec delete_device(Pid :: pid(), DeviceUUID :: binary()) -> ok.
delete_device(Pid, DeviceUUID) when is_pid(Pid), is_binary(DeviceUUID) ->
    gen_statem:call(Pid, {delete_device, DeviceUUID}).

-spec activate_device(Pid :: pid(), DeviceUUID :: binary(), Auth :: boolean()) -> ok | {error, Reason :: any()}.
activate_device(Pid, DeviceUUID, Auth) when is_pid(Pid), is_binary(DeviceUUID), is_boolean(Auth) ->
    gen_statem:call(Pid, {activate_device, DeviceUUID, Auth}).

-spec heartbeat(Pid :: pid()) -> no_return().
heartbeat(undefined) ->
    ok;
heartbeat(Pid) when is_pid(Pid) ->
    gen_statem:cast(Pid, heartbeat).

%% @doc Creates a gen_statem process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
start_link(Name, UUID) when is_atom(Name), is_binary(UUID) ->
    gen_statem:start_link({local, Name}, ?MODULE, [UUID], []).

%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================

%% @private
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
%% gen_statem:start_link/[3,4], this function is called by the new
%% process to initialize.
init([UUID]) ->
    case host_bo:get_host_by_uuid(UUID) of
        {ok, #{<<"id">> := HostId, <<"authorize_status">> := AuthorizeStatus}} ->
            %% 通过host_id注册别名, 可以避免通过查询数据库获取HostPid
            AliasName = get_alias_name(HostId),
            global:register_name(AliasName, self()),
            Aes = list_to_binary(iot_util:rand_bytes(32)),

            %% 心跳检测机制
            erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),

            StateName = case AuthorizeStatus =:= 1 of
                            true  -> ?STATE_ACTIVATED;
                            false -> ?STATE_DENIED
                        end,
            {ok, StateName, #state{host_id = HostId, uuid = UUID, aes = Aes, has_session = false}};
        undefined ->
            lager:warning("[iot_host] host uuid: ~p, loaded from mysql failed", [UUID]),
            ignore
    end.

%% @private
%% @doc This function is called by a gen_statem when it needs to find out
%% the callback mode of the callback module.
callback_mode() ->
    handle_event_function.

%% @private
%% @doc If callback_mode is handle_event_function, then whenever a
%% gen_statem receives an event from call/2, cast/2, or as a normal
%% process message, this function is called.

handle_event({call, From}, get_metric, _, State = #state{metrics = Metrics}) ->
    {keep_state, State, [{reply, From, {ok, Metrics}}]};

handle_event({call, From}, get_aes, _, State = #state{aes = Aes}) ->
    {keep_state, State, [{reply, From, {ok, Aes}}]};

%% 获取主机的状态
handle_event({call, From}, get_status, _, State = #state{host_id = HostId, channel_pid = ChannelPid, heartbeat_counter = HeartbeatCounter, metrics = Metrics, has_session = HasSession}) ->
    %% 启动主机相关的devices
    {ok, Devices} = device_bo:get_host_devices(HostId),
    DeviceInfos = lists:map(fun(DeviceUUID) ->
        DevicePid = iot_device:get_pid(DeviceUUID),
        case iot_device:is_activated(DevicePid) of
            true  -> {DeviceUUID, <<"activated">>};
            false -> {DeviceUUID, <<"denied">>}
        end
                            end, Devices),

    HasChannel = (ChannelPid /= undefined),
    Reply = #{
        <<"has_channel">> => HasChannel,
        <<"has_session">> => HasSession,
        <<"heartbeat_counter">> => HeartbeatCounter,
        <<"metrics">> => Metrics,
        <<"device_infos">> => DeviceInfos
    },
    {keep_state, State, [{reply, From, {ok, Reply}}]};

%% 发送普通格式的消息, 激活的时候，会话时创建不成功的; 发送aes类型的命令的时候，必须要求session是存在的
handle_event({call, From}, {publish_message, ReceiverPid, CommandType, {aes, Command0}}, ?STATE_ACTIVATED,
    State = #state{uuid = UUID, aes = AES, channel_pid = ChannelPid, has_session = true}) ->

    lager:debug("[iot_host] host: ~p, will publish aes message: ~p", [UUID, Command0]),
    Command = iot_cipher_aes:encrypt(AES, Command0),
    %% 通过websocket发送请求
    Ref = ws_channel:publish(ChannelPid, ReceiverPid, <<CommandType:8, Command/binary>>),

    {keep_state, State, [{reply, From, {ok, Ref}}]};

%% 只要channel存在，就负责将消息推送到边缘端主机
handle_event({call, From}, {publish_message, ReceiverPid, CommandType, Command}, _,
    State = #state{uuid = UUID, channel_pid = ChannelPid}) when is_binary(Command), is_pid(ChannelPid) ->

    %% 通过websocket发送请求
    lager:debug("[iot_host] host: ~p, will publish message: ~p", [UUID, Command]),
    Ref = ws_channel:publish(ChannelPid, ReceiverPid, <<CommandType:8, Command/binary>>),

    {keep_state, State, [{reply, From, {ok, Ref}}]};

handle_event({call, From}, {publish_message, _, _, _}, _, State = #state{uuid = UUID}) ->
    lager:debug("[iot_host] uuid: ~p, publish_message invalid state: ~p", [UUID, state_map(State)]),
    {keep_state, State, [{reply, From, {error, <<"主机离线，发送命令失败"/utf8>>}}]};

%% 发送指令时, 指令要通过aes加密，必须要求session是存在的
handle_event({call, From}, {publish_directive, ReceiverPid, Directive0}, ?STATE_ACTIVATED,
    State = #state{uuid = UUID, aes = AES, channel_pid = ChannelPid, has_session = true}) ->

    lager:debug("[iot_host] host: ~p, will publish_directive: ~p", [UUID, Directive0]),
    Directive = iot_cipher_aes:encrypt(AES, Directive0),
    %% 通过websocket发送请求
    Ref = ws_channel:publish(ChannelPid, ReceiverPid, <<16:8, Directive/binary>>),

    {keep_state, State, [{reply, From, {ok, Ref}}]};

%% 其他情况下，发送指令是失败的
handle_event({call, From}, {publish_directive, _, Directive}, _, State = #state{uuid = UUID}) ->
    lager:debug("[iot_host] uuid: ~p, publish_directive: ~p, invalid state: ~p", [UUID, Directive, state_map(State)]),
    {keep_state, State, [{reply, From, {error, <<"主机离线，发送指令失败"/utf8>>}}]};

%% 发送指令时, 指令要通过aes加密，必须要求session是存在的
handle_event({call, From}, {send_directive, Directive0}, ?STATE_ACTIVATED,
    State = #state{uuid = UUID, aes = AES, channel_pid = ChannelPid, has_session = true}) ->

    lager:debug("[iot_host] host: ~p, will publish_directive: ~p", [UUID, Directive0]),
    Directive = iot_cipher_aes:encrypt(AES, Directive0),
    %% 通过websocket发送请求
    ws_channel:send(ChannelPid, <<16:8, Directive/binary>>),

    {keep_state, State, [{reply, From, ok}]};

%% 其他情况下，发送指令是失败的
handle_event({call, From}, {send_directive, Directive}, _, State = #state{uuid = UUID}) ->
    lager:debug("[iot_host] uuid: ~p, send_directive: ~p, invalid state: ~p", [UUID, Directive, state_map(State)]),
    {keep_state, State, [{reply, From, {error, <<"主机离线，发送指令失败"/utf8>>}}]};

%% 激活主机
handle_event({call, From}, {activate, true}, _, State = #state{uuid = UUID, aes = Aes, channel_pid = ChannelPid}) when is_pid(ChannelPid) ->
    BinReply = jiffy:encode(#{<<"auth">> => true, <<"aes">> => Aes}, [force_utf8]),
    ws_channel:send(ChannelPid, <<8:8, BinReply/binary>>),
    lager:debug("[iot_host] uuid: ~p, activate: true, will send message: ~p", [UUID, BinReply]),
    {next_state, ?STATE_ACTIVATED, State, [{reply, From, ok}]};
handle_event({call, From}, {activate, true}, _, State = #state{uuid = UUID, channel_pid = undefined}) ->
    lager:debug("[iot_host] uuid: ~p, activate: true, no channel", [UUID]),
    {next_state, ?STATE_ACTIVATED, State, [{reply, From, ok}]};

%% 关闭授权
handle_event({call, From}, {activate, false}, _, State = #state{uuid = UUID, channel_pid = ChannelPid}) when is_pid(ChannelPid) ->
    BinReply = jiffy:encode(#{<<"auth">> => false}, [force_utf8]),
    ws_channel:send(ChannelPid, <<8:8, BinReply/binary>>),
    ws_channel:stop(ChannelPid, closed),
    lager:debug("[iot_host] uuid: ~p, activate: false, will send message: ~p", [UUID, BinReply]),
    {next_state, ?STATE_DENIED, State#state{channel_pid = undefined, has_session = false}, [{reply, From, ok}]};

handle_event({call, From}, {activate, false}, _, State = #state{uuid = UUID, channel_pid = undefined}) ->
    lager:debug("[iot_host] uuid: ~p, activate: false, no channel", [UUID]),
    {next_state, ?STATE_DENIED, State#state{has_session = false}, [{reply, From, ok}]};

%% 绑定channel
handle_event({call, From}, {attach_channel, ChannelPid}, _, State = #state{uuid = UUID, channel_pid = undefined}) ->
    lager:debug("[iot_host] attach_channel host_id uuid: ~p, channel: ~p", [UUID, ChannelPid]),
    erlang:monitor(process, ChannelPid),
    {keep_state, State#state{channel_pid = ChannelPid}, [{reply, From, ok}]};

handle_event({call, From}, {attach_channel, _}, _, State = #state{uuid = UUID, channel_pid = OldChannelPid}) ->
    lager:notice("[iot_host] attach_channel host_id uuid: ~p, old channel exists: ~p", [UUID, OldChannelPid]),
    {keep_state, State, [{reply, From, {error, <<"channel existed">>}}]};

%% 授权通过后，才能将主机的状态设置为在线状态
handle_event({call, From}, {create_session, PubKey}, ?STATE_ACTIVATED, State = #state{uuid = UUID, aes = Aes}) ->
    Reply = #{<<"a">> => true, <<"aes">> => Aes},
    EncReply = iot_cipher_rsa:encode(Reply, PubKey),
    {ok, AffectedRow} = host_bo:change_status(UUID, ?HOST_ONLINE),
    report_event(UUID, ?HOST_ONLINE),
    lager:debug("[iot_host] host_id(session) uuid: ~p, create_session, will change status, affected_row: ~p", [UUID, AffectedRow]),
    {keep_state, State#state{has_session = true}, [{reply, From, {ok, <<10:8, EncReply/binary>>}}]};

handle_event({call, From}, {create_session, PubKey}, ?STATE_DENIED, State = #state{uuid = UUID}) ->
    lager:debug("[iot_host] host_id(denied) uuid: ~p, create_session, will not change host status", [UUID]),
    Reply = #{<<"a">> => false, <<"aes">> => <<"">>},
    EncReply = iot_cipher_rsa:encode(Reply, PubKey),
    {keep_state, State#state{has_session = false}, [{reply, From, {ok, <<10:8, EncReply/binary>>}}]};

%% 重新加载设备信息
handle_event({call, From}, {reload_device, DeviceUUID}, _, State) ->
    case iot_device_sup:ensured_device_started(DeviceUUID) of
        {ok, DevicePid} ->
            iot_device:reload(DevicePid),
            {keep_state, State, [{reply, From, ok}]};
        {error, Reason} ->
            {keep_state, State, [{reply, From, {error, Reason}}]}
    end;

%% 删除设备
handle_event({call, From}, {delete_device, DeviceUUID}, _, State) ->
    case iot_device:get_pid(DeviceUUID) of
        undefined ->
            ok;
        DevicePid when is_pid(DevicePid) ->
            iot_device_sup:delete_device(DeviceUUID)
    end,
    {keep_state, State, [{reply, From, ok}]};

%% 激活设备
handle_event({call, From}, {activate_device, DeviceUUID, Auth}, _, State) ->
    case iot_device_sup:ensured_device_started(DeviceUUID) of
        {ok, DevicePid} ->
            iot_device:auth(DevicePid, Auth),

            {keep_state, State, [{reply, From, ok}]};
        {error, Reason} ->
            {keep_state, State, [{reply, From, {error, Reason}}]}
    end;

%% 需要将消息转换成json格式然后再处理, 需要在host进程里面处理, 数据带有props，服务端暂时未用到
handle_event(cast, {handle, {data, Data}}, ?STATE_ACTIVATED, State = #state{aes = AES, has_session = true}) ->
    PlainData = iot_cipher_aes:decrypt(AES, Data),
    case catch jiffy:decode(PlainData, [return_maps]) of
        Info when is_map(Info) ->
            handle_data(Info, State);
        Other ->
            lager:notice("[iot_host] the data is invalid json: ~p", [Other])
    end,
    {keep_state, State};

%% ping的数据是通过aes加密后的，因此需要在有会话的情况下才行
handle_event(cast, {handle, {ping, CipherMetric}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, aes = AES, has_session = true}) ->
    MetricsInfo = iot_cipher_aes:decrypt(AES, CipherMetric),
    case catch jiffy:decode(MetricsInfo, [return_maps]) of
        Metrics when is_map(Metrics) ->
            lager:debug("[iot_host] host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
            {keep_state, State#state{metrics = Metrics}};
        Other ->
            lager:warning("[iot_host] host_id: ~p, ping is invalid json: ~p", [UUID, Other]),
            {keep_state, State}
    end;

handle_event(cast, {handle, {inform, Info0}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, host_id = HostId, aes = AES, has_session = true}) ->
    Info = iot_cipher_aes:decrypt(AES, Info0),
    case catch jiffy:decode(Info, [return_maps]) of
        #{<<"at">> := At, <<"services">> := ServiceInforms} ->
            lager:debug("[iot_host] host: ~p, service infos is: ~p", [UUID, ServiceInforms]),
            lists:foreach(fun(#{<<"props">> := Props, <<"name">> := Name, <<"version">> := Version, <<"version_copy">> := VersionCopy, <<"status">> := Status}) ->
                %% props 主机id:场景id:微服务id
                {SceneId, MicroId} = parse_props(Props),
                micro_inform_log:insert(#{
                    <<"host_id">> => HostId,
                    <<"scene_id">> => SceneId,
                    <<"service_name">> => Name,
                    <<"version">> => Version,
                    <<"version_copy">> => VersionCopy,
                    <<"status">> => Status,
                    <<"created_at">> => At
                }),
                micro_set_bo:change_status(HostId, SceneId, MicroId, Status)
                          end, ServiceInforms);
        Error ->
            lager:warning("[iot_host] inform get error: ~p", [Error])
    end,
    {keep_state, State};

handle_event(cast, {handle, {feedback_step, Info0}}, ?STATE_ACTIVATED, State = #state{aes = AES, has_session = true}) ->
    Info = iot_cipher_aes:decrypt(AES, Info0),
    case catch jiffy:decode(Info, [return_maps]) of
        Data = #{<<"task_id">> := TaskId, <<"code">> := Code} ->
            Result = scene_feedback_step:insert(#{
                <<"task_id">> => TaskId,
                <<"code">> => Code,
                <<"created_at">> => iot_util:current_time()
            }),
            lager:debug("[iot_host] feedback_step info: ~p, insert result: ~p", [Data, Result]);
        Other ->
            lager:warning("[iot_host] feedback_step error: ~p", [Other])
    end,
    {keep_state, State};

handle_event(cast, {handle, {feedback_result, Info0}}, ?STATE_ACTIVATED, State = #state{aes = AES, has_session = true}) ->
    Info = iot_cipher_aes:decrypt(AES, Info0),
    case catch jiffy:decode(Info, [return_maps]) of
        #{<<"task_id">> := TaskId, <<"time">> := Time, <<"code">> := Code, <<"reason">> := Reason, <<"error">> := Error, <<"type">> := Type} ->
            scene_feedback:insert(#{
                <<"task_id">> => TaskId,
                <<"task_type">> => Type,
                <<"code">> => Code,
                <<"reason">> => Reason,
                <<"error">> => Error,
                <<"created_at">> => Time
            });
        Other ->
            lager:warning("[iot_host] feedback_result error: ~p", [Other])
    end,
    {keep_state, State};

handle_event(cast, {handle, {event, Event0}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, aes = AES, has_session = true}) ->
    EventText = iot_cipher_aes:decrypt(AES, Event0),
    lager:debug("[iot_host] uuid: ~p, get event: ~p", [UUID, EventText]),
    case catch jiffy:decode(EventText, [return_maps]) of
        #{<<"event_type">> := ?EVENT_DEVICE, <<"params">> := #{<<"device_uuid">> := DeviceUUID, <<"status">> := Status}} ->
            DevicePid = iot_device:get_pid(DeviceUUID),
            iot_device:change_status(DevicePid, Status);
        Event when is_map(Event) ->
            lager:warning("[iot_host] host: ~p, event: ~p, not supported", [UUID, Event]);
        Other ->
            lager:warning("[iot_host] host: ~p, event error: ~p", [UUID, Other])
    end,
    {keep_state, State};

handle_event(cast, {handle, {ai_event, Event0}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, aes = AES, has_session = true}) ->
    EventText = iot_cipher_aes:decrypt(AES, Event0),
    lager:debug("[iot_host] uuid: ~p, get ai_event: ~p", [UUID, EventText]),
    case catch jiffy:decode(EventText, [return_maps]) of
        #{<<"event_type">> := EventType, <<"params">> := Params0 = #{<<"device_uuid">> := DeviceUUID, <<"props">> := Props}} ->
            case iot_device:is_alive(DeviceUUID) of
                error ->
                    lager:notice("[iot_host] uuid: ~p, device_uuid: ~p is not alive, get ai_event: ~p", [UUID, DeviceUUID, EventText]),
                    ok;
                {ok, DevicePid} ->
                    Params = maps:remove(<<"props">>, Params0),
                    {SceneId, MicroId} = parse_props(Props),

                    %% 保存数据到mysql
                    Message = iolist_to_binary(jiffy:encode(Params, [force_utf8])),
                    case ai_event_logs_bo:insert(UUID, DeviceUUID, SceneId, MicroId, EventType, Message) of
                        {ok, LogId} ->
                            catch iot_api:ai_event(LogId);
                        _ ->
                            ok
                    end,
                    iot_device:change_status(DevicePid, ?DEVICE_ONLINE),

                    %iot_ai_router:route_uuid(DeviceUUID, EventType, Params)
                    iot_device:ai_event(DevicePid, EventType, Params)
            end;
        Event when is_map(Event) ->
            lager:warning("[iot_host] host: ~p, event: ~p, not supported", [UUID, Event]);
        Other ->
            lager:warning("[iot_host] host: ~p, event error: ~p", [UUID, Other])
    end,
    {keep_state, State};

%% 心跳机制
handle_event(cast, heartbeat, _, State = #state{heartbeat_counter = HeartbeatCounter}) ->
    {keep_state, State#state{heartbeat_counter = HeartbeatCounter + 1}};

%% 没有收到心跳包，主机下线, 设备状态不变
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{uuid = UUID, heartbeat_counter = 0, channel_pid = ChannelPid}) ->
    lager:warning("[iot_host] uuid: ~p, heartbeat lost, devices will unknown", [UUID]),
    {ok, #{<<"status">> := Status}} = host_bo:get_host_by_uuid(UUID),
    case Status of
        ?HOST_NOT_JOINED ->
            lager:debug("[iot_host] host: ~p, host_maybe_offline, host not joined, can not change to offline", [UUID]);
        ?HOST_OFFLINE ->
            lager:debug("[iot_host] host: ~p, host_maybe_offline, host now is offline, do nothing", [UUID]);
        ?HOST_ONLINE ->
            {ok, _} = host_bo:change_status(UUID, ?HOST_OFFLINE),
            report_event(UUID, ?HOST_OFFLINE)
    end,

    %% 关闭channel，主机需要重新连接，才能保存状态的一致
    is_pid(ChannelPid) andalso ws_channel:stop(ChannelPid, closed),
    erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),

    {keep_state, State#state{channel_pid = undefined, has_session = false, heartbeat_counter = 0}};

%% 其他情况下需要重置系统计数器
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{}) ->
    erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
    {keep_state, State#state{heartbeat_counter = 0}};

%% 当websocket断开的时候，主机的状态不一定改变；主机的状态改变通过心跳机制，会话状态需要改变
handle_event(info, {'DOWN', _Ref, process, ChannelPid, Reason}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
    lager:warning("[iot_host] uuid: ~p, channel: ~p, down with reason: ~p, has_session: ~p, state: ~p", [UUID, ChannelPid, Reason, HasSession, State]),
    {keep_state, State#state{channel_pid = undefined, has_session = false}};

handle_event(info, {'DOWN', _Ref, process, Pid, Reason}, _, State = #state{uuid = UUID}) ->
    lager:debug("[iot_host] uuid: ~p, process_pid: ~p, down with reason: ~p, state: ~p", [UUID, Pid, Reason, State]),
    {keep_state, State};

handle_event(Event, Info, StateName, State = #state{uuid = UUID}) ->
    lager:warning("[iot_host] host: ~p, event: ~p, unknown message: ~p, state_name: ~p, state: ~p", [UUID, Event, Info, StateName, state_map(State)]),
    {keep_state, State}.

%% @private
%% @doc This function is called by a gen_statem when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_statem terminates with
%% Reason. The return value is ignored.
terminate(Reason, _StateName, _State = #state{uuid = UUID, has_session = HasSession}) ->
    lager:debug("[iot_host] host: ~p, terminate with reason: ~p, has_session: ~p", [UUID, Reason, HasSession]),
    ok.

%% @private
%% @doc Convert process state when code is changed
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
    {ok, StateName, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% 处理相关数据
handle_data(#{<<"device_uuid">> := DeviceUUID, <<"service_name">> := ServiceName, <<"at">> := Timestamp, <<"fields">> := FieldsList, <<"tags">> := Tags}, #state{uuid = UUID})
    when is_binary(DeviceUUID), DeviceUUID /= <<>> ->

    case iot_device:get_pid(DeviceUUID) of
        undefined ->
            lager:warning("[iot_host] host uuid: ~p, device uuid: ~p not found, fields: ~p, tags: ~p", [UUID, DeviceUUID, FieldsList, Tags]),
            ok;
        DevicePid when is_pid(DevicePid) ->
            case iot_device:is_activated(DevicePid) of
                true ->
                    %% 查找终端设备对应的点位信息
                    iot_router:route_uuid(DeviceUUID, FieldsList, Timestamp),

                    %% 数据写入influxdb
                    NTags = Tags#{<<"uuid">> => UUID, <<"service_name">> => ServiceName, <<"device_uuid">> => DeviceUUID},
                    influx_client:write_data(DeviceUUID, NTags, FieldsList, Timestamp),

                    iot_device:change_status(DevicePid, ?DEVICE_ONLINE),

                    %% 将数据写入到设备的内存缓存中
                    DeviceVals = iot_device:serialize(FieldsList),
                    %% 时间同样精确到毫秒
                    FormattedTimestamp = iot_util:format_timestamp(Timestamp),
                    DeviceDataList = lists:map(fun(Val) -> #device_data{tags = NTags, timestamp = FormattedTimestamp, val = Val} end, DeviceVals),
                    iot_device:data(DevicePid, DeviceDataList);
                false ->
                    lager:warning("[iot_host] host uuid: ~p, device_uuid: ~p not activated, fields: ~p, tags: ~p", [UUID, DeviceUUID, FieldsList, Tags])
            end
    end;

handle_data(#{<<"service_name">> := ServiceName, <<"at">> := Timestamp, <<"fields">> := FieldsList, <<"tags">> := Tags}, #state{uuid = UUID}) ->
    %% 查找终端设备对应的点位信息
    iot_router:route_uuid(UUID, FieldsList, Timestamp),

    %% 数据写入influxdb
    NTags = Tags#{<<"uuid">> => UUID, <<"service_name">> => ServiceName},
    influx_client:write_data(UUID, NTags, FieldsList, Timestamp).

-spec report_event(UUID :: binary(), NewStatus :: integer()) -> no_return().
report_event(UUID, NewStatus) when is_binary(UUID), is_integer(NewStatus) ->
    TextMap = #{
        0 => <<"离线"/utf8>>,
        1 => <<"在线"/utf8>>
    },

    %% 设备的状态信息上报给中电
    Timestamp = iot_util:timestamp_of_seconds(),
    FieldsList = [#{
        <<"key">> => <<"host_status">>,
        <<"value">> => NewStatus,
        <<"value_text">> => maps:get(NewStatus, TextMap),
        <<"unit">> => 0,
        <<"type">> => <<"DI">>,
        <<"name">> => <<"主机状态"/utf8>>,
        <<"timestamp">> => Timestamp
    }],
    iot_router:route_uuid(UUID, FieldsList, Timestamp),
    lager:debug("[iot_host] host_uuid: ~p, route fields: ~p", [UUID, FieldsList]).

%% 将当前的state转换成map
state_map(#state{host_id = HostId, uuid = UUID, aes = Aes, has_session = HasSession, heartbeat_counter = HeartbeatCounter, channel_pid = ChannelPid, metrics = Metrics}) ->
    #{
        host_id => HostId,
        uuid => UUID,
        aes => Aes,
        has_session => HasSession,
        heartbeat_counter => HeartbeatCounter,
        channel_pid => ChannelPid,
        metrics => Metrics
    }.

%% props 主机id:场景id:微服务id
-spec parse_props(Props :: undefined | binary()) -> {SceneId :: integer(), MicroId :: integer()}.
parse_props(Props) when is_binary(Props) ->
    %% props 主机id:场景id:微服务id
    [_, SceneId0, MicroId0] = binary:split(Props, <<":">>, [global]),
    SceneId = binary_to_integer(SceneId0),
    MicroId = binary_to_integer(MicroId0),
    {SceneId, MicroId}.