%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(amqp10_client_socket).

-feature(maybe_expr, enable).

-export([connect/3,
         set_active_once/1,
         send/2,
         close/1]).

-type socket() :: {tcp, gen_tcp:socket()} |
                  {ssl, ssl:sslsocket()} |
                  {ws, pid(), gun:stream_ref()}.

-export_type([socket/0]).

-define(TCP_OPTS, [binary,
                   {packet, 0},
                   {active, false},
                   {nodelay, true}]).

-spec connect(inet:hostname() | inet:ip_address(),
              inet:port_number(),
              amqp10_client_connection:connection_config()) ->
    {ok, socket()} | {error, any()}.
connect(Host, Port, #{ws_path := Path} = Opts) ->
    GunOpts0 = maps:get(ws_opts, Opts, #{}),
    HTTP2Opts = maps:get(http2_opts, GunOpts0, #{}),
    GunOpts1 = GunOpts0#{http2_opts => HTTP2Opts#{notify_settings_changed => true}},
    GunOpts = maps:merge(#{tcp_opts => [{nodelay, true}]},
                         GunOpts1),
    maybe
        {ok, _Started} ?= application:ensure_all_started(gun),
        {ok, Pid} ?= gun:open(Host, Port, GunOpts),
        MRef = monitor(process, Pid),
        {ok, HttpVsn} ?= gun:await_up(Pid, MRef),
        ok ?= case HttpVsn of
            http ->
                ok;
            http2 ->
                receive
                    {gun_notify, Pid, settings_changed, #{enable_connect_protocol := true}} ->
                        ok
                after 5000 ->
                    {error, {ws_enable_connect_protocol, timeout}}
                end
        end,
        {ok, StreamRef} ?= ws_upgrade(Pid, Path),
        {ok, {ws, Pid, StreamRef}}
    end;
connect(Host, Port, #{tls_opts := {secure_port, Opts0}}) ->
    Opts = rabbit_ssl_options:fix_client(Opts0),
    case ssl:connect(Host, Port, ?TCP_OPTS ++ Opts) of
        {ok, S} ->
            {ok, {ssl, S}};
        Err ->
            Err
    end;
connect(Host, Port, _) ->
    case gen_tcp:connect(Host, Port, ?TCP_OPTS) of
        {ok, S} ->
            {ok, {tcp, S}};
        Err ->
            Err
    end.

ws_upgrade(Pid, Path) ->
    StreamRef = gun:ws_upgrade(Pid,
                               Path,
                               [{<<"cache-control">>, <<"no-cache">>}],
                               #{protocols => [{<<"amqp">>, gun_ws_h}]}),
    receive
        {gun_upgrade, Pid, StreamRef, [<<"websocket">>], _Headers} ->
            {ok, StreamRef};
        {gun_response, Pid, _, _, Status, Headers} ->
            {error, {ws_upgrade, Status, Headers}};
        {gun_error, Pid, StreamRef, Reason} ->
            {error, {ws_upgrade, Reason}}
    after 5000 ->
              {error, {ws_upgrade, timeout}}
    end.

-spec set_active_once(socket()) -> ok.
set_active_once({tcp, Sock}) ->
    ok = inet:setopts(Sock, [{active, once}]);
set_active_once({ssl, Sock}) ->
    ok = ssl:setopts(Sock, [{active, once}]);
set_active_once({ws, _Pid, _Ref}) ->
    %% Gun also has an active-like mode via the flow option and gun:update_flow.
    %% It will even make Gun stop reading from the socket if flow is zero.
    %% If needed, we can make use of it in future.
    ok.

-spec send(socket(), iodata()) ->
    ok | {error, any()}.
send({tcp, Socket}, Data) ->
    gen_tcp:send(Socket, Data);
send({ssl, Socket}, Data) ->
    ssl:send(Socket, Data);
send({ws, Pid, Ref}, Data) ->
    gun:ws_send(Pid, Ref, {binary, Data}).

-spec close(socket()) ->
    ok | {error, any()}.
close({tcp, Socket}) ->
    gen_tcp:close(Socket);
close({ssl, Socket}) ->
    ssl:close(Socket);
close({ws, Pid, Ref}) ->
    gun:ws_send(Pid, Ref, close),
    gun:shutdown(Pid).
