
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 06. 7月 2023 12:02
%%%-------------------------------------------------------------------
-module(endpoint).
-include("endpoint.hrl").

%% API
-export([start_link/1]).
-export([get_name/1, get_pid/1, forward/2, reload/2, clean_up/1]).
-export([get_alias_pid/1, is_support/1, get_protocol/1]).
-export([endpoint_record/1, parse_config/2]).

%%%===================================================================
%%% API
%%%===================================================================

-spec start_link(Endpoint :: #endpoint{}) -> {'ok', pid()} | 'ignore' | {'error', term()}.
start_link(Endpoint = #endpoint{id = Id, config = #http_endpoint{}}) ->
    LocalName = get_name(Id),
    endpoint_http:start_link(LocalName, Endpoint);
start_link(Endpoint = #endpoint{id = Id, config = #mqtt_endpoint{}}) ->
    LocalName = get_name(Id),
    endpoint_mqtt:start_link(LocalName, Endpoint);
start_link(Endpoint = #endpoint{id = Id, config = #kafka_endpoint{}}) ->
    LocalName = get_name(Id),
    endpoint_kafka:start_link(LocalName, Endpoint).

-spec get_name(Id :: integer()) -> term().
get_name(Id) when is_integer(Id) ->
    {endpoint, Id}.

-spec get_pid(Id :: integer()) -> undefined | pid().
get_pid(Id) when is_integer(Id) ->
    gproc:whereis_name({n, l, get_name(Id)}).

-spec get_alias_name(Name :: binary()) -> term().
get_alias_name(Name) when is_binary(Name) ->
    {endpoint_alias, Name}.

-spec get_alias_pid(Name :: binary()) -> undefined | pid().
get_alias_pid(Name) when is_binary(Name) ->
    gproc:whereis_name({n, l, get_alias_name(Name)}).

-spec forward(Pid :: pid(), Metric :: binary()) -> ok.
forward(Pid, Metric) when is_pid(Pid), is_binary(Metric) ->
    gen_server:cast(Pid, {forward, Metric}).

reload(Pid, NEndpoint = #endpoint{}) when is_pid(Pid) ->
    gen_server:cast(Pid, {reload, NEndpoint}).

-spec clean_up(Pid :: pid()) -> ok.
clean_up(Pid) when is_pid(Pid) ->
    gen_server:cast(Pid, cleanup).

-spec get_protocol(Endpoint :: #endpoint{}) -> atom().
get_protocol(#endpoint{config = #http_endpoint{}}) ->
    http;
get_protocol(#endpoint{config = #mqtt_endpoint{}}) ->
    mqtt;
get_protocol(#endpoint{config = #kafka_endpoint{}}) ->
    kafka.

-spec is_support(Protocol :: atom()) -> boolean().
is_support(Protocol) when is_atom(Protocol) ->
    {ok, Props} = application:get_env(endpoint, endpoints),
    SupportProtocols = proplists:get_value(support_protocols, Props, []),
    lists:member(Protocol, SupportProtocols).

-spec endpoint_record(EndpointInfo :: #{}) -> error | {ok, Endpoint :: #endpoint{}}.
endpoint_record(#{<<"id">> := Id, <<"matcher">> := Matcher, <<"title">> := Title, <<"type">> := Type, <<"config">> := ConfigJson,
    <<"status">> := Status, <<"updated_at">> := UpdatedAt, <<"created_at">> := CreatedAt}) ->
    case parse_config(Type, ConfigJson) of
        {ok, Config} ->
            {ok, #endpoint {
                id = Id,
                matcher = Matcher,
                title = Title,
                config = Config,
                status = Status,
                updated_at = UpdatedAt,
                created_at = CreatedAt
            }};
        {error, _Reason} ->
            error
    end.

-spec parse_config(Protocol :: binary(), Config :: map()) -> {ok, #mqtt_endpoint{} | #kafka_endpoint{} | #http_endpoint{}} | {error, Errors :: [Error :: binary()]}.
parse_config(<<"mqtt">>, #{<<"host">> := Host, <<"port">> := Port0, <<"client_id">> := ClientId, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}) ->
    {Port, PortErrors} = case parse_integer(Port0) of
        {ok, ParsedPort} ->
            {ParsedPort, []};
        error ->
            {undefined, [<<"port invalid">>]}
    end,

    CheckTerms = case Port of
        undefined ->
            [{host, Host}, {username, Username}, {password, Password}, {topic, Topic}, {qos, Qos}];
        _ ->
            [{host, Host}, {port, Port}, {username, Username}, {password, Password}, {topic, Topic}, {qos, Qos}]
    end,
    Errors = PortErrors ++ lists:filtermap(fun(Term) ->
        case check_mqtt_argument(Term) of
            ok ->
                false;
            {error, Error} ->
                {true, Error}
        end
                             end, CheckTerms),
    case Errors =:= [] of
        true ->
            {ok, #mqtt_endpoint{
                host = Host,
                port = Port,
                client_id = ClientId,
                username = Username,
                password = Password,
                topic = Topic,
                qos = Qos
            }};
        false ->
            {error, Errors}
    end;
parse_config(<<"http">>, C = #{<<"url">> := Url, <<"pool_size">> := PoolSize}) ->
    Token = maps:get(<<"token">>, C, <<>>),
    Errors = lists:filtermap(fun(Term) ->
        case check_http_argument(Term) of
            ok ->
                false;
            {error, Error} ->
                {true, Error}
        end
                             end, [{url, Url}, {token, Token}, {pool_size, PoolSize}]),
    case Errors =:= [] of
        true ->
            {ok, #http_endpoint{
                url = Url,
                token = Token,
                pool_size = PoolSize
            }};
        false ->
            {error, Errors}
    end;
parse_config(<<"kafka">>, #{<<"sasl_config">> := #{<<"username">> := Username, <<"password">> := Password, <<"mechanism">> := Mechanism0}, <<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
    Errors = lists:filtermap(fun(Term) ->
        case check_kafka_argument(Term) of
            ok ->
                false;
            {error, Error} ->
                {true, Error}
        end
                             end, [{username, Username}, {password, Password}, {topic, Topic}, {mechanism, Mechanism0}, {bootstrap_servers, BootstrapServers}]),
    case Errors =:= [] of
        true ->
            Mechanism = parse_kafka_mechanism(Mechanism0),
            {ok, #kafka_endpoint{
                sasl_config = {Mechanism, Username, Password},
                bootstrap_servers = parse_kafka_bootstrap_servers(BootstrapServers),
                topic = Topic
            }};
        false ->
            {error, Errors}
    end;
parse_config(<<"kafka">>, #{<<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
    Errors = lists:filtermap(fun(Term) ->
        case check_kafka_argument(Term) of
            ok ->
                false;
            {error, Error} ->
                {true, Error}
        end
                             end, [{topic, Topic}, {bootstrap_servers, BootstrapServers}]),
    case Errors =:= [] of
        true ->
            {ok, #kafka_endpoint{
                sasl_config = undefined,
                bootstrap_servers = parse_kafka_bootstrap_servers(BootstrapServers),
                topic = Topic
            }};
        false ->
            {error, Errors}
    end;
parse_config(_, _) ->
    {error, invalid_config}.

-spec parse_kafka_bootstrap_servers(BootstrapServers :: [binary()]) -> Servers :: [{Host :: string(), Port :: integer()}].
parse_kafka_bootstrap_servers(BootstrapServers) when is_list(BootstrapServers) ->
    lists:map(fun(S) ->
        {ok, Host, Port} = parse_kafka_bootstrap_server(S),
        {Host, Port}
              end, BootstrapServers).

-spec parse_kafka_mechanism(Mechanism0 :: binary()) -> atom().
parse_kafka_mechanism(Mechanism0) when is_binary(Mechanism0) ->
    case Mechanism0 of
        <<"sha_256">> ->
            scram_sha_256;
        <<"sha_512">> ->
            scram_sha_512;
        <<"plain">> ->
            plain;
        _ ->
            plain
    end.

-spec check_kafka_argument(tuple()) -> ok | {error, Reason :: binary()}.
check_kafka_argument({mechanism, Mechanism}) ->
    case lists:member(Mechanism, [<<"sha_256">>, <<"sha_512">>, <<"plain">>]) of
        true ->
            ok;
        false ->
            {error, <<"mechanism invalid">>}
    end;
check_kafka_argument({username, Username}) ->
    case Username /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"username is empty">>}
    end;
check_kafka_argument({password, Password}) ->
    case Password /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"password is empty">>}
    end;
check_kafka_argument({topic, Topic}) ->
    case Topic /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"topic is empty">>}
    end;
check_kafka_argument({bootstrap_servers, BootstrapServers}) ->
    case is_list(BootstrapServers) andalso length(BootstrapServers) > 0 of
        true ->
            InvalidServers = lists:filtermap(fun(S) ->
                case parse_kafka_bootstrap_server(S) of
                    {ok, _Host, _Port} ->
                        false;
                    error ->
                        {true, S}
                end
                                             end, BootstrapServers),

            case InvalidServers =:= [] of
                true ->
                    ok;
                false ->
                    {error, iolist_to_binary([<<"bootstrap_servers: ">>, lists:join(<<",">>, InvalidServers), <<" format is error">>])}
            end;
        false ->
            {error, <<"bootstrap_servers is empty">>}
    end.

-spec check_http_argument(tuple()) -> ok | {error, Reason :: binary()}.
check_http_argument({url, Url}) ->
    case is_binary(Url) andalso Url /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"url is empty">>}
    end;
check_http_argument({token, Token}) ->
    case is_binary(Token) of
        true ->
            ok;
        false ->
            {error, <<"token invalid">>}
    end;
check_http_argument({pool_size, PoolSize}) ->
    case is_integer(PoolSize) andalso PoolSize > 0 of
        true ->
            ok;
        false ->
            {error, <<"pool_size invalid">>}
    end.

-spec check_mqtt_argument(tuple()) -> ok | {error, Reason :: binary()}.
check_mqtt_argument({host, Host}) ->
    case Host /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"host is empty">>}
    end;
check_mqtt_argument({port, Port}) ->
    case is_integer(Port) andalso Port > 0 of
        true ->
            ok;
        false ->
            {error, <<"port invalid">>}
    end;
check_mqtt_argument({username, Username}) ->
    case Username /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"username is empty">>}
    end;
check_mqtt_argument({password, Password}) ->
    case Password /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"password is empty">>}
    end;
check_mqtt_argument({topic, Topic}) ->
    case Topic /= <<>> of
        true ->
            ok;
        false ->
            {error, <<"topic is empty">>}
    end;
check_mqtt_argument({qos, Qos}) ->
    case is_integer(Qos) andalso lists:member(Qos, [0, 1, 2]) of
        true ->
            ok;
        false ->
            {error, <<"qos invalid">>}
    end.

-spec parse_integer(term()) -> {ok, integer()} | error.
parse_integer(Value) when is_integer(Value) ->
    {ok, Value};
parse_integer(Value) when is_binary(Value) ->
    try binary_to_integer(Value) of
        Int ->
            {ok, Int}
    catch
        _:_ ->
            error
    end;
parse_integer(_) ->
    error.

-spec parse_kafka_bootstrap_server(term()) -> {ok, string(), integer()} | error.
parse_kafka_bootstrap_server(Server) when is_binary(Server) ->
    case binary:split(Server, <<":">>) of
        [Host0, Port0] ->
            Host = binary_to_list(Host0),
            case parse_integer(Port0) of
                {ok, Port} when Host =/= [], Port > 0 ->
                    {ok, Host, Port};
                _ ->
                    error
            end;
        _ ->
            error
    end;
parse_kafka_bootstrap_server(_) ->
    error.
