-module(tfte_topology).

-behaviour(gen_statem).


%%% INCLUDES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

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


%%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% API functions
-export([start_link/0]).
-export([context_updated/0]).

% Behaviour gen_statem functions
-export([init/1]).
-export([callback_mode/0]).
-export([handle_event/4]).
-export([terminate/3]).
-export([code_change/4]).


%%% Records %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-record(data, {
    uuid :: map(),
    sub :: term() | undefined,
    obj :: map() | undefined,
    devices = #{} :: map(),
    links = #{} :: map()
}).


%%% MACROS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-define(SUBSCRIBE_RETRY_TIMEOUT, 1000).
-define(RETRIEVE_RETRY_TIMEOUT, 10000).


%%% API FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

start_link() ->
    gen_statem:start_link(?MODULE, [], []).

context_updated() ->
    gen_statem:cast(?MODULE, context_updated).


%%% BEHAVIOUR gen_statem FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

init([]) ->
    {ok, ContextName} = application:get_env(tfte, context),
    {ok, TopoName} = application:get_env(tfte, topology),
    ContextUUID = #{context_uuid => #{uuid => ContextName}},
    TopoUUID = #{context_id => ContextUUID,
                 topology_uuid => #{uuid => TopoName}},
    ?LOG_INFO("Starting topology ~s service handler...", [TopoName]),
    {ok, retrieve, #data{uuid = TopoUUID}}.

callback_mode() -> [handle_event_function, state_enter].

%-- RETRIEVE STATE -------------------------------------------------------------
handle_event(enter, _, retrieve, _Data) ->
    {keep_state_and_data, [{state_timeout, 0, do_retrieve}]};
handle_event(state_timeout, do_retrieve, retrieve, #data{uuid = UUID} = Data) ->
    ?LOG_DEBUG("Retrieving topology ~p...", [UUID]),
    case get_object(UUID) of
        error ->
            {keep_state_and_data, [{state_timeout, ?RETRIEVE_RETRY_TIMEOUT, do_retrieve}]};
        {ok, Topology} ->
            ?LOG_DEBUG("Got topology: ~p", [Topology]),
            {next_state, subscribe, Data#data{obj = Topology}}
    end;
handle_event(cast, context_updated, retrieve, _Data) ->
    {keep_state_and_data, [{state_timeout, 0, do_retrieve}]};
%-- SUBSCRIBE STATE ------------------------------------------------------------
handle_event(enter, _, subscribe, #data{sub = undefined}) ->
    {keep_state_and_data, [{state_timeout, 0, do_suscribe}]};
handle_event(enter, _, subscribe, Data) ->
    % We already have a topology subscription
    {next_state, ready, Data};
handle_event(state_timeout, do_suscribe, subscribe, #data{uuid = UUID} = Data) ->
    ?LOG_DEBUG("Subscribing to topology events...", []),
    case do_subscribe(UUID) of
        {ok, Sub} ->
            ?LOG_INFO("Subscribed to topology events", []),
            Data2 = #data{obj = Obj} = Data#data{sub = Sub},
            #{device_ids := DeviceIds, link_ids := LinkIds} = Obj,
            case update_topology(Data2, DeviceIds, LinkIds) of
                {ok, Data3} ->
                    tfte_server:topology_ready(Obj),
                    {next_state, ready, Data3};
                {error, Reason} ->
                    ?LOG_ERROR("Failed to load topology: ~p", [Reason]),
                    statem_rollback_to_retrieve(Data2)
            end;
        {error, Reason} ->
            ?LOG_ERROR("Failed to subscribe to topology service events: ~p", [Reason]),
            {next_state, retrieve, [{state_timeout, ?SUBSCRIBE_RETRY_TIMEOUT, do_retrieve}]}
    end;
%-- READY STATE ----------------------------------------------------------------
handle_event(enter, _, ready, _Data) ->
    keep_state_and_data;
handle_event(info, {headers, Id, Value}, ready,
             #data{sub = #{stream_id := Id}}) ->
    %TODO: Handle HTTP errors ???
    ?LOG_DEBUG("Received topology stream header: ~p", [Value]),
    keep_state_and_data;
handle_event(info, {data, Id, #{event := Event}}, ready,
             #data{sub = #{stream_id := Id}} = Data) ->
    ?LOG_DEBUG("Received topology event: ~p", [Event]),
    handle_topology_event(Data, Event);
handle_event(info, {'DOWN', Ref, process, Pid, Reason}, ready,
             #data{sub = #{stream_id := Id, monitor_ref := Ref, stream_pid := Pid}} = Data) ->
    ?LOG_DEBUG("Topology subscription is down: ~p", [Reason]),
    Data2 = Data#data{sub = undefined},
    Info = receive
        {trailers, Id, {Status, Message, Metadata}} ->
            {Reason, Status, Message, Metadata}
    after 0 ->
        Reason
    end,
    ?LOG_ERROR("Topology subscription error: ~p", [Info]),
    {next_state, retrieve, Data2};
handle_event(cast, context_updated, ready, _Data) ->
    keep_state_and_data;
%-- ANY STATE ------------------------------------------------------------------
handle_event(info, Msg, StateName, _Data) ->
    ?LOG_WARNING("Unexpected topology message in state ~w: ~p", [StateName, Msg]),
    keep_state_and_data.

terminate(Reason, _State, _Data) ->
    ?LOG_INFO("Topology service handler terminated: ~p", [Reason]),
    ok.

code_change(_OldVsn, OldState, OldData, _Extra) ->
    {ok, OldState, OldData}.


%%% INTERNAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

statem_rollback_to_retrieve(#data{sub = undefined} = Data) ->
    {next_state, retrieve, Data, [{state_timeout, ?RETRIEVE_RETRY_TIMEOUT, do_retrieve}]};
statem_rollback_to_retrieve(#data{sub = Sub} = Data) ->
    grpcbox_client:close_send_and_recv(Sub),
    Data2 = Data#data{sub = undefined},
    {next_state, retrieve, Data2, [{state_timeout, ?RETRIEVE_RETRY_TIMEOUT, do_retrieve}]}.

handle_topology_event(#data{uuid = UUID} = Data,
                      #{event_type := 'EVENTTYPE_UPDATE'} = Event) ->
    case get_object(UUID) of
        error ->
            statem_rollback_to_retrieve(Data);
        {ok, #{device_ids := DeviceIds, link_ids := LinkIds} = Topology} ->
            ?LOG_DEBUG("Got new topology: ~p", [Topology]),
            Data2 = Data#data{obj = Topology},
            case update_topology(Data2, DeviceIds, LinkIds) of
                {ok, Data3} ->
                    tfte_server:topology_event(Event),
                    {keep_state, Data3};
                {error, Reason} ->
                    ?LOG_ERROR("Failed to update topology: ~p", [Reason]),
                    statem_rollback_to_retrieve(Data2)
            end
    end;
handle_topology_event(_Data, Event) ->
    tfte_server:topology_event(Event),
    keep_state_and_data.

update_topology(Data, DeviceIds, LinkIds) ->
    try
        {Data2, Events} = update_devices(Data, DeviceIds, []),
        {Data3, Events2} = update_links(Data2, LinkIds, Events),
        post_topology_events(lists:reverse(Events2)),
        {ok, Data3}
    catch
        throw:Reason ->
            {error, Reason}
    end.

post_topology_events(Events) ->
    lists:foreach(fun post_topology_event/1, Events).

post_topology_event({device_added, Id, Device}) ->
    epce_ted:device_added(Id, Device);
post_topology_event({device_updated, Id, Device}) ->
    epce_ted:device_updated(Id, Device);
post_topology_event({device_deleted, Id}) ->
    epce_ted:device_deleted(Id);
post_topology_event({link_added, Id, Link}) ->
    epce_ted:link_added(Id, Link);
post_topology_event({link_updated, Id, Link}) ->
    epce_ted:link_updated(Id, Link);
post_topology_event({link_deleted, Id}) ->
    epce_ted:link_deleted(Id).

update_devices(#data{devices = OldDevices} = Data, DeviceIds, Events) ->
    {NewDevices, Events2} = update_devices(OldDevices, #{}, DeviceIds, Events),
    {Data#data{devices = NewDevices}, Events2}.

update_devices(OldDevices, NewDevices, [], Events) ->
    Events2 = [{device_deleted, post_process_device_id(I)}
               || I <- maps:keys(OldDevices)] ++ Events,
    {NewDevices, Events2};
update_devices(OldDevices, NewDevices, [Id | Rest], Events) ->
    case get_device(Id) of
        error -> throw({device_retrieval_error, Id});
        {ok, Device} ->
            Id2 = post_process_device_id(Id),
            Device2 = post_process_device(Device),
            NewDevices2 = NewDevices#{Id => Device},
            case maps:take(Id, OldDevices) of
                error ->
                    % New device
                    Events2 = [{device_added, Id2, Device2} | Events],
                    update_devices(OldDevices, NewDevices2, Rest, Events2);
                {Device, OldDevices2} ->
                    % Device did not change
                    update_devices(OldDevices2, NewDevices2, Rest, Events);
                {_OldDevice, OldDevices2} ->
                    % Device changed
                    Events2 = [{device_updated, Id2, Device2} | Events],
                    update_devices(OldDevices2, NewDevices2, Rest, Events2)
            end
    end.

update_links(#data{links = OldLinks} = Data, LinksIds, Events) ->
    {NewLinks, Events2} = update_links(OldLinks, #{}, LinksIds, Events),
    {Data#data{links = NewLinks}, Events2}.

update_links(OldLinks, NewLinks, [], Events) ->
    Events2 = [{link_deleted, post_process_link_id(I)}
               || I <- maps:keys(OldLinks)] ++ Events,
    {NewLinks, Events2};
update_links(OldLinks, NewLinks, [Id | Rest], Events) ->
    case get_link(Id) of
        error -> throw({link_retrieval_error, Id});
        {ok, Link} ->
            Id2 = post_process_link_id(Id),
            Link2 = post_process_link(Link),
            NewLinks2 = NewLinks#{Id => Link},
            case maps:take(Id, OldLinks) of
                error ->
                    % New Link
                    Events2 = [{link_added, Id2, Link2} | Events],
                    update_links(OldLinks, NewLinks2, Rest, Events2);
                {Link, OldLinks2} ->
                    % Link did not change
                    update_links(OldLinks2, NewLinks2, Rest, Events);
                {_OldLink, OldLinks2} ->
                    % Link changed
                    Events2 = [{link_updated, Id2, Link2} | Events],
                    update_links(OldLinks2, NewLinks2, Rest, Events2)
            end
    end.

post_process_device_id(#{device_uuid := #{uuid := Name}}) ->
    Name.

post_process_device(Device) ->
    #{id => device_id(Device),
      type => device_type(Device),
      pcc_address => device_pcc_address(Device),
      mpls_label => device_mpls_label(Device),
      status => device_status(Device),
      endpoints => device_endpoints(Device)}.

device_id(#{device_id := Id}) ->
    post_process_device_id(Id).

device_type(#{device_type := Type}) ->
    Type.

device_status(#{device_operational_status := 'DEVICEOPERATIONALSTATUS_UNDEFINED'}) ->
    undefined;
device_status(#{device_operational_status := 'DEVICEOPERATIONALSTATUS_DISABLED'}) ->
    disabled;
device_status(#{device_operational_status := 'DEVICEOPERATIONALSTATUS_ENABLED'}) ->
    enabled.

device_mpls_label(Device) ->
    case device_config_value(<<"mpls_label">>, Device) of
        undefined -> undefined;
        LabelBin ->
            try binary_to_integer(LabelBin)
            catch error:badarg -> undefined
            end
    end.

device_pcc_address(Device) ->
    case device_config_value(<<"pcc_address">>, Device) of
        undefined -> undefined;
        AddressBin ->
            case inet_parse:address(binary_to_list(AddressBin)) of
                {ok, Address} -> Address;
                {error,einval} -> undefined
            end
    end.

device_config_value(Key, #{device_config := #{config_rules := Rules}}) ->
    device_config_value(Key, Rules);
device_config_value(_Key, []) ->
    undefined;
device_config_value(Key, [#{action := 'CONFIGACTION_SET',
                          config_rule := {custom, Rule}} | Rest]) ->
    case Rule of
        #{resource_key := Key, resource_value := Value} -> Value;
        _ -> device_config_value(Key, Rest)
    end;
device_config_value(Key, [_Rule | Rest]) ->
    device_config_value(Key, Rest).


device_endpoints(Device) ->
    device_endpoints(Device, []).

device_endpoints(#{device_endpoints := Endpoints}, Acc) ->
    device_endpoints(Endpoints, Acc);
device_endpoints([], Acc) ->
    lists:reverse(Acc);
device_endpoints([#{endpoint_id := #{endpoint_uuid := #{uuid := Name}}} | Rest], Acc) ->
    device_endpoints(Rest, [Name | Acc]).

post_process_link_id(#{link_uuid := #{uuid := Name}}) ->
    Name.

post_process_link(Link) ->
    #{id => link_id(Link),
      endpoints => link_endpoints(Link)}.

link_id(#{link_id := Id}) ->
    post_process_link_id(Id).

link_endpoints(Link) ->
    link_endpoints(Link, []).

link_endpoints(#{link_endpoint_ids := Endpoints}, Acc) ->
    link_endpoints(Endpoints, Acc);
link_endpoints([], Acc) ->
    lists:reverse(Acc);
link_endpoints([#{device_id := #{device_uuid := #{uuid := DevName}},
                  endpoint_uuid := #{uuid := EndpointName}} | Rest], Acc) ->
    Endpoint = #{
        device => DevName,
        endpoint => EndpointName
    },
    link_endpoints(Rest, [Endpoint | Acc]).


%-- GRPC UNTILITY FUNCTION -----------------------------------------------------

grpc_opts() ->
    #{channel => context}.

do_subscribe(UUID) ->
    context_context_service_client:get_topology_events(UUID, grpc_opts()).

get_object(UUID) ->
    case context_context_service_client:get_topology(UUID, grpc_opts()) of
        {error, Reason} -> 
            ?LOG_ERROR("Local error while retrieving the topology object: ~p", [Reason]),
            error;
        {error, Reason, _Headers} ->
            ?LOG_ERROR("Remote error while retrieving the topology object: ~p", [Reason]),
            error;
        {ok, Result, _Headers} ->
            {ok, Result}
    end.

get_device(UUID) ->
    case context_context_service_client:get_device(UUID, grpc_opts()) of
        {error, Reason} -> 
            ?LOG_ERROR("Local error while retrieving a device object: ~p", [Reason]),
            error;
        {error, Reason, _Headers} ->
            ?LOG_ERROR("Remote error while retrieving a device object: ~p", [Reason]),
            error;
        {ok, Result, _Headers} ->
            {ok, Result}
    end.

get_link(UUID) ->
    case context_context_service_client:get_link(UUID, grpc_opts()) of
        {error, Reason} -> 
            ?LOG_ERROR("Local error while retrieving a link object: ~p", [Reason]),
            error;
        {error, Reason, _Headers} ->
            ?LOG_ERROR("Remote error while retrieving a link object: ~p", [Reason]),
            error;
        {ok, Result, _Headers} ->
            {ok, Result}
    end.
