%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 VMware, Inc. or its affiliates.  All rights reserved.
%%

-module(rabbit_control_misc).

-export([emitting_map/4, emitting_map/5, emitting_map_with_exit_handler/4,
         emitting_map_with_exit_handler/5, wait_for_info_messages/6,
         spawn_emitter_caller/7, await_emitters_termination/1,
         print_cmd_result/2]).

-spec emitting_map(pid(), reference(), fun(), list()) -> 'ok'.
-spec emitting_map(pid(), reference(), fun(), list(), atom()) -> 'ok'.
-spec emitting_map_with_exit_handler
        (pid(), reference(), fun(), list()) -> 'ok'.
-spec emitting_map_with_exit_handler
        (pid(), reference(), fun(), list(), 'continue') -> 'ok'.

-type fold_fun() :: fun((Item :: term(), AccIn :: term()) -> AccOut :: term()).

-spec wait_for_info_messages(pid(), reference(), fold_fun(), InitialAcc, timeout(), non_neg_integer()) -> OK | Err when
      InitialAcc :: term(), Acc :: term(), OK :: {ok, Acc}, Err :: {error, term()}.
-spec spawn_emitter_caller(node(), module(), atom(), [term()], reference(), pid(), timeout()) -> 'ok'.
-spec await_emitters_termination([pid()]) -> 'ok'.

-spec print_cmd_result(atom(), term()) -> 'ok'.

emitting_map(AggregatorPid, Ref, Fun, List) ->
    emitting_map(AggregatorPid, Ref, Fun, List, continue),
    AggregatorPid ! {Ref, finished},
    ok.

emitting_map(AggregatorPid, Ref, Fun, List, continue) ->
    _ = emitting_map0(AggregatorPid, Ref, Fun, List, fun step/4),
    ok.

emitting_map_with_exit_handler(AggregatorPid, Ref, Fun, List) ->
    emitting_map_with_exit_handler(AggregatorPid, Ref, Fun, List, continue),
    AggregatorPid ! {Ref, finished},
    ok.

emitting_map_with_exit_handler(AggregatorPid, Ref, Fun, List, continue) ->
    _ = emitting_map0(AggregatorPid, Ref, Fun, List, fun step_with_exit_handler/4),
    ok.

emitting_map0(AggregatorPid, Ref, Fun, List, StepFun) ->
    [StepFun(AggregatorPid, Ref, Fun, Item) || Item <- List].

step(AggregatorPid, Ref, Fun, Item) ->
    AggregatorPid ! {Ref, Fun(Item), continue},
    ok.

step_with_exit_handler(AggregatorPid, Ref, Fun, Item) ->
    Noop = make_ref(),
    case rabbit_misc:with_exit_handler(
           fun () -> Noop end,
           fun () -> Fun(Item) end) of
        Noop ->
            ok;
        Res  ->
            AggregatorPid ! {Ref, Res, continue},
            ok
    end.

%% Invokes RPC for async info collection in separate (but linked to
%% the caller) process. Separate process waits for RPC to finish and
%% in case of errors sends them in wait_for_info_messages/5-compatible
%% form to aggregator process. Calling process is then expected to
%% do blocking call of wait_for_info_messages/5.
%%
%% Remote function MUST use calls to emitting_map/4 (and other
%% emitting_map's) to properly deliver requested information to an
%% aggregator process.
%%
%% If for performance reasons several parallel emitting_map's need to
%% be run, remote function MUST NOT return until all this
%% emitting_map's are done. And during all this time remote RPC
%% process MUST be linked to emitting
%% processes. await_emitters_termination/1 helper can be used as a
%% last statement of remote function to ensure this behaviour.
spawn_emitter_caller(Node, Mod, Fun, Args, Ref, Pid, Timeout) ->
    _ = spawn_monitor(
          fun () ->
                  case rpc_call_emitter(Node, Mod, Fun, Args, Ref, Pid, Timeout) of
                      {error, _} = Error        ->
                          Pid ! {Ref, error, Error};
                      {bad_argument, _} = Error ->
                          Pid ! {Ref, error, Error};
                      {badrpc, _} = Error       ->
                          Pid ! {Ref, error, Error};
                      _                         ->
                          ok
                  end
          end),
    ok.

rpc_call_emitter(Node, Mod, Fun, Args, Ref, Pid, Timeout) ->
    rabbit_misc:rpc_call(Node, Mod, Fun, Args++[Ref, Pid], Timeout).

%% Aggregator process expects correct numbers of explicits ACKs about
%% finished emission process. While everything is linked, we still
%% need somehow to wait for termination of all emitters before
%% returning from RPC call - otherwise links will be just broken with
%% reason 'normal' and we can miss some errors, and subsequently
%% hang.
await_emitters_termination(Pids) ->
    Monitors = [erlang:monitor(process, Pid) || Pid <- Pids],
    collect_monitors(Monitors).

collect_monitors([]) ->
    ok;
collect_monitors([Monitor|Rest]) ->
    receive
        {'DOWN', Monitor, process, _Pid, normal} ->
            collect_monitors(Rest);
        {'DOWN', Monitor, process, _Pid, noproc} ->
            %% There is a link and a monitor to a process. Matching
            %% this clause means that process has gracefully
            %% terminated even before we've started monitoring.
            collect_monitors(Rest);
        {'DOWN', _, process, Pid, Reason} when Reason =/= normal,
                                               Reason =/= noproc ->
            exit({emitter_exit, Pid, Reason})
    end.

%% Wait for result of one or more calls to emitting_map-family
%% functions.
%%
%% Number of expected acknowledgments is specified by ChunkCount
%% argument. Most common usage will be with ChunkCount equals to
%% number of live nodes, but it's not mandatory - thus more generic
%% name of 'ChunkCount' was chosen.
wait_for_info_messages(Pid, Ref, Fun, Acc0, Timeout, ChunkCount) ->
    _ = notify_if_timeout(Pid, Ref, Timeout),
    wait_for_info_messages(Ref, Fun, Acc0, ChunkCount).

wait_for_info_messages(Ref, Fun, Acc0, ChunksLeft) ->
    receive
        {Ref, finished} when ChunksLeft =:= 1 ->
            {ok, Acc0};
        {Ref, finished} ->
            wait_for_info_messages(Ref, Fun, Acc0, ChunksLeft - 1);
        {Ref, {timeout, T}} ->
            exit({error, {timeout, (T / 1000)}});
        {Ref, []} ->
            wait_for_info_messages(Ref, Fun, Acc0, ChunksLeft);
        {Ref, Result, continue} ->
            wait_for_info_messages(Ref, Fun, Fun(Result, Acc0), ChunksLeft);
        {Ref, error, Error} ->
            {error, simplify_emission_error(Error)};
        {'DOWN', _MRef, process, _Pid, normal} ->
            wait_for_info_messages(Ref, Fun, Acc0, ChunksLeft);
        {'DOWN', _MRef, process, _Pid, Reason} ->
            {error, simplify_emission_error(Reason)};
        _Msg ->
            wait_for_info_messages(Ref, Fun, Acc0, ChunksLeft)
    end.

simplify_emission_error({badrpc, {'EXIT', {{nocatch, EmissionError}, _Stacktrace}}}) ->
    EmissionError;
simplify_emission_error({{nocatch, EmissionError}, _Stacktrace}) ->
    EmissionError;
simplify_emission_error({error, _} = Error) ->
    Error;
simplify_emission_error({bad_argument, _} = Error) ->
    Error;
simplify_emission_error(Anything) ->
    {error, Anything}.

notify_if_timeout(_, _, infinity) ->
    ok;
notify_if_timeout(Pid, Ref, Timeout) ->
    erlang:send_after(Timeout, Pid, {Ref, {timeout, Timeout}}).

print_cmd_result(authenticate_user, _Result) -> io:format("Success~n");
print_cmd_result(join_cluster, already_member) -> io:format("The node is already a member of this cluster~n").
