%%%-------------------------------------------------------------------
%% @doc Unix domain socket control service.
%% @end
%%%-------------------------------------------------------------------

-module(ctrl_server).
-behaviour(gen_server).

-export([start_link/0]).

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

-define(SERVER, ?MODULE).
-define(DEFAULT_SOCKET_PATH, "/var/lib/iot/ctl.sock").
-define(DEFAULT_BACKLOG, 128).

-record(state, {
    listen_socket :: gen_tcp:socket(),
    socket_path :: file:filename_all(),
    acceptor :: pid()
}).

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

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

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

-spec init([]) -> {ok, #state{}} | {stop, term()}.
init([]) ->
    ok = iot_log:set_metadata(),
    Props = application:get_env(iot, ctrl_server, []),
    SocketPath = proplists:get_value(socket_path, Props, ?DEFAULT_SOCKET_PATH),
    Backlog = proplists:get_value(backlog, Props, ?DEFAULT_BACKLOG),

    ok = ensure_socket_dir(SocketPath),
    ok = delete_stale_socket(SocketPath),

    ListenOpts = [
        binary,
        {packet, line},
        {active, false},
        {backlog, Backlog},
        {ifaddr, {local, SocketPath}}
    ],
    case gen_tcp:listen(0, ListenOpts) of
        {ok, ListenSocket} ->
            {ok, Acceptor} = ctrl_acceptor:start_link(ListenSocket),
            logger:debug("[ctrl_server] start at socket: ~ts", [SocketPath]),
            {ok, #state{listen_socket = ListenSocket, socket_path = SocketPath, acceptor = Acceptor}};
        {error, Reason} ->
            logger:error("[ctrl_server] failed to listen on socket ~ts: ~p", [SocketPath, Reason]),
            {stop, Reason}
    end.

-spec handle_call(term(), {pid(), term()}, #state{}) -> {reply, ok, #state{}}.
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{}}.
handle_info(Info, State = #state{}) ->
    logger:warning("[ctrl_server] ignore unknown info: ~p", [Info]),
    {noreply, State}.

-spec terminate(term(), #state{}) -> ok.
terminate(_Reason, #state{listen_socket = ListenSocket, socket_path = SocketPath}) ->
    gen_tcp:close(ListenSocket),
    ok = delete_stale_socket(SocketPath),
    ok.

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

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

-spec ensure_socket_dir(file:filename_all()) -> ok | {error, term()}.
ensure_socket_dir(SocketPath) ->
    case filelib:ensure_dir(SocketPath) of
        ok ->
            ok;
        {error, Reason} ->
            logger:error("[ctrl_server] failed to ensure socket dir for ~ts: ~p",
                [SocketPath, Reason]),
            {error, Reason}
    end.

-spec delete_stale_socket(file:filename_all()) -> ok.
delete_stale_socket(SocketPath) ->
    case file:delete(SocketPath) of
        ok ->
            ok;
        {error, enoent} ->
            ok;
        {error, Reason} ->
            logger:warning("[ctrl_server] failed to delete stale socket ~ts: ~p",
                [SocketPath, Reason]),
            ok
    end.
