-module(oidcc_http_util).

-export([async_http/3, async_http/5]).
-export([sync_http/3, sync_http/5]).
-export([sync_http/4, sync_http/6]).
-export([uncompress_body_if_needed/2]).
-export([request_timeout/1]).

-include_lib("public_key/include/public_key.hrl").

sync_http(Method, Url, Header) ->
    sync_http(Method, Url, Header, false).

sync_http(Method, Url, Header, ContentType, Body) ->
    sync_http(Method, Url, Header, ContentType, Body, false).

sync_http(Method, Url, Header, UseCache) ->
    perform_request(Method, Url, Header, undefined, <<>>, [], UseCache).

sync_http(Method, Url, Header, ContentType, Body, UseCache) ->
    perform_request(Method, Url, Header, ContentType, Body, [], UseCache).

async_http(Method, Url, Header) ->
    async_http(Method, Url, Header, undefined, <<>>).

async_http(Method, Url, Header, ContentType, Body) ->
    RequestId = erlang:make_ref(),
    Caller = self(),
    spawn_link(fun() ->
                  async_http_perform(Caller, RequestId, Method, Url, Header, ContentType, Body)
               end),
    {ok, RequestId}.

async_http_perform(Caller, RequestId, Method, Url, Header, ContentType, Body) ->
    Response =
        case perform_request(Method, Url, Header, ContentType, Body, [], false) of
            {error, _} = Error ->
                Error;
            {ok,
             #{status := StatusCode,
               header := RespHeaders,
               body := InBody}} ->
                {{<<>>, StatusCode, <<>>}, RespHeaders, InBody}
        end,
    Caller ! {http, {RequestId, Response}}.

request_timeout(Unit) ->
    Timeout =
        case application:get_env(oidcc, http_request_timeout, undefined) of
            T when is_integer(T), T > 0 ->
                T;
            _ ->
                300
        end,
    case Unit of
        ms ->
            Timeout * 1000;
        s ->
            Timeout
    end.

perform_request(Method, Url, Header, ContentType, Body, Options, true) ->
    case oidcc_http_cache:lookup_http_call(Method, {Method, Url, Header, ContentType, Body})
    of
        {ok, pending} ->
            wait_for_cache(Method, {Method, Url, Header, ContentType, Body});
        {ok, Res} ->
            Res;
        {error, _} ->
            request_or_wait(Method, Url, Header, ContentType, Body, Options)
    end;
perform_request(Method, Url, Header, ContentType, Body, Options, false) ->
    perform_http_request(Method, Url, Header, ContentType, Body, Options).

perform_http_request(Method, Url, Header, ContentType, Body, Options) ->
    Headers1 =
        case ContentType of
            undefined ->
                Header;
            _ ->
                [{<<"content-type">>, ContentType} | Header]
        end,
    Res = hackney:request(Method, Url, Headers1, Body, Options ++ [{follow_redirect, true}]),
    normalize_result(Res).

request_or_wait(Method, Url, Header, ContentType, Body, Options) ->
    case oidcc_http_cache:enqueue_http_call(Method, {Method, Url, Header, ContentType, Body})
    of
        true ->
            Result = perform_http_request(Method, Url, Header, ContentType, Body, Options),
            ok =
                oidcc_http_cache:cache_http_result(Method,
                                                   {Method, Url, Header, ContentType, Body},
                                                   Result),
            Result;
        _ ->
            wait_for_cache(Method, {Method, Url, Header, ContentType, Body})
    end.

wait_for_cache(Method, Request) ->
    case oidcc_http_cache:lookup_http_call(Method, Request) of
        {ok, pending} ->
            timer:sleep(500),
            wait_for_cache(Method, Request);
        {ok, Result} ->
            Result
    end.

normalize_result({ok, StatusCode, RespHeaders, ClientRef}) ->
    {ok, Body} = hackney:body(ClientRef),
    {ok,
     #{status => StatusCode,
       header => RespHeaders,
       body => Body}};
normalize_result({ok, StreamId}) ->
    {ok, StreamId};
normalize_result({error, _} = Error) ->
    Error.

uncompress_body_if_needed(Body, Header) when is_list(Header) ->
    Encoding = lists:keyfind(<<"content-encoding">>, 1, Header),
    uncompress_body_if_needed(Body, Encoding);
uncompress_body_if_needed(Body, false) ->
    {ok, Body};
uncompress_body_if_needed(Body, {_, <<"gzip">>}) ->
    {ok, zlib:gunzip(Body)};
uncompress_body_if_needed(Body, {_, <<"deflate">>}) ->
    Z = zlib:open(),
    ok = zlib:inflateInit(Z),
    {ok, zlib:inflate(Z, Body)};
uncompress_body_if_needed(_Body, {_, Compression}) ->
    erlang:error({unsupported_encoding, Compression}).

-module(oidcc_http_util).

-export([async_http/3, async_http/5]).
-export([sync_http/3, sync_http/5]).
-export([sync_http/4, sync_http/6]).
-export([uncompress_body_if_needed/2]).
-export([request_timeout/1]).

-include_lib("public_key/include/public_key.hrl").

sync_http(Method, Url, Header) ->
    sync_http(Method, Url, Header, false).

sync_http(Method, Url, Header, ContentType, Body) ->
    sync_http(Method, Url, Header, ContentType, Body, false).

sync_http(Method, Url, Header, UseCache) ->
    perform_request(Method, Url, Header, undefined, <<>>, [], UseCache).

sync_http(Method, Url, Header, ContentType, Body, UseCache) ->
    perform_request(Method, Url, Header, ContentType, Body, [], UseCache).

async_http(Method, Url, Header) ->
    async_http(Method, Url, Header, undefined, <<>>).

async_http(Method, Url, Header, ContentType, Body) ->
    RequestId = erlang:make_ref(),
    Caller = self(),
    spawn_link(fun() ->
                  async_http_perform(Caller, RequestId, Method, Url, Header, ContentType, Body)
               end),
    {ok, RequestId}.

async_http_perform(Caller, RequestId, Method, Url, Header, ContentType, Body) ->
    Response =
        case perform_request(Method, Url, Header, ContentType, Body, [], false) of
            {error, _} = Error ->
                Error;
            {ok,
             #{status := StatusCode,
               header := RespHeaders,
               body := InBody}} ->
                {{<<>>, StatusCode, <<>>}, RespHeaders, InBody}
        end,
    Caller ! {http, {RequestId, Response}}.

request_timeout(Unit) ->
    Timeout =
        case application:get_env(oidcc, http_request_timeout, undefined) of
            T when is_integer(T), T > 0 ->
                T;
            _ ->
                300
        end,
    case Unit of
        ms ->
            Timeout * 1000;
        s ->
            Timeout
    end.

perform_request(Method, Url, Header, ContentType, Body, Options, true) ->
    case oidcc_http_cache:lookup_http_call(Method, {Method, Url, Header, ContentType, Body})
    of
        {ok, pending} ->
            wait_for_cache(Method, {Method, Url, Header, ContentType, Body});
        {ok, Res} ->
            Res;
        {error, _} ->
            request_or_wait(Method, Url, Header, ContentType, Body, Options)
    end;
perform_request(Method, Url, Header, ContentType, Body, Options, false) ->
    perform_http_request(Method, Url, Header, ContentType, Body, Options).

perform_http_request(Method, Url, Header, ContentType, Body, Options) ->
    Headers1 =
        case ContentType of
            undefined ->
                Header;
            _ ->
                [{<<"content-type">>, ContentType} | Header]
        end,
    Res = hackney:request(Method, Url, Headers1, Body, Options ++ [{follow_redirect, true}]),
    normalize_result(Res).

request_or_wait(Method, Url, Header, ContentType, Body, Options) ->
    case oidcc_http_cache:enqueue_http_call(Method, {Method, Url, Header, ContentType, Body})
    of
        true ->
            Result = perform_http_request(Method, Url, Header, ContentType, Body, Options),
            ok =
                oidcc_http_cache:cache_http_result(Method,
                                                   {Method, Url, Header, ContentType, Body},
                                                   Result),
            Result;
        _ ->
            wait_for_cache(Method, {Method, Url, Header, ContentType, Body})
    end.

wait_for_cache(Method, Request) ->
    case oidcc_http_cache:lookup_http_call(Method, Request) of
        {ok, pending} ->
            timer:sleep(500),
            wait_for_cache(Method, Request);
        {ok, Result} ->
            Result
    end.

normalize_result({ok, StatusCode, RespHeaders, ClientRef}) ->
    {ok, Body} = hackney:body(ClientRef),
    {ok,
     #{status => StatusCode,
       header => RespHeaders,
       body => Body}};
normalize_result({ok, StreamId}) ->
    {ok, StreamId};
normalize_result({error, _} = Error) ->
    Error.

uncompress_body_if_needed(Body, Header) when is_list(Header) ->
    Encoding = lists:keyfind(<<"content-encoding">>, 1, Header),
    uncompress_body_if_needed(Body, Encoding);
uncompress_body_if_needed(Body, false) ->
    {ok, Body};
uncompress_body_if_needed(Body, {_, <<"gzip">>}) ->
    {ok, zlib:gunzip(Body)};
uncompress_body_if_needed(Body, {_, <<"deflate">>}) ->
    Z = zlib:open(),
    ok = zlib:inflateInit(Z),
    {ok, zlib:inflate(Z, Body)};
uncompress_body_if_needed(_Body, {_, Compression}) ->
    erlang:error({unsupported_encoding, Compression}).
