%% -------------------------------------------------------------------
%%
%% derived from riaknostic - automated diagnostic tools for Riak
%%
%% Copyright (c) 2011 Basho Technologies, Inc.  All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License.  You may obtain
%% a copy of the License at
%%
%%   http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied.  See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%%
%% File renamed from riaknostic_node.erl to weatherreport_node.erl and
%% modified to work with Apache CouchDB
%%
%% Copyright (c) 2014 Cloudant
%%
%% -------------------------------------------------------------------

%% @doc Functions that help diagnostics interact with the local
%% node or other members of the cluster.
-module(weatherreport_node).

-export([
    can_connect/0,
    can_connect_all/0,
    pid/0,
    local_command/2,
    local_command/3,
    local_command/4,
    multicall/5,
    nodename/0
]).

%% @doc Calls the given 0-arity module and function on the local
%% node and returns the result of that call.
%% @equiv local_command(Module, Function, [])
%% @see can_connect/0.
-spec local_command(Module :: atom(), Function :: atom()) -> term().
local_command(Module, Function) ->
    local_command(Module, Function, []).

%% @doc Calls the given module and function with the given arguments
%% on the local node and returns the result of that call.
%% @equiv local_command(Module, Function, Args, 5000)
%% @see can_connect/0
-spec local_command(Module :: atom(), Function :: atom(), Args :: [term()]) -> term().
local_command(Module, Function, Args) ->
    local_command(Module, Function, Args, weatherreport_config:timeout()).

%% @doc Calls the given module and function with the given arguments
%% on the local node and returns the result of that call,
%% returning an error if the call doesn't complete within the given
%% timeout.
%% @equiv rpc:call(NodeName, Module, Function, Args, Timeout)
%% @see can_connect/0
-spec local_command(Module :: atom(), Function :: atom(), Args :: [term()], Timeout :: integer()) ->
    term().
local_command(Module, Function, Args, Timeout) ->
    case is_cluster_node() of
        true ->
            weatherreport_log:log(
                node(),
                debug,
                "Local function call: ~p:~p(~p)",
                [Module, Function, Args]
            ),
            apply(Module, Function, Args);
        _ ->
            weatherreport_log:log(
                node(),
                debug,
                "Local RPC: ~p:~p(~p) [~p]",
                [Module, Function, Args, Timeout]
            ),
            rpc:call(nodename(), Module, Function, Args, Timeout)
    end.

%% @doc Call rpc:multicall/5 from the local cluster node rather than the
%% escript.
-spec multicall(
    [node()], Module :: atom(), Function :: atom(), Args :: [term()], Timeout :: integer()
) -> term().
multicall(Nodes, Module, Function, Args, Timeout) ->
    case local_command(rpc, multicall, [Nodes, Module, Function, Args, Timeout]) of
        {badrpc, Reason} ->
            {[{badrpc, Reason}], []};
        Resp ->
            Resp
    end.

%% @doc Retrieves the operating system's process ID of the local
%% node.
%% @equiv local_command(os, getpid)
%% @see can_connect/0
-spec pid() -> string().
pid() ->
    local_command(os, getpid).

%% @doc Attempts to connect to the local node if it is not
%% already, and returns whether connection was successful.
-spec can_connect() -> true | false.
can_connect() ->
    case is_connected() or is_cluster_node() of
        true ->
            true;
        false ->
            weatherreport_log:log(
                node(),
                debug,
                "Not connected to the local cluster node, trying to connect. alive:~p connect_failed:~p",
                [is_alive(), connect_failed()]
            ),
            maybe_connect()
    end.

-spec can_connect_all() -> true | false.
can_connect_all() ->
    case is_connected() of
        true ->
            case weatherreport_check_nodes_connected:check([]) of
                [] -> true;
                _ -> false
            end;
        false ->
            false
    end.

nodename() ->
    Name =
        case weatherreport_config:node_name() of
            undefined ->
                atom_to_list(node());
            {_, NodeName} ->
                NodeName
        end,
    case string:tokens(Name, "@") of
        [_Node, _Host] ->
            list_to_atom(Name);
        [Node] ->
            [_, Host] = string:tokens(atom_to_list(node()), "@"),
            list_to_atom(lists:concat([Node, "@", Host]))
    end.

%% Private functions
is_cluster_node() ->
    nodename() =:= node().

is_connected() ->
    is_alive() andalso connect_failed() =/= true.

maybe_connect() ->
    case connect_failed() of
        true -> false;
        _ -> try_connect()
    end.

try_connect() ->
    TargetNode = nodename(),
    case is_alive() of
        true -> ok;
        _ -> start_net()
    end,
    case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
        {true, pong} ->
            application:set_env(weatherreport, connect_failed, false),
            weatherreport_log:log(
                node(),
                debug,
                "Connected to local cluster node ~p.",
                [TargetNode]
            ),
            true;
        _ ->
            application:set_env(weatherreport, connect_failed, true),
            weatherreport_log:log(
                node(),
                warning,
                "Could not connect to the local cluster node ~p, some checks will not run.",
                [TargetNode]
            ),
            false
    end.

connect_failed() ->
    case application:get_env(weatherreport, connect_failed) of
        {ok, true} -> true;
        undefined -> undefined;
        _ -> false
    end.

start_net() ->
    weatherreport_log:log(node(), debug, "Starting distributed Erlang."),
    {Type, NodeName} = weatherreport_config:node_name(),
    ThisNode = append_node_suffix(NodeName, "_diag"),
    {ok, _} = net_kernel:start([ThisNode, Type]),
    case weatherreport_config:cookie() of
        undefined ->
            % Don't set cookie to undefined so we can pick up the ~/.erlang.cookie
            ok;
        Cookie when is_atom(Cookie) ->
            erlang:set_cookie(node(), Cookie)
    end.

append_node_suffix(Name, Suffix) ->
    case string:tokens(Name, "@") of
        [Node, Host] ->
            list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host]));
        [Node] ->
            list_to_atom(lists:concat([Node, Suffix, os:getpid()]))
    end.
