%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 30. 5月 2023 10:48
%%%-------------------------------------------------------------------
-module(influx_client).
-author("aresei").

-behaviour(gen_server).

%% API
-export([start_link/1, write/4, write/5, write_data/4]).
-export([get_precision/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-define(SERVER, ?MODULE).

-define(INFLUX_POOl, influx_pool).

-define(DEFAULT_BUCKET, <<"metric">>).
-define(DEFAULT_ORG, <<"nannong">>).

-record(state, {
    host,
    port,
    token :: binary()
}).

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

%% 数据过滤器
data_filter(#{<<"key">> := Key}) when is_binary(Key), Key /= <<>> ->
    true;
data_filter(_) ->
    false.

%% 获取时间标识符号
-spec get_precision(Timestamp :: integer()) -> binary().
get_precision(Timestamp) when is_integer(Timestamp) ->
    case length(integer_to_list(Timestamp)) of
        10 ->
            <<"s">>;
        13 ->
            <<"ms">>;
        16 ->
            <<"u">>;
        19 ->
            <<"ns">>;
        _ ->
            <<"ms">>
    end.

-spec write_data(Measurement :: binary(), Tags :: map(), FieldsList :: list(), Timestamp :: integer()) -> no_return().
write_data(Measurement, Tags, FieldsList, Timestamp) when is_binary(Measurement), is_map(Tags), is_list(FieldsList), is_integer(Timestamp) ->
    %% 过来掉没有key的选项
    NFieldsList = lists:filter(fun data_filter/1, FieldsList),
    case length(NFieldsList) > 0 of
        true ->
            %% 按照设备的uuid进行分组
            Points = lists:map(fun(Fields = #{<<"key">> := Key}) ->
                Values = maps:remove(<<"key">>, Fields),
                NFields = #{Key => Values},
                influx_point:new(Measurement, Tags, NFields, Timestamp)
                               end, NFieldsList),
            Precision = influx_client:get_precision(Timestamp),

            %poolboy:transaction(influx_pool_backup, fun(Pid) -> influx_client:write(Pid, ?DEFAULT_BUCKET, ?DEFAULT_ORG, Precision, Points) end);
            poolboy:transaction(influx_pool, fun(Pid) -> influx_client:write(Pid, ?DEFAULT_BUCKET, ?DEFAULT_ORG, Precision, Points) end);
        false ->
            ok
    end.

-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Points :: list()) -> no_return().
write(Pid, Bucket, Org, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_list(Points) ->
    write(Pid, Bucket, Org, <<"ms">>, Points).

%% Precision的值为: ms|ns|s; 表示时间的精度，默认为毫秒(ms)
-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Precision :: binary(), Points :: list()) -> no_return().
write(Pid, Bucket, Org, Precision, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_binary(Precision), is_list(Points) ->
    gen_server:cast(Pid, {write, Bucket, Org, Precision, Points}).

%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(Opts :: list()) ->
    {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Opts) when is_list(Opts) ->
    gen_server:start_link(?MODULE, [Opts], []).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
    {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term()} | ignore).
init([InfluxProps]) ->
    Token = proplists:get_value(token, InfluxProps),
    Host = proplists:get_value(host, InfluxProps),
    Port = proplists:get_value(port, InfluxProps),

    {ok, #state{host = Host, port = Port, token = Token}}.

%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
    State :: #state{}) ->
    {reply, Reply :: term(), NewState :: #state{}} |
    {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
    {noreply, NewState :: #state{}} |
    {noreply, NewState :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
    {stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Request, _From, State = #state{}) ->
    {reply, ok, State}.

%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
    {noreply, NewState :: #state{}} |
    {noreply, NewState :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: #state{}}).
handle_cast({write, Bucket, Org, Precision, Points}, State = #state{host = Host, port =  Port, token = Token}) ->
    %% 处理headers
    Headers = [
        {<<"Accept">>, <<"application/json">>},
        {<<"Accept-Encoding">>, <<"identity">>},
        {<<"Content-Type">>, <<"text/plain; charset=utf-8">>},
        {<<"Content-Encoding">>, <<"gzip">>},
        {<<"Authorization">>, <<"Token ", Token/binary>>}
    ],

    %% 处理points
    PointLines = lists:map(fun influx_point:normalized/1, Points),
    Body = iolist_to_binary(lists:join(<<"\n">>, PointLines)),
    %% gzip压缩
    GZipBody = zlib:gzip(Body),
    Query = uri_string:compose_query([{<<"bucket">>, Bucket}, {<<"org">>, Org}, {<<"precision">>, Precision}]),

    Url = uri_string:normalize(#{
        scheme => "http",
        host => Host,
        port => Port,
        path => "/api/v2/write",
        query => Query
    }),

    lager:debug("[influx_client] url is: ~p, headers: ~p, body: ~ts", [Url, Headers, Body]),
    case hackney:request(post, Url, Headers, GZipBody, [{pool, false}]) of
        {ok, StatusCode, _RespHeaders, ClientRef} ->
            case hackney:body(ClientRef) of
                {ok, RespBody} ->
                    lager:debug("[influx_client] status_code: ~p, response body is: ~p", [StatusCode, RespBody]);
                {error, Error} ->
                    lager:warning("[influx_client] status_code: ~p, error is: ~p", [Error])
            end,
            hackney:close(ClientRef);
        {error, Reason} ->
            lager:warning("[influx_client] request result is: ~p", [Reason])
    end,

    {noreply, State}.

%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
    {noreply, NewState :: #state{}} |
    {noreply, NewState :: #state{}, timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: #state{}}).
handle_info(_Info, State = #state{}) ->
    {noreply, State}.

%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
    State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
    ok.

%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
    Extra :: term()) ->
    {ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
