%%%-------------------------------------------------------------------
%%% @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, create_bucket/3, get_orgs/1]).

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

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

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

-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}).

-spec create_bucket(Pid :: pid(), OrgId :: binary(), Bucket :: binary()) -> ok.
create_bucket(Pid, OrgId, Bucket) when is_pid(Pid), is_binary(OrgId), is_binary(Bucket) ->
    gen_server:call(Pid, {create_bucket, OrgId, Bucket}).

-spec get_orgs(Pid :: pid()) -> {ok, Orgs :: list()} | {error, Reason :: any()}.
get_orgs(Pid) when is_pid(Pid) ->
    gen_server:call(Pid, get_orgs).

%% @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(get_orgs, _From, State = #state{token = Token, host = Host, port = Port}) ->
    %% 处理headers
    Headers = [
        {<<"Accept">>, <<"application/json">>},
        {<<"Authorization">>, <<"Token ", Token/binary>>}
    ],

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

    lager:debug("[influx_client] url is: ~p, headers: ~p", [Url, Headers]),
    case hackney:request(get, Url, Headers, [], [{pool, false}]) of
        {ok, 200, _RespHeaders, ClientRef} ->
            case hackney:body(ClientRef) of
                {ok, RespBody} ->
                    hackney:close(ClientRef),
                    case catch jiffy:decode(RespBody, [return_maps]) of
                        #{<<"orgs">> := Orgs} ->
                            {reply, {ok, Orgs}, State};
                        Error ->
                            {reply, Error, State}
                    end;
                {error, Error} ->
                    hackney:close(ClientRef),
                    {reply, {error, Error}, State}
            end;
        {ok, StatusCode, _, ClientRef} ->
            case hackney:body(ClientRef) of
                {ok, RespBody} ->
                    hackney:close(ClientRef),
                    {reply, {error, {StatusCode, RespBody}}, State};
                {error, Error} ->
                    hackney:close(ClientRef),
                    {reply, {error, Error}, State}
            end;
        {error, Reason} ->
            {reply, {error, Reason}, State}
    end;

handle_call({create_bucket, OrgId, Bucket}, _From, State = #state{host = Host, port =  Port, token = Token}) ->
    %% 处理headers
    Headers = [
        {<<"Content-Type">>, <<"application/json">>},
        {<<"Authorization">>, <<"Token ", Token/binary>>}
    ],

    Req = #{
        <<"name">> => Bucket,
        <<"orgID">> => OrgId,
        <<"retentionRules">> => [
            #{
                <<"type">> => <<"expire">>,
                <<"everySeconds">> => 7776000
            }
        ]
    },
    Body = iolist_to_binary(jiffy:encode(Req, [force_utf8])),

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

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

    {reply, ok, 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
    }),

    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] url: ~p, lines: ~p, first line: ~ts, status_code: ~p, response is: ~p",
                        [Url, length(Points), hd(PointLines), StatusCode, RespBody]);
                {error, Error} ->
                    lager:warning("[influx_client] url: ~p, status_code: ~p, error is: ~p", [Url, Error])
            end,
            hackney:close(ClientRef);
        {error, Reason} ->
            lager:warning("[influx_client] url: ~p, request result is: ~p", [Url, 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
%%%===================================================================
