%%%-------------------------------------------------------------------
%% @doc Single control socket channel.
%% @end
%%%-------------------------------------------------------------------

-module(ctrl_channel).
-behaviour(gen_server).

-export([start/0, set_socket/2]).

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

-record(state, {
    socket = undefined :: gen_tcp:socket() | undefined
}).

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

-spec start() -> {ok, pid()} | ignore | {error, term()}.
start() ->
    gen_server:start(?MODULE, [], []).

-spec set_socket(pid(), gen_tcp:socket()) -> ok.
set_socket(Pid, Socket) ->
    gen_server:call(Pid, {set_socket, Socket}).

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

-spec init([]) -> {ok, #state{}}.
init([]) ->
    ok = iot_log:set_metadata(),
    {ok, #state{}}.

-spec handle_call(term(), {pid(), term()}, #state{}) ->
    {reply, term(), #state{}} | {stop, term(), term(), #state{}}.
handle_call({set_socket, Socket}, _From, State = #state{socket = undefined}) ->
    case inet:setopts(Socket, [{packet, 2}, {active, once}]) of
        ok ->
            {reply, ok, State#state{socket = Socket}};
        {error, Reason} ->
            logger:warning("[ctrl_channel] failed to activate socket: ~p", [Reason]),
            gen_tcp:close(Socket),
            {stop, Reason, {error, Reason}, State}
    end;
handle_call(_Request, _From, State = #state{}) ->
    {reply, ok, State}.

-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
handle_cast(_Request, State = #state{}) ->
    {noreply, State}.

-spec handle_info(term(), #state{}) -> {noreply, #state{}} | {stop, term(), #state{}}.
handle_info({tcp, Socket, Data}, State = #state{socket = Socket}) ->
    ok = send_response(Socket, dispatch(Data)),
    ok = inet:setopts(Socket, [{active, once}]),
    {noreply, State};
handle_info({tcp_closed, Socket}, State = #state{socket = Socket}) ->
    {stop, normal, State};
handle_info({tcp_error, Socket, Reason}, State = #state{socket = Socket}) ->
    logger:warning("[ctrl_channel] socket error: ~p", [Reason]),
    {stop, Reason, State};
handle_info(Info, State = #state{}) ->
    logger:warning("[ctrl_channel] ignore unknown info: ~p", [Info]),
    {noreply, State}.

-spec terminate(term(), #state{}) -> ok.
terminate(_Reason, #state{socket = undefined}) ->
    ok;
terminate(_Reason, #state{socket = Socket}) ->
    gen_tcp:close(Socket),
    ok.

-spec code_change(term(), #state{}, term()) -> {ok, #state{}}.
code_change(_OldVsn, State = #state{}, _Extra) ->
    {ok, State}.

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

-spec send_response(gen_tcp:socket(), {ok, iodata()} | {error, iodata()}) -> ok.
send_response(Socket, {ok, Response}) ->
    Reply = json:encode(#{<<"result">> => Response}),
    gen_tcp:send(Socket, Reply);
send_response(Socket, {error, Response}) ->
    Reply = json:encode(#{<<"error">> => #{<<"message">> => Response}}),
    gen_tcp:send(Socket, Reply).

-spec dispatch(binary()) -> {ok, iodata()} | {error, iodata()}.
dispatch(Data) when is_binary(Data) ->
    Request = json:decode(Data),
    handle_request(Request).

-spec handle_request(binary()) -> {ok, iodata()} | {error, iodata()}.
handle_request(#{<<"method">> := <<"ping">>}) ->
    {ok, <<"pong">>};

handle_request(#{<<"method">> := <<"add_client">>, <<"params">> := #{<<"uuid">> := UUID, <<"token">> := Token}}) ->
    case efka_client_store:register(UUID, Token) of
        ok ->
            {ok, <<"OK">>};
        {error, Reason} ->
            ReadableReason = readable_binary(Reason),
            {error, <<"add failed: ", ReadableReason/binary>>}
    end;
handle_request(Command) ->
    logger:warning("[ctrl_channel] unsupported command: ~p", [Command]),
    {error, <<"error unsupported_command\n">>}.

-spec readable_binary(Term :: any()) -> binary().
readable_binary(Term) ->
    iolist_to_binary(io_lib:format("~p", [Term])).
