%%%----------------------------------------------------------------------
%%% File    : stun_logger.erl
%%% Author  : Holger Weiss <holger@zedat.fu-berlin.de>
%%% Purpose : Wrap OTP Logger for STUN/TURN logging
%%% Created : 19 Jul 2020 by Holger Weiss <holger@zedat.fu-berlin.de>
%%%
%%%
%%% Copyright (C) 2020-2023 ProcessOne, SARL. 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.
%%%
%%%----------------------------------------------------------------------

-module(stun_logger).
-author('holger@zedat.fu-berlin.de').
-export([start/0, stop/0, set_metadata/2, set_metadata/3, set_metadata/4,
	 set_metadata/5, add_metadata/1, make_id/0, encode_addr/1,
	 encode_transport/1]).

-type sub_domain() :: listener | stun | turn.
-type transport() :: udp | tcp | tls | auto.
-type sock_mod() :: gen_udp | gen_tcp | fast_tls.

%% API.

-ifdef(USE_OLD_LOGGER).
-export([log/2, log/3]).

-spec start() -> ok.
start() ->
    ok.

-spec stop() -> ok.
stop() ->
    ok.

-spec set_metadata(sub_domain(), transport()) -> ok.
set_metadata(SubDomain, Transport) ->
    put(?MODULE, #{domain => [stun, SubDomain],
		   stun_transport => encode_transport(Transport)}),
    ok.

-spec set_metadata(sub_domain(), transport(), binary()) -> ok.
set_metadata(SubDomain, Transport, ID) ->
    put(?MODULE, #{domain => [stun, SubDomain],
		   stun_transport => encode_transport(Transport),
		   stun_session_id => ID}),
    ok.

-spec set_metadata(sub_domain(), sock_mod(), binary(),
		   {inet:ip_address(), inet:port_number()},
		   binary() | undefined) -> ok.
set_metadata(SubDomain, SockMod, ID, Addr, User) ->
    put(?MODULE, #{domain => [stun, SubDomain],
		   stun_transport => encode_transport(SockMod),
		   stun_session_id => ID,
		   stun_client => encode_addr(Addr),
		   stun_user => User}),
    ok.

-spec add_metadata(logger:metadata()) -> ok.
add_metadata(Meta) ->
    put(?MODULE, maps:merge(get(?MODULE), Meta)),
    ok.

-spec log(info | warning | error, iodata() | atom() | map()) -> ok.
log(Level, #{verbatim := {Format, Args}}) ->
    log(Level, Format, Args);
log(Level, Text) ->
    {Format, Args} = format_msg(Text, get(?MODULE)),
    case Level of
	info ->
	    error_logger:info_msg(Format, Args);
	warning ->
	    error_logger:warning_msg(Format, Args);
	error ->
	    error_logger:error_msg(Format, Args)
    end.

-spec log(info | warning | error, io:format(), [term()]) -> ok.
log(Level, Format, Args) ->
    Text = io_lib:format(Format, Args),
    log(Level, Text).
-else.
-export([filter/2]).

-spec start() -> ok.
start() ->
    case logger:add_primary_filter(stun, {fun ?MODULE:filter/2, none}) of
	ok ->
	    ok;
	{error, {already_exist, _}} ->
	    ok
    end.

-spec stop() -> ok.
stop() ->
    case logger:remove_primary_filter(stun) of
	ok ->
	    ok;
	{error, {not_found, _}} ->
	    ok
    end.

-spec set_metadata(sub_domain(), transport()) -> ok.
set_metadata(SubDomain, Transport) ->
    logger:set_process_metadata(#{
      domain => [stun, SubDomain],
      stun_transport => encode_transport(Transport)}).

-spec set_metadata(sub_domain(), transport(), binary()) -> ok.
set_metadata(SubDomain, Transport, ID) ->
    logger:set_process_metadata(
      #{domain => [stun, SubDomain],
	stun_transport => encode_transport(Transport),
	stun_session_id => ID}).

-spec set_metadata(sub_domain(), sock_mod(), binary(),
		   {inet:ip_address(), inet:port_number()},
		   binary() | undefined) -> ok.
set_metadata(SubDomain, SockMod, ID, Addr, User) ->
    logger:set_process_metadata(
      #{domain => [stun, SubDomain],
	stun_transport => encode_transport(SockMod),
	stun_session_id => ID,
	stun_client => encode_addr(Addr),
	stun_user => User}).

-spec add_metadata(logger:metadata()) -> ok.
add_metadata(Meta) ->
    logger:update_process_metadata(Meta).

-spec filter(logger:log_event(), logger:filter_arg()) -> logger:filter_return().
filter(#{meta := #{domain := [stun | _]},
	 msg := {report, #{verbatim := Msg}}} = Event,
       _Extra) ->
    Event#{msg := Msg};
filter(#{meta := #{domain := [stun | _],
		   stun_transport := _Transport} = Meta,
	 msg := {Format, Args}} = Event,
       _Extra) when Format =/= report ->
    Text = case Format of
	       string ->
		   Args;
	       _ ->
		   io_lib:format(Format, Args)
	   end,
    Event#{msg := format_msg(Text, Meta)};
filter(_Event, _Extra) ->
    ignore.
-endif.

-spec set_metadata(sub_domain(), sock_mod(), binary(),
		   {inet:ip_address(), inet:port_number()}) -> ok.
set_metadata(SubDomain, SockMod, ID, Addr) ->
    set_metadata(SubDomain, SockMod, ID, Addr, undefined).

-spec make_id() -> binary().
make_id() ->
    iolist_to_binary(
      io_lib:format("~12.36.0b", [p1_rand:uniform(16#4000000000000000)])).

-spec encode_addr({inet:ip_address(), inet:port_number()} | inet:ip_address())
      -> iolist().
encode_addr({Addr, Port}) when is_tuple(Addr) ->
    [encode_addr(Addr), [ $: | integer_to_list(Port)]];
encode_addr({0, 0, 0, 0, 0, 16#FFFF, D7, D8}) ->
    encode_addr({D7 bsr 8, D7 band 255, D8 bsr 8, D8 band 255});
encode_addr({_, _, _, _, _, _, _, _} = Addr) ->
    [$[, inet:ntoa(Addr), $]];
encode_addr(Addr) ->
    inet:ntoa(Addr).

-spec encode_transport(transport() | sock_mod()) -> binary().
encode_transport(udp) -> <<"UDP">>;
encode_transport(tcp) -> <<"TCP">>;
encode_transport(tls) -> <<"TLS">>;
encode_transport(auto) -> <<"TCP|TLS">>;
encode_transport(gen_udp) -> <<"UDP">>;
encode_transport(gen_tcp) -> <<"TCP">>;
encode_transport(fast_tls) -> <<"TLS">>.

%% Internal functions.

-spec format_msg(iodata() | atom(), map()) -> {io:format(), [term()]}.
format_msg(Text, #{stun_transport := Transport,
		   stun_session_id := ID,
		   stun_user := User,
		   stun_client := Client,
		   stun_relay := Relay}) ->
    {"~s [~s, session ~s, ~s, client ~s, relay ~s]",
     [Text, Transport, ID, format_user(User), Client, Relay]};
format_msg(Text, #{stun_transport := Transport,
		   stun_session_id := ID,
		   stun_user := User,
		   stun_client := Client}) ->
    {"~s [~s, session ~s, ~s, client ~s]",
     [Text, Transport, ID, format_user(User), Client]};
format_msg(Text, #{stun_transport := Transport,
		   stun_session_id := ID}) ->
    {"~s [~s, session ~s]",
     [Text, Transport, ID]};
format_msg(Text, #{stun_transport := Transport}) ->
    {"~s [~s]",
     [Text, Transport]}.

-spec format_user(iodata() | undefined) -> iodata().
format_user(undefined) -> <<"anonymous">>;
format_user(User) -> [<<"user ">>, User].
