%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1998-2024. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%
-module(snmp_notification_mib).
-moduledoc """
Instrumentation Functions for SNMP-NOTIFICATION-MIB

The module `snmp_notification_mib` implements the instrumentation functions for
the SNMP-NOTIFICATION-MIB, and functions for configuring the database.

The configuration files are described in the SNMP User's Manual.
""".

%% Avoid warning for local function error/1 clashing with autoimported BIF.
-compile({no_auto_import,[error/1]}).
-export([configure/1, reconfigure/1, invalidate_cache/0,
	 snmpNotifyTable/1, snmpNotifyTable/3,
	 snmpNotifyFilterTable/3, snmpNotifyFilterProfileTable/3,
	 get_targets/0, get_targets/1]).
-export([add_notify/3, delete_notify/1]).
-export([check_notify/1]).

-export_type([
              notify_name/0,
              notify_tag/0,
              notify_type/0
             ]).

-include("snmpa_internal.hrl").
-include("SNMP-NOTIFICATION-MIB.hrl").
-include("SNMPv2-TC.hrl").
-include("snmp_tables.hrl").

-define(VMODULE,"NOTIFICATION-MIB").
-include("snmp_verbosity.hrl").

-ifndef(default_verbosity).
-define(default_verbosity,silence).
-endif.


-doc "`SnmpAdminString (SIZE(1..32))`".
-type notify_name() :: snmp_framework_mib:admin_string().
-type notify_tag()  :: snmp_target_mib:tag_value().
-type notify_type() :: trap | inform.


%%-----------------------------------------------------------------
%% Func: configure/1
%% Args: Dir is the directory where the configuration files are found.
%% Purpose: If the tables doesn't exist, this function reads
%%          the config-files for the notify tables, and
%%          inserts the data.  This means that the data in the tables
%%          survive a reboot.  However, the StorageType column is
%%          checked for each row.  If volatile, the row is deleted.
%% Returns: ok
%% Fails: exit(configuration_error)
%%-----------------------------------------------------------------

-doc """
This function is called from the supervisor at system start-up.

Inserts all data in the configuration files into the database and destroys all
old rows with StorageType `volatile`. The rows created from the configuration
file will have StorageType `nonVolatile`.

If an error is found in the configuration file, it is reported using the
function `config_err/2` of the error report module, and the function fails with
reason `configuration_error`.

`ConfDir` is a string which points to the directory where the configuration
files are found.

The configuration file read is: `notify.conf`.

""".
-spec configure(ConfDir) -> snmp:void() when
      ConfDir :: string().

configure(ConfDir) ->
    set_sname(),
    case db(snmpNotifyTable) of
        {_, mnesia} ->
            ?vlog("notification table in mnesia: cleanup",[]),
            gc_tabs();
        TabDb ->
	    case snmpa_local_db:table_exists(TabDb) of
		true ->
		    ?vlog("notification table exist: cleanup",[]),
		    gc_tabs();
		false ->
		    ?vlog("notification table does not exist: reconfigure",[]),
		    reconfigure(ConfDir)
	    end
    end.

%%-----------------------------------------------------------------
%% Func: reconfigure/1
%% Args: Dir is the directory where the configuration files are found.
%% Purpose: Reads the config-files for the notify tables, and
%%          inserts the data.  Makes sure that all old data in
%%          the tables are deleted, and the new data inserted.
%%          This function makes sure that all (and only) 
%%          config-file-data are in the tables. 
%% Returns: ok
%% Fails: exit(configuration_error)
%%-----------------------------------------------------------------

-doc """
Inserts all data in the configuration files into the database and destroys all
old data, including the rows with StorageType `nonVolatile`. The rows created
from the configuration file will have StorageType `nonVolatile`.

Thus, the data in the SNMP-NOTIFICATION-MIB, after this function has been
called, is from the configuration files.

If an error is found in the configuration file, it is reported using the
function `config_err/2` of the error report module, and the function fails with
reason `configuration_error`.

`ConfDir` is a string which points to the directory where the configuration
files are found.

The configuration file read is: `notify.conf`.

""".
-spec reconfigure(ConfDir) -> snmp:void() when
      ConfDir :: string().

reconfigure(ConfDir) ->
    set_sname(),
    case (catch do_reconfigure(ConfDir)) of
	ok ->
	    ok;
	{error, Reason} ->
	    ?vinfo("reconfigure error: ~p", [Reason]),
	    config_err("reconfigure failed: ~p", [Reason]),
	    exit(configuration_error);
	Error ->
	    ?vinfo("reconfigure failed: ~p", [Error]),
	    config_err("reconfigure failed: ~p", [Error]),
	    exit(configuration_error)
    end.

do_reconfigure(Dir) ->
    ?vdebug("read notify config files",[]),
    Notifs = read_notify_config_files(Dir),
    init_tabs(Notifs),
    ?vdebug("invalidate cache",[]),
    invalidate_cache(),
    ok.


read_notify_config_files(Dir) ->
    ?vdebug("read notify config file",[]),
    FileName = "notify.conf", 
    Gen =
	fun (D, Reason) ->
		info_msg("failed reading config file ~s"
			 "~n   Config Dir: ~s"
			 "~n   Reason:     ~p",
			 [FileName, D, Reason]),
		ok
	end,
    Order  = fun snmp_conf:no_order/2,
    Filter = fun snmp_conf:no_filter/1,
    Check  = fun (Entry, State) -> {check_notify(Entry), State} end,
    [Notifs] = 
	snmp_conf:read_files(Dir, [{FileName, Gen, Order, Check, Filter}]),
    Notifs.

-doc false.
check_notify({Name, Tag, Type}) ->
    snmp_conf:check_string(Name, {gt, 0}),
    snmp_conf:check_string(Tag),
    {ok, Val} = snmp_conf:check_atom(Type, [{trap, 1}, {inform, 2}]),
    Notify = {Name, Tag, Val, 
	      ?'StorageType_nonVolatile', ?'RowStatus_active'},
    {ok, Notify};
check_notify(X) ->
    error({invalid_notify, X}).


init_tabs(Notifs) ->
    ?vdebug("create notify table",[]),
    snmpa_local_db:table_delete(db(snmpNotifyTable)),
    snmpa_local_db:table_create(db(snmpNotifyTable)),
    ?vdebug("initiate notify table",[]),
    init_notify_table(Notifs).
    
init_notify_table([Row | T]) ->
    Key = element(1, Row),
    snmpa_local_db:table_create_row(db(snmpNotifyTable), Key, Row),
    init_notify_table(T);
init_notify_table([]) -> true.

table_cre_row(Tab, Key, Row) ->
    snmpa_mib_lib:table_cre_row(db(Tab), Key, Row).

table_del_row(Tab, Key) ->
    snmpa_mib_lib:table_del_row(db(Tab), Key).


-doc """
Adds a notify definition to the agent config. Equivalent to one line in the
`notify.conf` file.

""".
-spec add_notify(Name, Tag, Type) -> {ok, Key} | {error, Reason} when
      Name   :: notify_name(),
      Tag    :: notify_tag(),
      Type   :: notify_type(),
      Key    :: term(),
      Reason :: term().

%% FIXME: does not work with mnesia
add_notify(Name, Tag, Type) ->
    Notif = {Name, Tag, Type},
    case (catch check_notify(Notif)) of
	{ok, Row} ->
	    Key = element(1, Row),
	    case table_cre_row(snmpNotifyTable, Key, Row) of
		true ->
		    {ok, Key};
		false ->
		    {error, create_failed}
	    end;
	{error, Reason} ->
	    {error, Reason};
	Error ->
	    {error, Error}
    end.


-doc "Delete a notify definition from the agent config.".
-spec delete_notify(Key) -> ok | {error, Reason} when
      Key    :: term(),
      Reason :: term().

%% FIXME: does not work with mnesia
delete_notify(Key) ->
    case table_del_row(snmpNotifyTable, Key) of
	true ->
	    ok;
	false ->
	    {error, delete_failed}
    end.
    
gc_tabs() ->
    DB  = db(snmpNotifyTable), 
    STC = stc(snmpNotifyTable),
    FOI = foi(snmpNotifyTable),
    snmpa_mib_lib:gc_tab(DB, STC, FOI),
    ok.

			    
%%-----------------------------------------------------------------
%% Func: get_targets()
%%       get_targets(NotifyName) -> [Target]
%% Types: Target = {DestAddr, TargetName, TargetParams, NotifyType}
%%        NotifyName = string()  - the INDEX
%%        DestAddr = {TDomain, TAddr}
%%        TagrgetName = string()
%%        TargetParams = {MpModel, SecModel, SecName, SecLevel}
%%        NotifyType = trap | {inform, Timeout, Retry}
%% Purpose: Returns a list of all targets.  Called by snmpa_trap
%%          when a trap should be sent.
%%          If a NotifyName is specified, the targets for that
%%          name is returned.
%%-----------------------------------------------------------------
-doc false.
get_targets() ->
    TargetsFun = fun find_targets/0,
    snmpa_target_cache:targets(TargetsFun).

-doc false.
get_targets(NotifyName) ->
    TargetsFun = fun find_targets/0,
    snmpa_target_cache:targets(TargetsFun, NotifyName).


%%-----------------------------------------------------------------
%% We use a cache of targets to avoid searching the tables each
%% time a trap is sent.  When some of the 3 tables (notify,
%% targetAddr, targetParams) is modified, the cache is invalidated.
%%-----------------------------------------------------------------

-doc false.
invalidate_cache() ->
    snmpa_target_cache:invalidate().
    

%% Ret: [{NotifyName, {DestAddr, TargetName, TargetParams, NotifyType}}]
%%   NotifyType = trap | {inform, Timeout. Retry}
%%   DestAddr = {Domain, Addr} ; e.g. {snmpUDPDomain, {IPasList, UdpPort}}

find_targets() ->
    TargAddrs = snmp_target_mib:get_target_addrs(),
    %% TargAddrs = [{TagList,DestAddr,TargetName,TargetParams,Timeout,Retry}]
    {_, Db} = db(snmpNotifyTable),
    find_targets([], TargAddrs, Db, []).
find_targets(Key, TargAddrs, Db, Res) ->
    case table_next(snmpNotifyTable, Key) of
	endOfTable -> 
	    Res;
        NextKey when Db == mnesia ->
            case mnesia:snmp_get_row(snmpNotifyTable, NextKey) of
                {ok, #snmpNotifyTable{
                   snmpNotifyTag = Tag,
                   snmpNotifyType = Type,
                   snmpNotifyRowStatus = ?'RowStatus_active'}} ->
                    ?vtrace("found notify entry for ~w"
                            "~n   Tag:     ~w"
                            "~n   Type:    ~w", [NextKey, Tag, Type]),
                    Targs = get_targets(TargAddrs, Tag, Type, NextKey),
                    find_targets(NextKey, TargAddrs, Db, Targs ++ Res);
                {ok, #snmpNotifyTable{
                   snmpNotifyTag       = Tag,
                   snmpNotifyType      = Type,
                   snmpNotifyRowStatus = RowStatus}} ->
                    ?vtrace("found invalid notify entry for ~w"
                            "~n   Tag:       ~w"
                            "~n   Type:      ~w"
			    "~n   RowStatus: ~p", 
			    [NextKey, Tag, Type, RowStatus]), 
                    find_targets(NextKey, TargAddrs, Db, Res);
                _ ->
                    ?vtrace("notify entry not found for ~w", [NextKey]),
                    find_targets(NextKey, TargAddrs, Db, Res)
            end;
 	NextKey -> 
	    Elements = [?snmpNotifyTag, ?snmpNotifyType, ?snmpNotifyRowStatus],
	    case snmpNotifyTable(get, NextKey, Elements) of
		[{value, Tag}, {value, Type}, {value, ?'RowStatus_active'}] ->
		    ?vtrace("found notify entry for ~w"
			    "~n   Tag:     ~w"
			    "~n   Type:    ~w", [NextKey, Tag, Type]),
		    Targs = get_targets(TargAddrs, Tag, Type, NextKey),
		    find_targets(NextKey, TargAddrs, Db, Targs ++ Res);
		[{value, Tag1}, {value, Type1}, {value, RowStatus}] ->
		    ?vtrace("found invalid notify entry for ~w"
			    "~n   Tag:       ~w"
			    "~n   Type:      ~w"
			    "~n   RowStatus: ~w", 
			    [NextKey, Tag1, Type1, RowStatus]),
		    find_targets(NextKey, TargAddrs, Db, Res);
		_ ->
                    ?vtrace("notify entry not found for ~w", [NextKey]),
		    find_targets(NextKey, TargAddrs, Db, Res)
	    end
    end.

get_targets([{TagList, Addr, TargetName, Params, Timeout, Retry}|T],
	    Tag, Type, Name) ->
    case snmp_misc:is_tag_member(Tag, TagList) of
	true -> 
	    ?vtrace("tag ~w *is* member", [Tag]),
	    [{Name, {Addr, TargetName, Params, type(Type, Timeout, Retry)}}|
	     get_targets(T, Tag, Type, Name)];
	false ->
	    ?vtrace("tag ~w is *not* member", [Tag]),
	    get_targets(T, Tag, Type, Name)
    end;
get_targets([], _Tag, _Type, _Name) ->
    [].

type(trap, _, _) -> trap;
type(1,    _, _) -> trap;                                  %% OTP-4329
type(inform, Timeout, Retry) -> {inform, Timeout, Retry};
type(2,      Timeout, Retry) -> {inform, Timeout, Retry}.  %% OTP-4329


%%-----------------------------------------------------------------
%% Instrumentation Functions
%%-----------------------------------------------------------------
%% Op = print - Used for debugging purposes
-doc false.
snmpNotifyTable(print) ->
    Table = snmpNotifyTable, 
    DB    = db(Table),
    FOI   = foi(Table),
    PrintRow = 
	fun(Prefix, Row) ->
		lists:flatten(
		  io_lib:format("~sName:        ~p"
				"~n~sTag:         ~p"
				"~n~sType:        ~p (~w)"
				"~n~sStorageType: ~p (~w)"
				"~n~sStatus:      ~p (~w)", 
				[Prefix, element(?snmpNotifyName, Row),
				 Prefix, element(?snmpNotifyTag, Row),
				 Prefix, element(?snmpNotifyType, Row),
				 case element(?snmpNotifyType, Row) of
				     ?snmpNotifyType_inform -> inform;
				     ?snmpNotifyType_trap -> trap;
				     _ -> undefined
				 end,
				 Prefix, element(?snmpNotifyStorageType, Row),
				 case element(?snmpNotifyStorageType, Row) of
				     ?'snmpNotifyStorageType_readOnly' -> readOnly;
				     ?'snmpNotifyStorageType_permanent' -> permanent;
				     ?'snmpNotifyStorageType_nonVolatile' -> nonVolatile;
				     ?'snmpNotifyStorageType_volatile' -> volatile;
				     ?'snmpNotifyStorageType_other' -> other;
				     _ -> undefined
				 end,
				 Prefix, element(?snmpNotifyRowStatus, Row),
				 case element(?snmpNotifyRowStatus, Row) of
				     ?'snmpNotifyRowStatus_destroy' -> destroy;
				     ?'snmpNotifyRowStatus_createAndWait' -> createAndWait;
				     ?'snmpNotifyRowStatus_createAndGo' -> createAndGo;
				     ?'snmpNotifyRowStatus_notReady' -> notReady;
				     ?'snmpNotifyRowStatus_notInService' -> notInService;
				     ?'snmpNotifyRowStatus_active' -> active;
				     _ -> undefined
				 end]))
	end,
    snmpa_mib_lib:print_table(Table, DB, FOI, PrintRow);
%% Op == new | delete
snmpNotifyTable(Op) ->
    snmp_generic:table_func(Op, db(snmpNotifyTable)).

%% Op == get | is_set_ok | set | get_next
-doc false.
snmpNotifyTable(get, RowIndex, Cols) ->
    get(snmpNotifyTable, RowIndex, Cols);
snmpNotifyTable(get_next, RowIndex, Cols) ->
    next(snmpNotifyTable, RowIndex, Cols);
snmpNotifyTable(set, RowIndex, Cols0) ->
    case (catch verify_snmpNotifyTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    invalidate_cache(),
	    %% invalidate_cache(RowIndex),
	    Db = db(snmpNotifyTable),
	    snmp_generic:table_func(set, RowIndex, Cols, Db);
	Error ->
	    Error
    end;
snmpNotifyTable(is_set_ok, RowIndex, Cols0) ->
    case (catch verify_snmpNotifyTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    Db = db(snmpNotifyTable),
	    snmp_generic:table_func(is_set_ok, RowIndex, Cols, Db);
	Error ->
	    Error
    end;
snmpNotifyTable(Op, Arg1, Arg2) ->
    snmp_generic:table_func(Op, Arg1, Arg2, db(snmpNotifyTable)).


verify_snmpNotifyTable_cols([], Cols) ->
    {ok, lists:reverse(Cols)};
verify_snmpNotifyTable_cols([{Col, Val0}|Cols], Acc) ->
    Val = verify_snmpNotifyTable_col(Col, Val0),
    verify_snmpNotifyTable_cols(Cols, [{Col, Val}|Acc]).

verify_snmpNotifyTable_col(?snmpNotifyName, Name) ->
    case (catch snmp_conf:check_string(Name, {gt, 0})) of
	ok ->
	    Name;
	_ ->
	    wrongValue(?snmpNotifyName)
    end;
verify_snmpNotifyTable_col(?snmpNotifyTag, Tag) ->
    case (catch snmp_conf:check_string(Tag)) of
	ok ->
	    Tag;
	_ ->
	    wrongValue(?snmpNotifyTag)
    end;
verify_snmpNotifyTable_col(?snmpNotifyType, Type) ->
    case Type of
	trap   -> 1;
	inform -> 2;
	1      -> 1;
	2      -> 2;
	_      -> wrongValue(?snmpNotifyType)
    end;
verify_snmpNotifyTable_col(_, Val) ->
    Val.


%%-----------------------------------------------------------------
%% In this version of the agent, we don't support notification
%% filters.
%%-----------------------------------------------------------------
-doc false.
snmpNotifyFilterTable(get, _RowIndex, Cols) ->
    lists:map(fun(_Col) -> {noValue, noSuchObject} end, Cols);
snmpNotifyFilterTable(get_next, _RowIndex, Cols) ->
    lists:map(fun(_Col) -> endOfTable end, Cols);
snmpNotifyFilterTable(is_set_ok, _RowIndex, Cols) ->
    {notWritable, element(1, hd(Cols))}.

-doc false.
snmpNotifyFilterProfileTable(get, _RowIndex, Cols) ->
    lists:map(fun(_Col) -> {noValue, noSuchObject} end, Cols);
snmpNotifyFilterProfileTable(get_next, _RowIndex, Cols) ->
    lists:map(fun(_Col) -> endOfTable end, Cols);
snmpNotifyFilterProfileTable(is_set_ok, _RowIndex, Cols) ->
    {notWritable, element(1, hd(Cols))}.


db(X) -> snmpa_agent:db(X).

fa(snmpNotifyTable) -> ?snmpNotifyTag.
 
foi(snmpNotifyTable) -> ?snmpNotifyName.
 
noc(snmpNotifyTable) -> 5.

stc(snmpNotifyTable) -> ?snmpNotifyStorageType.
 
next(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_next(db(Name), RowIndex, Cols,
                                   fa(Name), foi(Name), noc(Name)).

table_next(Name, RestOid) ->
    snmp_generic:table_next(db(Name), RestOid).

 
get(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_get(db(Name), RowIndex, Cols, foi(Name)).


wrongValue(V) -> throw({wrongValue, V}).


%% -----

set_sname() ->
    set_sname(get(sname)).

set_sname(undefined) ->
    put(sname, conf);
set_sname(_) -> %% Keep it, if already set.
    ok.

error(Reason) ->
    throw({error, Reason}).

info_msg(F, A) -> 
    ?snmpa_info("[NOTIFICATION-MIB]: " ++ F, A).

config_err(F, A) ->
    snmpa_error:config_err("[NOTIFICATION-MIB]: " ++ F, A).
