%% Copyright (C) 2024 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
%% Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
%%
%% All Rights Reserved
%%
%% SPDX-License-Identifier: AGPL-3.0-or-later
%%
%% This program is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation; either version 3 of the
%% License, or (at your option) any later version.
%%
%% This program is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%% GNU General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with this program.  If not, see <https://www.gnu.org/licenses/>.
%%
%% Additional Permission under GNU AGPL version 3 section 7:
%%
%% If you modify this Program, or any covered work, by linking or
%% combining it with runtime libraries of Erlang/OTP as released by
%% Ericsson on https://www.erlang.org (or a modified version of these
%% libraries), containing parts covered by the terms of the Erlang Public
%% License (https://www.erlang.org/EPLICENSE), the licensors of this
%% Program grant you additional permission to convey the resulting work
%% without the need to license the runtime libraries of Erlang/OTP under
%% the GNU Affero General Public License. Corresponding Source for a
%% non-source form of such a combination shall include the source code
%% for the parts of the runtime libraries of Erlang/OTP used as well as
%% that of the covered work.

-module(erab_fsm).
-behaviour(gen_statem).

-export([init/1,
         callback_mode/0,
         erab_wait_setup_req/3,
         session_establish/3,
         erab_wait_setup_rsp/3,
         session_modify/3,
         erab_setup/3,
         session_delete/3,
         erab_wait_release_rsp/3,
         code_change/4,
         terminate/3]).
-export([start_link/1,
         erab_setup_req/2,
         erab_setup_rsp/2,
         erab_modify_req/2,
         erab_modify_rsp/2,
         erab_modify_ind/2,
         erab_modify_cnf/2,
         erab_release/2,
         erab_release_cmd/1,
         erab_release_rsp/1,
         erab_release_ind/1,
         fetch_info/1,
         shutdown/1]).

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


-define(ERAB_T_WAIT_SETUP_RSP, 5_000).
-define(ERAB_T_WAIT_RELEASE_RSP, 5_000).
-define(ERAB_T_SESSION_ESTABLISH, 2_000).
-define(ERAB_T_SESSION_MODIFY, 2_000).
-define(ERAB_T_SESSION_DELETE, 2_000).

-type teid() :: 0..16#ffffffff.
-type addr() :: << _:32 >> | << _:128 >>.

-type teid_addr() :: {teid(), %% GTP-U TEID
                      addr()  %% GTP-U Transport Layer Address
                     }.

-type mod_kind() :: setup_rsp |
                    modify_req |
                    modify_rsp_nack |
                    modify_ind |
                    modify_cnf_nack.
-type rel_kind() :: cmd | ind.

-record(erab_state, {uid :: term(), %% unique E-RAB identifier
                     from :: undefined | gen_statem:from(), %% destination to use when replying
                     u2c :: undefined | teid_addr(), %% GTP-U params for UPF -> Core
                     c2u :: undefined | teid_addr(), %% GTP-U params for UPF <- Core
                     a2u :: undefined | teid_addr(), %% GTP-U params for UPF <- Access
                     u2a :: undefined | teid_addr(), %% GTP-U params for UPF -> Access
                     u2cm :: undefined | teid_addr(), %% GTP-U params for UPF -> Core (modified)
                     u2am :: undefined | teid_addr(), %% GTP-U params for UPF -> Access (modified)
                     seid_loc :: undefined | pfcp_peer:pfcp_seid(), %% local SEID, chosen by us
                     seid_rem :: undefined | pfcp_peer:pfcp_seid(), %% remote SEID, chosen by the UPF
                     mod_kind :: undefined | mod_kind(), %% E-RAB MODIFY kind
                     rel_kind :: undefined | rel_kind() %% E-RAB RELEASE kind
                    }).

-type erab_state() :: #erab_state{}.

-type iface() :: 'Core' | 'Access'.
-type pfcp_ie() :: map().


%% ------------------------------------------------------------------
%% public API
%% ------------------------------------------------------------------

%% @doc Allocate and start an E-RAB FSM process
%% @param UID  *unique* E-RAB identifier.
%% @returns process ID on success;  an error otherwise.
%% @end
-spec start_link(term()) -> gen_statem:start_ret().
start_link(UID) ->
    gen_statem:start_link(?MODULE, [UID], []).


%% @doc Indicate reception of E-RAB setup request (from core).
%% @param Pid  PID of an erab_fsm.
%% @param F_TEID  TEID and bind address indicated by the MME.
%% @returns TEID and Addr to be sent to the eNB;  an error otherwise.
%% @end
-spec erab_setup_req(pid(), teid_addr()) -> {ok, teid_addr()} |
                                            {error, term()}.
erab_setup_req(Pid, F_TEID) ->
    gen_statem:call(Pid, {?FUNCTION_NAME, F_TEID}).


%% @doc Indicate reception of E-RAB setup response (from access).
%% @param Pid  PID of an erab_fsm.
%% @param F_TEID  TEID and bind address indicated by the eNB.
%% @returns TEID and Addr to be sent to the MME;  an error otherwise.
%% @end
-spec erab_setup_rsp(pid(), teid_addr()) -> {ok, teid_addr()} |
                                            {error, term()}.
erab_setup_rsp(Pid, F_TEID) ->
    gen_statem:call(Pid, {?FUNCTION_NAME, F_TEID}).


%% @doc Indicate reception of E-RAB modify request (from core).
%% @param Pid  PID of an erab_fsm.
%% @param F_TEID  TEID and bind address indicated by the MME.
%% @returns TEID and Addr to be sent to the eNB;  an error otherwise.
%% @end
-spec erab_modify_req(pid(), teid_addr()) -> {ok, teid_addr()} |
                                             {error, term()}.
erab_modify_req(Pid, F_TEID) ->
    gen_statem:call(Pid, {?FUNCTION_NAME, F_TEID}).


%% @doc Indicate reception of E-RAB modify response (from access).
%% @param Pid  PID of an erab_fsm.
%% @param Res  ack  if eNB indicates successful modification;
%%             nack if eNB indicates unsuccessful modification.
%% @returns ok or an error.
%% @end
-spec erab_modify_rsp(pid(), ack | nack) -> ok | {error, term()}.
erab_modify_rsp(Pid, Res) ->
    gen_statem:call(Pid, {?FUNCTION_NAME, Res}).


%% @doc Indicate reception of E-RAB modify indication (from access).
%% @param Pid  PID of an erab_fsm.
%% @param F_TEID  TEID and bind address indicated by the eNB.
%% @returns TEID and Addr to be sent to the MME;  an error otherwise.
%% @end
-spec erab_modify_ind(pid(), teid_addr()) -> {ok, teid_addr()} |
                                             {error, term()}.
erab_modify_ind(Pid, F_TEID) ->
    gen_statem:call(Pid, {?FUNCTION_NAME, F_TEID}).


%% @doc Indicate reception of E-RAB modify confirmation (from core).
%% @param Pid  PID of an erab_fsm.
%% @param Res  ack  if MME indicates successful modification;
%%             nack if MME indicates unsuccessful modification.
%% @returns ok or an error.
%% @end
-spec erab_modify_cnf(pid(), ack | nack) -> ok | {error, term()}.
erab_modify_cnf(Pid, Res) ->
    gen_statem:call(Pid, {?FUNCTION_NAME, Res}).


-spec erab_release(pid(), rel_kind()) -> ok.
erab_release(Pid, cmd) ->
    erab_release_cmd(Pid);
erab_release(Pid, ind) ->
    erab_release_ind(Pid).


-spec erab_release_cmd(pid()) -> ok.
erab_release_cmd(Pid) ->
    gen_statem:cast(Pid, ?FUNCTION_NAME).


-spec erab_release_rsp(pid()) -> ok.
erab_release_rsp(Pid) ->
    gen_statem:cast(Pid, ?FUNCTION_NAME).


-spec erab_release_ind(pid()) -> ok.
erab_release_ind(Pid) ->
    gen_statem:cast(Pid, ?FUNCTION_NAME).


-spec fetch_info(pid()) -> proplists:proplist().
fetch_info(Pid) ->
    gen_statem:call(Pid, ?FUNCTION_NAME).


-spec shutdown(pid()) -> ok.
shutdown(Pid) ->
    gen_statem:stop(Pid).


%% ------------------------------------------------------------------
%% gen_statem API
%% ------------------------------------------------------------------

init([UID]) ->
    set_logging_prefix(UID),
    %% request a unieue SEID for this E-RAB FSM
    {ok, SEID} = pfcp_peer:seid_alloc(),
    {ok, erab_wait_setup_req,
     #erab_state{uid = UID,
                 seid_loc = SEID}}.


callback_mode() ->
    [state_functions, state_enter].


%% state WAIT_SETUP_REQ :: wait E-RAB SETUP Req from core
erab_wait_setup_req(enter, ?FUNCTION_NAME, S) ->
    ?LOG_DEBUG("State enter: ~p", [?FUNCTION_NAME]),
    {keep_state, S};

erab_wait_setup_req({call, From},
                    {erab_setup_req, U2C},
                    #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB SETUP Req (U2C ~s)", [pp(U2C)]),
    {next_state, session_establish,
     S#erab_state{from = From,
                  u2c = U2C}};

erab_wait_setup_req(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% state SESSION_ESTABLISH :: PFCP session establishment
session_establish(enter, OldState, #erab_state{from = From} = S) ->
    ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
    case session_establish_req(S) of
        ok ->
            {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout
             [{state_timeout, ?ERAB_T_SESSION_ESTABLISH, ?FUNCTION_NAME}]};
        {error, Reason} ->
            {stop_and_reply,
             {shutdown, Reason},
             {reply, From, {error, Reason}}}
    end;

session_establish(state_timeout, _Timeout,
                  #erab_state{from = From}) ->
    ?LOG_ERROR("PFCP session establishment timeout"),
    {stop_and_reply,
     {shutdown, timeout},
     {reply, From, {error, {timeout, ?FUNCTION_NAME}}}};

session_establish(info, #pfcp{} = PDU,
                  #erab_state{from = From,
                              seid_loc = SEID_Rsp} = S) ->
    case PDU of
        #pfcp{type = session_establishment_response,
              seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req
              ie = #{pfcp_cause := 'Request accepted',
                     f_seid := #f_seid{seid = F_SEID},
                     created_pdr := [#{f_teid := F_TEID_C2U},
                                     #{f_teid := F_TEID_A2U}]}} ->
            C2U = {F_TEID_C2U#f_teid.teid, f_teid_addr(F_TEID_C2U)},
            A2U = {F_TEID_A2U#f_teid.teid, f_teid_addr(F_TEID_A2U)},
            ?LOG_DEBUG("PFCP session established (C2U ~s, A2U ~s)", [pp(C2U), pp(A2U)]),
            {next_state, erab_wait_setup_rsp,
             S#erab_state{from = undefined,
                          seid_rem = F_SEID, %% SEID to be used in further requests from us
                          c2u = C2U, a2u = A2U},
             [{reply, From, {ok, A2U}}]};
        _ ->
            ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]),
            {stop_and_reply,
             {shutdown, unexp_pdu},
             {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}}
    end;

session_establish(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% state WAIT_SETUP_RSP :: wait E-RAB SETUP Rsp from access
erab_wait_setup_rsp(enter, OldState, S) ->
    ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
    {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout
     [{state_timeout, ?ERAB_T_WAIT_SETUP_RSP, ?FUNCTION_NAME}]};

erab_wait_setup_rsp(state_timeout, _Timeout, _S) ->
    ?LOG_ERROR("Timeout waiting for E-RAB SETUP Rsp"),
    {stop, {shutdown, timeout}};

erab_wait_setup_rsp({call, From},
                    {erab_setup_rsp, U2A},
                    #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB SETUP Rsp (U2A ~s)", [pp(U2A)]),
    {next_state, session_modify,
     S#erab_state{mod_kind = setup_rsp,
                  from = From,
                  u2a = U2A}};

erab_wait_setup_rsp(cast, erab_release_cmd,
                    #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB RELEASE Cmd"),
    {next_state, session_delete,
     S#erab_state{rel_kind = cmd}};

erab_wait_setup_rsp(cast, erab_release_ind,
                    #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB RELEASE Ind"),
    {next_state, session_delete,
     S#erab_state{rel_kind = ind}};

erab_wait_setup_rsp(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% state SESSION_MODIFY :: PFCP session modification
session_modify(enter, OldState, S) ->
    ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
    ok = session_modify_req(S),
    {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout
     [{state_timeout, ?ERAB_T_SESSION_MODIFY, ?FUNCTION_NAME}]};

session_modify(state_timeout, _Timeout,
               #erab_state{from = From}) ->
    ?LOG_ERROR("PFCP session modification timeout"),
    {stop_and_reply,
     {shutdown, timeout},
     {reply, From, {error, {timeout, ?FUNCTION_NAME}}}};

session_modify(info, #pfcp{type = session_modification_response,
                           seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req
                           ie = #{pfcp_cause := 'Request accepted'}},
               #erab_state{mod_kind = ModKind,
                           from = From,
                           seid_loc = SEID_Rsp} = S) ->
    ?LOG_DEBUG("PFCP session modified"),
    Reply = case ModKind of
        setup_rsp ->
            {ok, S#erab_state.c2u};
        modify_req ->
            {ok, S#erab_state.a2u};
        modify_rsp_nack ->
            ok;
        modify_ind ->
            {ok, S#erab_state.c2u};
        modify_cnf_nack ->
            ok
    end,
    {next_state, erab_setup,
     S#erab_state{mod_kind = undefined,
                  from = undefined},
     [{reply, From, Reply}]};

session_modify(info, #pfcp{} = PDU,
               #erab_state{from = From}) ->
    ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]),
    {stop_and_reply,
     {shutdown, unexp_pdu},
     {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}};

session_modify(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% state SETUP :: E-RAB is fully setup
erab_setup(enter, OldState, S) ->
    ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
    {keep_state, S};

erab_setup({call, From},
           {erab_modify_req, U2CM},
           #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB MODIFY Req (U2CM ~p)", [pp(U2CM)]),
    {next_state, session_modify,
     S#erab_state{mod_kind = modify_req,
                  from = From,
                  u2cm = U2CM}};

erab_setup({call, From},
           {erab_modify_rsp, Res},
           #erab_state{u2cm = undefined}) ->
    %% Ignore erab_modify_rsp events without prior erab_modify_req.  This happens
    %% when the S1AP E-RAB MODIFY Req contains no F-TEID (nothing to modify).
    ?LOG_DEBUG("Rx E-RAB MODIFY Rsp (~p), F-TEID remains unchanged", [Res]),
    {keep_state_and_data, {reply, From, ok}};

erab_setup({call, From},
           {erab_modify_rsp, Res},
           #erab_state{u2cm = U2CM} = S0) ->
    ?LOG_DEBUG("Rx E-RAB MODIFY Rsp (~p)", [Res]),
    case Res of
        ack ->
            {keep_state,
             S0#erab_state{u2c = U2CM,
                           u2cm = undefined},
             {reply, From, ok}};
        nack ->
            %% revert PFCP session params
            {next_state, session_modify,
             S0#erab_state{mod_kind = modify_rsp_nack,
                           u2cm = undefined,
                           from = From}}
    end;

erab_setup({call, From},
           {erab_modify_ind, U2AM},
           #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB MODIFY Ind (U2AM ~p)", [pp(U2AM)]),
    {next_state, session_modify,
     S#erab_state{mod_kind = modify_ind,
                  from = From,
                  u2am = U2AM}};

erab_setup({call, From},
           {erab_modify_cnf, Res},
           #erab_state{u2am = undefined}) ->
    %% Ignore erab_modify_cnf events without prior erab_modify_ind.  This happens
    %% when the S1AP E-RAB MODIFY Ind contains no F-TEID (nothing to modify).
    ?LOG_DEBUG("Rx E-RAB MODIFY Cnf (~p), F-TEID remains unchanged", [Res]),
    {keep_state_and_data, {reply, From, ok}};

erab_setup({call, From},
           {erab_modify_cnf, Res},
           #erab_state{u2am = U2AM} = S0) ->
    ?LOG_DEBUG("Rx E-RAB MODIFY Cnf (~p)", [Res]),
    case Res of
        ack ->
            {keep_state,
             S0#erab_state{u2a = U2AM,
                           u2am = undefined},
             {reply, From, ok}};
        nack ->
            %% revert PFCP session params
            {next_state, session_modify,
             S0#erab_state{mod_kind = modify_cnf_nack,
                           u2am = undefined,
                           from = From}}
    end;

erab_setup(cast, erab_release_cmd,
           #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB RELEASE Cmd"),
    {next_state, session_delete,
     S#erab_state{rel_kind = cmd}};

erab_setup(cast, erab_release_ind,
           #erab_state{} = S) ->
    ?LOG_DEBUG("Rx E-RAB RELEASE Ind"),
    {next_state, session_delete,
     S#erab_state{rel_kind = ind}};

erab_setup(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% state SESSION_DELETE :: PFCP session deletion
session_delete(enter, OldState,
               #erab_state{seid_rem = SEID_Req} = S) ->
    ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
    ok = pfcp_peer:session_delete_req(SEID_Req),
    {next_state, ?FUNCTION_NAME, %% loop transition to enable state_timeout
     %% unset seid_rem to prevent terminate/3 from deleting the session again
     S#erab_state{seid_rem = undefined},
     [{state_timeout, ?ERAB_T_SESSION_DELETE, ?FUNCTION_NAME}]};

session_delete(state_timeout, _Timeout, _S) ->
    ?LOG_ERROR("PFCP session deletion timeout"),
    {stop, {shutdown, timeout}};

session_delete(info, #pfcp{} = PDU,
               #erab_state{seid_loc = SEID_Rsp,
                           rel_kind = RelKind} = S) ->
    case PDU of
        #pfcp{type = session_deletion_response,
              seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req
              ie = #{pfcp_cause := 'Request accepted'}} ->
            ?LOG_DEBUG("PFCP session deleted"),
            case RelKind of
                cmd -> %% E-RAB RELEASE CMD => wait for the RSP
                    {next_state, erab_wait_release_rsp, S};
                ind -> %% E-RAB RELEASE IND => terminate immediately
                    {stop, normal}
            end;
        _ ->
            ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]),
            {stop, {shutdown, unexp_pdu}}
    end;

session_delete(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% state WAIT_RELEASE_RSP :: wait E-RAB RELEASE Rsp
erab_wait_release_rsp(enter, OldState, S) ->
    ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]),
    {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout
     [{state_timeout, ?ERAB_T_WAIT_RELEASE_RSP, ?FUNCTION_NAME}]};

erab_wait_release_rsp(state_timeout, _Timeout, _S) ->
    ?LOG_ERROR("Timeout waiting for E-RAB RELEASE Rsp"),
    {stop, {shutdown, timeout}};

erab_wait_release_rsp(cast, erab_release_rsp,
                      #erab_state{}) ->
    ?LOG_DEBUG("Rx E-RAB RELEASE Rsp, we're done"),
    {stop, normal}; %% we're done!

erab_wait_release_rsp(cast, erab_release_cmd,
                      #erab_state{}) ->
    ?LOG_INFO("Rx E-RAB RELEASE Cmd while releasing (ignored)"),
    keep_state_and_data;

erab_wait_release_rsp(cast, erab_release_ind,
                      #erab_state{}) ->
    ?LOG_INFO("Rx E-RAB RELEASE Ind while releasing (ignored)"),
    keep_state_and_data;

erab_wait_release_rsp(Event, EventData, S) ->
    handle_event(?FUNCTION_NAME, Event, EventData, S).


%% Event handler for all states
handle_event(State, {call, From}, fetch_info,
             #erab_state{} = S) ->
    Info = [{state, State},
            {uid, S#erab_state.uid},
            {f_teid_u2c, S#erab_state.u2c},
            {f_teid_c2u, S#erab_state.c2u},
            {f_teid_a2u, S#erab_state.a2u},
            {f_teid_u2a, S#erab_state.u2a},
            {seid_loc, S#erab_state.seid_loc},
            {seid_rem, S#erab_state.seid_rem}],
    %% omit props with Value =:= undefined
    Reply = lists:filter(fun({_, V}) -> V =/= undefined end, Info),
    {keep_state_and_data, {reply, From, Reply}};

handle_event(State, {call, From}, EventData, _S) ->
    ?LOG_ERROR("Unexpected call in state ~p: ~p", [State, EventData]),
    {keep_state_and_data, {reply, From, {error, {unexpected_call, State}}}};

handle_event(State, Event, EventData, _S) ->
    ?LOG_ERROR("Unexpected event ~p in state ~p: ~p", [Event, State, EventData]),
    keep_state_and_data.


code_change(_Vsn, State, S, _Extra) ->
    {ok, State, S}.


terminate(Reason, State, S) ->
    ?LOG_NOTICE("Terminating in state ~p, reason ~p", [State, Reason]),
    case S of
        %% PFCP session is not established or was deleted
        #erab_state{seid_rem = undefined} ->
            ok;
        %% PFCP session is established, so we terminate it
        #erab_state{seid_rem = SEID_Req} ->
            ?LOG_INFO("Sending Session Deletion Req"),
            pfcp_peer:session_delete_req(SEID_Req)
    end.


%% ------------------------------------------------------------------
%% private API
%% ------------------------------------------------------------------

%% set process metadata for the logger
set_logging_prefix(UID) ->
    Prefix = io_lib:format("E-RAB ~p", [UID]),
    logger:set_process_metadata(#{prefix => Prefix}).


-spec session_establish_req(erab_state()) -> pfcp_peer:pfcp_session_rsp().
session_establish_req(#erab_state{seid_loc = F_SEID, %% used as F-SEID
                                  u2c = U2C}) ->
    %% Packet Detection Rules
    OHR = #outer_header_removal{header = 'GTP-U/UDP/IPv4'},
    PDRs = [#{pdr_id => {pdr_id, 1}, %% -- for Core -> Access
              far_id => {far_id, 1}, %% see FARs below
              precedence => {precedence, 255},
              outer_header_removal => OHR,
              pdi => pdi('Core')},
            #{pdr_id => {pdr_id, 2}, %% -- for Access -> Core
              far_id => {far_id, 2}, %% see FARs below
              precedence => {precedence, 255},
              outer_header_removal => OHR,
              pdi => pdi('Access')}],
    %% Forwarding Action Rules
    FPars = #{outer_header_creation => ohc(U2C),
              destination_interface => {destination_interface, 'Core'}},
    FParsNI = add_net_inst(FPars, 'Core'), %% optional Network Instance IE
    FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access
              %% We don't know the Access side TEID / GTP-U address yet, so we set
              %% this FAR to DROP and modify it when we get E-RAB SETUP RESPONSE.
              apply_action => #{'DROP' => []}},
            #{far_id => {far_id, 2}, %% -- for Access -> Core
              apply_action => #{'FORW' => []},
              forwarding_parameters => FParsNI}],
    pfcp_peer:session_establish_req(F_SEID, PDRs, FARs).


-spec session_modify_req(erab_state()) -> pfcp_peer:pfcp_session_rsp().
session_modify_req(#erab_state{mod_kind = setup_rsp,
                               seid_rem = SEID,
                               u2a = U2A}) ->
    %% E-RAB SETUP Rsp - eNB indicates the access side F-TEID,
    %% so we modify the respective FAR (id=1), which was previously
    %% set to DROP, to FORW.
    session_modify_req(SEID, 1, U2A);

session_modify_req(#erab_state{mod_kind = modify_req,
                               seid_rem = SEID,
                               u2cm = U2CM}) ->
    %% E-RAB MODIFY Req - MME orders modification of U2C F-TEID,
    %% so we modify the respective FAR (id=2)
    session_modify_req(SEID, 2, U2CM);

session_modify_req(#erab_state{mod_kind = modify_rsp_nack,
                               seid_rem = SEID,
                               u2c = U2C}) ->
    %% E-RAB MODIFY Rsp (NACK) - eNB NACK's E-RAB modification
    %% so we modify the respective FAR (id=2)
    session_modify_req(SEID, 2, U2C);

session_modify_req(#erab_state{mod_kind = modify_ind,
                               seid_rem = SEID,
                               u2am = U2AM}) ->
    %% E-RAB MODIFY Ind - eNB orders modification of U2A F-TEID,
    %% so we modify the respective FAR (id=1)
    session_modify_req(SEID, 1, U2AM);

session_modify_req(#erab_state{mod_kind = modify_cnf_nack,
                               seid_rem = SEID,
                               u2a = U2A}) ->
    %% E-RAB MODIFY Cnf (NACK) - MME NACK's E-RAB modification
    %% so we modify the respective FAR (id=1)
    session_modify_req(SEID, 1, U2A).


-spec session_modify_req(SEID, FAR_ID, F_TEID) -> Result
    when SEID :: pfcp_peer:pfcp_seid(),
         FAR_ID :: integer(),
         F_TEID :: teid_addr(),
         Result :: pfcp_peer:pfcp_session_rsp().
session_modify_req(SEID, FAR_ID, F_TEID) ->
    %% Forwarding Action Rules
    Iface = far_id2iface(FAR_ID),
    FPars = #{outer_header_creation => ohc(F_TEID)},
    FParsNI = add_net_inst(FPars, Iface), %% optional Network Instance IE
    FARs = [#{far_id => {far_id, FAR_ID},
              apply_action => #{'FORW' => []},
              update_forwarding_parameters => FParsNI}],
    pfcp_peer:session_modify_req(SEID, [], FARs).


-spec far_id2iface(integer()) -> iface().
far_id2iface(1) -> 'Access';
far_id2iface(2) -> 'Core'.


%% Network Instance IE env parameter name
-spec net_inst_param(iface()) -> atom().
net_inst_param('Core') -> pfcp_net_inst_core;
net_inst_param('Access') -> pfcp_net_inst_access.


%% if configured, add Network Instance IE (optional)
-spec add_net_inst(pfcp_ie(), iface()) -> pfcp_ie().
add_net_inst(IE, Iface) ->
    Param = net_inst_param(Iface),
    case application:get_env(osmo_s1gw, Param) of
        {ok, NI} ->
            IE#{network_instance => NI};
        undefined ->
            IE
    end.


%% Packet Detection Information IE generator
-spec pdi(iface()) -> pfcp_ie().
pdi(SrcIface) ->
    IE = #{f_teid => #f_teid{teid = choose,
                             ipv4 = choose},
           source_interface => {source_interface, SrcIface}},
    %% if configured, add Network Instance IE (optional)
    add_net_inst(IE, SrcIface).


%% Outer Header Creation IE generator
ohc({TEID, Addr}) ->
    OHC = #outer_header_creation{n6 = false,
                                 n19 = false,
                                 type = 'GTP-U',
                                 teid = TEID},
    case Addr of
        << _:32 >> ->
            OHC#outer_header_creation{ipv4 = Addr};
        << _:128 >> ->
            OHC#outer_header_creation{ipv6 = Addr}
    end.


%% select an address from F-TEID (prefer IPv6 over IPv4)
f_teid_addr(#f_teid{ipv6 = << IPv6:16/bytes >>}) ->
    IPv6;
f_teid_addr(#f_teid{ipv4 = << IPv4:4/bytes >>}) ->
    IPv4;
f_teid_addr(#f_teid{}) ->
    erlang:error("F-TEID IE contains no IPv4/IPv6").


%% universal pretty printer
-spec pp(teid_addr() | any()) -> string().
pp({TEID, << IPv4:4/bytes >>}) ->
    pp_f_teid({TEID, IPv4});

pp({TEID, << IPv6:16/bytes >>}) ->
    pp_f_teid({TEID, IPv6});

pp(Arg) ->
    Str = io_lib:format("~p", [Arg]),
    lists:flatten(Str).


%% pretty printer for F-TEID (example: 0x00000001@127.0.0.1)
-spec pp_f_teid(teid_addr()) -> string().
pp_f_teid({TEID, AddrBin}) ->
    Addr = list_to_tuple(binary_to_list(AddrBin)),
    Str = io_lib:format("0x~8.16.0b@~s", [TEID, inet:ntoa(Addr)]),
    lists:flatten(Str).


%% vim:set ts=4 sw=4 et: