%% 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) 2017-2025 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries.
%%
%% @hidden
-module(ra_server_sup_sup).

-behaviour(supervisor).

-define(MUTABLE_CONFIG_KEYS,
        [cluster_name,
         metrics_key,
         broadcast_time,
         tick_timeout,
         install_snap_rpc_timeout,
         await_condition_timeout,
         max_pipeline_count,
         ra_event_formatter,
         min_recovery_checkpoint_interval]).

%% API functions
-export([start_server/2,
         restart_server/3,
         stop_server/2,
         delete_server/2,
         remove_all/1,
         start_link/1,
         recover_config/2,
         % for rpcs only
         start_server_rpc/3,
         restart_server_rpc/3,
         delete_server_rpc/2,
         prepare_server_stop_rpc/2]).

%% Supervisor callbacks
-export([init/1]).

-include("ra.hrl").

-spec start_server(System :: atom(), ra_server:ra_server_config()) ->
    supervisor:startchild_ret() |
    {error, not_new | system_not_started | nodedown |
    invalid_initial_machine_version} | {badrpc, term()}.
start_server(System, #{id := NodeId,
                       uid := UId} = Config)
  when is_atom(System) ->
    Node = ra_lib:ra_server_id_node(NodeId),
    rpc:call(Node, ?MODULE, start_server_rpc, [System, UId, Config]).

-spec restart_server(atom(), ra_server_id(), ra_server:mutable_config()) ->
    supervisor:startchild_ret() | {badrpc, term()} |
    {error, system_not_started | nodedown}.
restart_server(System, {RaName, Node}, AddConfig) ->
    rpc:call(Node, ?MODULE, restart_server_rpc,
             [System, {RaName, Node}, AddConfig]).

start_server_rpc(System, UId, Config0) ->
    case ra_system:fetch(System) of
        undefined ->
            {error, system_not_started};
        SysCfg ->
            Config = Config0#{system_config => SysCfg},
            %% check that the server isn't already registered
            case ra_directory:name_of(System, UId) of
                undefined ->
                    case validate_config(Config) of
                        ok ->
                            case ra_system:lookup_name(System, server_sup) of
                                {ok, Name} ->
                                    start_child(Name, Config);
                                Err ->
                                    Err
                            end;
                        Err ->
                            Err
                    end;
                Name ->
                    case whereis(Name) of
                        undefined ->
                            {error, not_new};
                        Pid ->
                            {error, {already_started, Pid}}
                    end
            end
    end.

validate_config(#{system_config := SysConf} = Config) ->
    Strat = maps:get(machine_upgrade_strategy, SysConf, all),
    case Config of
        #{initial_machine_version := InitMacVer,
          machine := {module, Mod, Args}} when Strat == all ->
            MacVer = ra_machine:version({machine, Mod, Args}),
            if MacVer < InitMacVer ->
                   {error, invalid_initial_machine_version};
               true ->
                   ok
            end;
        _ ->
            ok
    end.


restart_server_rpc(System, {RaName, _Node}, AddConfig)
  when is_atom(System) ->
    case ra_system:fetch(System) of
        undefined ->
            {error, system_not_started};
        SysCfg ->
            case recover_config(System, RaName) of
                {ok, Config0} ->
                    MutConfig = maps:with(?MUTABLE_CONFIG_KEYS, AddConfig),
                    {ok, Name} = ra_system:lookup_name(System, server_sup),
                    Config = case maps:merge(Config0, MutConfig) of
                                 Config0 ->
                                     %% the config has not changed
                                     Config0#{system_config => SysCfg,
                                              has_changed => false};
                                 Config1 ->
                                     Config1#{system_config => SysCfg,
                                              has_changed => true}
                             end,
                    start_child(Name, Config);
                Err ->
                    Err
            end
    end.

-spec stop_server(System :: atom(), RaNodeId :: ra_server_id()) ->
    ok | {error, term()}.
stop_server(System, ServerId) when is_atom(System) ->
    Node = ra_lib:ra_server_id_node(ServerId),
    RaName = ra_lib:ra_server_id_to_local_name(ServerId),
    Res = rpc:call(Node, ?MODULE,
                   prepare_server_stop_rpc, [System, RaName]),
    case Res of
        {error, _} = Err ->
            Err;
        {ok, Pid, SrvSup} when is_pid(Pid) ->
            supervisor:terminate_child({SrvSup, Node}, Pid);
        {ok, undefined, _} ->
            %% no parent - no need to stop
            ok;
        Err ->
            {error, Err}
    end.

prepare_server_stop_rpc(System, RaName) ->
    case ra_system:fetch(System) of
        undefined ->
            {error, system_not_started};
        #{names := #{server_sup := SrvSup} = Names} ->
            Parent =  ra_directory:where_is_parent(Names, RaName),
            {ok, Parent, SrvSup}
    end.

-spec delete_server(atom(), ServerId :: ra_server_id()) ->
    ok | {error, term()} | {badrpc, term()}.
delete_server(System, ServerId) when is_atom(System) ->
    Node = ra_lib:ra_server_id_node(ServerId),
    Name = ra_lib:ra_server_id_to_local_name(ServerId),
    case stop_server(System, ServerId) of
        ok ->
            rpc:call(Node, ?MODULE, delete_server_rpc, [System, Name]);
        {error, _} = Err -> Err
    end.

delete_server_rpc(System, RaName) ->
    case ra_system:fetch(System) of
        undefined ->
            {error, system_not_started};
        #{data_dir := _SysDir,
          names := #{log_meta := Meta,
                     server_sup := SrvSup} = Names} ->
            ?INFO("Deleting server ~w and its data directory.",
                  [RaName]),
            %% TODO: better handle and report errors
            %% UId could be `undefined' here
            UId = ra_directory:uid_of(Names, RaName),
            Pid = ra_directory:where_is(Names, RaName),
            ra_log_meta:delete(Meta, UId),
            Dir = ra_env:server_data_dir(System, UId),
            _ = supervisor:terminate_child(SrvSup, UId),
            _ = delete_data_directory(Dir),
            _ = ra_directory:unregister_name(Names, UId),
            %% forcefully clean up ETS tables
            ?CATCH(ets:delete(ra_log_metrics, UId)),
            ?CATCH(ets:delete(ra_log_snapshot_state, UId)),
            ?CATCH(ets:delete(ra_state, RaName)),
            ?CATCH(ets:delete(ra_open_file_metrics, Pid)),
            ?CATCH(ra_counters:delete({RaName, node()})),
            ?CATCH(ra_leaderboard:clear(RaName)),
            ok
    end.

delete_data_directory(Directory) ->
    DeleteFunction = fun() ->
                             try ra_lib:recursive_delete(Directory) of
                                 ok ->
                                     % moving on
                                     ok
                             catch
                                 _:_ = Err ->
                                     ?WARN("ra: delete_server/1 failed to delete directory ~ts. "
                                           "Error: ~p", [Directory, Err]),
                                     error
                             end
                     end,
    case DeleteFunction() of
        ok ->
            ok;
        _ ->
            spawn(fun() ->
                          ra_lib:retry(DeleteFunction, 2)
                  end)
    end.

remove_all(System) when is_atom(System) ->
    #{names := #{server_sup := Sup}} = ra_system:fetch(System),
    _ = [begin
             ?DEBUG("ra: terminating child ~w in system ~ts", [Pid, System]),
             supervisor:terminate_child(Sup, Pid)
         end
         || {_, Pid, _, _} <- supervisor:which_children(Sup)],
    ok.

recover_config(System, RaName) ->
    case ra_directory:uid_of(System, RaName) of
        undefined ->
            {error, name_not_registered};
        UId ->
            Dir = ra_env:server_data_dir(System, UId),
            case ra_directory:where_is(System, UId) of
                Pid when is_pid(Pid) ->
                    case is_process_alive(Pid) of
                        true ->
                            {error, {already_started, Pid}};
                        false ->
                            ra_log:read_config(Dir)
                    end;
                _ ->
                    % can it be made generic without already knowing the config state?
                    ra_log:read_config(Dir)
            end
    end.

-spec start_link(ra_system:config()) ->
    {ok, pid()} | ignore | {error, term()}.
start_link(#{names := #{server_sup := Name}}) ->
    supervisor:start_link({local, Name}, ?MODULE, []).

init([]) ->
    SupFlags = #{strategy => simple_one_for_one},
    ChildSpec = #{id => undefined,
                  type => supervisor,
                  restart => temporary,
                  start => {ra_server_sup, start_link, []}},
    {ok, {SupFlags, [ChildSpec]}}.

start_child(Name, Config) ->
    Ref = make_ref(),
    try supervisor:start_child(Name, [Config#{reply_to => {Ref, self()}}]) of
        {ok, Pid} ->
            %% we have started the process now and have to wait for reply
            %% that is sent after init but before state machine recovery
            MRef = erlang:monitor(process, Pid),
            receive
                {Ref, ok} ->
                    _ = erlang:demonitor(MRef),
                    {ok, Pid};
                {'DOWN', MRef, _, _, Reason} ->
                    ?ERROR("Ra: failed to start ra server ~ts, err ~s",
                           [Name, Reason]),
                    {error, Reason}
            end;
        Err ->
            Err
    catch
        exit:{noproc, _} ->
            {error, system_not_started};
        exit:{{nodedown, _}, _} ->
            {error, nodedown}
    end.
