%%%-------------------------------------------------------------------
%%% @doc Owns one container deployment task stream.
%%% @end
%%%-------------------------------------------------------------------
-module(iot_container_task).

-behaviour(gen_server).

%% API
-export([start_link/2]).
-export([get_pid/2, subscribe/3, stream/3, close/2]).

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

-define(MAX_EVENTS, 200).
-define(STOP_AFTER_CLOSE, 300000).

-record(state, {
    uuid :: binary(),
    task_id :: integer(),
    status = pending :: pending | running | success | fail,
    events = [] :: [{binary(), binary()}],
    listeners = #{} :: #{pid() => reference()},
    close_reason = undefined :: undefined | binary(),
    stop_ref = undefined :: undefined | reference()
}).

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

-spec start_link(binary(), integer()) -> {ok, pid()} | ignore | {error, term()}.
start_link(UUID, TaskId) when is_binary(UUID), is_integer(TaskId) ->
    gen_server:start_link({via, gproc, {n, l, name(UUID, TaskId)}}, ?MODULE, [UUID, TaskId], []).

-spec get_pid(binary(), integer()) -> undefined | pid().
get_pid(UUID, TaskId) when is_binary(UUID), is_integer(TaskId) ->
    gproc:whereis_name({n, l, name(UUID, TaskId)}).

-spec subscribe(binary(), integer(), pid()) -> ok | {error, term()}.
subscribe(UUID, TaskId, ListenerPid) when is_binary(UUID), is_integer(TaskId), is_pid(ListenerPid) ->
    case iot_container_task_sup:ensure_started(UUID, TaskId) of
        {ok, Pid} ->
            gen_server:call(Pid, {subscribe, ListenerPid});
        {error, Reason} ->
            {error, Reason}
    end.

-spec stream(pid(), binary(), binary()) -> ok.
stream(Pid, Type, Stream) when is_pid(Pid), is_binary(Type), is_binary(Stream) ->
    gen_server:cast(Pid, {stream, Type, Stream}).

-spec close(pid(), binary()) -> ok.
close(Pid, Reason) when is_pid(Pid), is_binary(Reason) ->
    gen_server:cast(Pid, {close, Reason}).

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

-spec init([binary() | integer()]) -> {ok, #state{}}.
init([UUID, TaskId]) ->
    ok = iot_log:set_metadata(),
    {ok, #state{uuid = UUID, task_id = TaskId}}.

-spec handle_call(term(), {pid(), term()}, #state{}) ->
    {reply, term(), #state{}}.
handle_call({subscribe, ListenerPid}, _From, State0 = #state{task_id = TaskId, events = Events, listeners = Listeners, close_reason = CloseReason}) ->
    MonRef = erlang:monitor(process, ListenerPid),
    maps:get(ListenerPid, Listeners, undefined) =/= undefined andalso erlang:demonitor(maps:get(ListenerPid, Listeners), [flush]),
    lists:foreach(fun({Type, Stream}) ->
        ListenerPid ! {stream_data, TaskId, Type, Stream}
    end, Events),
    case CloseReason of
        undefined ->
            ok;
        Reason ->
            ListenerPid ! {stream_close, TaskId, Reason}
    end,
    {reply, ok, State0#state{listeners = maps:put(ListenerPid, MonRef, Listeners)}};
handle_call(_Request, _From, State) ->
    {reply, ok, State}.

-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
handle_cast({stream, Type, Stream}, State0 = #state{task_id = TaskId, events = Events0, listeners = Listeners}) ->
    Event = {Type, Stream},
    Events = trim_events(Events0 ++ [Event]),
    maps:foreach(fun(ListenerPid, _MonRef) ->
        ListenerPid ! {stream_data, TaskId, Type, Stream}
    end, Listeners),
    {noreply, State0#state{status = running, events = Events}};
handle_cast({close, Reason}, State0 = #state{task_id = TaskId, listeners = Listeners}) ->
    maps:foreach(fun(ListenerPid, _MonRef) ->
        ListenerPid ! {stream_close, TaskId, Reason}
    end, Listeners),
    Status = close_status(Reason),
    {noreply, ensure_stop_timer(State0#state{status = Status, close_reason = Reason})};
handle_cast(_Request, State) ->
    {noreply, State}.

-spec handle_info(term(), #state{}) -> {noreply, #state{}} | {stop, normal, #state{}}.
handle_info({'DOWN', _Ref, process, ListenerPid, _Reason}, State = #state{listeners = Listeners}) ->
    {noreply, State#state{listeners = maps:remove(ListenerPid, Listeners)}};
handle_info(stop_after_close, State) ->
    {stop, normal, State};
handle_info(_Info, State) ->
    {noreply, State}.

-spec terminate(term(), #state{}) -> ok.
terminate(_Reason, _State) ->
    ok.

-spec code_change(term(), #state{}, term()) -> {ok, #state{}}.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

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

-spec name(binary(), integer()) -> term().
name(UUID, TaskId) ->
    {iot_container_task, UUID, TaskId}.

-spec trim_events([{binary(), binary()}]) -> [{binary(), binary()}].
trim_events(Events) ->
    Len = length(Events),
    case Len > ?MAX_EVENTS of
        true ->
            lists:nthtail(Len - ?MAX_EVENTS, Events);
        false ->
            Events
    end.

-spec close_status(binary()) -> success | fail.
close_status(<<"success">>) ->
    success;
close_status(_) ->
    fail.

-spec ensure_stop_timer(#state{}) -> #state{}.
ensure_stop_timer(State = #state{stop_ref = undefined}) ->
    Ref = erlang:send_after(?STOP_AFTER_CLOSE, self(), stop_after_close),
    State#state{stop_ref = Ref};
ensure_stop_timer(State) ->
    State.
