%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2024, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 23. 7月 2024 14:51
%%%-------------------------------------------------------------------
-module(iot_jinzhi_endpoint).
-author("anlicheng").

-behaviour(gen_server).

%% API
-export([start_link/0]).

-export([get_pid/0, batch_forward/3]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-define(SERVER, ?MODULE).

%% 消息重发间隔
-define(RETRY_INTERVAL, 5000).
%% 系统id
-define(SYS_ID, <<"ZNWLZJJKXT">>).

-record(state, {
    id = 1,
    url :: string(),
    logger_pid :: pid(),
    pri_key :: public_key:private_key(),
    %% 定时器对应关系
    timer_map = #{}
}).

%%%===================================================================
%%% API
%%%===================================================================

-spec get_pid() -> undefined | pid().
get_pid() ->
    whereis(?MODULE).

-spec batch_forward(LocationCode :: binary(), DynamicLocationCode :: binary(), Events :: list()) -> no_return().
batch_forward(LocationCode, DynamicLocationCode, Events) when is_binary(LocationCode), is_binary(DynamicLocationCode), is_list(Events) ->
    gen_server:cast(?MODULE, {batch_forward, LocationCode, DynamicLocationCode, Events}).

%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
    {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
    {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term()} | ignore).
init([]) ->
    {ok, Opts} = application:get_env(iot, jinzhi),

    PriFile = proplists:get_value(pri_key, Opts),
    Url = proplists:get_value(url, Opts),

    {ok, LoggerPid} = iot_logger:start_link("ai_event_data"),
    PriKey = generate_private_key(PriFile),

    {ok, #state{url = Url, logger_pid = LoggerPid, pri_key = PriKey}}.

%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
    State :: #state{}) ->
    {reply, Reply :: term(), NewState :: #state{}} |
    {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
    {noreply, NewState :: #state{}} |
    {noreply, NewState :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
    {stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Request, _From, State = #state{}) ->
    {reply, ok, State}.

%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
    {noreply, NewState :: #state{}} |
    {noreply, NewState :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: #state{}}).
handle_cast({batch_forward, LocationCode, DynamicLocationCode, Events}, State = #state{id = Id, timer_map = TimerMap, pri_key = PriKey, url = Url}) ->
    ReqBody = format_events(LocationCode, DynamicLocationCode, Events, PriKey),
    catch do_post(Url, Id, ReqBody),
    TimerRef = erlang:start_timer(?RETRY_INTERVAL, self(), {repost_ticker, Id, ReqBody}),

    {noreply, State#state{id = Id + 1, timer_map = maps:put(Id, TimerRef, TimerMap)}}.

%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
    {noreply, NewState :: #state{}} |
    {noreply, NewState :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: #state{}}).
handle_info({ack, Id, Body, RespBody}, State = #state{timer_map = TimerMap, logger_pid = LoggerPid}) ->
    %% 记录日志
    iot_logger:write(LoggerPid, [Body, RespBody]),
    case maps:take(Id, TimerMap) of
        error ->
            {noreply, State};
        {TimerRef, NTimerMap} ->
            is_reference(TimerRef) andalso erlang:cancel_timer(TimerRef),
            {noreply, State#state{timer_map = NTimerMap}}
    end;

handle_info({timeout, _, {repost_ticker, Id, ReqBody}}, State = #state{timer_map = TimerMap, url = Url}) ->
    lager:debug("[iot_jinzhi_endpoint] repost data: ~p", [ReqBody]),
    catch do_post(Url, Id, ReqBody),
    TimerRef = erlang:start_timer(?RETRY_INTERVAL, self(), {repost_ticker, Id, ReqBody}),

    {keep_state, State#state{timer_map = maps:put(Id, TimerRef, TimerMap)}};

handle_info({'DOWN', _MRef, process, _Pid, normal}, State) ->
    {noreply, State};

handle_info({'DOWN', _MRef, process, _Pid, Reason}, State) ->
    lager:notice("[iot_jinzhi_endpoint] task process down with reason: ~p", [Reason]),
    {noreply, State}.

%% @private
%% @doc This function is called by a gen_server 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_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
    State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
    ok.

%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
    Extra :: term()) ->
    {ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec do_post(Url :: string(), Id :: integer(), Body :: binary()) -> no_return().
do_post(Url, Id, Body) when is_list(Url), is_integer(Id), is_binary(Body) ->
    ReceiverPid = self(),
    %% 异步提交任务
    spawn_monitor(fun() ->
        Headers = [
            {<<"content-type">>, <<"application/json">>}
        ],
        case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
            {ok, 200, _, ClientRef} ->
                {ok, RespBody} = hackney:body(ClientRef),
                hackney:close(ClientRef),
                ReceiverPid ! {ack, Id, Body, RespBody};
            {ok, HttpCode, _, ClientRef} ->
                {ok, RespBody} = hackney:body(ClientRef),
                hackney:close(ClientRef),
                lager:warning("[iot_jinzhi_endpoint] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]);
            {error, Reason} ->
                lager:warning("[iot_jinzhi_endpoint] send body: ~p, get error is: ~p", [Body, Reason])
        end
                  end).

%% 格式话要发送的数据，避免多次格式化处理
-spec format_events(LocationCode :: binary(), DynamicLocationCode :: binary(), Events :: list(), PriKey :: public_key:private_key()) -> binary().
format_events(LocationCode, DynamicLocationCode, Events, PriKey) when is_binary(LocationCode), is_binary(DynamicLocationCode), is_list(Events) ->
    DeviceInfo = format_event(LocationCode, hd(Events)),
    ReqData = #{
        <<"sign">> => sign(DeviceInfo, PriKey),
        <<"sysId">> => ?SYS_ID,
        <<"taskId">> => <<"">>,
        <<"count">> => length(Events),
        <<"deviceInfo">> => DeviceInfo
    },
    iolist_to_binary(jiffy:encode(ReqData, [force_utf8])).

format_event(LocationCode, {_EventType, #{<<"event_code">> := EventCode, <<"description">> := Description, <<"datetime">> := Datetime, <<"attachments">> := Attachments0}}) ->
    Attachments = lists:map(fun(#{<<"filename">> := Filename}) ->
        {ok, FileUrl} = iot_util:file_uri(Filename),
        Name = filename:basename(FileUrl),
        #{<<"name">> => Name, <<"url">> => FileUrl}
                            end, Attachments0),

    % <<"occurrenceTime">> => <<"2023-06-10 12:00:00">>,

    #{
        <<"location">> => LocationCode,
        <<"category">> => EventCode,
        <<"description">> => Description,
        <<"occurrenceTime">> => Datetime,
        <<"attachments">> => Attachments
    }.

-spec generate_private_key(PriFile :: string()) -> public_key:private_key().
generate_private_key(PriFile) when is_list(PriFile) ->
    PriKeyFile =  code:priv_dir(iot) ++ "/" ++ PriFile,
    %% 私钥保存解析后的
    {ok, PriKeyData} = file:read_file(PriKeyFile),
    PriDerData = base64:decode(PriKeyData),
    public_key:der_decode('PrivateKeyInfo', PriDerData).

%% 数据签名
-spec sign(M :: #{}, PrivateKey :: public_key:private_key()) -> binary().
sign(M, PrivateKey) when is_map(M) ->
    Json = serialize(M),
    Hash = iolist_to_binary(io_lib:format("~64.16.0b", [binary:decode_unsigned(crypto:hash(sha256, Json))])),
    RsaEncoded = public_key:encrypt_private(Hash, PrivateKey),

    base64:encode(RsaEncoded).

%% 简单的序列号，sign签名
-spec serialize(M :: map()) -> JsonString :: binary().
serialize(M) when is_map(M) ->
    L = maps:to_list(M),
    L1 = lists:sort(fun({K, _}, {K1, _}) -> K < K1 end, L),
    serialize(L1, []).
serialize([], Target) ->
    B = iolist_to_binary(lists:join(<<$,>>, lists:reverse(Target))),
    <<${, B/binary, $}>>;
serialize([{K, V}|T], Target) ->
    V1 = if
             is_integer(V) ->
                 integer_to_binary(V);
             is_float(V) ->
                 float_to_binary(V);
             is_binary(V) ->
                 <<$", V/binary, $">>;
             is_boolean(V) andalso V ->
                 <<"true">>;
             is_boolean(V) andalso not V ->
                 <<"false">>;
             is_list(V) ->
                 Items = lists:map(fun(E) -> serialize(E) end, V),
                 V0 = iolist_to_binary(lists:join(<<$,>>, Items)),
                 <<$[, V0/binary, $]>>
         end,
    Item = <<$", K/binary, $", $:, V1/binary>>,
    serialize(T, [Item|Target]).