%% Copyright (C) 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(rest_server).

-export([metrics_list/1,
         pfcp_assoc_state/1,
         pfcp_heartbeat/1,
         enb_list/1,
         enb_info/1,
         enb_delete/1,
         enb_erab_list/1,
         erab_list/1,
         erab_info/1,
         erab_delete/1
        ]).

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

-include("s1gw_metrics.hrl").


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

%% MetricsList :: Get a list of metrics
metrics_list(#{query_parameters := QP}) ->
    L0 = case proplists:get_value(<< "type" >>, QP, << "all" >>) of
        << "counter" >> -> metric_list([ctr]);
        << "gauge" >> -> metric_list([gauge]);
        << "all" >> -> metric_list([])
    end,
    case proplists:get_value(<< "path" >>, QP) of
        undefined ->
            {200, [], L0};
        Path ->
            case lists:filter(fun(M) -> metric_filter_path(M, Path) end, L0) of
                [] -> {404, [], undefined};
                L1 -> {200, [], L1}
            end
    end.


%% PfcpAssocState :: Get the PFCP association state
pfcp_assoc_state(#{}) ->
    Info0 = pfcp_peer:fetch_info(),
    Info1 = maps:update_with(laddr, fun inet:ntoa/1, Info0),
    Info2 = maps:update_with(raddr, fun inet:ntoa/1, Info1),
    {200, [], rsp_map(Info2)}.

%% TODO: PfcpAssocSetup :: Initiate the PFCP Association Setup procedure
%% TODO: PfcpAssocRelease :: Initiate the PFCP Association Release procedure


%% PfcpHeartbeat :: Send a PFCP Heartbeat Request to the peer
pfcp_heartbeat(#{}) ->
    case pfcp_peer:heartbeat_req() of
        ok ->
            {200, [], rsp_map(#{success => true})};
        {error, Error} ->
            {200, [], rsp_map(#{success => false,
                                message => Error})}
    end.


%% EnbList :: Get a list of eNB connections
enb_list(#{}) ->
    EnbList = enb_registry:fetch_enb_list(),
    {200, [], lists:map(fun enb_item/1, EnbList)}.


%% EnbInfo :: Get information about a specific eNB
enb_info(#{path_parameters := PP}) ->
    [{<< "EnbId" >>, << ID/bytes >>}] = PP,
    case fetch_enb_info(ID) of
        [EnbInfo | _] ->
            {200, [], enb_item(EnbInfo)};
        [] ->
            {404, [], undefined};
        error ->
            {500, [], undefined}
    end.


%% EnbDelete :: Force disconnect an eNB
enb_delete(#{path_parameters := PP}) ->
    [{<< "EnbId" >>, << ID/bytes >>}] = PP,
    case fetch_enb_info(ID) of
        [EnbInfo | _] ->
            Pid = maps:get(pid, EnbInfo),
            shutdown(enb_proxy, Pid);
        [] ->
            {404, [], undefined};
        error ->
            {500, [], undefined}
    end.


%% EnbErabList :: Get E-RAB list for a specific eNB
enb_erab_list(#{path_parameters := PP}) ->
    [{<< "EnbId" >>, << ID/bytes >>}] = PP,
    case fetch_enb_info(ID) of
        [EnbInfo | _] ->
            Rsp = fetch_erab_list(EnbInfo),
            {200, [], Rsp};
        [] ->
            {404, [], undefined};
        error ->
            {500, [], undefined}
    end.


%% ErabList :: Get E-RAB list for all eNBs
erab_list(#{}) ->
    EnbList = enb_registry:fetch_enb_list(),
    ErabList = lists:map(fun fetch_erab_list/1, EnbList),
    {200, [], lists:flatten(ErabList)}.


%% ErabInfo :: Get information about a specific E-RAB
erab_info(#{path_parameters := PP}) ->
    [{<< "ErabId" >>, << ID/bytes >>}] = PP,
    case fetch_erab_info(ID) of
        {ok, ErabInfo} ->
            {200, [], ErabInfo};
        error ->
            {404, [], undefined}
    end.


%% ErabDelete :: Terminate an E-RAB FSM process
erab_delete(#{path_parameters := PP}) ->
    [{<< "ErabId" >>, << ID/bytes >>}] = PP,
    case ID of
        << "pid:", Val/bytes >> ->
            Pid = parse_pid(Val),
            shutdown(erab_fsm, Pid);
        _ ->
            ?LOG_ERROR("Unhandled E-RAB ID ~p", [ID]),
            {400, [], undefined}
    end.


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

-spec metric_list(list()) -> [map()].
metric_list(Path) ->
    L = exometer:get_values(Path),
    lists:map(fun metric_item/1, L).


-spec metric_item({Name, Props}) -> map()
    when Name :: s1gw_metrics:metric(),
         Props :: proplists:proplist().
metric_item({Name, Props}) ->
    Value = proplists:get_value(value, Props),
    #{<< "type" >> => metric_type(Name),
      << "name" >> => metric_name(Name),
      << "value" >> => Value}.


-spec metric_type(s1gw_metrics:metric()) -> binary().
metric_type([ctr | _]) -> << "counter" >>;
metric_type([gauge | _]) -> << "gauge" >>.


-spec metric_filter_path(map(), binary()) -> boolean().
metric_filter_path(#{<< "name" >> := Name}, Path) ->
    Len = erlang:min(byte_size(Name), byte_size(Path)),
    binary:part(Name, 0, Len) =:= Path.


-spec metric_name(s1gw_metrics:metric()) -> binary().
metric_name([_Type | P0]) ->
    %% turn each list member into a string
    P1 = lists:map(fun thing_to_list/1, P0),
    %% put the dot-separator in between
    P2 = string:join(P1, "."),
    list_to_binary(P2).


%% stolen from exometer_report_statsd.git
thing_to_list(X) when is_atom(X) -> atom_to_list(X);
thing_to_list(X) when is_integer(X) -> integer_to_list(X);
thing_to_list(X) when is_binary(X) -> X;
thing_to_list(X) when is_list(X) -> X.


-spec enb_item(enb_registry:enb_info()) -> map().
enb_item(EnbInfo) ->
    M0 = #{handle => maps:get(handle, EnbInfo),
           pid => pid_to_list(maps:get(pid, EnbInfo)),
           state => maps:get(state, EnbInfo),
           uptime => maps:get(uptime, EnbInfo),
           erab_count => 0},
    M1 = enb_item_add_enb_info(M0, EnbInfo),
    M2 = enb_item_add_enb_conn_info(M1, EnbInfo),
    M3 = enb_item_add_mme_conn_info(M2, EnbInfo),
    rsp_map(M3).


-spec enb_item_add_enb_info(map(), enb_registry:enb_info()) -> map().
enb_item_add_enb_info(M0, #{genb_id_str := GlobalENBId}) ->
    %% TODO: add enb_id and plmn_id
    M0#{genb_id => GlobalENBId};

enb_item_add_enb_info(M0, _) -> M0.


-spec enb_item_add_enb_conn_info(map(), enb_registry:enb_info()) -> map().
enb_item_add_enb_conn_info(M0, #{enb_conn_info := ConnInfo}) ->
    M0#{enb_saddr => inet:ntoa(maps:get(addr, ConnInfo)),
        enb_sport => maps:get(port, ConnInfo),
        enb_sctp_aid => maps:get(aid, ConnInfo)
       };

enb_item_add_enb_conn_info(M0, _) -> M0.


-spec enb_item_add_mme_conn_info(map(), enb_registry:enb_info()) -> map().
enb_item_add_mme_conn_info(M0, #{mme_conn_info := ConnInfo}) ->
    Pid = maps:get(handler, ConnInfo),
    ERABs = s1ap_proxy:fetch_erab_list(Pid),
    M0#{mme_daddr => maps:get(mme_addr, ConnInfo), %% XXX inet:ntoa
        mme_dport => maps:get(mme_port, ConnInfo),
        %% TODO: mme_sport
        mme_sctp_aid => maps:get(mme_aid, ConnInfo),
        erab_count => length(ERABs)
       };

enb_item_add_mme_conn_info(M0, _) -> M0.


-spec fetch_enb_info(binary()) -> [enb_registry:enb_info()] | error.
fetch_enb_info(<< "handle:", Val/bytes >>) ->
    Handle = binary_to_integer(Val),
    case enb_registry:fetch_enb_info(Handle) of
        {ok, EnbInfo} -> [EnbInfo];
        error -> []
    end;

fetch_enb_info(<< "pid:", Val/bytes >>) ->
    Pid = parse_pid(Val),
    case enb_registry:fetch_enb_info(Pid) of
        {ok, EnbInfo} -> [EnbInfo];
        error -> []
    end;

fetch_enb_info(<< "genbid:", Val/bytes >>) ->
    GlobalENBId = binary_to_list(Val),
    enb_registry:fetch_enb_list({genb_id_str, GlobalENBId});

fetch_enb_info(<< "enb-sctp-aid:", Val/bytes >>) ->
    Aid = binary_to_integer(Val),
    enb_registry:fetch_enb_list({enb_sctp_aid, Aid});

fetch_enb_info(<< "mme-sctp-aid:", Val/bytes >>) ->
    Aid = binary_to_integer(Val),
    enb_registry:fetch_enb_list({mme_sctp_aid, Aid});

%% TODO: '^enb-conn:[0-9:.]+-[0-9]+$'
fetch_enb_info(ID) ->
    ?LOG_ERROR("Unhandled eNB ID ~p", [ID]),
    error.


-spec fetch_erab_info(binary()) -> {ok, erab_fsm:erab_info()} | error.
fetch_erab_info(<< "pid:", Val/bytes >>) ->
    Pid = parse_pid(Val),
    %% guard against non-existent process IDs
    %% TODO: check if the given Pid is actually an erab_fsm
    try erab_list_item({pid, Pid}) of
        ErabInfo -> {ok, ErabInfo}
    catch
        exit:{noproc, _} -> error
    end;

fetch_erab_info(ID) ->
    ?LOG_ERROR("Unhandled E-RAB ID ~p", [ID]),
    error.


-spec fetch_erab_list(enb_registry:enb_info()) -> [map()].
fetch_erab_list(#{mme_conn_info := ConnInfo}) ->
    Pid = maps:get(handler, ConnInfo), %% s1ap_proxy process pid
    ERABs = s1ap_proxy:fetch_erab_list(Pid),
    lists:map(fun erab_list_item/1, ERABs);

fetch_erab_list(_) -> [].


-spec erab_list_item({term(), pid()}) -> map().
erab_list_item({_, Pid}) ->
    %% XXX: E-RAB FSM process might be dead here
    Info = erab_fsm:fetch_info(Pid),
    {MmeUeId, ErabId} = maps:get(uid, Info),
    M0 = #{mme_ue_id => MmeUeId,
           erab_id => ErabId,
           state => maps:get(state, Info),
           pid => pid_to_list(Pid)},
    M1 = erab_list_item_add_seid(Info, M0),
    M2 = erab_list_item_add_f_teid(f_teid_u2c, Info, M1),
    M3 = erab_list_item_add_f_teid(f_teid_c2u, Info, M2),
    M4 = erab_list_item_add_f_teid(f_teid_a2u, Info, M3),
    M5 = erab_list_item_add_f_teid(f_teid_u2a, Info, M4),
    rsp_map(M5).


-spec erab_list_item_add_seid(erab_fsm:erab_info(), map()) -> map().
erab_list_item_add_seid(Info, M0) ->
    %% local SEID is always known/present
    M1 = M0#{pfcp_lseid => maps:get(seid_loc, Info)},
    %% remote SEID may or may not be known/present
    case maps:find(seid_rem, Info) of
        {ok, SEID} -> M1#{pfcp_rseid => SEID};
        error -> M1
    end.


-spec erab_list_item_add_f_teid(atom(), erab_fsm:erab_info(), map()) -> map().
erab_list_item_add_f_teid(Key, Info, M0) ->
    case maps:find(Key, Info) of
        {ok, {TEID, AddrBin}} ->
            Addr = list_to_tuple(binary_to_list(AddrBin)),
            M0#{Key => #{teid => TEID,
                         tla => inet:ntoa(Addr)}};
        error -> M0
    end.


-spec parse_pid(binary() | list()) -> pid().
parse_pid(Data) when is_binary(Data) ->
    parse_pid(binary_to_list(Data));

parse_pid(Data) when is_list(Data) ->
    list_to_pid("<" ++ Data ++ ">").


-spec shutdown(module(), pid()) -> erf:response().
shutdown(Module, Pid) ->
    ?LOG_NOTICE("Shutting down ~p process (pid ~p)", [Module, Pid]),
    try Module:shutdown(Pid) of
        ok -> {200, [], undefined}
    catch
        exit:noproc -> {404, [], undefined};
        exit:{noproc, _} -> {404, [], undefined};
        Exception:Type ->
            ?LOG_ERROR("Caught an exception: ~p:~p", [Exception, Type]),
            {500, [], undefined}
    end.


%% Convert the given response map to a format acceptable by the erf
-spec rsp_map(map()) -> map().
rsp_map(M) ->
    Fun = fun(K, V) -> {bval(K), bval(V)} end,
    maps:from_list([Fun(K, V) || {K, V} <- maps:to_list(M)]).


bval(V) when is_boolean(V) -> V;
bval(V) when is_atom(V) -> atom_to_binary(V);
bval(V) when is_list(V) -> list_to_binary(V);
bval(V) when is_map(V) -> rsp_map(V);
bval(V) -> V.


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