%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 08. 5月 2025 13:00
%%%-------------------------------------------------------------------
-module(http_protocol).
-author("anlicheng").

%% API
-export([init/2]).

-define(MAX_BODY_BYTES, 10 * 1024 * 1024).

init(Req0, Opts = [Mod|_]) ->
    ok = iot_log:set_metadata(),
    Method = binary_to_list(cowboy_req:method(Req0)),
    Path = binary_to_list(cowboy_req:path(Req0)),
    GetParams0 = cowboy_req:parse_qs(Req0),
    GetParams = maps:from_list(GetParams0),
    case parse_body(Req0) of
        {ok, PostParams, Req1} ->
            handle_request(Mod, Method, Path, GetParams, PostParams, Req1, Opts);
        {error, StatusCode, Resp, Req1} ->
            Req2 = cowboy_req:reply(StatusCode, #{
                <<"Content-Type">> => <<"application/json;charset=utf-8">>
            }, Resp, Req1),
            {ok, Req2, Opts}
    end.

handle_request(Mod, Method, Path, GetParams, PostParams, Req1, Opts) ->
    try Mod:handle_request(Method, Path, GetParams, PostParams) of
        {ok, StatusCode, Resp} ->
            logger:debug("[http_protocol] request path: ~p, get_params: ~p, post_params: ~p, response: ~ts",
                [Path, GetParams, PostParams, Resp]),
            AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>),
            Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of
                       true ->
                           Resp1 = zlib:gzip(Resp),
                           cowboy_req:reply(StatusCode, #{
                               <<"Content-Type">> => <<"application/json;charset=utf-8">>,
                               <<"Content-Encoding">> => <<"gzip">>
                           }, Resp1, Req1);
                       false ->
                           cowboy_req:reply(StatusCode, #{
                               <<"Content-Type">> => <<"application/json;charset=utf-8">>
                           }, Resp, Req1)
                   end,
            {ok, Req2, Opts}
    catch
        throw:Error ->
            ErrResp = iot_util:json_error(-1, Error),
            Req2 = cowboy_req:reply(404, #{
                <<"Content-Type">> => <<"application/json;charset=utf-8">>
            }, ErrResp, Req1),
            {ok, Req2, Opts};
        _:Error:Stack ->
            logger:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
            Req2 = cowboy_req:reply(500, #{
                <<"Content-Type">> => <<"text/html;charset=utf-8">>
            }, <<"Internal Server Error">>, Req1),
            {ok, Req2, Opts}
    end.

%% 判断是否支持gzip
supported_gzip(AcceptEncoding) when is_binary(AcceptEncoding) ->
    binary:match(AcceptEncoding, <<"gzip">>) =/= nomatch.

parse_body(Req0) ->
    ContentType = cowboy_req:header(<<"content-type">>, Req0),
    case ContentType of
        <<"application/json", _/binary>> ->
            case read_body(Req0) of
                {ok, Body, Req1} ->
                    decode_json_body(Body, Req1);
                {error, payload_too_large, Req1} ->
                    {error, 413, iot_util:json_error(413, <<"payload too large">>), Req1}
            end;
        <<"application/x-www-form-urlencoded">> ->
            case cowboy_req:read_urlencoded_body(Req0) of
                {ok, PostParams0, Req1} ->
                    PostParams = maps:from_list(PostParams0),
                    {ok, PostParams, Req1}
            end;
        _ ->
            {ok, #{}, Req0}
    end.

%% 读取请求体
read_body(Req) ->
    read_body(Req, <<>>).
read_body(Req, AccData) ->
    case cowboy_req:read_body(Req) of
        {ok, Data, Req1} ->
            NAccData = <<AccData/binary, Data/binary>>,
            case byte_size(NAccData) =< ?MAX_BODY_BYTES of
                true ->
                    {ok, NAccData, Req1};
                false ->
                    {error, payload_too_large, Req1}
            end;
        {more, Data, Req1} ->
            NAccData = <<AccData/binary, Data/binary>>,
            case byte_size(NAccData) =< ?MAX_BODY_BYTES of
                true ->
                    read_body(Req1, NAccData);
                false ->
                    {error, payload_too_large, Req1}
            end
    end.

decode_json_body(<<"">>, Req) ->
    {ok, #{}, Req};
decode_json_body(Body, Req) ->
    case catch json:decode(Body) of
        {'EXIT', _} ->
            {error, 400, iot_util:json_error(400, <<"invalid json">>), Req};
        {error, _} ->
            {error, 400, iot_util:json_error(400, <<"invalid json">>), Req};
        Decoded when is_map(Decoded); is_list(Decoded) ->
            {ok, Decoded, Req};
        _ ->
            {error, 400, iot_util:json_error(400, <<"invalid json">>), Req}
    end.
