%% 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-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%

-module(rabbit_shovel_behaviour).

-export([
         % dynamic calls
         parse/3,
         connect_dest/1,
         connect_source/1,
         init_dest/1,
         init_source/1,
         close_dest/1,
         close_source/1,
         handle_dest/2,
         handle_source/2,
         source_uri/1,
         dest_uri/1,
         source_protocol/1,
         dest_protocol/1,
         source_endpoint/1,
         dest_endpoint/1,
         forward/3,
         ack/3,
         nack/3,
         status/1,
         % common functions
         decr_remaining_unacked/1,
         decr_remaining/2,
         incr_forwarded/1
        ]).

%% It's a rabbit registry class, to easily support new protocols
-export([added_to_rabbit_registry/2,
         removed_from_rabbit_registry/1]).

-include_lib("kernel/include/logger.hrl").

-type tag() :: non_neg_integer().
-type uri() :: string() | binary().
-type ack_mode() :: 'no_ack' | 'on_confirm' | 'on_publish'.
-type source_config() :: #{module => atom(),
                           uris => [uri()],
                           atom() => term()
                          }.
-type dest_config() :: #{module => atom(),
                         uris => [uri()],
                         atom() => term()
                        }.
-type state() :: #{source => source_config(),
                   dest => dest_config(),
                   ack_mode => ack_mode(),
                   atom() => term()}.

-type definition() :: proplists:proplist().
-type mandatory() :: mandatory | optional.

-rabbit_registry_class(shovel_protocol).

-export_type([state/0, source_config/0, dest_config/0, uri/0, tag/0]).

-callback parse(binary(), {source | destination, Conf :: proplists:proplist()}) ->
    source_config() | dest_config().

-callback parse_source(definition()) -> {source_config(), Headers :: proplists:proplist()}.
-callback parse_dest({VHost :: binary(), Name :: binary()}, ClusterName :: atom(), definition(), Headers :: proplists:proplist()) -> dest_config().

-callback validate_src(definition()) -> [ok | {error, Format :: string(), Args :: list()}].
-callback validate_dest(definition()) -> [ok | {error, Format :: string(), Args :: list()}].

-callback validate_src_funs(definition(), User :: binary()) ->
    [{Tag :: binary(), fun((Name :: atom(), Value :: term()) -> ok | {error, string()}), mandatory()}].
-callback validate_dest_funs(definition(), User :: binary()) ->
    [{Tag :: binary(), fun((Name :: atom(), Value :: term()) -> ok | {error, string()}), mandatory()}].

-callback connect_source(state()) -> state().
-callback connect_dest(state()) -> state().

-callback init_source(state()) -> state().
-callback init_dest(state()) -> state().

-callback source_uri(state()) -> uri().
-callback dest_uri(state()) -> uri().

-callback source_protocol(state()) -> atom().
-callback dest_protocol(state()) -> atom().

-callback source_endpoint(state()) -> proplists:proplist().
-callback dest_endpoint(state()) -> proplists:proplist().

-callback close_dest(state()) -> ok.
-callback close_source(state()) -> ok.

-callback handle_source(Msg :: any(), state()) ->
    not_handled | state() | {stop, any()}.
-callback handle_dest(Msg :: any(), state()) ->
    not_handled | state() | {stop, any()}.

-callback ack(Tag :: tag(), Multi :: boolean(), state()) -> state().
-callback nack(Tag :: tag(), Multi :: boolean(), state()) -> state().
-callback forward(Tag :: tag(), Msg :: mc:state(), state()) ->
    state() | {stop, any()}.
-callback status(state()) -> rabbit_shovel_status:shovel_status().
-callback pending_count(state()) -> non_neg_integer().

added_to_rabbit_registry(_Type, _ModuleName) ->
    ok.
removed_from_rabbit_registry(_Type) ->
    ok.

-spec parse(atom(), binary(), {source | destination, proplists:proplist()}) ->
    source_config() | dest_config().
parse(Mod, Name, Conf) ->
    Mod:parse(Name, Conf).

-spec connect_source(state()) -> state().
connect_source(State = #{source := #{module := Mod}}) ->
    Mod:connect_source(State).

-spec connect_dest(state()) -> state().
connect_dest(State = #{dest := #{module := Mod}}) ->
    Mod:connect_dest(State).

-spec init_source(state()) -> state().
init_source(State = #{source := #{module := Mod}}) ->
    Mod:init_source(State).

-spec init_dest(state()) -> state().
init_dest(State = #{dest := #{module := Mod}}) ->
    Mod:init_dest(State).

-spec close_source(state()) -> ok.
close_source(State = #{source := #{module := Mod}}) ->
    Mod:close_source(State).

-spec close_dest(state()) -> ok.
close_dest(State = #{dest := #{module := Mod}}) ->
    Mod:close_dest(State).

-spec handle_source(any(), state()) ->
    not_handled | state() | {stop, any()}.
handle_source(Msg, State = #{source := #{module := Mod}}) ->
    Mod:handle_source(Msg, State).

-spec handle_dest(any(), state()) ->
    not_handled | state() | {stop, any()}.
handle_dest(Msg, State = #{dest := #{module := Mod}}) ->
    Mod:handle_dest(Msg, State).

source_uri(#{source := #{module := Mod}} = State) ->
    Mod:source_uri(State).

dest_uri(#{dest := #{module := Mod}} = State) ->
    Mod:dest_uri(State).

source_protocol(#{source := #{module := Mod}} = State) ->
    Mod:source_protocol(State).

dest_protocol(#{dest := #{module := Mod}} = State) ->
    Mod:dest_protocol(State).

source_endpoint(#{source := #{module := Mod}} = State) ->
    Mod:source_endpoint(State).

dest_endpoint(#{dest := #{module := Mod}} = State) ->
    Mod:dest_endpoint(State).

-spec forward(tag(), mc:state(), state()) ->
    state() | {stop, any()}.
forward(Tag, Msg, #{dest := #{module := Mod}} = State) ->
    Mod:forward(Tag, Msg, State).

-spec ack(tag(), boolean(), state()) -> state().
ack(Tag, Multi, #{source := #{module := Mod}} = State) ->
    Mod:ack(Tag, Multi, State).

-spec nack(tag(), boolean(), state()) -> state().
nack(Tag, Multi, #{source := #{module := Mod}} = State) ->
    Mod:nack(Tag, Multi, State).

-spec status(state()) -> {rabbit_shovel_status:shovel_status(), rabbit_shovel_status:metrics()}.
status(#{dest := #{module := Mod}} = State) ->
    {Mod:status(State), metrics(State)}.

incr_forwarded(State = #{dest := Dest}) ->
    State#{dest => maps:put(forwarded, maps:get(forwarded, Dest, 0) + 1, Dest)}.

-spec metrics(state()) -> rabbit_shovel_status:metrics().
metrics(#{source := Source,
          dest := #{module := Mod}} = State) ->
    #{remaining => maps:get(remaining, Source, unlimited),
      remaining_unacked => maps:get(remaining_unacked, Source, 0),
      pending => Mod:pending_count(State),
      forwarded => maps:get(forwarded, maps:get(dest, State), 0)}.


%% Common functions

%% Count down until we stop publishing in on-confirm mode
decr_remaining_unacked(State = #{source := #{remaining_unacked := unlimited}}) ->
    State;
decr_remaining_unacked(State = #{source := #{remaining_unacked := 0}}) ->
    State;
decr_remaining_unacked(State = #{source := #{remaining_unacked := N} = Src}) ->
    State#{source => Src#{remaining_unacked =>  N - 1}}.

%% Count down until we shut down in all modes
decr_remaining(_N, State = #{source := #{remaining := unlimited}}) ->
    State;
decr_remaining(N, State = #{source := #{remaining := M} = Src,
                            name := Name}) ->
    case M > N of
        true  -> State#{source => Src#{remaining => M - N}};
        false ->
            ?LOG_INFO("shutting down Shovel '~ts', no messages left to transfer", [Name]),
            exit({shutdown, autodelete})
    end.
