%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 12. 3月 2023 21:27
%%%-------------------------------------------------------------------
-module(mqtt_postman).
-author("aresei").
-include("iot.hrl").

-behaviour(gen_server).

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

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

-record(state, {
    conn_pid :: pid(),
    topic :: binary(),
    qos = 0 :: integer(),
    inflight = #{}
}).

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

%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(Opts :: list(), Topic :: binary(), Qos :: integer()) ->
    {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Opts, Topic, Qos) when is_list(Opts) andalso is_binary(Topic) andalso (Qos == 0 orelse Qos == 1 orelse Qos == 2) ->
    gen_server:start_link(?MODULE, [Opts, Topic, Qos], []).

%%%===================================================================
%%% 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([Opts, Topic, Qos]) ->
    Opts1 = [{owner, self()} | Opts],
    {ok, ConnPid} = emqtt:start_link(Opts1),
    lager:debug("[mqtt_postman] start connect, options: ~p", [Opts1]),
    {ok, _} = emqtt:connect(ConnPid, 5000),
    lager:debug("[mqtt_postman] connect success, pid: ~p", [ConnPid]),

    {ok, #state{conn_pid = ConnPid, topic = Topic, qos = Qos}}.

%% @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(_Info, _From, 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(_Info, State) ->
    {noreply, State}.

%% @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({disconnected, ReasonCode, Properties}, State = #state{}) ->
    lager:debug("[mqtt_postman] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
    {stop, disconnected, State};
handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload}}, State = #state{conn_pid = _ConnPid}) ->
    lager:debug("[mqtt_postman] Recv a publish packet: ~p, payload: ~p", [Message, Payload]),
    {noreply, State};
handle_info({puback, #{packet_id := PacketId}}, State = #state{inflight = Inflight}) ->
    case maps:take(PacketId, Inflight) of
        {{ReceiverPid, AssocMessage}, RestInflight} ->
            ReceiverPid ! {ack, AssocMessage},
            {noreply, State#state{inflight = RestInflight}};
        error ->
            {noreply, State}
    end;

%% 转发信息
handle_info({post, ReceiverPid, Message}, State = #state{conn_pid = ConnPid, inflight = InFlight, topic = Topic, qos = Qos}) ->
    lager:debug("[mqtt_postman] will publish topic: ~p, message: ~ts, qos: ~p", [Topic, Message, Qos]),
    case emqtt:publish(ConnPid, Topic, #{}, Message, [{qos, Qos}, {retain, true}]) of
        ok ->
            ReceiverPid ! {ack, Message},
            {noreply, State};
        {ok, PacketId} ->
            {noreply, State#state{inflight = maps:put(PacketId, {ReceiverPid, Message}, InFlight)}};
        {error, Reason} ->
            lager:warning("[mqtt_postman] send message to topic: ~p, get error: ~p", [Topic, Reason]),
            {stop, Reason, State}
    end;
handle_info(stop, State) ->
    {stop, normal, State};

handle_info(Info, State = #state{}) ->
    lager:notice("[mqtt_postman] get unknown info: ~p", [Info]),
    {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{conn_pid = ConnPid}) when is_pid(ConnPid) ->
    ok = emqtt:disconnect(ConnPid),
    lager:debug("[mqtt_postman] terminate with reason: ~p", [Reason]),
    ok;
terminate(Reason, _State) ->
    lager:debug("[mqtt_postman] terminate with reason: ~p", [Reason]),
    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
%%%===================================================================