-module(rabbit_classic_queue).
-behaviour(rabbit_queue_type).
-behaviour(rabbit_policy_validator).

-include("amqqueue.hrl").
-include("rabbit_queue_type.hrl").
-include_lib("rabbit_common/include/rabbit.hrl").
-include_lib("kernel/include/logger.hrl").

%% TODO possible to use sets / maps instead of lists?
%% Check performance with QoS 1 and 1 million target queues.
-record(msg_status, {pending :: [pid()],
                     confirmed = [] :: [pid()]}).

-define(STATE, ?MODULE).
-record(?STATE, {
           pid :: pid(),
           unconfirmed = #{} :: #{non_neg_integer() => #msg_status{}},
           monitored = #{} :: #{pid() => ok}
          }).

-define(STATIC_KEYS, [name,
                      durable,
                      auto_delete,
                      arguments,
                      pid,
                      leader,
                      members,
                      owner_pid,
                      exclusive,
                      policy,
                      operator_policy,
                      effective_policy_definition,
                      type]).

-opaque state() :: #?STATE{}.

-export_type([state/0]).

-export([
         is_enabled/0,
         is_compatible/3,
         declare/2,
         delete/4,
         is_recoverable/1,
         recover/2,
         purge/1,
         policy_changed/1,
         stat/1,
         format/2,
         init/1,
         close/1,
         consume/3,
         cancel/3,
         handle_event/3,
         supports_stateful_delivery/0,
         deliver/3,
         settle/5,
         credit/6,
         dequeue/6,
         info/2,
         state_info/1,
         capabilities/0,
         notify_decorators/1
         ]).

-export([delete_crashed/1,
         delete_crashed/2,
         delete_crashed_internal/2,
         delete_crashed_in_backing_queue/1]).

-export([confirm_to_sender/3,
         send_rejection/3,
         deliver_to_consumer/5,
         send_credit_reply/7]).

-export([policy_apply_to_name/0,
         stop/1,
         list_with_minimum_quorum/0,
         drain/1,
         revive/0,
         queue_vm_stats_sups/0,
         queue_vm_ets/0]).

-export([validate_policy/1]).

-rabbit_boot_step(
   {rabbit_classic_queue_type,
    [{description, "Classic queue: queue type"},
     {mfa,      {rabbit_registry, register,
                    [queue, <<"classic">>, ?MODULE]}},
     {cleanup,  {rabbit_registry, unregister,
                 [queue, <<"classic">>]}},
     {requires, rabbit_registry},
     {enables, [?MODULE, rabbit_policy, queue_type_registrations]}]}).

-rabbit_boot_step(
   {?MODULE,
    [{description, "Deprecated queue-master-locator support."
      "Use queue-leader-locator instead."},
     {mfa, {rabbit_registry, register,
            [policy_validator, <<"queue-master-locator">>, ?MODULE]}},
     {mfa, {rabbit_registry, register,
            [operator_policy_validator, <<"queue-master-locator">>, ?MODULE]}},
     {requires, [rabbit_classic_queue_type]},
     {enables, recovery}]}).

validate_policy(Args) ->
    %% queue-master-locator was deprecated in 4.0
    Locator = proplists:get_value(<<"queue-master-locator">>, Args, unknown),
    case Locator of
        unknown ->
            ok;
        _ ->
            case rabbit_queue_location:master_locator_permitted() of
                true ->
                    case lists:member(Locator, rabbit_queue_location:queue_leader_locators()) of
                        true -> ok;
                        false -> {error, "~tp is not a valid master locator", [Locator]}
                    end;
                false ->
                    {error, "use of deprecated queue-master-locator argument is not permitted", []}
                end
    end.

-spec is_enabled() -> boolean().
is_enabled() -> application:get_env(rabbit, classic_queues_enabled, true).

-spec is_compatible(boolean(), boolean(), boolean()) -> boolean().
is_compatible(_, _, _) ->
    true.

validate_arguments(Args) ->
    case lists:keymember(<<"x-queue-master-locator">>, 1, Args) of
        false ->
            ok;
        true ->
            case rabbit_queue_location:master_locator_permitted() of
                true ->
                    ok;
                false ->
                    Warning = rabbit_deprecated_features:get_warning(
                                queue_master_locator),
                    {protocol_error, internal_error, "~ts", [Warning]}
            end
    end.

declare(Q, Node) when ?amqqueue_is_classic(Q) ->
    case validate_arguments(amqqueue:get_arguments(Q)) of
        ok -> do_declare(Q, Node);
        Error -> Error
    end.

do_declare(Q, Node) when ?amqqueue_is_classic(Q) ->
    QName = amqqueue:get_name(Q),
    VHost = amqqueue:get_vhost(Q),
    Node1 = case {Node, rabbit_amqqueue:is_exclusive(Q)} of
                {{ignore_location, Node0}, _} ->
                    Node0;
                {_, true} ->
                    Node;
                _ ->
                    {Node0, _} = rabbit_queue_location:select_leader_and_followers(Q, 1),
                    Node0
            end,
    case rabbit_vhost_sup_sup:get_vhost_sup(VHost, Node1) of
        {ok, _} ->
            gen_server2:call(
              rabbit_amqqueue_sup_sup:start_queue_process(Node1, Q),
              {init, new}, infinity);
        {error, Error} ->
            {protocol_error, internal_error, "Cannot declare a queue '~ts' on node '~ts': ~255p",
             [rabbit_misc:rs(QName), Node1, Error]}
    end.

delete(Q0, IfUnused, IfEmpty, ActingUser) when ?amqqueue_is_classic(Q0) ->
    QName = amqqueue:get_name(Q0),
    case rabbit_amqqueue:lookup(QName) of
        {ok, Q} ->
            QPid = amqqueue:get_pid(Q),
            case rabbit_process:is_process_alive(QPid) of
                true ->
                    delegate:invoke(QPid, {gen_server2, call,
                                           [{delete, IfUnused, IfEmpty, ActingUser},
                                            infinity]});
                false ->
                    #resource{name = Name, virtual_host = Vhost} = QName,
                    case IfEmpty of
                        true ->
                            ?LOG_ERROR("Queue ~ts in vhost ~ts is down. "
                                       "The queue may be non-empty. "
                                       "Refusing to force-delete.",
                                       [Name, Vhost]),
                            {error, not_empty};
                        false ->
                            ?LOG_WARNING("Queue ~ts in vhost ~ts is down. "
                                               "Forcing queue deletion.",
                                               [Name, Vhost]),
                            case delete_crashed_internal(Q, ActingUser) of
                                ok ->
                                    {ok, 0};
                                {error, timeout} = Err ->
                                    Err
                            end
                    end
            end;
        {error, not_found} ->
            %% Assume the queue was deleted
            {ok, 0}
    end.

is_recoverable(Q) when ?is_amqqueue(Q) and ?amqqueue_is_classic(Q) ->
    Node = node(),
    Node =:= amqqueue:qnode(Q) andalso
    (not rabbit_db_queue:consistent_exists(amqqueue:get_name(Q))
     orelse not rabbit_process:is_process_alive(amqqueue:get_pid(Q))).

recover(VHost, Queues) ->
    {ok, BQ} = application:get_env(rabbit, backing_queue_module),
    %% We rely on BQ:start/1 returning the recovery terms in the same
    %% order as the supplied queue names, so that we can zip them together
    %% for further processing in recover_durable_queues.
    {ok, OrderedRecoveryTerms} =
        BQ:start(VHost, [amqqueue:get_name(Q) || Q <- Queues]),
    case rabbit_amqqueue_sup_sup:start_for_vhost(VHost) of
        {ok, _}         ->
            RecoveredQs = recover_durable_queues(lists:zip(Queues,
                                                           OrderedRecoveryTerms)),
            FailedQs = find_missing_queues(Queues,RecoveredQs),
            {RecoveredQs, FailedQs};
        {error, Reason} ->
            ?LOG_ERROR("Failed to start queue supervisor for vhost '~ts': ~ts", [VHost, Reason]),
            throw({error, Reason})
    end.

find_missing_queues(Q1s, Q2s) when length(Q1s) == length(Q2s)->
    [];
find_missing_queues(Q1s, Q2s) ->
    find_missing_queues(lists:sort(Q1s), lists:sort(Q2s), []).

find_missing_queues([], _, Acc) ->
    Acc;
find_missing_queues(Q1s, [], Acc) ->
    Q1s ++ Acc;
find_missing_queues([Q1|Rem1], [Q2|Rem2] = Q2s, Acc) ->
    case amqqueue:get_name(Q1) == amqqueue:get_name(Q2) of
        true ->
            find_missing_queues(Rem1, Rem2, Acc);
        false ->
            find_missing_queues(Rem1, Q2s, [Q1|Acc])
    end.

-spec policy_changed(amqqueue:amqqueue()) -> ok.
policy_changed(Q) ->
    QPid = amqqueue:get_pid(Q),
    %% When using Khepri, projections are guaranteed to be atomic on
    %% the node that processes them, but there might be a slight delay
    %% until they're applied on other nodes. Some test suites fail
    %% intermittently, showing that rabbit_amqqueue_process is reading
    %% the old policy value.
    gen_server2:cast(QPid, {policy_changed, Q}).

stat(Q) ->
    delegate:invoke(amqqueue:get_pid(Q),
                    {gen_server2, call, [stat, infinity]}).


format(Q, Ctx) when ?is_amqqueue(Q) ->
    State = case amqqueue:get_state(Q) of
                live ->
                    running;
                S ->
                    S
            end,
    [{type, rabbit_queue_type:short_alias_of(?MODULE)},
     {state, State},
     {node, node(amqqueue:get_pid(Q))}
     | format_policy_fields(Q, Ctx)].

format_policy_fields(Q, Ctx) ->
    case maps:get(management_stats_disabled, Ctx, true) of
        true ->
            [{policy, i(policy, Q)},
             {operator_policy, i(operator_policy, Q)},
             {effective_policy_definition, i(effective_policy_definition, Q)}];
        false ->
            []
    end.

-spec init(amqqueue:amqqueue()) -> {ok, state()}.
init(Q) when ?amqqueue_is_classic(Q) ->
    {ok, #?STATE{pid = amqqueue:get_pid(Q)}}.

-spec close(state()) -> ok.
close(_State) ->
    ok.

consume(Q, Spec, State0) when ?amqqueue_is_classic(Q) ->
    QPid = amqqueue:get_pid(Q),
    QRef = amqqueue:get_name(Q),
    #{no_ack := NoAck,
      channel_pid := ChPid,
      limiter_pid := LimiterPid,
      limiter_active := LimiterActive,
      mode := Mode,
      consumer_tag := ConsumerTag,
      exclusive_consume := ExclusiveConsume,
      args := Args,
      ok_msg := OkMsg,
      acting_user :=  ActingUser} = Spec,
    case delegate:invoke(QPid,
                         {gen_server2, call,
                          [{basic_consume, NoAck, ChPid, LimiterPid,
                            LimiterActive, Mode, ConsumerTag,
                            ExclusiveConsume, Args, OkMsg, ActingUser},
                           infinity]}) of
        ok ->
            State = State0#?STATE{pid = QPid},
            {ok, ensure_monitor(QRef, State)};
        {error, exclusive_consume_unavailable} ->
            {error, access_refused, "~ts in exclusive use",
             [rabbit_misc:rs(QRef)]};
        {error, Reason} ->
            {error, internal_error, "failed consuming from classic ~ts: ~tp",
             [rabbit_misc:rs(QRef), Reason]}
    end.

cancel(Q, Spec, State) ->
    Pid = amqqueue:get_pid(Q),
    Request = {stop_consumer, Spec#{pid => self()}},
    case delegate:invoke(Pid, {gen_server2, call, [Request, infinity]}) of
        ok ->
            {ok, State#?STATE{pid = Pid}};
        Err ->
            Err
    end.

-spec settle(rabbit_amqqueue:name(), rabbit_queue_type:settle_op(),
             rabbit_types:ctag(), [non_neg_integer()], state()) ->
    {state(), rabbit_queue_type:actions()}.
settle(QName, Op, CTag, MsgIds, State) ->
    case rabbit_feature_flags:is_enabled('rabbitmq_4.3.0') of
        true -> settle_43(QName, Op, CTag, MsgIds, State);
        false -> settle_compat(QName, Op, CTag, MsgIds, State)
    end.

settle_43(_QName, {modify, DelFailed, Undel, Anns}, _CTag, MsgIds, State = #?STATE{pid = Pid}) ->
    Arg = {modify, MsgIds, DelFailed, Undel, Anns, self()},
    delegate:invoke_no_result(Pid, {gen_server2, cast, [Arg]}),
    {State, []};
settle_43(_QName, Op, _CTag, MsgIds, State = #?STATE{pid = Pid}) ->
    Arg = {Op, MsgIds, self()},
    delegate:invoke_no_result(Pid, {gen_server2, cast, [Arg]}),
    {State, []}.

settle_compat(QName, {modify, _DelFailed, Undel, _Anns}, CTag, MsgIds, State) ->
    %% translate modify into other op
    Op = case Undel of
             true ->
                 discard;
             false ->
                 requeue
         end,
    settle_compat(QName, Op, CTag, MsgIds, State);
settle_compat(_QName, Op, _CTag, MsgIds, State = #?STATE{pid = Pid}) ->
    Arg = case Op of
              complete ->
                  {ack, MsgIds, self()};
              _ ->
                  {reject, Op == requeue, MsgIds, self()}
          end,
    delegate:invoke_no_result(Pid, {gen_server2, cast, [Arg]}),
    {State, []}.

credit(_QName, Ctag, DeliveryCountRcv, LinkCreditRcv, Drain, #?STATE{pid = QPid} = State) ->
    Request = {credit, self(), Ctag, DeliveryCountRcv, LinkCreditRcv, Drain},
    delegate:invoke_no_result(QPid, {gen_server2, cast, [Request]}),
    {State, []}.

handle_event(QName, {confirm, MsgSeqNos, Pid}, #?STATE{unconfirmed = U0} = State) ->
    %% confirms should never result in rejections
    {Unconfirmed, ConfirmedSeqNos, []} =
        settle_seq_nos(MsgSeqNos, Pid, U0, confirm),
    Actions = [{settled, QName, ConfirmedSeqNos}],
    %% handle confirm event from queues
    %% in this case the classic queue should track each individual publish and
    %% the processes involved and only emit a settle action once they have all
    %% been received (or DOWN has been received).
    %% Hence this part of the confirm logic is queue specific.
    {ok, State#?STATE{unconfirmed = Unconfirmed}, Actions};
handle_event(_QName, {deliver, _, _, _} = Delivery, #?STATE{} = State) ->
    {ok, State, [Delivery]};
handle_event(QName, {reject_publish, SeqNo, _QPid},
             #?STATE{unconfirmed = U0} = State) ->
    %% It does not matter which queue rejected the message,
    %% if any queue did, it should not be confirmed.
    {U, Rejected} = reject_seq_no(SeqNo, U0),
    Actions = [{rejected, QName, maxlen, Rejected}],
    {ok, State#?STATE{unconfirmed = U}, Actions};
handle_event(QName, {down, Pid, Info}, #?STATE{monitored = Monitored,
                                               unconfirmed = U0} = State0) ->
    State = State0#?STATE{monitored = maps:remove(Pid, Monitored)},
    Actions0 = [{queue_down, QName}],
    case rabbit_misc:is_abnormal_exit(Info) of
        false when Info =:= normal ->
            %% queue was deleted
            {eol, []};
        false ->
            MsgSeqNos = maps:keys(
                          maps:filter(fun (_, #msg_status{pending = Pids}) ->
                                              lists:member(Pid, Pids)
                                      end, U0)),
            {Unconfirmed, Settled, Rejected} = settle_seq_nos(MsgSeqNos, Pid, U0, down),
            Actions1 = case Rejected of
                           [] ->
                               Actions0;
                           _ ->
                               [{rejected, QName, down, Rejected} | Actions0]
                       end,
            Actions = case Settled of
                          [] ->
                              Actions1;
                          _ ->
                              [{settled, QName, Settled} | Actions1]
                      end,
            {ok, State#?STATE{unconfirmed = Unconfirmed}, Actions};
        true ->
            %% any abnormal exit should be considered a full reject of the
            %% oustanding message ids
            MsgIds = maps:fold(
                          fun (SeqNo, Status, Acc) ->
                                  case lists:member(Pid, Status#msg_status.pending) of
                                      true ->
                                          [SeqNo | Acc];
                                      false ->
                                          Acc
                                  end
                          end, [], U0),
            U = maps:without(MsgIds, U0),
            {ok, State#?STATE{unconfirmed = U},
             [{rejected, QName, down, MsgIds} | Actions0]}
    end;
handle_event(_QName, Action, State) ->
    {ok, State, [Action]}.

supports_stateful_delivery() -> true.

-spec deliver([{amqqueue:target(), state()}],
              Delivery :: mc:state(),
              rabbit_queue_type:delivery_options()) ->
    {[{amqqueue:target(), state()}], rabbit_queue_type:actions()}.
deliver(Qs0, Msg0, Options) ->
    %% add guid to content here instead of in rabbit_basic:message/3,
    %% as classic queues are the only ones that need it
    %% @todo Do we need to regenerate it for every time it gets dead lettered?
    %%       We can likely do better and avoid rewriting to the shared message store.
    Msg = mc:prepare(store, mc:set_annotation(id, rabbit_guid:gen(), Msg0)),
    Mandatory = maps:get(mandatory, Options, false),
    MsgSeqNo = maps:get(correlation, Options, undefined),
    Flow = maps:get(flow, Options, noflow),
    Confirm = MsgSeqNo /= undefined,

    {MPids, Qs} = qpids(Qs0, Confirm, MsgSeqNo),
    Delivery = rabbit_basic:delivery(Mandatory, Confirm, Msg, MsgSeqNo, Flow),

    case Flow of
        %% Here we are tracking messages sent by the rabbit_channel
        %% process. We are accessing the rabbit_channel process
        %% dictionary.
        flow ->
            _ = [credit_flow:send(QPid) || QPid <- MPids],
            ok;
        noflow -> ok
    end,
    MMsg = {deliver, Delivery, false},
    delegate:invoke_no_result(MPids, {gen_server2, cast, [MMsg]}),
    {Qs, []}.

-spec dequeue(amqqueue:amqqueue(), NoAck :: boolean(),
              LimiterPid :: pid(), rabbit_types:ctag(),
              Timeout :: non_neg_integer() | infinity | undefined,
              state()) ->
    {ok, Count :: non_neg_integer(), rabbit_amqqueue:qmsg(), state()} |
    {empty, state()}.
dequeue(Q, NoAck, LimiterPid, _CTag, _Timeout, State0) ->
    QName = amqqueue:get_name(Q),
    QPid = amqqueue:get_pid(Q),
    State1 = State0#?STATE{pid = QPid},
    State = ensure_monitor(QName, State1),
    case delegate:invoke(QPid, {gen_server2, call,
                                [{basic_get, self(), NoAck, LimiterPid}, infinity]}) of
        empty ->
            {empty, State};
        {ok, Count, Msg} ->
            {ok, Count, Msg, State}
    end.

-spec state_info(state()) -> #{atom() := term()}.
state_info(_State) ->
    #{}.

%% general queue info
-spec info(amqqueue:amqqueue(), all_keys | rabbit_types:info_keys()) ->
    rabbit_types:infos().
info(Q, Items) ->
    AllStaticItems = is_list(Items) andalso
        lists:all(fun(I) -> lists:member(I, ?STATIC_KEYS) end, Items),
    case AllStaticItems of
        true ->
            static_info(Q, Items);
        false ->
            info_call(Q, Items)
    end.

info_call(Q, Items) ->
    QPid = amqqueue:get_pid(Q),
    Req = case Items of
              all_keys -> info;
              _ -> {info, Items}
          end,
    case delegate:invoke(QPid, {gen_server2, call, [Req, infinity]}) of
        {ok, Result} ->
            Result;
        {error, _Err} ->
            [];
        Result when is_list(Result) ->
            %% this is a backwards compatibility clause
            Result
    end.

static_info(Q, Items) ->
    [{I, i(I, Q)} || I <- Items].

i(name, Q) ->
    amqqueue:get_name(Q);
i(durable, Q) ->
    amqqueue:is_durable(Q);
i(auto_delete, Q) ->
    amqqueue:is_auto_delete(Q);
i(arguments, Q) ->
    amqqueue:get_arguments(Q);
i(pid, Q) ->
    amqqueue:get_pid(Q);
i(leader, Q) ->
    node(i(pid, Q));
i(members, Q) ->
    [i(leader, Q)];
i(owner_pid, Q) when ?amqqueue_exclusive_owner_is(Q, none) ->
    '';
i(owner_pid, Q) ->
    amqqueue:get_exclusive_owner(Q);
i(exclusive, Q) ->
    ExclusiveOwner = amqqueue:get_exclusive_owner(Q),
    is_pid(ExclusiveOwner);
i(policy, Q) ->
    case rabbit_policy:name(Q) of
        none   -> '';
        Policy -> Policy
    end;
i(operator_policy, Q) ->
    case rabbit_policy:name_op(Q) of
        none   -> '';
        Policy -> Policy
    end;
i(effective_policy_definition, Q) ->
    case rabbit_policy:effective_definition(Q) of
        undefined -> #{};
        Def       -> Def
    end;
i(type, _) ->
    classic.

-spec purge(amqqueue:amqqueue()) ->
    {ok, non_neg_integer()}.
purge(Q) when ?is_amqqueue(Q) ->
    QPid = amqqueue:get_pid(Q),
    delegate:invoke(QPid, {gen_server2, call, [purge, infinity]}).

qpids(Qs, Confirm, MsgNo) ->
    lists:foldl(
      fun ({Q, S0}, {MPidAcc, Qs0}) ->
              QPid = amqqueue:get_pid(Q),
              QRef = amqqueue:get_name(Q),
              %% confirm record only if necessary
              S = case S0 of
                      stateless ->
                          S0;
                      #?STATE{unconfirmed = U0} ->
                          Rec = [QPid],
                          U = case Confirm of
                                  false ->
                                      U0;
                                  true ->
                                      U0#{MsgNo => #msg_status{pending = Rec}}
                              end,
                          S1 = S0#?STATE{pid = QPid,
                                         unconfirmed = U},
                          ensure_monitor(QRef, S1)
                  end,
              {[QPid | MPidAcc], [{Q, S} | Qs0]}
      end, {[], []}, Qs).

-spec delete_crashed(amqqueue:amqqueue()) -> ok.
delete_crashed(Q) ->
    delete_crashed(Q, ?INTERNAL_USER).

delete_crashed(Q, ActingUser) ->
    %% Delete from `rabbit_db_queue' from the queue's node. The deletion's
    %% change to the Khepri projection is immediately consistent on that node,
    %% so the call will block until that node has fully deleted and forgotten
    %% about the queue.
    Ret = rpc:call(amqqueue:qnode(Q), ?MODULE, delete_crashed_in_backing_queue,
                   [Q]),
    case Ret of
        {badrpc, {'EXIT', {undef, _}}} ->
            %% Compatibility: if the remote node doesn't yet expose this
            %% function, call it directly on this node.
            ok = delete_crashed_in_backing_queue(Q);
        ok ->
            ok
    end,
    ok = rabbit_amqqueue:internal_delete(Q, ActingUser).

delete_crashed_internal(Q, ActingUser) ->
    delete_crashed_in_backing_queue(Q),
    rabbit_amqqueue:internal_delete(Q, ActingUser).

delete_crashed_in_backing_queue(Q) ->
    {ok, BQ} = application:get_env(rabbit, backing_queue_module),
    BQ:delete_crashed(Q).

recover_durable_queues(QueuesAndRecoveryTerms) ->
    {Results, Failures} =
        gen_server2:mcall(
          [{rabbit_amqqueue_sup_sup:start_queue_process(node(), Q),
            {init, {self(), Terms}}} || {Q, Terms} <- QueuesAndRecoveryTerms]),
    _ = [?LOG_ERROR("Queue ~tp failed to initialise: ~tp",
                    [Pid, Error]) || {Pid, Error} <- Failures],
    [Q || {_, {new, Q}} <- Results].

capabilities() ->
    #{unsupported_policies => [%% Stream policies
                               <<"max-age">>, <<"stream-max-segment-size-bytes">>,
                               <<"initial-cluster-size">>,
                               %% Quorum policies
                               <<"delivery-limit">>, <<"dead-letter-strategy">>,
                               <<"max-in-memory-length">>, <<"max-in-memory-bytes">>, <<"target-group-size">>,
                               %% JMS policies
                               <<"selector-fields">>,
                               <<"selector-field-max-bytes">>],
      queue_arguments => [<<"x-expires">>, <<"x-message-ttl">>, <<"x-dead-letter-exchange">>,
                          <<"x-dead-letter-routing-key">>, <<"x-max-length">>,
                          <<"x-max-length-bytes">>, <<"x-max-priority">>,
                          <<"x-overflow">>, <<"x-queue-mode">>, <<"x-queue-version">>,
                          <<"x-single-active-consumer">>, <<"x-queue-type">>,
                          <<"x-queue-master-locator">>, <<"x-queue-leader-locator">>],
      consumer_arguments => [<<"x-priority">>],
      server_named => true,
      rebalance_module => undefined,
      can_redeliver => false,
      is_replicable => false,
      distribution_modes => [move]
     }.

notify_decorators(Q) when ?is_amqqueue(Q) ->
    QPid = amqqueue:get_pid(Q),
    delegate:invoke_no_result(QPid, {gen_server2, cast, [notify_decorators]}).

reject_seq_no(SeqNo, U0) ->
    reject_seq_no(SeqNo, U0, []).

reject_seq_no(SeqNo, U0, Acc) ->
    case maps:take(SeqNo, U0) of
        {_, U} ->
            {U, [SeqNo | Acc]};
        error ->
            {U0, Acc}
    end.

settle_seq_nos(MsgSeqNos, Pid, U0, Reason) ->
    lists:foldl(
      fun (SeqNo, {U, C0, R0}) ->
              case U of
                  #{SeqNo := Status0} ->
                      case update_msg_status(Reason, Pid, Status0) of
                          #msg_status{pending = [],
                                      confirmed = []} ->
                              %% no pending left and nothing confirmed
                              %% then we reject it
                              {maps:remove(SeqNo, U), C0, [SeqNo | R0]};
                          #msg_status{pending = [],
                                      confirmed = _} ->
                              %% this can be confirmed as there are no pending
                              %% and confirmed isn't empty
                              {maps:remove(SeqNo, U), [SeqNo | C0], R0};
                          MsgStatus ->
                              {U#{SeqNo => MsgStatus}, C0, R0}
                      end;
                  _ ->
                      {U, C0, R0}
              end
      end, {U0, [], []}, MsgSeqNos).

update_msg_status(confirm, Pid, #msg_status{pending = P,
                                            confirmed = C} = S) ->
    Rem = lists:delete(Pid, P),
    S#msg_status{pending = Rem, confirmed = [Pid | C]};
update_msg_status(down, Pid, #msg_status{pending = P} = S) ->
    S#msg_status{pending = lists:delete(Pid, P)}.

ensure_monitor(QName, #?STATE{pid = Pid,
                              monitored = Monitored} = State) ->
    case is_map_key(Pid, Monitored) of
        true ->
            State;
        false ->
            _Ref = erlang:monitor(process, Pid, [{tag, {'DOWN', QName}}]),
            State#?STATE{monitored = Monitored#{Pid => ok}}
    end.

%% part of channel <-> queue api
confirm_to_sender(Pid, QName, MsgSeqNos) ->
    Evt = {confirm, MsgSeqNos, self()},
    send_queue_event(Pid, QName, Evt).

send_rejection(Pid, QName, MsgSeqNo) ->
    Evt = {reject_publish, MsgSeqNo, self()},
    send_queue_event(Pid, QName, Evt).

deliver_to_consumer(Pid, QName, CTag, AckRequired, Message) ->
    Evt = {deliver, CTag, AckRequired, [Message]},
    send_queue_event(Pid, QName, Evt).

send_credit_reply(Pid, QName, Ctag, DeliveryCount, Credit, Avail, Drain) ->
    Evt = case rabbit_feature_flags:is_enabled('rabbitmq_4.3.0') of
              true ->
                  #credit_reply{ctag = Ctag,
                                delivery_count = DeliveryCount,
                                credit = Credit,
                                available = Avail,
                                drain = Drain,
                                properties = #{}};
              false ->
                  {credit_reply, Ctag, DeliveryCount, Credit, Avail, Drain}
          end,
    send_queue_event(Pid, QName, Evt).

send_queue_event(Pid, QName, Event) ->
    gen_server:cast(Pid, {queue_event, QName, Event}).

policy_apply_to_name() ->
    <<"classic_queues">>.

stop(VHost) ->
    ok = rabbit_amqqueue_sup_sup:stop_for_vhost(VHost),
    {ok, BQ} = application:get_env(rabbit, backing_queue_module),
    ok = BQ:stop(VHost).

list_with_minimum_quorum() ->
    [].

drain(_TransferCandidates) ->
    ok.

revive() ->
    ok.

queue_vm_stats_sups() ->
    {[queue_procs], [rabbit_vm:all_vhosts_children(rabbit_amqqueue_sup_sup)]}.

%% return nothing because of this line in rabbit_vm:
%% {msg_index,            MsgIndexETS + MsgIndexProc},
%% it mixes procs and ets,
%% TODO: maybe instead of separating sups and ets
%% I need vm_memory callback that just
%% returns proplist? And rabbit_vm calculates
%% Other as usual by substraction.
queue_vm_ets() ->
    {[], []}.
