% S2b: GTPv2C towards PGW
%
% 3GPP TS 29.274
%
% (C) 2023 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
% Author: Pau Espin Pedrol <pespin@sysmocom.de>
%
% All Rights Reserved
%
% 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(epdg_gtpc_s2b).
-author('Pau Espin Pedrol <pespin@sysmocom.de>').

-behaviour(gen_server).

-include_lib("gtplib/include/gtp_packet.hrl").

%% API Function Exports
-export([start_link/6]).
-export([terminate/2]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-export([code_change/3]).
-export([create_session_req/4, delete_session_req/1]).

%% Application Definitions
-define(SERVER, ?MODULE).
-define(SVC_NAME, ?MODULE).
-define(APP_ALIAS, ?MODULE).
-define(CALLBACK_MOD, epdg_gtpc_s2b_cb).
-define(ENV_APP_NAME, osmo_epdg).

-define(MCC, 901).
-define(MNC, 42).
-define(MNC_SIZE, 3).

-record(gtp_state, {
        socket,
        laddr_str       :: string(),
        laddr           :: inet:ip_address(),
        lport           :: non_neg_integer(),
        raddr_str       :: string(),
        raddr           :: inet:ip_address(),
        rport           :: non_neg_integer(),
        laddr_gtpu_str  :: string(),
        laddr_gtpu      :: inet:ip_address(),
        restart_counter :: 0..255,
        seq_no          :: 0..16#ffffff,
        next_local_control_tei :: 0..16#ffffffff,
        next_local_data_tei    :: 0..16#ffffffff,
        sessions = sets:new() :: sets:set()
}).

-record(gtp_bearer, {
    ebi                   :: non_neg_integer(),
    local_data_tei = 0    :: non_neg_integer(),
    remote_data_tei = 0    :: non_neg_integer()
}).

-record(gtp_session, {
    imsi                   :: binary(),
    pid                    :: pid(),
    apn                    :: binary(),
    raddr_str              :: string(),
    raddr                  :: inet:ip_address(),
    ue_ip                  :: inet:ip_address(),
    local_control_tei = 0  :: non_neg_integer(),
    remote_control_tei = 0 :: non_neg_integer(),
    default_bearer_id      :: non_neg_integer(),
    bearers = sets:new()    :: sets:set()
}).

start_link(LocalAddr, LocalPort, RemoteAddr, RemotePort, GtpuLocalIp, Options) ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [LocalAddr, LocalPort, RemoteAddr, RemotePort, GtpuLocalIp, Options], []).

peer_down(API, SvcName, {PeerRef, _} = Peer) ->
    % fixme: why do we still have ets here?
    (catch ets:delete(?MODULE, {API, PeerRef})),
    gen_server:cast(?SERVER, {peer_down, SvcName, Peer}),
    ok.

init(State) ->
    lager:info("epdg_gtpc_s2b: init(): ~p", [State]),
    [LocalAddr | [LocalPort | [RemoteAddr | [RemotePort | [GtpuLocalAddr | _]]]]] = State,
    lager:info("epdg_gtpc_s2b: Binding to IP ~s port ~p~n", [LocalAddr, LocalPort]),
    {ok, LocalAddrInet} = inet_parse:address(LocalAddr),
    {ok, RemoteAddrInet} = inet_parse:address(RemoteAddr),
    {ok, GtpuLocalAddrInet} = inet_parse:address(GtpuLocalAddr),
    Opts = [
        binary,
        {ip, LocalAddrInet},
        {active, true},
        {reuseaddr, true}
    ],
    Ret = gen_udp:open(LocalPort, Opts),
    case Ret of
        {ok, Socket} ->
            lager:info("epdg_gtpc_s2b: Socket is ~p~n", [Socket]),
            ok = connect({Socket, RemoteAddr, RemotePort}),
            St = #gtp_state{
                    socket = Socket,
                    laddr_str = LocalAddr,
                    laddr = LocalAddrInet,
                    lport = LocalPort,
                    raddr_str = RemoteAddr,
                    raddr = RemoteAddrInet,
                    rport = RemotePort,
                    laddr_gtpu_str = GtpuLocalAddr,
                    laddr_gtpu = GtpuLocalAddrInet,
                    restart_counter = 0,
                    seq_no = rand:uniform(16#FFFFFF),
                    next_local_control_tei = rand:uniform(16#FFFFFFFE),
                    next_local_data_tei = rand:uniform(16#FFFFFFFE)
                },
            {ok, St};
        {error, Reason} ->
            lager:error("GTPv2C UDP socket open error: ~w~n", [Reason])
    end.

create_session_req(Imsi, Apn, APCO, PGWAddrCandidateList) ->
    gen_server:call(?SERVER, {gtpc_create_session_req, {Imsi, Apn, APCO, PGWAddrCandidateList}}).

delete_session_req(Imsi) ->
    gen_server:call(?SERVER, {gtpc_delete_session_req, {Imsi}}).

handle_call({gtpc_create_session_req, {Imsi, Apn, APCO, PGWAddrCandidateList}}, {Pid, _Tag} = _From, State0) ->
    RemoteAddrStr = pick_gtpc_remote_address(PGWAddrCandidateList, State0),
    lager:debug("Selected PGW Remote Address ~p~n", [RemoteAddrStr]),
    {ok, RemoteAddrInet} = inet_parse:address(RemoteAddrStr),
    {Sess0, State1} = find_or_new_gtp_session(Imsi,
                        #gtp_session{pid = Pid,
                                     apn = list_to_binary(Apn),
                                     raddr_str = RemoteAddrInet,
                                     raddr = RemoteAddrInet},
                        State0),
    Req = gen_create_session_request(Sess0, APCO, State1),
    tx_gtp(Req, State1),
    State2 = inc_seq_no(State1),
    lager:debug("Waiting for CreateSessionResponse~n", []),
    {reply, ok, State2};

handle_call({gtpc_delete_session_req, {Imsi}}, _From, State) ->
    Sess = find_gtp_session_by_imsi(Imsi, State),
    case Sess of
        #gtp_session{imsi = Imsi} ->
            Req = gen_delete_session_request(Sess, State),
            tx_gtp(Req, State),
            State1 = inc_seq_no(State),
            {reply, ok, State1};
        undefined ->
            {reply, {error, imsi_unknown}, State}
    end.

%% @callback gen_server
handle_cast(stop, State) ->
    {stop, normal, State};
handle_cast(Req, State) ->
    lager:info("S2b handle_cast: ~p ~n", [Req]),
    {noreply, State}.

%% @callback gen_server
handle_info({udp, _Socket, IP, InPortNo, RxMsg}, State) ->
    lager:debug("S2b: Rx from IP ~p port ~p: ~p~n", [IP, InPortNo, RxMsg]),
    misc:spawn_wait_ret(fun() ->
                            rx_udp(IP, InPortNo, RxMsg, State)
                        end,
                        {noreply, State});
handle_info(Info, State) ->
    lager:info("S2b handle_info: ~p ~n", [Info]),
    {noreply, State}.

%% @callback gen_server
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% @callback gen_server
terminate(normal, State) ->
    udp_gen:close(State#gtp_state.socket),
    ok;
terminate(shutdown, _State) ->
    ok;
terminate({shutdown, _Reason}, _State) ->
    ok;
terminate(_Reason, _State) ->
    ok.

%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------

inc_seq_no(State) ->
    NewSeqNr = State#gtp_state.seq_no +1,
    State#gtp_state{seq_no = NewSeqNr}.

% Skip 0
inc_tei(Tei) when Tei >= 16#FFFFFFFE -> 16#00000001;
% Skip 0xffffffff
inc_tei(Tei) -> (Tei + 1) rem 16#FFFFFFFF.

% Skip 0, 0xffffffff
dec_tei(Tei) when Tei =< 1 -> 16#FFFFFFFE;
dec_tei(Tei) -> (Tei - 1) rem 16#FFFFFFFF.

new_gtp_session(Imsi, SessTpl, State) ->
    DefaultBearer = #gtp_bearer{
        ebi = 5,
        local_data_tei = find_unused_local_teid(State)
    },
    Sess = SessTpl#gtp_session{imsi = Imsi,
        local_control_tei = find_unused_local_teic(State),
        default_bearer_id = DefaultBearer#gtp_bearer.ebi,
        bearers = sets:add_element(DefaultBearer, sets:new())
    },
    NewSt = State#gtp_state{next_local_control_tei = inc_tei(Sess#gtp_session.local_control_tei),
                            next_local_data_tei = inc_tei(DefaultBearer#gtp_bearer.local_data_tei),
                            sessions = sets:add_element(Sess, State#gtp_state.sessions)},
    {Sess, NewSt}.

% returns Sess if found, undefined it not
find_gtp_session_by_imsi(Imsi, State) ->
    {Imsi, Res} = sets:fold(
                    fun(SessIt = #gtp_session{imsi = LookupImsi}, {LookupImsi, _AccIn}) -> {LookupImsi, SessIt};
                       (_, AccIn) -> AccIn
                    end,
                    {Imsi, undefined},
                    State#gtp_state.sessions),
    Res.

find_or_new_gtp_session(Imsi, SessTpl, State) ->
    Sess = find_gtp_session_by_imsi(Imsi, State),
    case Sess of
        #gtp_session{imsi = Imsi} ->
            {Sess, State};
        undefined ->
            new_gtp_session(Imsi, SessTpl, State)
    end.

update_gtp_session(OldSess, NewSess, State) ->
    SetRemoved = sets:del_element(OldSess, State#gtp_state.sessions),
    SetUpdated = sets:add_element(NewSess, SetRemoved),
    State#gtp_state{sessions = SetUpdated}.

delete_gtp_session(Sess, State) ->
    SetRemoved = sets:del_element(Sess, State#gtp_state.sessions),
    State#gtp_state{sessions = SetRemoved}.

gtp_session_find_bearer_by_ebi(Sess, Ebi) ->
    {Ebi, Res} = sets:fold(
                    fun(BearerIt = #gtp_bearer{ebi = LookupEbi}, {LookupEbi, _AccIn}) -> {LookupEbi, BearerIt};
                       (_, AccIn) -> AccIn
                    end,
                    {Ebi, undefined},
                    Sess#gtp_session.bearers),
    Res.

gtp_session_find_bearer_by_local_teid(Sess, LocalTEID) ->
    {LocalTEID, Res} = sets:fold(
                    fun(BearerIt = #gtp_bearer{ebi = LookupLocalTEID}, {LookupLocalTEID, _AccIn}) -> {LookupLocalTEID, BearerIt};
                        (_, AccIn) -> AccIn
                    end,
                    {LocalTEID, undefined},
                    Sess#gtp_session.bearers),
    Res.

gtp_session_add_bearer(Sess, Bearer) ->
    lager:debug("Add bearer ~p to session ~p~n", [Bearer, Sess]),
    Sess#gtp_session{bearers = sets:add_element(Bearer, Sess#gtp_session.bearers)}.

gtp_session_update_bearer(Sess, OldBearer, NewBearer) ->
    SetRemoved = sets:del_element(OldBearer, Sess#gtp_session.bearers),
    SetUpdated = sets:add_element(NewBearer, SetRemoved),
    Sess#gtp_session{bearers = SetUpdated}.

gtp_session_del_bearer(Sess, Bearer) ->
    lager:debug("Remove bearer ~p from session ~p~n", [Bearer, Sess]),
    Sess1 = Sess#gtp_session{bearers = sets:del_element(Bearer, Sess#gtp_session.bearers)},
    case Sess1#gtp_session.default_bearer_id of
    Ebi when Ebi == Bearer#gtp_bearer.ebi -> Sess1#gtp_session{default_bearer_id = undefined};
    _ -> Sess1
    end.

gtp_session_default_bearer(Sess) ->
    gtp_session_find_bearer_by_ebi(Sess, Sess#gtp_session.default_bearer_id).

% returns Sess if found, undefined it not
find_gtp_session_by_local_teic(LocalControlTei, State) ->
    {LocalControlTei, Res} = sets:fold(
        fun(SessIt = #gtp_session{local_control_tei = LookupLTEIC}, {LookupLTEIC, _AccIn}) -> {LookupLTEIC, SessIt};
           (_, AccIn) -> AccIn
        end,
        {LocalControlTei, undefined},
        State#gtp_state.sessions),
    Res.

% returns Sess if found, undefined it not
find_gtp_session_by_local_teid(LocalControlTeid, State) ->
    {LocalControlTeid, Res} = sets:fold(
        fun(SessIt = #gtp_session{}, {LookupLTEID, AccIn}) ->
            case gtp_session_find_bearer_by_local_teid(SessIt, LookupLTEID) of
                undefined -> {LookupLTEID, AccIn};
                _ -> {LookupLTEID, SessIt}
            end
        end,
        {LocalControlTeid, undefined},
        State#gtp_state.sessions),
    Res.

find_unused_local_teic(State, TeicEnd, TeicEnd) ->
    case find_gtp_session_by_local_teic(TeicEnd, State) of
        undefined -> TeicEnd;
        _ -> undefined
    end;
find_unused_local_teic(State, TeicIt, TeicEnd) ->
    case find_gtp_session_by_local_teic(TeicIt, State) of
    undefined -> TeicIt;
    _ -> find_unused_local_teic(State, inc_tei(TeicIt), TeicEnd)
    end.
find_unused_local_teic(State) ->
    find_unused_local_teic(State, State#gtp_state.next_local_control_tei,
                           dec_tei(State#gtp_state.next_local_control_tei)).

find_unused_local_teid(State, TeidEnd, TeidEnd) ->
    case find_gtp_session_by_local_teid(TeidEnd, State) of
        undefined -> TeidEnd;
        _ -> undefined
    end;
find_unused_local_teid(State, TeidIt, TeidEnd) ->
    case find_gtp_session_by_local_teid(TeidIt, State) of
    undefined -> TeidIt;
    _ -> find_unused_local_teid(State, inc_tei(TeidIt), TeidEnd)
    end.
find_unused_local_teid(State) ->
    find_unused_local_teid(State, State#gtp_state.next_local_data_tei,
                           dec_tei(State#gtp_state.next_local_data_tei)).

pick_gtpc_remote_address(PGWAddrCandidateList, State) ->
    case PGWAddrCandidateList of
    [] ->
        %% Pick default address from configuration:
        State#gtp_state .raddr_str;
    [Head|_Tail] ->
        % TODO: pick a compatible address with .laddr from PGWAddrCandidateList is exists.
        Head
    end.

%% connect/2
connect(Name, {Socket, RemoteAddr, RemotePort}) ->
    lager:info("~s connecting to IP ~s port ~p~n", [Name, RemoteAddr, RemotePort]),
    gen_udp:connect(Socket, RemoteAddr, RemotePort).

connect(Address) ->
    connect(?SVC_NAME, Address).

rx_udp(IP, InPortNo, RxMsg, State) ->
    Req = gtp_packet:decode(RxMsg),
    lager:debug("S2b: Rx from IP ~p port ~p: ~p~n", [IP, InPortNo, Req]),
    rx_gtp(Req, State).

rx_gtp(Resp = #gtp{version = v2, type = create_session_response}, State0) ->
    Sess0 = find_gtp_session_by_local_teic(Resp#gtp.tei, State0),
    case Sess0 of
        undefined ->
            lager:error("Rx unknown TEI ~p: ~p~n", [Resp#gtp.tei, Resp]),
            {noreply, State0};
        Sess0 ->
            % Do GTP specific msg parsing here, pass only relevant fields:
            % First lookup Cause:
            #{{v2_cause,0} := #v2_cause{instance = 0, v2_cause = GtpCauseAtom}} = Resp#gtp.ie,
            GtpCause = gtp_utils:enum_v2_cause(GtpCauseAtom),
            case gtp_utils:v2_cause_successful(GtpCause) of
            true -> rx_gtp_create_session_response_successful(Resp, Sess0, State0);
            false -> rx_gtp_create_session_response_failure(GtpCause, Sess0, State0)
            end
        end;

rx_gtp(Resp = #gtp{version = v2, type = delete_session_response}, State0) ->
    Sess = find_gtp_session_by_local_teic(Resp#gtp.tei, State0),
    case Sess of
        undefined ->
            lager:error("Rx unknown TEI ~p: ~p~n", [Resp#gtp.tei, Resp]),
            {noreply, State0};
        Sess ->
            State1 = delete_gtp_session(Sess, State0),
            epdg_ue_fsm:received_gtpc_delete_session_response(Sess#gtp_session.pid, Resp),
            {noreply, State1}
        end;

rx_gtp(Req = #gtp{version = v2, type = create_bearer_request, ie = IEs}, State) ->
    Sess = find_gtp_session_by_local_teic(Req#gtp.tei, State),
    case Sess of
    undefined ->
        lager:error("Rx unknown TEI ~p: ~p~n", [Req#gtp.tei, Req]),
        {noreply, State};
    Sess ->
        #{{v2_bearer_context,0} := #v2_bearer_context{instance = 0, group = BearerIE}} = IEs,
        #{{v2_eps_bearer_id,0} := #v2_eps_bearer_id{instance = 0, eps_bearer_id = Ebi}} = BearerIE,
        #{{v2_fully_qualified_tunnel_endpoint_identifier,Ebi} :=
            #v2_fully_qualified_tunnel_endpoint_identifier{
                instance = Ebi,
                interface_type = _Interface,
                key = RemoteDataTei, ipv4 = _IP4, ipv6 = _IP6}} = BearerIE,
        Sess1 = gtp_session_add_bearer(Sess, #gtp_bearer{ebi = Ebi, remote_data_tei = RemoteDataTei}),
        State1 = update_gtp_session(Sess, Sess1, State),
        Resp = gen_create_bearer_response(Req, Sess1, request_accepted, State1),
        tx_gtp(Resp, State1),
        {noreply, State}
    end;

rx_gtp(Req = #gtp{version = v2, type = delete_bearer_request, ie = IEs}, State) ->
    Sess = find_gtp_session_by_local_teic(Req#gtp.tei, State),
    case Sess of
        undefined ->
            lager:error("Rx unknown TEI ~p: ~p~n", [Req#gtp.tei, Req]),
            {noreply, State};
        Sess ->
            #{{v2_cause,0} := _CauseIE,
              {v2_eps_bearer_id,0} := #v2_eps_bearer_id{instance = 0, eps_bearer_id = Ebi}} = IEs,
            % GtpCause = gtp_utils:enum_v2_cause(CauseIE#v2_cause.v2_cause)
            Bearer = gtp_session_find_bearer_by_ebi(Sess, Ebi),
            Resp = gen_delete_bearer_response(Req, Sess, request_accepted, State),
            tx_gtp(Resp, State),
            epdg_ue_fsm:received_gtpc_delete_bearer_request(Sess#gtp_session.pid),
            Sess1 = gtp_session_del_bearer(Sess, Bearer),
            State1 = update_gtp_session(Sess, Sess1, State),
            {noreply, State1}
        end;

rx_gtp(Req, State) ->
    lager:error("S2b: UNIMPLEMENTED Rx: ~p~n", [Req]),
    {noreply, State}.


rx_gtp_create_session_response_successful(Resp, Sess0, State0) ->
    #{{v2_fully_qualified_tunnel_endpoint_identifier,1} :=
        #v2_fully_qualified_tunnel_endpoint_identifier{
            interface_type = 32, %% "S2b PGW GTP-C"
            key = RemoteTEIC, ipv4 = _IPc4, ipv6 = _IPc6},
      {v2_pdn_address_allocation,0} := Paa,
      {v2_bearer_context,0} := #v2_bearer_context{instance = 0, group = BearerIE}} = Resp#gtp.ie,
    % Parse BearerContext:
    #{{v2_eps_bearer_id,0} := #v2_eps_bearer_id{instance = 0, eps_bearer_id = Ebi},
      {v2_fully_qualified_tunnel_endpoint_identifier,4} :=
        #v2_fully_qualified_tunnel_endpoint_identifier{
            interface_type = 33, %% "S2b-U PGW GTP-U"
            key = RemoteTEID, ipv4 = IPu4, ipv6 = IPu6}
     } = BearerIE,
    Bearer = gtp_session_find_bearer_by_ebi(Sess0, Ebi),
    Sess1 = gtp_session_update_bearer(Sess0, Bearer, Bearer#gtp_bearer{remote_data_tei = RemoteTEID}),
    Sess2 = Sess1#gtp_session{remote_control_tei = RemoteTEIC},
    lager:info("s2b: Updated Session after create_session_response: ~p~n", [Sess2]),
    State1 = update_gtp_session(Sess0, Sess2, State0),
    case maps:find({v2_additional_protocol_configuration_options,0}, Resp#gtp.ie) of
    {ok, APCO_dec} ->
        lager:debug("s2b: APCO_dec: ~p~n", [APCO_dec]),
        APCO = gtp_packet:encode_protocol_config_opts(APCO_dec#v2_additional_protocol_configuration_options.config);
    error ->
        lager:notice("s2b: APCO not found in CreateSessionResp!~n", []),
        APCO = undefined
    end,
    ResInfo0 = #{
        apn => binary_to_list(Sess0#gtp_session.apn),
        eua => conv:gtp2_paa_to_epdg_eua(Paa),
        local_teid => Bearer#gtp_bearer.local_data_tei,
        remote_teid => RemoteTEID,
        remote_ipv4 => IPu4,
        remote_ipv6 => IPu6
    },
    case APCO of
    undefined -> ResInfo = ResInfo0;
    _ -> ResInfo = maps:put(apco, APCO, ResInfo0)
    end,
    epdg_ue_fsm:received_gtpc_create_session_response(Sess0#gtp_session.pid, {ok, ResInfo}),
    {noreply, State1}.

rx_gtp_create_session_response_failure(GtpCause, Sess, State0) ->
    epdg_ue_fsm:received_gtpc_create_session_response(Sess#gtp_session.pid, {error, GtpCause}),
    State1 = delete_gtp_session(Sess, State0),
    {noreply, State1}.

tx_gtp(Req, State) ->
    lager:info("s2b: Tx ~p~n", [Req]),
    Msg = gtp_packet:encode(Req),
    gen_udp:send(State#gtp_state.socket, State#gtp_state.raddr, State#gtp_state.rport, Msg).

%% 7.2.1 Create Session Request
gen_create_session_request(#gtp_session{imsi = Imsi,
                                    apn = Apn,
                                    local_control_tei = LocalCtlTEI} = Sess,
                           APCO,
                           #gtp_state{laddr = LocalAddr,
                                      laddr_gtpu = LocalAddrGtpu,
                                      restart_counter = RCnt,
                                      seq_no = SeqNo}) ->
    Bearer = gtp_session_default_bearer(Sess),
    BearersIE = [#v2_bearer_level_quality_of_service{
                    pci = 1, pl = 10, pvi = 0, label = 8,
                    maximum_bit_rate_for_uplink      = 0,
                    maximum_bit_rate_for_downlink    = 0,
                    guaranteed_bit_rate_for_uplink   = 0,
                    guaranteed_bit_rate_for_downlink = 0
                  },
                  #v2_eps_bearer_id{eps_bearer_id = Bearer#gtp_bearer.ebi},
                  #v2_fully_qualified_tunnel_endpoint_identifier{
                    instance = Bearer#gtp_bearer.ebi,
                    interface_type = 31, %% "S2b-U ePDG GTP-U"
                    key = Bearer#gtp_bearer.local_data_tei,
                    ipv4 = conv:ip_to_bin(LocalAddrGtpu)
                  }
                ],
    APCO_decoded = gtp_packet:decode_protocol_config_opts(APCO),
    IEs = [#v2_international_mobile_subscriber_identity{imsi = Imsi},
           #v2_serving_network{
            plmn_id = gtp_utils:plmn_to_bin(?MCC, ?MNC, ?MNC_SIZE)
           },
           #v2_rat_type{rat_type = 3}, %% 3 = WLAN
           #v2_fully_qualified_tunnel_endpoint_identifier{
                instance = 0,
                interface_type = 30, %% "S2b ePDG GTP-C"
                key = LocalCtlTEI,
                ipv4 = conv:ip_to_bin(LocalAddr)
            },
            #v2_access_point_name{instance = 0, apn = [Apn]},
            #v2_selection_mode{mode = 0},
            #v2_pdn_address_allocation{type = ipv4, address = <<0,0,0,0>>},
            #v2_bearer_context{group = BearersIE},
            #v2_recovery{restart_counter = RCnt},
            #v2_additional_protocol_configuration_options{instance = 0, config = APCO_decoded}
          ],
    #gtp{version = v2, type = create_session_request, tei = 0, seq_no = SeqNo, ie = IEs}.

%% 7.2.9 Delete Session Request
gen_delete_session_request(#gtp_session{remote_control_tei = RemoteCtlTEI} = Sess,
                           #gtp_state{laddr = LocalAddr,
                                      seq_no = SeqNo}) ->
    Bearer = gtp_session_default_bearer(Sess),
    IEs = [#v2_eps_bearer_id{eps_bearer_id = Bearer#gtp_bearer.ebi},
           #v2_fully_qualified_tunnel_endpoint_identifier{
               instance = 0,
               interface_type = 30, %% "S2b ePDG GTP-C"
               key = Bearer#gtp_bearer.local_data_tei,
               ipv4 = conv:ip_to_bin(LocalAddr)
           }
    ],
    #gtp{version = v2, type = delete_session_request, tei = RemoteCtlTEI, seq_no = SeqNo, ie = IEs}.

gen_create_bearer_response(Req = #gtp{version = v2, type = create_bearer_request},
                           Sess = #gtp_session{remote_control_tei = RemoteCtlTEI},
                           GtpCause,
                           #gtp_state{laddr_gtpu = LocalAddrGtpu,
                                      restart_counter = RCnt}) ->
    Bearer = gtp_session_default_bearer(Sess),
    BearersIE = [#v2_bearer_level_quality_of_service{
        pci = 1, pl = 10, pvi = 0, label = 8,
        maximum_bit_rate_for_uplink      = 0,
        maximum_bit_rate_for_downlink    = 0,
        guaranteed_bit_rate_for_uplink   = 0,
        guaranteed_bit_rate_for_downlink = 0
        },
        #v2_eps_bearer_id{eps_bearer_id = Bearer#gtp_bearer.ebi},
        #v2_fully_qualified_tunnel_endpoint_identifier{
        instance = 0,
        interface_type = 31, %% "S2b-U ePDG GTP-U"
        key = Bearer#gtp_bearer.local_data_tei,
        ipv4 = conv:ip_to_bin(LocalAddrGtpu)
        }
    ],
    IEs = [#v2_cause{v2_cause = GtpCause},
           #v2_bearer_context{group = BearersIE},
           #v2_recovery{restart_counter = RCnt}
    ],
    #gtp{version = v2,
         type = create_bearer_response,
         tei = RemoteCtlTEI,
         seq_no = Req#gtp.seq_no,
         ie = IEs}.

gen_delete_bearer_response(Req = #gtp{version = v2, type = delete_bearer_request},
                           Sess = #gtp_session{remote_control_tei = RemoteCtlTEI},
                           GtpCause,
                           #gtp_state{restart_counter = RCnt}) ->
    IEs = [#v2_recovery{restart_counter = RCnt},
           #v2_cause{v2_cause = GtpCause}
          ],
    #gtp{version = v2,
         type = delete_bearer_response,
         tei = RemoteCtlTEI,
         seq_no = Req#gtp.seq_no,
         ie = IEs}.