%%%-------------------------------------------------------------------
%%% @author
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 22. 9月 2023 16:38
%%%-------------------------------------------------------------------
-module(iot_host).
-author("aresei").
-include("iot.hrl").
-include("domain_model.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([lookup_pid/1]).
-export([get_metric/1, get_status/1, kill/1]).
%% 通讯相关
-export([pub/4, attach_channel/2]).
-export([deploy_container/3, start_container/2, stop_container/2, remove_container/2, kill_container/2, config_container/3, get_containers/1, await_reply/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(),
    %% 主机状态
    host_status :: integer(),

    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),
    gproc:whereis_name({n, l, Name}).

-spec lookup_pid(UUID :: binary()) -> {ok, pid()} | {error, Reason :: any()}.
lookup_pid(UUID) when is_binary(UUID) ->
    case get_pid(UUID) of
        undefined ->
            {error, <<"host not found">>};
        HostPid ->
            {ok, HostPid}
    end.

-spec get_name(UUID :: binary()) -> term().
get_name(UUID) when is_binary(UUID) ->
    {iot_host, UUID}.

-spec get_alias_name(HostId :: integer()) -> term().
get_alias_name(HostId0) when is_integer(HostId0) ->
    {iot_host_id, HostId0}.

-spec kill(UUID :: binary()) -> ok | true.
kill(UUID) when is_binary(UUID) ->
    case get_pid(UUID) of
        undefined ->
            ok;
        Pid ->
            exit(Pid, kill)
    end.

%% 处理消息
-spec handle(Pid :: pid(), Packet :: tuple()) -> ok.
handle(Pid, Packet) when is_pid(Pid) ->
    gen_statem:cast(Pid, {handle, Packet}).

-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 | {error, term()}.
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}).

-type request_ref() :: binary().

-spec get_containers(Pid :: pid()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
get_containers(Pid) when is_pid(Pid) ->
    container_call(Pid, docker_container_builder:list_request()).

-spec config_container(Pid :: pid(), ContainerName :: binary(), ConfigJson :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
config_container(Pid, ContainerName, ConfigJson) when is_pid(Pid), is_binary(ContainerName), is_binary(ConfigJson) ->
    container_call(Pid, docker_container_builder:config_request(ContainerName, ConfigJson)).

-spec deploy_container(Pid :: pid(), TaskId :: integer(), Config :: map()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
deploy_container(Pid, TaskId, Config) when is_pid(Pid), is_integer(TaskId), is_map(Config) ->
    case docker_container_builder:deploy_request(TaskId, Config) of
        {ok, Request} ->
            container_call(Pid, Request);
        {error, Reason} ->
            {error, Reason}
    end.

-spec start_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
start_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
    container_call(Pid, docker_container_builder:start_request(ContainerName)).

-spec stop_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
stop_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
    container_call(Pid, docker_container_builder:stop_request(ContainerName)).

-spec kill_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
kill_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
    container_call(Pid, docker_container_builder:kill_request(ContainerName)).

-spec remove_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
remove_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
    container_call(Pid, docker_container_builder:remove_request(ContainerName)).

-spec await_reply(Pid :: pid(), Ref :: request_ref(), Timeout :: integer()) ->
    ok | {ok, Result :: term()} | {error, Reason :: term()}.
await_reply(Pid, Ref, Timeout) when is_pid(Pid), is_binary(Ref), is_integer(Timeout) ->
    receive
        {command_reply, Ref, ok} ->
            ok;
        {command_reply, Ref, {ok, Result}} ->
            {ok, Result};
        {command_reply, Ref, {error, Reason}} ->
            {error, Reason}
    after Timeout ->
        ok = gen_statem:call(Pid, {cancel_command_call, Ref}),
        flush_reply(Ref),
        {error, timeout}
    end.

-spec pub(Pid :: pid(), Topic :: binary(), Qos :: integer(),  Content :: binary()) -> ok | {error, Reason :: any()}.
pub(Pid, Topic, Qos, Content) when is_pid(Pid), is_binary(Topic), is_integer(Qos), is_binary(Content) ->
    gen_statem:call(Pid, {pub, Topic, Qos, Content}).

-spec heartbeat(Pid :: undefined | pid()) -> ok.
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_binary(UUID) ->
    gen_statem:start_link({via, gproc, {n, l, 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]) ->
    ok = iot_log:set_metadata(),
    case iot_api_client:get_host_by_uuid(UUID) of
        {ok, #host_info{id = HostId, authorize_status = AuthorizeStatus, status = HostStatus}} ->
            %% 通过host_id注册别名, 可以避免通过查询数据库获取HostPid
            AliasName = get_alias_name(HostId),
            global:register_name(AliasName, self()),

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

            StateName = case AuthorizeStatus =:= ?HOST_AUTHORIZED of
                            true  -> ?STATE_ACTIVATED;
                            false -> ?STATE_DENIED
                        end,
            {ok, StateName, #state{host_id = HostId, uuid = UUID, host_status = HostStatus, has_session = false}};
        undefined ->
            logger:warning("[iot_host] host uuid: ~p, load host info 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_status, _, State = #state{channel_pid = ChannelPid, heartbeat_counter = HeartbeatCounter, metrics = Metrics, has_session = HasSession, host_status = HostStatus}) ->
    HasChannel = (ChannelPid /= undefined),
    Reply = #{
        <<"has_channel">> => HasChannel,
        <<"has_session">> => HasSession,
        <<"host_status">> => HostStatus,
        <<"heartbeat_counter">> => HeartbeatCounter,
        <<"metrics">> => Metrics
    },
    {keep_state, State, [{reply, From, {ok, Reply}}]};

handle_event({call, From}, {container_call, ReceiverPid, Request}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
    case HasSession andalso is_pid(ChannelPid) of
        true ->
            Ref = request_ref(),
            ok = ssl_channel:container_call(ChannelPid, ReceiverPid, Ref, Request),
            {keep_state, State, [{reply, From, {ok, Ref}}]};
        false ->
            logger:debug("[iot_host] uuid: ~p, invalid state: ~p", [UUID, state_map(State)]),
            {keep_state, State, [{reply, From, {error, <<"主机离线，发送命令失败"/utf8>>}}]}
    end;

handle_event({call, From}, {cancel_command_call, Ref}, _, State = #state{channel_pid = ChannelPid}) ->
    case is_pid(ChannelPid) of
        true ->
            ok = ssl_channel:cancel_command_call(ChannelPid, Ref),
            {keep_state, State, [{reply, From, ok}]};
        false ->
            {keep_state, State, [{reply, From, ok}]}
    end;

%% 发送指令时, pub/sub
handle_event({call, From}, {pub, Topic, Qos, Content}, StateName, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
    case StateName of
        ?STATE_ACTIVATED ->
            case HasSession andalso is_pid(ChannelPid) of
                true ->
                    logger:debug("[iot_host] host: ~p, publish to topic: ~p, content: ~p", [UUID, Topic, Content]),
                    %% 通过websocket发送消息
                    ssl_channel:pub(ChannelPid, Topic, Qos, Content),

                    {keep_state, State, [{reply, From, ok}]};
                false ->
                    logger:debug("[iot_host] uuid: ~p, publish to topic: ~p, content: ~p, invalid state: ~p", [UUID, Topic, Content, state_map(State)]),
                    {keep_state, State, [{reply, From, {error, <<"主机离线，发送失败"/utf8>>}}]}
            end;
        ?STATE_DENIED ->
            {keep_state, State, [{reply, From, {error, <<"主机未激活，发送失败"/utf8>>}}]}
    end;

%% 激活/关闭授权只修改 iot 本地授权状态；efka 连接保持在线，数据是否处理由 host 状态决定。
handle_event({call, From}, {activate, Auth}, _, State = #state{uuid = UUID}) ->
    NStateName = case Auth of
                     true ->
                         ?STATE_ACTIVATED;
                     false ->
                         ?STATE_DENIED
                 end,
    logger:debug("[iot_host] uuid: ~p, state_name change to ~p", [UUID, NStateName]),
    {next_state, NStateName, State, [{reply, From, ok}]};

%% 绑定channel
handle_event({call, From}, {attach_channel, ChannelPid}, StateName, State = #state{uuid = UUID, channel_pid = OldChannelPid, host_status = HostStatus}) ->
    case OldChannelPid == undefined orelse OldChannelPid =:= ChannelPid of
        true ->
            case StateName of
                ?STATE_ACTIVATED ->
                    erlang:monitor(process, ChannelPid),
                    %% 更新主机为在线状态
                    NState = mark_host_online(UUID, HostStatus, State#state{channel_pid = ChannelPid, has_session = true}),
                    {keep_state, NState, [{reply, From, ok}]};
                %% 主机未激活
                ?STATE_DENIED ->
                    logger:notice("[iot_host] attach_channel host_id uuid: ~p, channel: ~p, host denied locally", [UUID, ChannelPid]),
                    erlang:monitor(process, ChannelPid),
                    NState = mark_host_online(UUID, HostStatus, State#state{channel_pid = ChannelPid, has_session = true}),
                    {keep_state, NState, [{reply, From, ok}]}
            end;
        false ->
            logger:notice("[iot_host] attach_channel host_id uuid: ~p, old channel exists: ~p", [UUID, OldChannelPid]),
            {keep_state, State, [{reply, From, {error, <<"channel existed">>}}]}
    end;

%% 数据分发
handle_event(cast, {handle, {data, RouteKey, MetricBin}}, ?STATE_ACTIVATED,
    State = #state{uuid = UUID, has_session = true}) ->
    logger:debug("[iot_host] metric_data host: ~p, route_key: ~p, metric: ~p", [UUID, RouteKey, MetricBin]),
    endpoint_subscription:publish(get_route_key(RouteKey), MetricBin),
    {keep_state, State};

handle_event(cast, {handle, {data, RouteKey, MetricBin}}, StateName,
    State = #state{uuid = UUID, has_session = true}) ->
    logger:notice("[iot_host] host_uuid: ~p, state_name: ~p, metric_data route_key: ~p, metric: ~p, discard", [UUID, StateName, RouteKey, MetricBin]),
    {keep_state, State};

%% ping的数据是通过aes加密后的，因此需要在有会话的情况下才行
handle_event(cast, {handle, {ping, Metrics}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
    logger:debug("[iot_host] ping host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
    {keep_state, State#state{metrics = Metrics}};

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

%% UDP 心跳丢失但 SSL channel 仍在时，不能把 host 标记为离线。
handle_event(info, {timeout, _, heartbeat_ticker}, _,
    State = #state{uuid = UUID, heartbeat_counter = 0, host_status = HostStatus, channel_pid = ChannelPid}) when is_pid(ChannelPid) ->

    erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
    logger:warning("[iot_host] uuid: ~p, udp heartbeat lost but ssl channel is alive: ~p", [UUID, ChannelPid]),
    NState = mark_host_online(UUID, HostStatus, State#state{heartbeat_counter = 0}),
    {keep_state, NState};

%% 没有收到 UDP 心跳且没有 SSL channel，主机下线, 设备状态不变
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{uuid = UUID, heartbeat_counter = 0, host_status = HostStatus}) ->
    logger:warning("[iot_host] uuid: ~p, heartbeat lost, devices will unknown", [UUID]),
    erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
    NState = mark_host_offline(UUID, HostStatus, State#state{channel_pid = undefined, has_session = false, heartbeat_counter = 0}),
    {keep_state, NState};

%% 其他情况下需要重置系统计数器
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}) ->
    logger: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}) ->
    logger: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}) ->
    logger: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}) ->
    logger: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
%%%===================================================================

-spec container_call(Pid :: pid(), Request :: term()) ->
    {ok, Ref :: request_ref()} | {error, Reason :: any()}.
container_call(Pid, Request) when is_pid(Pid) ->
    gen_statem:call(Pid, {container_call, self(), Request}).

-spec get_route_key(binary()) -> binary().
get_route_key(<<"">>) ->
    <<"/">>;
get_route_key(RouteKey) when is_binary(RouteKey) ->
    RouteKey.

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

flush_reply(Ref) ->
    receive
        {command_reply, Ref, _Reply} ->
            ok
    after 0 ->
        ok
    end.

-spec request_ref() -> request_ref().
request_ref() ->
    crypto:strong_rand_bytes(16).

-spec mark_host_online(binary(), integer(), #state{}) -> #state{}.
mark_host_online(UUID, HostStatus, State) ->
    case maybe_mark_host_online(UUID, HostStatus) of
        keep_status ->
            State;
        {next_status, NStatus} ->
            State#state{host_status = NStatus}
    end.

-spec mark_host_offline(binary(), integer(), #state{}) -> #state{}.
mark_host_offline(UUID, HostStatus, State) ->
    case maybe_mark_host_offline(UUID, HostStatus) of
        keep_status ->
            State;
        {next_status, NStatus} ->
            State#state{host_status = NStatus}
    end.

-spec maybe_mark_host_online(binary(), integer()) -> {next_status, NStatus :: integer()} | keep_status.
maybe_mark_host_online(UUID, HostStatus) ->
    case HostStatus of
        ?HOST_OFFLINE ->
            case iot_api_client:change_host_status(UUID, ?HOST_ONLINE) of
                {ok, Result} ->
                    logger:debug("[iot_host] uuid: ~p, change host status online success: ~p", [UUID, Result]),
                    {next_status, ?HOST_ONLINE};
                {error, Reason} ->
                    logger:warning("[iot_host] uuid: ~p, change host status online failed: ~p", [UUID, Reason]),
                    keep_status
            end;
        _ ->
            keep_status
    end.

-spec maybe_mark_host_offline(binary(), integer()) -> {next_status, NStatus :: integer()} | keep_status.
maybe_mark_host_offline(UUID, HostStatus) ->
    case HostStatus of
        ?HOST_ONLINE ->
            case iot_api_client:change_host_status(UUID, ?HOST_OFFLINE) of
                {ok, Result} ->
                    logger:debug("[iot_host] uuid: ~p, change host status offline success: ~p", [UUID, Result]),
                    {next_status, ?HOST_OFFLINE};
                {error, Reason} ->
                    logger:warning("[iot_host] uuid: ~p, change host status offline failed: ~p", [UUID, Reason]),
                    keep_status
            end;
        _ ->
            keep_status
    end.
