% Licensed 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.

-module(mem3_sync_security).

-export([maybe_sync/2, maybe_sync_int/2]).
-export([go/0, go/1]).

-include_lib("mem3/include/mem3.hrl").

maybe_sync(#shard{} = Src, #shard{} = Dst) ->
    case is_local(Src#shard.name) of
        false ->
            spawn(?MODULE, maybe_sync_int, [Src, Dst]);
        true ->
            ok
    end.

maybe_sync_int(#shard{name = Name} = Src, Dst) ->
    DbName = mem3:dbname(Name),
    case fabric:get_all_security(DbName, [{shards, [Src, Dst]}]) of
        {ok, WorkerObjs} ->
            Objs = [Obj || {_Worker, Obj} <- WorkerObjs],
            case length(lists:usort(Objs)) of
                1 -> ok;
                2 -> go(DbName)
            end;
        {error, no_majority} ->
            go(DbName);
        Else ->
            Args = [DbName, Else],
            couch_log:error("Error checking security objects for ~s :: ~p", Args)
    end.

go() ->
    {ok, Dbs} = fabric:all_dbs(),
    lists:foreach(fun handle_existing_db/1, Dbs).

go(DbName) when is_binary(DbName) ->
    handle_existing_db(DbName).

handle_existing_db(DbName) ->
    try handle_db(DbName) of
        _ -> ok
    catch
        error:database_does_not_exist ->
            couch_log:error(
                "Db was deleted while getting security"
                " object. DbName: ~p",
                [DbName]
            ),
            ok
    end.

handle_db(DbName) ->
    ShardCount = length(mem3:shards(DbName)),
    case get_all_security(DbName) of
        {ok, SecObjs} ->
            case is_ok(SecObjs, ShardCount) of
                ok ->
                    ok;
                {fixable, SecObj} ->
                    couch_log:info("Sync security object for ~p: ~p", [DbName, SecObj]),
                    case fabric:set_security(DbName, SecObj) of
                        ok ->
                            ok;
                        Error ->
                            couch_log:error(
                                "Error setting security object in ~p: ~p",
                                [DbName, Error]
                            )
                    end;
                broken ->
                    couch_log:error("Bad security object in ~p: ~p", [DbName, SecObjs])
            end;
        Error ->
            couch_log:error("Error getting security objects for ~p: ~p", [
                DbName, Error
            ])
    end.

get_all_security(DbName) ->
    case fabric:get_all_security(DbName) of
        {ok, SecObjs} ->
            SecObjsDict = lists:foldl(
                fun({_, SO}, Acc) ->
                    dict:update_counter(SO, 1, Acc)
                end,
                dict:new(),
                SecObjs
            ),
            {ok, dict:to_list(SecObjsDict)};
        Error ->
            Error
    end.

is_ok([_], _) ->
    % One security object is the happy case
    ok;
is_ok([_, _] = SecObjs0, ShardCount) ->
    % Figure out if we have a simple majority of security objects
    % and if so, use that as the correct value. Otherwise we abort
    % and rely on human intervention.
    {Count, SecObj} = lists:max([{C, O} || {O, C} <- SecObjs0]),
    case Count >= ((ShardCount div 2) + 1) of
        true -> {fixable, SecObj};
        false -> broken
    end;
is_ok(_, _) ->
    % Anything else requires human intervention
    broken.

is_local(<<"shards/", _/binary>>) ->
    false;
is_local(_) ->
    true.
