%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 24. 12月 2023 15:42
%%%-------------------------------------------------------------------
-module(iot_api_client).
-author("anlicheng").
-include("domain_model.hrl").

%% API
-export([ai_event/1]).

-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).

-export([get_all_hosts/0, get_host_by_id/1, get_host_by_uuid/1, change_host_status/2]).
-export([get_host_devices/1, get_device_by_uuid/1, change_device_status/2]).
-export([get_all_endpoints/0, get_endpoint/1]).

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

-spec get_all_hosts() -> [HostUUID :: binary()].
get_all_hosts() ->
    case do_get("/get_all_hosts", []) of
        {ok, Ids} ->
            Ids;
        _ ->
            []
    end.

-spec get_host_by_uuid(UUID :: binary()) -> undefined | {ok, HostInfo :: #host_info{}}.
get_host_by_uuid(UUID) when is_binary(UUID) ->
    case do_get("/get_host_by_uuid", [{<<"uuid">>, UUID}]) of
        {ok, HostInfo} ->
            case host_info_record(HostInfo) of
                {ok, Record} ->
                    {ok, Record};
                error ->
                    undefined
            end;
        _ ->
            undefined
    end.

-spec get_host_by_id(HostId :: integer()) -> undefined | {ok, HostInfo :: #host_info{}}.
get_host_by_id(HostId) when is_integer(HostId) ->
    case do_get("/get_host_by_id", [{<<"host_id">>, integer_to_binary(HostId)}]) of
        {ok, HostInfo} ->
            case host_info_record(HostInfo) of
                {ok, Record} ->
                    {ok, Record};
                error ->
                    undefined
            end;
        _ ->
            undefined
    end.

%% 修改主机的状态
-spec change_host_status(UUID :: binary(), Status :: integer()) -> {ok, Result :: any()} | {error, Reason :: any()}.
change_host_status(UUID, NStatus) when is_binary(UUID), is_integer(NStatus) ->
    do_post("/change_host_status", #{<<"uuid">> => UUID, <<"new_status">> => NStatus}).

-spec get_host_devices(HostId :: integer()) -> {ok, Devices :: [#device_info{}]} | {error, Reason::any()}.
get_host_devices(HostId) when is_integer(HostId) ->
    case do_get("/get_host_devices", [{<<"host_id">>, integer_to_binary(HostId)}]) of
        {ok, DeviceInfos} when is_list(DeviceInfos) ->
            device_info_records(DeviceInfos);
        {ok, _Other} ->
            {error, invalid_device_infos};
        Error ->
            Error
    end.

-spec get_device_by_uuid(DeviceUUID :: binary()) -> {ok, DeviceInfo :: #device_info{}} | undefined.
get_device_by_uuid(DeviceUUID) when is_binary(DeviceUUID) ->
    case do_get("/get_device_by_uuid", [{<<"device_uuid">>, DeviceUUID}]) of
        {ok, DeviceInfo} ->
            case device_info_record(DeviceInfo) of
                {ok, Record} ->
                    {ok, Record};
                error ->
                    undefined
            end;
        _ ->
            undefined
    end.

%% 修改主机的状态
-spec change_device_status(DeviceUUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
change_device_status(DeviceUUID, NStatus) when is_binary(DeviceUUID), is_integer(NStatus) ->
    do_post("/change_device_status", #{<<"device_uuid">> => DeviceUUID, <<"new_status">> => NStatus}).

%%%-------------------------------------------------------------------
%% endpoint相关的api
%%%-------------------------------------------------------------------
%% API

-spec get_all_endpoints() -> [Endpoint :: map()].
get_all_endpoints() ->
    case do_get("/get_all_endpoints", []) of
        {ok, Endpoints} ->
            Endpoints;
        _ ->
            []
    end.

-spec get_endpoint(Id :: integer()) -> undefined | {ok, EndpointInfo :: map()}.
get_endpoint(Id) when is_integer(Id) ->
    case do_get("/get_endpoint", [{<<"id">>, integer_to_binary(Id)}]) of
        {ok, EndpointInfo} when is_map(EndpointInfo) ->
            {ok, EndpointInfo};
        _ ->
            undefined
    end.

ai_event(Id) when is_integer(Id) ->
    Token = iot_util:md5(<<?API_TOKEN/binary, (integer_to_binary(Id))/binary, ?API_TOKEN/binary>>),
    {ok, Url} = application:get_env(iot, api_url),

    Headers = [
        {<<"content-type">>, <<"application/json">>}
    ],
    ReqData = #{
        <<"token">> => Token,
        <<"id">> => Id
    },
    Body = iolist_to_binary(json:encode(ReqData)),
    case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
        {ok, 200, _, ClientRef} ->
            {ok, RespBody} = hackney:body(ClientRef),
            logger:debug("[iot_api_client] send body: ~p, get error is: ~p", [Body, RespBody]),
            hackney:close(ClientRef);
        {ok, HttpCode, _, ClientRef} ->
            {ok, RespBody} = hackney:body(ClientRef),
            hackney:close(ClientRef),
            logger:warning("[iot_api_client] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]);
        {error, Reason} ->
            logger:warning("[iot_api_client] send body: ~p, get error is: ~p", [Body, Reason])
    end.

%%%-------------------------------------------------------------------
%% helper methods
%%%-------------------------------------------------------------------

-spec do_post(Path :: string(), Params :: map()) -> {ok, Resp :: any()} | {error, Reason :: any()}.
do_post(Path, Params) when is_list(Path), is_map(Params) ->
    {ok, BaseUrl} = application:get_env(iot, api_url),
    Headers = [
        {<<"content-type">>, <<"application/json">>},
        {<<"Accept">>, <<"application/json">>}
    ],
    Url = BaseUrl ++ Path,
    Body = iolist_to_binary(json:encode(Params)),
    case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
        {ok, 200, _, ClientRef} ->
            {ok, RespBody} = hackney:body(ClientRef),
            logger:debug("[iot_api_client] request url: ~p, send body: ~p, get response is: ~p", [Url, Body, RespBody]),
            hackney:close(ClientRef),
            case catch json:decode(RespBody) of
                #{<<"result">> := Result} ->
                    {ok, Result};
                #{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
                    {error, {Code, Message}};
                {error, Reason} ->
                    {error, Reason};
                Other ->
                    {error, Other}
            end;
        {ok, HttpCode, _, ClientRef} ->
            {ok, RespBody} = hackney:body(ClientRef),
            hackney:close(ClientRef),
            logger:warning("[iot_api_client] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, {HttpCode, RespBody}]),
            {error, {HttpCode, RespBody}};
        {error, Reason} ->
            logger:warning("[iot_api_client] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, Reason]),
            {error, Reason}
    end.

-spec do_get(Path :: string(), Params :: [{Key :: binary(), Val :: binary()}]) -> {ok, Resp :: any()} | {error, Reason :: any()}.
do_get(Path, Params) when is_list(Path), is_list(Params) ->
    {ok, BaseUrl} = application:get_env(iot, api_url),
    Headers = [
        {<<"Accept">>, <<"application/json">>}
    ],

    Url = case length(Params) > 0 of
              true ->
                  QS = binary_to_list(uri_string:compose_query(Params)),
                  BaseUrl ++ Path ++ "?" ++ QS;
              false ->
                  BaseUrl ++ Path
          end,

    case hackney:request(get, Url, Headers, <<>>, [{pool, false}]) of
        {ok, 200, _, ClientRef} ->
            {ok, RespBody} = hackney:body(ClientRef),
            hackney:close(ClientRef),
            logger:debug("[iot_api_client] url: ~p, get response is: ~p", [Url, RespBody]),
            case catch json:decode(RespBody) of
                #{<<"result">> := Result} ->
                    {ok, Result};
                #{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
                    {error, {Code, Message}};
                {error, Reason} ->
                    {error, Reason};
                Other ->
                    {error, Other}
            end;
        {ok, HttpCode, _, ClientRef} ->
            {ok, RespBody} = hackney:body(ClientRef),
            hackney:close(ClientRef),
            logger:warning("[iot_api_client] request url: ~p, get error is: ~p", [Url, {HttpCode, RespBody}]),
            {error, {HttpCode, RespBody}};
        {error, Reason} ->
            logger:warning("[iot_api_client] request url: ~p, get error is: ~p", [Url, Reason]),
            {error, Reason}
    end.

-spec host_info_record(map()) -> {ok, #host_info{}} | error.
host_info_record(#{<<"id">> := Id, <<"uuid">> := UUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status})
        when is_integer(Id), is_binary(UUID), is_integer(AuthorizeStatus), is_integer(Status) ->
    {ok, #host_info{
        id = Id,
        uuid = UUID,
        authorize_status = AuthorizeStatus,
        status = Status
    }};
host_info_record(_) ->
    error.

-spec device_info_record(map()) -> {ok, #device_info{}} | error.
device_info_record(#{<<"id">> := Id, <<"host_id">> := HostId, <<"device_uuid">> := DeviceUUID, <<"status">> := Status})
        when is_integer(Id), is_integer(HostId), is_binary(DeviceUUID), is_integer(Status) ->
    {ok, #device_info{
        id = Id,
        host_id = HostId,
        device_uuid = DeviceUUID,
        status = Status
    }};
device_info_record(_) ->
    error.

-spec device_info_records([map()]) -> {ok, [#device_info{}]} | {error, invalid_device_info}.
device_info_records(DeviceInfos) ->
    lists:foldr(fun(DeviceInfo, Acc) ->
        case {device_info_record(DeviceInfo), Acc} of
            {{ok, Record}, {ok, Records}} ->
                {ok, [Record | Records]};
            {error, _} ->
                {error, invalid_device_info};
            {_, Error = {error, _}} ->
                Error
        end
    end, {ok, []}, DeviceInfos).
