%% Copyright (C) 2024-2025 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(s1ap_proxy).
-behaviour(gen_server).

-export([init/1,
         handle_info/2,
         handle_call/3,
         handle_cast/2,
         terminate/2]).
-export([start_link/1,
         set_genb_id/2,
         process_pdu/2,
         fetch_erab/2,
         fetch_erab_list/1,
         shutdown/1]).

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

-include("s1gw_metrics.hrl").

-include("S1AP-PDU-Descriptions.hrl").
-include("S1AP-PDU-Contents.hrl").
-include("S1AP-Containers.hrl").
-include("S1AP-Constants.hrl").
-include("S1AP-IEs.hrl").


-type s1ap_ie_id() :: non_neg_integer().
-type s1ap_ie_val() :: tuple().

-type mme_ue_id() :: 0..16#ffffffff.
-type enb_ue_id() :: 0..16#ffffff.
-type erab_id() :: 0..16#ff.
-type erab_uid() :: {mme_ue_id(), erab_id()}.

-record(proxy_state, {owner :: pid(),
                      erabs :: dict:dict(K :: erab_uid(),
                                         V :: pid()),
                      genb_id_str :: undefined | string(),
                      mme_ue_id :: undefined | mme_ue_id(),
                      enb_ue_id :: undefined | enb_ue_id(),
                      erab_id :: undefined | erab_id(),
                      path :: [s1ap_ie_id()]
                     }).

-type proxy_state() :: #proxy_state{}.
-type proxy_action() :: forward | reply | drop.

-export_type([proxy_action/0]).


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

-spec start_link(pid()) -> gen_server:start_ret().
start_link(Owner) ->
    gen_server:start_link(?MODULE, [Owner], []).


-spec set_genb_id(pid(), string()) -> ok | {error, term()}.
set_genb_id(Pid, GlobalENBId) ->
    gen_server:call(Pid, {?FUNCTION_NAME, GlobalENBId}).


-type process_pdu_result() :: {proxy_action(), binary()}.
-spec process_pdu(pid(), binary()) -> process_pdu_result().
process_pdu(Pid, PDU) ->
    gen_server:call(Pid, {?FUNCTION_NAME, PDU}).


%% Fetch a single E-RAB from the registry (by UID)
-spec fetch_erab(pid(), erab_uid()) -> {ok, pid()} | error.
fetch_erab(Pid, UID) ->
    gen_server:call(Pid, {?FUNCTION_NAME, UID}).


%% Fetch all E-RABs from the registry in form of a list
-spec fetch_erab_list(pid()) -> [{erab_uid(), pid()}].
fetch_erab_list(Pid) ->
    gen_server:call(Pid, ?FUNCTION_NAME).


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


%% ------------------------------------------------------------------
%% gen_server API
%% ------------------------------------------------------------------

init([Owner]) ->
    process_flag(trap_exit, true),
    {ok, #proxy_state{erabs = dict:new(),
                      owner = Owner,
                      path = []}}.


handle_call({process_pdu, OrigData}, _From,
            #proxy_state{} = S0) ->
    {Reply, S1} = handle_pdu_bin(OrigData, S0),
    {reply, Reply, S1};

handle_call({fetch_erab, UID}, _From,
            #proxy_state{erabs = ERABs} = S) ->
    {reply, dict:find(UID, ERABs), S};

handle_call(fetch_erab_list, _From,
            #proxy_state{erabs = ERABs} = S) ->
    {reply, dict:to_list(ERABs), S};

handle_call({set_genb_id, GlobalENBId}, _From,
            #proxy_state{genb_id_str = undefined} = S) ->
    ?LOG_DEBUG("Global-eNB-ID is set: ~s", [GlobalENBId]),
    %% use that as a context for logging
    osmo_s1gw:set_log_prefix("eNB " ++ GlobalENBId),
    %% register per-eNB metrics
    ctr_reg_all(GlobalENBId),
    {reply, ok, S#proxy_state{genb_id_str = GlobalENBId}};

handle_call({set_genb_id, _GlobalENBId}, _From,
            #proxy_state{genb_id_str = GlobalENBId} = S) ->
    ?LOG_ERROR("Global-eNB-ID is already set: ~s", [GlobalENBId]),
    {reply, {error, ealready}, S};

handle_call(Info, From,
            #proxy_state{} = S) ->
    ?LOG_ERROR("unknown ~p() from ~p: ~p", [?FUNCTION_NAME, From, Info]),
    {reply, {error, not_implemented}, S}.


handle_cast(Info, #proxy_state{} = S) ->
    ?LOG_ERROR("unknown ~p(): ~p", [?FUNCTION_NAME, Info]),
    {noreply, S}.


handle_info({'EXIT', Pid, Reason},
            #proxy_state{erabs = ERABs} = S) ->
    ?LOG_DEBUG("Child process ~p terminated with reason ~p", [Pid, Reason]),
    Fun = fun(_Key, Val) -> Val =/= Pid end,
    {noreply, S#proxy_state{erabs = dict:filter(Fun, ERABs)}};

handle_info(Info, #proxy_state{} = S) ->
    ?LOG_ERROR("unknown ~p(): ~p", [?FUNCTION_NAME, Info]),
    {noreply, S}.


terminate(Reason, #proxy_state{}) ->
    ?LOG_NOTICE("Terminating, reason ~p", [Reason]),
    ok.


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

-spec erab_uid(erab_id(), proxy_state()) -> erab_uid().
erab_uid(ERABId, #proxy_state{mme_ue_id = MmeUeId}) ->
    {MmeUeId, ERABId}.


-spec erab_for_each(MMEUeId, Fun, ERABs) -> ok
    when MMEUeId :: mme_ue_id(),
         Fun :: fun((pid()) -> term()),
         ERABs :: [{erab_uid(), pid()}].
erab_for_each(MMEUEId, Fun,
              [{{MMEUEId, _}, Pid} | ERABs]) ->
    Fun(Pid), %% it's a match!
    erab_for_each(MMEUEId, Fun, ERABs);

erab_for_each(MMEUeId, Fun,
              [_ERAB | ERABs]) ->
    erab_for_each(MMEUeId, Fun, ERABs);

erab_for_each(_MMEUeId, _Fun, []) ->
    ok.


%% register a single per-eNB counter
-spec ctr_reg(C, GlobalENBId) -> C
    when C :: [ctr | _],
         GlobalENBId :: string().
ctr_reg([ctr, s1ap, proxy | _] = C0, GlobalENBId) ->
    C1 = s1gw_metrics:enb_metric(C0, GlobalENBId),
    %% counter may already exist, so catch exceptions here
    catch exometer:new(C1, counter),
    C0;

ctr_reg(C0, _GlobalENBId) -> C0.


%% register all per-eNB counters
-spec ctr_reg_all(string()) -> list().
ctr_reg_all(GlobalENBId) ->
    Ctrs = s1gw_metrics:ctr_list(),
    lists:map(fun(Name) -> ctr_reg(Name, GlobalENBId) end, Ctrs).


%% increment the global and/or per-eNB counters
-spec ctr_inc(C0, S) -> term()
    when C0 :: [ctr | _],
         S :: proxy_state().
ctr_inc([ctr | _] = C0, S) ->
    case S#proxy_state.genb_id_str of
        undefined ->
            %% increment the global counter only
            s1gw_metrics:ctr_inc(C0);
        GlobalENBId ->
            %% increment both the global and per-eNB counters
            C1 = s1gw_metrics:enb_metric(C0, GlobalENBId),
            s1gw_metrics:ctr_inc(C0),
            s1gw_metrics:ctr_inc(C1)
    end.


-spec tla_str(binary()) -> string().
tla_str(TLA0) ->
    TLA1 = list_to_tuple(binary_to_list(TLA0)),
    inet:ntoa(TLA1).


%% Process an S1AP PDU (binary)
-spec handle_pdu_bin(binary(), proxy_state()) -> {process_pdu_result(), proxy_state()}.
handle_pdu_bin(OrigData, S0) ->
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, S0),
    try s1ap_utils:decode_pdu(OrigData) of
        {ok, PDU} ->
            ?LOG_DEBUG("Rx S1AP PDU: ~p", [PDU]),
            try handle_pdu(PDU, S0) of
                {{Action, NewPDU}, S1} ->
                    {ok, NewData} = s1ap_utils:encode_pdu(NewPDU),
                    ?LOG_DEBUG("Tx (~p) S1AP PDU: ~p", [Action, NewPDU]),
                    case Action of
                        forward ->
                            ctr_inc(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, S1),
                            ctr_inc(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, S1);
                        reply ->
                            ctr_inc(?S1GW_CTR_S1AP_PROXY_OUT_PKT_REPLY_ALL, S1)
                    end,
                    {{Action, NewData}, S1};
                {forward, S1} ->
                    ?LOG_DEBUG("Tx (forward) S1AP PDU unmodified"),
                    ctr_inc(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, S1),
                    ctr_inc(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, S1),
                    {{forward, OrigData}, S1};
                {drop, S1} ->
                    ?LOG_NOTICE("Drop received S1AP PDU: ~p", [PDU]),
                    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, S1),
                    {{drop, OrigData}, S1}
            catch
                Exception:Reason:StackTrace ->
                    ?LOG_ERROR("An exception occurred: ~p, ~p, ~p", [Exception, Reason, StackTrace]),
                    ctr_inc(?S1GW_CTR_S1AP_PROXY_EXCEPTION, S0),
                    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S0),
                    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, S0),
                    {{drop, OrigData}, S0}
            end;
        {error, {asn1, Error}} ->
            ?LOG_ERROR("S1AP PDU decoding failed: ~p", [Error]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_DECODE_ERROR, S0),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, S0),
            {{drop, OrigData}, S0}
    catch
        Exception:Reason:StackTrace ->
            ?LOG_ERROR("An exception occurred: ~p, ~p, ~p", [Exception, Reason, StackTrace]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_EXCEPTION, S0),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_DECODE_ERROR, S0),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, S0),
            {{drop, OrigData}, S0}
    end.


%% Process an S1AP PDU (decoded)
-spec handle_pdu(PDU, S0) -> {Result, S1}
    when PDU :: s1ap_utils:s1ap_pdu(),
         S0 :: proxy_state(),
         S1 :: proxy_state(),
         Result :: {forward | reply, s1ap_utils:s1ap_pdu()} | forward | drop.


%% 9.1.3.1 E-RAB SETUP REQUEST
handle_pdu({Outcome = initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-E-RABSetup',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing E-RAB SETUP REQUEST"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_REQ, S0),
    case handle_ies(?'id-E-RABToBeSetupListBearerSUReq',
                    C0#'E-RABSetupRequest'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'E-RABSetupRequest'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'InitiatingMessage'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process E-RAB SETUP REQUEST: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            PDU = build_erab_setup_response_failure(S1),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_OUT_PKT_REPLY_ERAB_SETUP_RSP, S1),
            {{reply, PDU}, S1} %% reply PDU back to sender
    end;

%% 9.1.3.2 E-RAB SETUP RESPONSE
handle_pdu({Outcome = successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-E-RABSetup',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing E-RAB SETUP RESPONSE"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_RSP, S0),
    case handle_ies(?'id-E-RABSetupListBearerSURes',
                    C0#'E-RABSetupResponse'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'E-RABSetupResponse'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'SuccessfulOutcome'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process E-RAB SETUP RESPONSE: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {drop, S1} %% drop this PDU
    end;

%% 9.1.3.3 E-RAB MODIFY REQUEST
handle_pdu({Outcome = initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-E-RABModify',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing E-RAB MODIFY REQUEST"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MODIFY_REQ, S0),
    case handle_ies(?'id-E-RABToBeModifiedListBearerModReq',
                    C0#'E-RABModifyRequest'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'E-RABModifyRequest'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'InitiatingMessage'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process E-RAB MODIFY REQUEST: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {drop, S1} %% drop this PDU
    end;

%% 9.1.3.4 E-RAB MODIFY RESPONSE
handle_pdu({successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-E-RABModify',
                                 value = C0}}, S0) ->
    ?LOG_DEBUG("Processing E-RAB MODIFY RESPONSE"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MODIFY_RSP, S0),
    %% there's nothing to patch in this PDU, so we forward it as-is
    %% TODO: check result of handle_ies(), inc. ?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR
    {_, S1} = handle_ies(?'id-E-RABModifyListBearerModRes',
                         C0#'E-RABModifyResponse'.protocolIEs, S0),
    {_, S2} = handle_ies(?'id-E-RABFailedToModifyList',
                         C0#'E-RABModifyResponse'.protocolIEs, S1),
    {forward, S2};

%% 9.1.3.5 E-RAB RELEASE COMMAND
handle_pdu({initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-E-RABRelease',
                                 value = C0}}, S0) ->
    ?LOG_DEBUG("Processing E-RAB RELEASE COMMAND"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_CMD, S0),
    %% there's nothing to patch in this PDU, so we forward it as-is
    case handle_ies(?'id-E-RABToBeReleasedList',
                    C0#'E-RABReleaseCommand'.protocolIEs, S0) of
        {{ok, _}, S1} ->
            {forward, S1};
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process E-RAB RELEASE COMMAND: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {forward, S1}
    end;

%% 9.1.3.6 E-RAB RELEASE RESPONSE
handle_pdu({successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-E-RABRelease',
                                 value = C0}}, S0) ->
    ?LOG_DEBUG("Processing E-RAB RELEASE RESPONSE"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_RSP, S0),
    %% there's nothing to patch in this PDU, so we forward it as-is
    case handle_ies(?'id-E-RABReleaseListBearerRelComp',
                    C0#'E-RABReleaseResponse'.protocolIEs, S0) of
        {{ok, _}, S1} ->
            {forward, S1};
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process E-RAB RELEASE RESPONSE: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {forward, S1}
    end;

%% 9.1.3.7 E-RAB RELEASE INDICATION
handle_pdu({initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-E-RABReleaseIndication',
                                 value = C0}}, S0) ->
    ?LOG_DEBUG("Processing E-RAB RELEASE INDICATION"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_IND, S0),
    %% there's nothing to patch in this PDU, so we forward it as-is
    case handle_ies(?'id-E-RABReleasedList',
                    C0#'E-RABReleaseIndication'.protocolIEs, S0) of
        {{ok, _}, S1} ->
            {forward, S1};
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process E-RAB RELEASE INDICATION: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {forward, S1}
    end;

%% 9.1.3.8 E-RAB MODIFICATION INDICATION
handle_pdu({Outcome = initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-E-RABModificationIndication',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing E-RAB MODIFICATION INDICATION"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_IND, S0),
    IEs0 = C0#'E-RABModificationIndication'.protocolIEs,
    %% E-RAB to be Modified List
    %% TODO: handle {error, Reason}
    {{ok, IEs1}, S1} = handle_ies(?'id-E-RABToBeModifiedListBearerModInd', IEs0, S0),
    %% E-RAB not to be Modified List
    %% TODO: handle {error, Reason}
    {{ok, IEs2}, S2} = handle_ies(?'id-E-RABNotToBeModifiedListBearerModInd', IEs1, S1),
    C1 = C0#'E-RABModificationIndication'{protocolIEs = IEs2},
    PDU = {Outcome, Msg#'InitiatingMessage'{value = C1}},
    {{forward, PDU}, S2};

%% 9.1.3.9 E-RAB MODIFICATION CONFIRM
handle_pdu({successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-E-RABModificationIndication',
                                 value = C0}}, S0) ->
    ?LOG_DEBUG("Processing E-RAB MODIFICATION CONFIRM"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_CNF, S0),
    IEs = C0#'E-RABModificationConfirm'.protocolIEs,
    %% E-RAB Modify List
    %% TODO: handle {error, Reason}
    {_, S1} = handle_ies(?'id-E-RABModifyListBearerModConf', IEs, S0),
    %% E-RAB Failed to Modify List
    %% TODO: handle {error, Reason}
    {_, S2} = handle_ies(?'id-E-RABFailedToModifyListBearerModConf', IEs, S1),
    %% E-RAB To Be Released List
    %% TODO: handle {error, Reason}
    {_, S3} = handle_ies(?'id-E-RABToBeReleasedListBearerModConf', IEs, S2),
    %% there's nothing to patch in this PDU, so we forward it as-is
    {forward, S3};

%% 9.1.4.1 INITIAL CONTEXT SETUP REQUEST
handle_pdu({Outcome = initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-InitialContextSetup',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing INITIAL CONTEXT SETUP REQUEST"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_REQ, S0),
    case handle_ies(?'id-E-RABToBeSetupListCtxtSUReq',
                    C0#'InitialContextSetupRequest'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'InitialContextSetupRequest'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'InitiatingMessage'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process INITIAL CONTEXT SETUP REQUEST: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {drop, S1} %% drop this PDU
    end;

%% 9.1.4.3 INITIAL CONTEXT SETUP RESPONSE
handle_pdu({Outcome = successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-InitialContextSetup',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing INITIAL CONTEXT SETUP RESPONSE"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_RSP, S0),
    %% TODO: handle optional E-RAB Failed to Setup List IE
    case handle_ies(?'id-E-RABSetupListCtxtSURes',
                    C0#'InitialContextSetupResponse'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'InitialContextSetupResponse'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'SuccessfulOutcome'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process INITIAL CONTEXT SETUP RESPONSE: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {drop, S1} %% drop this PDU
    end;

%% 9.1.4.5 UE CONTEXT RELEASE REQUEST
handle_pdu({initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-UEContextReleaseRequest'}},
           #proxy_state{} = S) ->
    ?LOG_DEBUG("Processing UE CONTEXT RELEASE REQUEST"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_REQ, S),
    {forward, S}; %% forward as-is, there's nothing to patch

%% 9.1.4.6 UE CONTEXT RELEASE COMMAND
handle_pdu({initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-UEContextRelease',
                                 value = #'UEContextReleaseCommand'{protocolIEs = IEs}}},
           #proxy_state{erabs = ERABs} = S) ->
    ?LOG_DEBUG("Processing UE CONTEXT RELEASE COMMAND"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_CMD, S),
    %% fetch MME-UE-S1AP-ID value from UE-S1AP-IDs (mandatory IE)
    #'ProtocolIE-Field'{id = ?'id-UE-S1AP-IDs',
                        value = S1APIDs} = lists:nth(1, IEs),
    MMEUEId = case S1APIDs of
        {'uE-S1AP-ID-pair', #'UE-S1AP-ID-pair'{'mME-UE-S1AP-ID' = Val}} -> Val;
        {'mME-UE-S1AP-ID', Val} -> Val
    end,
    %% poke E-RAB FSMs with the matching MME-UE-S1AP-ID
    erab_for_each(MMEUEId,
                  fun erab_fsm:erab_release_cmd/1,
                  dict:to_list(ERABs)),
    {forward, S}; %% forward as-is, there's nothing to patch

%% 9.1.4.7 UE CONTEXT RELEASE COMPLETE
handle_pdu({successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-UEContextRelease',
                                 value = #'UEContextReleaseComplete'{protocolIEs = IEs}}},
           #proxy_state{erabs = ERABs} = S) ->
    ?LOG_DEBUG("Processing UE CONTEXT RELEASE COMPLETE"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_COMPL, S),
    %% fetch MME-UE-S1AP-ID value (mandatory IE)
    #'ProtocolIE-Field'{id = ?'id-MME-UE-S1AP-ID',
                        value = MMEUEId} = lists:nth(1, IEs),
    %% poke E-RAB FSMs with the matching MME-UE-S1AP-ID
    erab_for_each(MMEUEId,
                  fun erab_fsm:erab_release_rsp/1,
                  dict:to_list(ERABs)),
    {forward, S}; %% forward as-is, there's nothing to patch

%% 9.1.5.2 HANDOVER COMMAND
handle_pdu({successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-HandoverPreparation',
                                 value = C0}}, S0) ->
    ?LOG_DEBUG("Processing HANDOVER COMMAND"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_CMD, S0),
    case handle_ies(?'id-E-RABtoReleaseListHOCmd',
                    C0#'HandoverCommand'.protocolIEs, S0) of
        {{ok, _}, S1} ->
            {forward, S1}; %% forward as-is, there's nothing to patch
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process HANDOVER COMMAND: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {forward, S1}
    end;

%% 9.1.5.4 HANDOVER REQUEST
handle_pdu({Outcome = initiatingMessage,
            #'InitiatingMessage'{procedureCode = ?'id-HandoverResourceAllocation',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing HANDOVER REQUEST"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_REQ, S0),
    case handle_ies(?'id-E-RABToBeSetupListHOReq',
                    C0#'HandoverRequest'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'HandoverRequest'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'InitiatingMessage'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process HANDOVER REQUEST: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {drop, S1} %% drop this PDU
    end;

%% 9.1.5.5 HANDOVER REQUEST ACKNOWLEDGE,
handle_pdu({Outcome = successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = ?'id-HandoverResourceAllocation',
                                 value = C0} = Msg}, S0) ->
    ?LOG_DEBUG("Processing HANDOVER REQUEST ACKNOWLEDGE"),
    ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_REQ_ACK, S0),
    %% handle the list of admitted E-RABs
    {Result, S2} = case handle_ies(?'id-E-RABAdmittedList',
                                   C0#'HandoverRequestAcknowledge'.protocolIEs, S0) of
        {{ok, IEs}, S1} ->
            C1 = C0#'HandoverRequestAcknowledge'{protocolIEs = IEs},
            PDU = {Outcome, Msg#'SuccessfulOutcome'{value = C1}},
            {{forward, PDU}, S1}; %% forward patched PDU
        {{error, Reason}, S1} ->
            ?LOG_NOTICE("Failed to process HANDOVER REQUEST ACKNOWLEDGE: ~p", [Reason]),
            ctr_inc(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, S1),
            {drop, S1} %% drop this PDU
    end,
    %% handle the (optional) list of failed E-RABs
    %% no #proxy_state modification is expected here
    handle_ies(?'id-E-RABFailedToSetupListHOReqAck',
               C0#'HandoverRequestAcknowledge'.protocolIEs, S2),
    {Result, S2};


%% TODO: 9.1.5.8 PATH SWITCH REQUEST :: (M) Transport Layer Address
%% TODO: 9.1.5.9 PATH SWITCH REQUEST ACKNOWLEDGE :: (M) Transport Layer Address

%% Proxy all other messages unmodified
handle_pdu(_PDU, S) ->
    {forward, S}.


%% Handle a single IE (Information Element)
-type handle_ie_result() :: {ok, s1ap_ie_val()} | {error, term()}.
-spec handle_ie(Path, Content, State) -> Result
    when Path :: [s1ap_ie_id()],
         Content :: s1ap_ie_val(),
         State :: proxy_state(),
         Result :: {handle_ie_result(),
                    proxy_state()}.

%% E-RAB SETUP REQUEST related IEs
handle_ie([?'id-E-RABToBeSetupListBearerSUReq'], C, S) ->
    %% This IE contains a list of BearerSUReq, so patch inner IEs
    handle_ies(?'id-E-RABToBeSetupItemBearerSUReq', C, S);

handle_ie([?'id-E-RABToBeSetupItemBearerSUReq',
           ?'id-E-RABToBeSetupListBearerSUReq'],
          #'E-RABToBeSetupItemBearerSUReq'{'e-RAB-ID' = ERABId,
                                           'transportLayerAddress' = TLA_In,
                                           'gTP-TEID' = << TEID_In:32/big >>} = C0, S0) ->
    %% start and register an E-RAB FSM
    {Pid, S1} = erab_fsm_start_reg(ERABId, S0),
    case erab_fsm:erab_setup_req(Pid, {TEID_In, TLA_In}) of
        {ok, {TEID_Out, TLA_Out}} ->
            C1 = C0#'E-RABToBeSetupItemBearerSUReq'{'transportLayerAddress' = TLA_Out,
                                                    'gTP-TEID' = << TEID_Out:32/big >>},
            {{ok, C1}, S1};
        {error, Reason} ->
            {{error, Reason}, S1}
    end;

%% E-RAB SETUP RESPONSE related IEs
handle_ie([?'id-E-RABSetupListBearerSURes'], C, S) ->
    %% This IE contains a list of BearerSURes, so patch inner IEs
    handle_ies(?'id-E-RABSetupItemBearerSURes', C, S);

handle_ie([?'id-E-RABSetupItemBearerSURes',
           ?'id-E-RABSetupListBearerSURes'],
          #'E-RABSetupItemBearerSURes'{'e-RAB-ID' = ERABId,
                                       'transportLayerAddress' = TLA_In,
                                       'gTP-TEID' = << TEID_In:32/big >>} = C0, S) ->
    %% signal eNB's GTP-U address to the parent process
    signal_enb_addr(TLA_In, S),
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            case erab_fsm:erab_setup_rsp(Pid, {TEID_In, TLA_In}) of
                {ok, {TEID_Out, TLA_Out}} ->
                    C1 = C0#'E-RABSetupItemBearerSURes'{'transportLayerAddress' = TLA_Out,
                                                        'gTP-TEID' = << TEID_Out:32/big >>},
                    {{ok, C1}, S};
                {error, Reason} ->
                    {{error, Reason}, S}
            end;
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.3 E-RAB MODIFY REQUEST related IEs
handle_ie([?'id-E-RABToBeModifiedListBearerModReq'], C, S) ->
    %% This IE contains a list of BearerModReq, so patch inner IEs
    handle_ies(?'id-E-RABToBeModifiedItemBearerModReq', C, S);

handle_ie([?'id-E-RABToBeModifiedItemBearerModReq',
           ?'id-E-RABToBeModifiedListBearerModReq'],
          #'E-RABToBeModifiedItemBearerModReq'{'e-RAB-ID' = ERABId,
                                               'iE-Extensions' = E0} = C0, S0) ->
    %% The IE-Extensions may optionally contain F-TEID, so patch inner IEs
    case E0 of
        %% no extensions means no F-TEID, so nothing to patch
        asn1_NOVALUE ->
            {{ok, C0}, S0};
        _ ->
            case handle_ies(?'id-TransportInformation',
                            E0, S0#proxy_state{erab_id = ERABId}) of
                {{ok, E1}, S1} ->
                    C1 = C0#'E-RABToBeModifiedItemBearerModReq'{'iE-Extensions' = E1},
                    {{ok, C1}, S1};
                Error ->
                    Error
            end
    end;

handle_ie([?'id-TransportInformation',
           ?'id-E-RABToBeModifiedItemBearerModReq',
           ?'id-E-RABToBeModifiedListBearerModReq'],
          #'TransportInformation'{'transportLayerAddress' = TLA_In,
                                  'uL-GTP-TEID' = << TEID_In:32/big >>} = C0, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(S#proxy_state.erab_id, S) of
        {ok, Pid} ->
            case erab_fsm:erab_modify_req(Pid, {TEID_In, TLA_In}) of
                {ok, {TEID_Out, TLA_Out}} ->
                    C1 = C0#'TransportInformation'{'transportLayerAddress' = TLA_Out,
                                                   'uL-GTP-TEID' = << TEID_Out:32/big >>},
                    {{ok, C1}, S};
                {error, Reason} ->
                    {{error, Reason}, S}
            end;
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered",
                       [erab_uid(S#proxy_state.erab_id, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.4 E-RAB MODIFY RESPONSE related IEs
handle_ie([?'id-E-RABModifyListBearerModRes'], C, S) ->
    %% This IE contains a list of BearerModRes, so patch inner IEs
    handle_ies(?'id-E-RABModifyItemBearerModRes', C, S);

handle_ie([?'id-E-RABModifyItemBearerModRes',
           ?'id-E-RABModifyListBearerModRes'],
          #'E-RABModifyItemBearerModRes'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_modify_rsp(Pid, ack),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

handle_ie([?'id-E-RABFailedToModifyList'], C, S) ->
    %% This IE contains a list of E-RABItem, so patch inner IEs
    handle_ies(?'id-E-RABItem', C, S);

handle_ie([?'id-E-RABItem',
           ?'id-E-RABFailedToModifyList'],
          #'E-RABItem'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_modify_rsp(Pid, nack),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.5 E-RAB RELEASE COMMAND related IEs
handle_ie([?'id-E-RABToBeReleasedList'], C, S) ->
    %% This IE contains a list of E-RABItem
    handle_ies(?'id-E-RABItem', C, S);

handle_ie([?'id-E-RABItem',
           ?'id-E-RABToBeReleasedList'],
          #'E-RABItem'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_release(Pid, cmd),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.6 E-RAB RELEASE RESPONSE related IEs
handle_ie([?'id-E-RABReleaseListBearerRelComp'], C, S) ->
    %% This IE contains a list of E-RABReleaseItemBearerRelComp
    handle_ies(?'id-E-RABReleaseItemBearerRelComp', C, S);

handle_ie([?'id-E-RABReleaseItemBearerRelComp',
           ?'id-E-RABReleaseListBearerRelComp'],
          #'E-RABReleaseItemBearerRelComp'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_release_rsp(Pid),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.7 E-RAB RELEASE INDICATION related IEs
handle_ie([?'id-E-RABReleasedList'], C, S) ->
    %% This IE contains a list of E-RABItem
    handle_ies(?'id-E-RABItem', C, S);

handle_ie([?'id-E-RABItem',
           ?'id-E-RABReleasedList'],
          #'E-RABItem'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_release(Pid, ind),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.8 E-RAB MODIFICATION INDICATION related IEs
handle_ie([?'id-E-RABToBeModifiedListBearerModInd'], C, S) ->
    %% This IE contains a list of BearerModInd, so patch inner IEs
    handle_ies(?'id-E-RABToBeModifiedItemBearerModInd', C, S);

handle_ie([?'id-E-RABToBeModifiedItemBearerModInd',
           ?'id-E-RABToBeModifiedListBearerModInd'],
          #'E-RABToBeModifiedItemBearerModInd'{'e-RAB-ID' = ERABId,
                                               'transportLayerAddress' = TLA_In,
                                               'dL-GTP-TEID' = << TEID_In:32/big >>} = C0, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            case erab_fsm:erab_modify_ind(Pid, {TEID_In, TLA_In}) of
                {ok, {TEID_Out, TLA_Out}} ->
                    C1 = C0#'E-RABToBeModifiedItemBearerModInd'{'transportLayerAddress' = TLA_Out,
                                                                'dL-GTP-TEID' = << TEID_Out:32/big >>},
                    {{ok, C1}, S};
                {error, Reason} ->
                    {{error, Reason}, S}
            end;
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

handle_ie([?'id-E-RABNotToBeModifiedListBearerModInd'], C, S) ->
    %% This IE contains a list of BearerModInd, so patch inner IEs
    handle_ies(?'id-E-RABNotToBeModifiedItemBearerModInd', C, S);

handle_ie([?'id-E-RABNotToBeModifiedItemBearerModInd',
           ?'id-E-RABNotToBeModifiedListBearerModInd'],
          #'E-RABNotToBeModifiedItemBearerModInd'{'e-RAB-ID' = ERABId} = C0, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            %% Ignore F-TEID in the original message, just replace it with C2U F-TEID
            Info = erab_fsm:fetch_info(Pid),
            case maps:find(f_teid_c2u, Info) of
                {ok, {TEID, TLA}} ->
                    C1 = C0#'E-RABNotToBeModifiedItemBearerModInd'{'transportLayerAddress' = TLA,
                                                                   'dL-GTP-TEID' = << TEID:32/big >>},
                    {{ok, C1}, S};
                error ->
                    ?LOG_ERROR("E-RAB-ID ~p :: missing C2U F-TEID", [erab_uid(ERABId, S)]),
                    {{error, missing_f_teid}, S}
            end;
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% 9.1.3.9 E-RAB MODIFICATION CONFIRM related IEs
handle_ie([?'id-E-RABModifyListBearerModConf'], C, S) ->
    %% This IE contains a list of BearerModConf, so patch inner IEs
    handle_ies(?'id-E-RABModifyItemBearerModConf', C, S);

handle_ie([?'id-E-RABModifyItemBearerModConf',
           ?'id-E-RABModifyListBearerModConf'],
          #'E-RABModifyItemBearerModConf'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_modify_cnf(Pid, ack),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

handle_ie([?'id-E-RABFailedToModifyListBearerModConf'], C, S) ->
    %% This IE contains a list of E-RABItem, so patch inner IEs
    handle_ies(?'id-E-RABItem', C, S);

handle_ie([?'id-E-RABItem',
           ?'id-E-RABFailedToModifyListBearerModConf'],
          #'E-RABItem'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_modify_cnf(Pid, nack),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

handle_ie([?'id-E-RABToBeReleasedListBearerModConf'], C, S) ->
    %% This IE contains a list of E-RABItem, so patch inner IEs
    handle_ies(?'id-E-RABItem', C, S);

handle_ie([?'id-E-RABItem',
           ?'id-E-RABToBeReleasedListBearerModConf'],
          #'E-RABItem'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_release_ind(Pid),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% INITIAL CONTEXT SETUP REQUEST related IEs
handle_ie([?'id-E-RABToBeSetupListCtxtSUReq'], C, S) ->
    %% This IE contains a list of CtxtSUReq, so patch inner IEs
    handle_ies(?'id-E-RABToBeSetupItemCtxtSUReq', C, S);

handle_ie([?'id-E-RABToBeSetupItemCtxtSUReq',
           ?'id-E-RABToBeSetupListCtxtSUReq'],
          #'E-RABToBeSetupItemCtxtSUReq'{'e-RAB-ID' = ERABId,
                                         'transportLayerAddress' = TLA_In,
                                         'gTP-TEID' = << TEID_In:32/big >>} = C0, S0) ->
    %% start and register an E-RAB FSM
    {Pid, S1} = erab_fsm_start_reg(ERABId, S0),
    case erab_fsm:erab_setup_req(Pid, {TEID_In, TLA_In}) of
        {ok, {TEID_Out, TLA_Out}} ->
            C1 = C0#'E-RABToBeSetupItemCtxtSUReq'{'transportLayerAddress' = TLA_Out,
                                                  'gTP-TEID' = << TEID_Out:32/big >>},
            {{ok, C1}, S1};
        {error, Reason} ->
            {{error, Reason}, S1}
    end;

%% INITIAL CONTEXT SETUP RESPONSE related IEs
handle_ie([?'id-E-RABSetupListCtxtSURes'], C, S) ->
    %% This IE contains a list of CtxtSURes, so patch inner IEs
    handle_ies(?'id-E-RABSetupItemCtxtSURes', C, S);

handle_ie([?'id-E-RABSetupItemCtxtSURes',
           ?'id-E-RABSetupListCtxtSURes'],
          #'E-RABSetupItemCtxtSURes'{'e-RAB-ID' = ERABId,
                                     'transportLayerAddress' = TLA_In,
                                     'gTP-TEID' = << TEID_In:32/big >>} = C0, S) ->
    %% signal eNB's GTP-U address to the parent process
    signal_enb_addr(TLA_In, S),
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            case erab_fsm:erab_setup_rsp(Pid, {TEID_In, TLA_In}) of
                {ok, {TEID_Out, TLA_Out}} ->
                    C1 = C0#'E-RABSetupItemCtxtSURes'{'transportLayerAddress' = TLA_Out,
                                                      'gTP-TEID' = << TEID_Out:32/big >>},
                    {{ok, C1}, S};
                {error, Reason} ->
                    {{error, Reason}, S}
            end;
        error ->
            ?LOG_ERROR("E-RAB ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% HANDOVER COMMAND related IEs
handle_ie([?'id-E-RABtoReleaseListHOCmd'], C, S) ->
    %% This IE contains a list of E-RABItem, so patch inner IEs
    handle_ies(?'id-E-RABItem', C, S);

handle_ie([?'id-E-RABItem',
           ?'id-E-RABtoReleaseListHOCmd'],
          #'E-RABItem'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_release_ind(Pid),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% HANDOVER REQUEST related IEs
handle_ie([?'id-E-RABToBeSetupListHOReq'], C, S) ->
    %% This IE contains a list of E-RABToBeSetupItemHOReq, so patch inner IEs
    handle_ies(?'id-E-RABToBeSetupItemHOReq', C, S);

handle_ie([?'id-E-RABToBeSetupItemHOReq',
           ?'id-E-RABToBeSetupListHOReq'],
          #'E-RABToBeSetupItemHOReq'{'e-RAB-ID' = ERABId,
                                     'transportLayerAddress' = TLA_In,
                                     'gTP-TEID' = << TEID_In:32/big >>} = C0, S0) ->
    %% start and register an E-RAB FSM
    {Pid, S1} = erab_fsm_start_reg(ERABId, S0),
    case erab_fsm:erab_setup_req(Pid, {TEID_In, TLA_In}) of
        {ok, {TEID_Out, TLA_Out}} ->
            C1 = C0#'E-RABToBeSetupItemHOReq'{'transportLayerAddress' = TLA_Out,
                                              'gTP-TEID' = << TEID_Out:32/big >>},
            {{ok, C1}, S1};
        {error, Reason} ->
            {{error, Reason}, S1}
    end;

%% HANDOVER REQUEST ACKNOWLEDGE related IEs
handle_ie([?'id-E-RABAdmittedList'], C, S) ->
    %% This IE contains a list of E-RABAdmittedItem, so patch inner IEs
    handle_ies(?'id-E-RABAdmittedItem', C, S);

handle_ie([?'id-E-RABAdmittedItem',
           ?'id-E-RABAdmittedList'],
          #'E-RABAdmittedItem'{'e-RAB-ID' = ERABId,
                               'transportLayerAddress' = TLA_In,
                               'gTP-TEID' = << TEID_In:32/big >>} = C0, S) ->
    %% signal eNB's GTP-U address to the parent process
    signal_enb_addr(TLA_In, S),
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            case erab_fsm:erab_setup_rsp(Pid, {TEID_In, TLA_In}) of
                {ok, {TEID_Out, TLA_Out}} ->
                    C1 = C0#'E-RABAdmittedItem'{'transportLayerAddress' = TLA_Out,
                                                'gTP-TEID' = << TEID_Out:32/big >>},
                    {{ok, C1}, S};
                {error, Reason} ->
                    {{error, Reason}, S}
            end;
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

handle_ie([?'id-E-RABFailedToSetupListHOReqAck'], C, S) ->
    %% This IE contains a list of id-E-RABFailedtoSetupItemHOReqAck, so patch inner IEs
    handle_ies(?'id-E-RABFailedtoSetupItemHOReqAck', C, S);

handle_ie([?'id-E-RABFailedtoSetupItemHOReqAck',
           ?'id-E-RABFailedToSetupListHOReqAck'],
          #'E-RABFailedToSetupItemHOReqAck'{'e-RAB-ID' = ERABId} = C, S) ->
    %% poke E-RAB FSM
    case erab_fsm_find(ERABId, S) of
        {ok, Pid} ->
            ok = erab_fsm:erab_release_ind(Pid),
            {{ok, C}, S};
        error ->
            ?LOG_ERROR("E-RAB-ID ~p is not registered", [erab_uid(ERABId, S)]),
            {{error, erab_not_registered}, S}
    end;

%% Catch-all variant, which should not be called normally
handle_ie(P, C, S) ->
    ?LOG_ERROR("[BUG] Unhandled S1AP IE: ~p, ~p", [P, C]),
    {{error, unhandled_ie}, S}.


%% Iterate over the given list of 'ProtocolIE-Field' IEs,
%% calling function handle_ie/3 for IEs matching the given IEI.
%% Additionally look for {MME,eNB}-UE-S1AP-ID IEs and store their values.
-type handle_ies_result() :: {ok, list()} | {error, term()}.
-spec handle_ies(s1ap_ie_id(), list(), proxy_state()) -> {handle_ies_result(), proxy_state()}.
handle_ies(IEI, IEs, #proxy_state{path = P} = S0) ->
    %% prepend IEI to the path and call handle_ies/4
    {Result, S1} = handle_ies([], IEI, IEs,
                              S0#proxy_state{path = [IEI | P]}),
    %% remove IEI from the path and return the result
    {Result, S1#proxy_state{path = P}}.

handle_ies(Acc, IEI, [IE | IEs],
           #proxy_state{path = P} = S0) ->
    case IE of
        #'ProtocolIE-Field'{id = IEI, value = C0} ->
            case handle_ie(P, C0, S0) of
                {{ok, C1}, S1} ->
                    NewIE = IE#'ProtocolIE-Field'{value = C1},
                    handle_ies([NewIE | Acc], IEI, IEs, S1);
                {{error, Reason}, S1} ->
                    {{error, Reason}, S1}
            end;
        #'ProtocolExtensionField'{id = IEI, extensionValue = C0} ->
            case handle_ie(P, C0, S0) of
                {{ok, C1}, S1} ->
                    NewIE = IE#'ProtocolExtensionField'{extensionValue = C1},
                    handle_ies([NewIE | Acc], IEI, IEs, S1);
                {{error, Reason}, S1} ->
                    {{error, Reason}, S1}
            end;
        #'ProtocolIE-Field'{id = ?'id-MME-UE-S1AP-ID', value = Id} ->
            S1 = S0#proxy_state{mme_ue_id = Id},
            handle_ies([IE | Acc], IEI, IEs, S1);
        #'ProtocolIE-Field'{id = ?'id-eNB-UE-S1AP-ID', value = Id} ->
            S1 = S0#proxy_state{enb_ue_id = Id},
            handle_ies([IE | Acc], IEI, IEs, S1);
        _ ->
            handle_ies([IE | Acc], IEI, IEs, S0)
    end;

handle_ies(Acc, _IEI, [], S) ->
    IEs = lists:reverse(Acc),
    {{ok, IEs}, S}.


build_erab_setup_response_failure(#proxy_state{erabs = ERABs,
                                               mme_ue_id = MmeUeId,
                                               enb_ue_id = EnbUeId}) ->
    %% FIXME: Currently we respond with E-RAB-ID of the first E-RAB in the registry.
    %% Instead, we need to iterate over E-RABs in the REQUEST and reject them all.
    [{_, FirstERABid}|_] = dict:fetch_keys(ERABs),
    Cause = {transport, 'transport-resource-unavailable'},
    ERABitem = #'E-RABItem'{'e-RAB-ID' = FirstERABid,
                            cause = Cause},
    ERABlist = [#'ProtocolIE-Field'{id = ?'id-E-RABItem',
                                    criticality = ignore,
                                    value = ERABitem}],
    IEs = [#'ProtocolIE-Field'{id = ?'id-MME-UE-S1AP-ID',
                               criticality = ignore,
                               value = MmeUeId},
           #'ProtocolIE-Field'{id = ?'id-eNB-UE-S1AP-ID',
                               criticality = ignore,
                               value = EnbUeId},
           #'ProtocolIE-Field'{id = ?'id-E-RABFailedToSetupListBearerSURes',
                               criticality = ignore,
                               value = ERABlist}],
    {successfulOutcome,
     #'SuccessfulOutcome'{procedureCode = ?'id-E-RABSetup',
                          criticality = reject,
                          value = #'E-RABSetupResponse'{protocolIEs = IEs}}}.


-spec erab_fsm_start_reg(erab_id(), proxy_state()) -> {pid(), proxy_state()}.
erab_fsm_start_reg(RABId, #proxy_state{erabs = ERABs} = S) ->
    UID = erab_uid(RABId, S),
    case dict:find(UID, ERABs) of
        {ok, Pid} ->
            ?LOG_ERROR("E-RAB ~p is already registered?!?", [UID]),
            {Pid, S}; %% return Pid of the existing erab_fsm process
        error ->
            {ok, Pid} = erab_fsm:start_link(UID),
            {Pid, S#proxy_state{erabs = dict:store(UID, Pid, ERABs)}}
    end.


-spec erab_fsm_find(erab_id(), proxy_state()) -> {ok, pid()} | error.
erab_fsm_find(RABId, #proxy_state{erabs = ERABs} = S) ->
    UID = erab_uid(RABId, S),
    dict:find(UID, ERABs).


%% Signal the eNB's GTP-U address to the parent process
-spec signal_enb_addr(binary(), proxy_state()) -> ok.
signal_enb_addr(TLA, #proxy_state{owner = Pid}) ->
    %% TODO: optimize to send this only once
    Pid ! {?MODULE, {enb_addr, tla_str(TLA)}},
    ok.


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